mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 18:34:36 -10:00
Re-verifies `docs/as-built/architecture/lifecycle-snapshot-restore.md`, `packaging-bootstrap-bundles.md` and `plugin-agent-image-context-pack.md` against main `a350c59b` and restamps them. Corrects restore-check's verdicts (an `unknown` branch and a `restore-preconditions` check), where a pod-aware bundle's content is routed (inside bootstrap, before any seat launches) and how guided compaction holds `/compact` behind a restore map, and adds the features since the old stamps, including topology rosters, restore receipts, `rig seat continue`, seat handover, GitHub folder links, `rig bundle check`, configurations and package identity, System World, recaps, skill loadouts, `rig compact` and `rig fork`. Part of the 0.6.6 developer documentation refresh. Text only.
This commit is contained in:
1 parent
fcaf1f8ee8
commit
aa4ee793cd
3 files changed
+452
-165
No files matched your search
@@ -11,8 +11,8 @@ applies-when: |
|
||||
and the CLI restore-packet command work.
|
||||
siblings: [daemon-core.md, agent-spec-and-startup.md, transport-and-transcripts.md]
|
||||
prerequisite-reads: [../README.md, agent-spec-and-startup.md]
|
||||
last-verified-against-source: 254122872cf477511514979a4300b695d77cd1f7
|
||||
last-updated: 2026-10-03
|
||||
last-verified-against-source: a350c59b5a5fb37ee4a21b1026b595068b603df1
|
||||
last-updated: 2026-10-05
|
||||
---
|
||||
|
||||
# Lifecycle — Snapshot, Restore, Continuity
|
||||
@@ -22,7 +22,7 @@ The durable-state half of the core product loop:
|
||||
captures serialized rig state; restore replays it honestly (no silent
|
||||
fresh-fallback); restore-check is a separate read-only readiness probe.
|
||||
|
||||
> Verified against source at main `254122872cf477511514979a4300b695d77cd1f7`. Each count below sits beside the
|
||||
> Verified against source at main `a350c59b5a5fb37ee4a21b1026b595068b603df1`. Each count below sits beside the
|
||||
> command that produces it; run the command from the repository root to refresh
|
||||
> it.
|
||||
|
||||
@@ -31,51 +31,55 @@ fresh-fallback); restore-check is a separate read-only readiness probe.
|
||||
All in `packages/daemon/src/domain/types.ts`. The spec/projection types live in
|
||||
`agent-spec-and-startup.md`; the snapshot/restore types live here.
|
||||
|
||||
- **NodeRestoreOutcome** (`types.ts:501`) — the per-node restore outcome
|
||||
- **NodeRestoreOutcome** (`types.ts:500`) — the per-node restore outcome
|
||||
projected into `rig ps`. **9** values
|
||||
(`grep '^export type NodeRestoreOutcome' packages/daemon/src/domain/types.ts | grep -o '"[^"]*"' | wc -l`):
|
||||
`resumed`, `rebuilt`, `fresh`, `fresh-primed`, `awaiting-decision`,
|
||||
`failed`, `attention_required`, `operator_recovered`, `n-a`. The restore
|
||||
result itself, `RestoreNodeResult.status` (`types.ts:476`), has the same
|
||||
result itself, `RestoreNodeResult.status` (`types.ts:475`), has the same
|
||||
values without `n-a`: **8**
|
||||
(`grep -E '^ status: "resumed"' packages/daemon/src/domain/types.ts | grep -o '"[^"]*"' | wc -l`).
|
||||
Where `restore-orchestrator.ts` sets each one:
|
||||
- `resumed` — `baseStatus = "resumed"` after a legacy resume (`:1212`); the
|
||||
status is returned only by `finishJoinedResume` (`:1499`, `:1561`) after
|
||||
it has rebound and verified the pane, otherwise that becomes
|
||||
`attention_required` (`:1533`).
|
||||
- `rebuilt` — a checkpoint file was written (`:1272`).
|
||||
- `resumed` — `baseStatus = "resumed"` after a legacy resume (`:1031`); the
|
||||
status is returned only by `finishJoinedResume` (defined `:1319`, called
|
||||
`:1245` and `:1312`, returning `resumed` at `:1381`) after it has rebound
|
||||
and verified the pane; otherwise that becomes `attention_required`
|
||||
(`:1353`, `:1377`).
|
||||
- `rebuilt` — a checkpoint file was written (`:1091`).
|
||||
- `fresh-primed` — the default for a deliberate non-resume launch
|
||||
(`:1194`).
|
||||
(`:988`).
|
||||
- `fresh` — only the skip path for a pod node whose live continuity state
|
||||
is already `restoring` (`:992`).
|
||||
is already `restoring` (`:786`).
|
||||
- `awaiting-decision` — the seat could not resume and nothing is running:
|
||||
before launch (`:1027`), or after launch once the blank session is rolled
|
||||
back (`:1208`, `:1230`, `:1243`, `:1259`, `:1417`).
|
||||
before launch (`:821`), or after launch once the blank session is rolled
|
||||
back (`:1027`, `:1049`, `:1062`, `:1078`, `:1237`).
|
||||
- `attention_required` — a live session waiting on a runtime prompt
|
||||
(`:1221`, `:1443`) or a harness that never started (`:1307`).
|
||||
- `failed` — occupant ambiguity (`:979`), launch failure (`:1118`), a
|
||||
checkpoint that cannot be delivered (`:1268`, `:1274`), a missing
|
||||
required startup file (`:1343`), or a startup error.
|
||||
(`:1040`, `:1263`) or a harness that never started (`:1126`).
|
||||
- `failed` — occupant ambiguity (`:773`, `:970`), launch failure (`:912`),
|
||||
a checkpoint that cannot be delivered (`:1087`, `:1093`), a missing
|
||||
required startup file (`:1162`), or a startup error (`:1272`, `:1283`,
|
||||
`:1293`, `:1301`).
|
||||
- `operator_recovered` — only from later reconciliation,
|
||||
`reconcileNodeRuntimeTruth` (`:1759`, event at `:1902`).
|
||||
- **RestoreRigResult** (`types.ts:445`) — the rig-level rollup, **4** values
|
||||
`reconcileNodeRuntimeTruth` (`:1587`, event at `:1724`).
|
||||
- **RestoreRigResult** (`types.ts:444`) — the rig-level rollup, **4** values
|
||||
(`grep '^export type RestoreRigResult' packages/daemon/src/domain/types.ts | grep -o '"[^"]*"' | wc -l`):
|
||||
`fully_restored`, `partially_restored`, `failed`, `not_attempted`
|
||||
(`rollupRestoreRigResult`, `restore-orchestrator.ts:82`).
|
||||
- **SnapshotData** (`types.ts:352`) — the serialized snapshot payload. It has
|
||||
`fully_restored`, `partially_restored`, `failed`, `not_attempted`.
|
||||
`rollupRestoreRigResult` (`restore-orchestrator.ts:82`) produces only the
|
||||
first three; `not_attempted` is set when pre-restore validation fails
|
||||
(`:282`).
|
||||
- **SnapshotData** (`types.ts:351`) — the serialized snapshot payload. It has
|
||||
**7** optional fields, kept optional so older snapshots still load
|
||||
(`sed -n '/^export interface SnapshotData/,/^}/p' packages/daemon/src/domain/types.ts | grep -c '?:'`):
|
||||
`activeSessionIdByNode?`, `activeOccupantsByNode?`, `topologyRoster?`,
|
||||
`pods?`, `continuityStates?`, `nodeStartupContext?`, `envReceipt?`.
|
||||
- **NodeStartupSnapshot** (`types.ts:345`) — persisted restore replay input:
|
||||
- **NodeStartupSnapshot** (`types.ts:344`) — persisted restore replay input:
|
||||
classification-free projection entries, resolved startup files, startup
|
||||
actions, runtime.
|
||||
- **PersistedProjectionEntry** (`types.ts:333`) — the classification-free
|
||||
- **PersistedProjectionEntry** (`types.ts:332`) — the classification-free
|
||||
restore replay seam: persists only entry identity + source metadata, NOT
|
||||
stale `classification`, `conflicts`, or `noOps` (Architecture Rule 10).
|
||||
Restore rebuilds each entry as `safe_projection` with empty `conflicts` and
|
||||
`noOps` (`restore-orchestrator.ts:1352`, `:1357`, `:1358`).
|
||||
`noOps` (`restore-orchestrator.ts:1171`, `:1176`, `:1177`).
|
||||
|
||||
## 2. Snapshot, restore, continuity domain services
|
||||
|
||||
@@ -93,9 +97,18 @@ All under `packages/daemon/src/domain/`:
|
||||
accept only snapshots that pass `isRestoreUsableSnapshotData` (`:283`).
|
||||
- `restore-orchestrator.ts` — resume, checkpoint delivery, startup replay,
|
||||
live continuity consultation, topology ordering (`computeRestorePlan`,
|
||||
`:876`, ordering only `delegates_to` and `spawned_by` edges, `:72`), and
|
||||
service boot gating before agent restore (`serviceOrchestrator.boot`,
|
||||
`:328`).
|
||||
`:670`, ordering only `delegates_to` and `spawned_by` edges, `:72`, among
|
||||
the roster's intended nodes, `:671`), and service boot gating before agent
|
||||
restore (`serviceOrchestrator.boot`, `:332`).
|
||||
- `restore-preconditions.ts` — `validatePreRestore` (`:7`), the one
|
||||
pre-restore validation shared by restore and restore-check (§3, §5.1).
|
||||
- `restore-topology.ts` — `resolveSnapshotRestoreTopology` (`:12`), which
|
||||
splits a snapshot's nodes into intended and excluded (§3).
|
||||
- `restore-attempt-receipt.ts` — `deriveRestoreAttemptReceipt` (`:42`), the
|
||||
read-only per-attempt receipt (§4).
|
||||
- `rehydrate-eligibility.ts` — `snapshotMatchesCurrentOccupants` (`:20`) and
|
||||
`assessCurrentStateRehydrateEligibility` (`:66`), shared by `routes/up.ts`
|
||||
and `routes/restore-check.ts`.
|
||||
- `active-occupant.ts` — the shared rule for which snapshot session row is a
|
||||
node's occupant (`resolveActiveSnapshotSession`, `:74`).
|
||||
|
||||
@@ -103,62 +116,97 @@ All under `packages/daemon/src/domain/`:
|
||||
|
||||
Restore behavior, each point checked in `restore-orchestrator.ts`:
|
||||
|
||||
- Selects the snapshot with `selectRestoreUsable` even when a snapshot id is
|
||||
given (`:252–253`), so wrong-rig and unusable snapshots are refused before
|
||||
any mutation. The selection (`RestoreSnapshotSelection`, `types.ts:396`)
|
||||
records whether it was explicit or automatic, why, and any newer usable
|
||||
alternative; it is carried on the result and on `restore.started`.
|
||||
- Restores only the nodes the snapshot's topology roster intends
|
||||
(`SnapshotData.topologyRoster`, `types.ts:370`). The roster comes from the
|
||||
latest `topology.roster_recorded` event (`snapshot-capture.ts:171`), from
|
||||
`rig snapshot <rigId> --intended-seats` (`packages/cli/src/commands/snapshot.ts:20`),
|
||||
or, for older snapshots, all current nodes. `resolveSnapshotRestoreTopology`
|
||||
(`restore-topology.ts:12`) reports every other node as excluded with reason
|
||||
`historical_not_in_intended_roster` (`:27`); the result carries
|
||||
`intendedRoster` and `excludedNodes` (`types.ts:440–441`).
|
||||
- Resolves each node's occupant from the snapshot's recorded active-occupant
|
||||
maps, not by picking the newest session row
|
||||
(`resolveActiveSnapshotSession`, `active-occupant.ts:74`, called at
|
||||
`restore-orchestrator.ts:974` and `:1171`). An ambiguous occupant fails the
|
||||
`restore-orchestrator.ts:768` and `:965`). An ambiguous occupant fails the
|
||||
node loudly (`activeOccupantAmbiguityError`, `active-occupant.ts:97`).
|
||||
- Consults live `continuity_state`; preserves state when a node is already
|
||||
`restoring` (`:985–998` — `SELECT status FROM continuity_state …` at
|
||||
`:987`; if `status === "restoring"` the node is skipped with a warning and
|
||||
reported as `fresh`, `:990–992`; `degraded` only adds a warning).
|
||||
`restoring` (`:778–792` — `SELECT status FROM continuity_state …` at
|
||||
`:781`; if `status === "restoring"` the node is skipped with a warning and
|
||||
reported as `fresh`, `:784–786`; `degraded` only adds a warning).
|
||||
- Reuses the seat's durably bound tmux session name when the prior binding
|
||||
has a usable one (`:841`), and otherwise falls back to the derived name
|
||||
with a warning (`:870`).
|
||||
- Replays restore-safe startup using persisted startup context;
|
||||
prefilters missing optional artifacts into warnings; **hard-fails a node if
|
||||
a required startup file is missing** (`:1343` — status `"failed"`, error
|
||||
a required startup file is missing** (`:1162` — status `"failed"`, error
|
||||
"Missing required startup files: …"). Before any mutation,
|
||||
`validatePreRestore` (`:585`) already blocks the whole restore when a node
|
||||
`validatePreRestore` (`restore-preconditions.ts:7`, called at
|
||||
`restore-orchestrator.ts:272`) already blocks the whole restore when a node
|
||||
that will consume replay is missing a required startup file
|
||||
(`required_startup_file_missing`, `:729`; outcome
|
||||
`pre_restore_validation_failed`, `:290`). A node that resumes its exact
|
||||
native session replays no startup files, actions, guidance or skills
|
||||
(`replayContained`, `:1288`); its saved Claude activity-hook selection is
|
||||
reapplied before native resume. A skipped hook reapply adds a warning without
|
||||
changing the restore outcome.
|
||||
(`required_startup_file_missing`, `restore-preconditions.ts:151`; outcome
|
||||
`pre_restore_validation_failed`, `restore-orchestrator.ts:292`). Its other
|
||||
blockers are `invalid_snapshot_data` (`restore-preconditions.ts:27`),
|
||||
`invalid_topology_roster` (`:78`), `checkpoint_missing_node_cwd` (`:91`),
|
||||
`startup_owner_root_missing` (`:139`), `service_rig_root_missing` (`:187`)
|
||||
and `service_compose_file_missing` (`:197`). The `/api/up` route returns
|
||||
that outcome as HTTP 409 with `status: "not_attempted"`
|
||||
(`routes/up.ts:161`). A node that resumes its exact native session replays
|
||||
no startup files, actions, guidance or skills (`replayContained`,
|
||||
`restore-orchestrator.ts:1108`); its saved Claude activity-hook selection
|
||||
is reapplied before native resume. A skipped hook reapply adds a warning
|
||||
without changing the restore outcome.
|
||||
- Writes a **transcript boundary marker before re-launch**
|
||||
(`writeBoundaryMarker`, `:1100`, called before `launchNode`).
|
||||
- Refuses to restore over live sessions (`:258–261` — `rig_not_stopped`: "Rig …
|
||||
has live sessions. Stop the rig with 'rig down' before restoring"). A tmux
|
||||
probe that fails also blocks it (`classifyRunningSessions`, `:812`).
|
||||
(`writeBoundaryMarker`, `:894`, called before `launchNode` at `:905`) when
|
||||
the transcript store is enabled.
|
||||
- Refuses to restore over live sessions (`:260–263` — `rig_not_stopped`: "Rig …
|
||||
has live sessions. Stop the rig with 'rig down' before restoring, or use the
|
||||
latest auto-pre-down snapshot."). A tmux probe that fails also blocks it
|
||||
(`classifyRunningSessions`, `:606`).
|
||||
- Checks that the harness actually resumed. The resume adapters judge the
|
||||
pane with `assessNativeResumeProbe` (`domain/native-resume-probe.ts:84`). A
|
||||
pod-aware resume counts as resumed only when startup reports `resumed`
|
||||
continuity or the launched session row carries the snapshot's resume type
|
||||
and token (`launchedSessionMatchesSnapshotResume`,
|
||||
`restore-orchestrator.ts:1564`); a reported `fresh` continuity without that
|
||||
proof rolls back to `awaiting-decision` (`:1405–1419`).
|
||||
`restore-orchestrator.ts:1384`); a reported `fresh` continuity without that
|
||||
proof rolls back to `awaiting-decision` (`:1222–1239`).
|
||||
- Saves a `rig up --non-interruptive` or `--no-non-interruptive` choice on
|
||||
the rig only after the live-session check and pre-restore validation pass
|
||||
(`:302`); see [adapters-and-runtimes.md](adapters-and-runtimes.md) for the
|
||||
launch flags.
|
||||
|
||||
`launchNodeSubset` (`:393`) and `launchSingleNode` restore chosen seats rather
|
||||
than the whole rig; unlike a full restore, a failed tmux probe there proceeds
|
||||
with a `liveness_probe_unknown` warning (`:511`).
|
||||
|
||||
**Restore-honesty rules** (texts in `architecture-rules-and-event-system.md`;
|
||||
cited here by number with the code that carries them):
|
||||
|
||||
- Rule 7 — `RESTORE_POLICY_LEVEL`
|
||||
(`packages/daemon/src/domain/profile-resolver.ts:103`) orders the three
|
||||
policies, and `resolveRestorePolicy` (`profile-resolver.ts:488`) rejects a
|
||||
(`packages/daemon/src/domain/profile-resolver.ts:105`) orders the three
|
||||
policies, and `resolveRestorePolicy` (`profile-resolver.ts:526`) rejects a
|
||||
profile or member value that broadens it.
|
||||
- Rule 14 — the outcome set is `RestoreNodeResult.status` (`types.ts:476`,
|
||||
- Rule 14 — the outcome set is `RestoreNodeResult.status` (`types.ts:475`,
|
||||
§1); `rebuilt` is set only when a checkpoint is written
|
||||
(`restore-orchestrator.ts:1272`).
|
||||
(`restore-orchestrator.ts:1091`).
|
||||
- Rule 15 — in `restore-orchestrator.ts`, a `resume_if_possible` seat with a
|
||||
session but no token stops as `awaiting-decision` before launch unless
|
||||
`--fresh` names it (`:1020–1029`); a failed resume kills the blank session
|
||||
(`rollbackToZeroSession`, `:1140`) and returns `awaiting-decision`
|
||||
(`:1230`); pod-aware resume passes
|
||||
`allowFreshFallback: !(isPodAware && resumeRequested)` (`:1399`). Fresh
|
||||
launch is explicit: `rig up --existing <rig> --fresh <seat...>`
|
||||
(`packages/cli/src/commands/up.ts:85`).
|
||||
`--fresh` names it (`:806–825`); a failed resume kills the blank session
|
||||
(`rollbackToZeroSession`, `:934`) and returns `awaiting-decision`
|
||||
(`:1049`); pod-aware resume passes
|
||||
`allowFreshFallback: !(isPodAware && resumeRequested)` (`:1218`). Fresh
|
||||
launch is explicit: `rig up --existing <rig> --fresh <seats...>`
|
||||
(`packages/cli/src/commands/up.ts:92`). On a terminal, `rig up` asks
|
||||
yes or no for each `awaiting-decision` seat and re-posts with the accepted
|
||||
seats as fresh (`up.ts:618`).
|
||||
- Rule 16 — `rig down` prints the snapshot id and a restore command
|
||||
(`packages/cli/src/commands/down.ts:236–246`); `rig up` on an existing rig
|
||||
prints per-node statuses and the attach command
|
||||
(`packages/cli/src/commands/up.ts:540–560`).
|
||||
(`packages/cli/src/commands/up.ts:583–616`).
|
||||
|
||||
(The full rule list lives in `architecture-rules-and-event-system.md`.)
|
||||
|
||||
@@ -166,17 +214,22 @@ cited here by number with the code that carries them):
|
||||
|
||||
- `rig down <rig>` (name or id, `packages/cli/src/commands/down.ts:104`)
|
||||
auto-captures an `auto-pre-down` snapshot before teardown when the rig has
|
||||
live sessions (`packages/daemon/src/domain/rig-teardown.ts:116`; an
|
||||
already-stopped rig returns at `:89–109` without one).
|
||||
live sessions (`packages/daemon/src/domain/rig-teardown.ts:116`, capture
|
||||
at `:127`, after refreshing resume metadata at `:118`; an already-stopped
|
||||
rig returns at `:93–114` without one).
|
||||
- `rig up <rig-name>`: a source with no `/` and no
|
||||
`.yaml`/`.yml`/`.rigbundle`/`.rigtopology` extension is a rig name
|
||||
(`packages/daemon/src/domain/up-command-router.ts:49–50`). The CLI first
|
||||
checks the spec library unless `--existing` is given, and refuses a name
|
||||
that matches both (`packages/cli/src/commands/up.ts:287–302`). The daemon
|
||||
finds the rig by name (`packages/daemon/src/routes/up.ts:222`) and
|
||||
restores from `selectRestoreUsable` (`routes/up.ts:105`), which prefers the
|
||||
newest `auto-pre-down` or `auto-periodic` snapshot, then the newest other
|
||||
usable one (`snapshot-repository.ts:75`).
|
||||
(`packages/daemon/src/domain/up-command-router.ts:49–50`). The CLI refuses
|
||||
a name that matches only archived rigs, with `rig unarchive` guidance
|
||||
(`packages/cli/src/commands/up.ts:290`). It then checks the spec library
|
||||
unless `--existing` is given, and refuses a name that matches both
|
||||
(`up.ts:338–353`). The daemon finds the rig by name, preferring unarchived
|
||||
rigs (`packages/daemon/src/routes/up.ts:222–225`), and restores from
|
||||
`selectRestoreUsable` (`routes/up.ts:105`), which prefers the newest
|
||||
`auto-pre-down` or `auto-periodic` snapshot, then the newest other usable
|
||||
one (`snapshot-repository.ts:75`).
|
||||
- `rig up <rig-name> --plan` is a read-only restore preview that never
|
||||
captures a snapshot (`routes/up.ts:126`).
|
||||
- If no usable snapshot exists, or the chosen one names an older occupant
|
||||
(`snapshotMatchesCurrentOccupants`, `routes/up.ts:109`), `rig up` captures
|
||||
an `auto-rehydrate` snapshot of current DB state when that state is
|
||||
@@ -187,77 +240,124 @@ cited here by number with the code that carries them):
|
||||
`To restore: rig up <name>` (or `rig restore <snapshotId> --rig <rigId>`
|
||||
when the name is not unique); `up` output includes node statuses and an
|
||||
`Attach:` command (anchors under Rule 16 above).
|
||||
- `rig restore status <attemptId> --rig <rigId>`
|
||||
(`packages/cli/src/commands/restore.ts:120`) reads
|
||||
`GET /api/rigs/:rigId/restore/status/:attemptId`
|
||||
(`packages/daemon/src/routes/snapshots.ts:76`). `deriveRestoreAttemptReceipt`
|
||||
(`restore-attempt-receipt.ts:42`) folds that attempt's events into a
|
||||
per-node view without writing anything.
|
||||
|
||||
### Single-seat recovery and handover
|
||||
|
||||
- **`rig seat continue <seat>`** — when a fresh launch stops at a native
|
||||
prompt before its startup context is delivered, the startup orchestrator
|
||||
fails it as `attention_required` and adds "run: rig seat continue
|
||||
<session>" (`startup-orchestrator.ts:600`). The command
|
||||
(`packages/cli/src/commands/seat.ts:632`) calls
|
||||
`POST /api/seat/continue/:seatRef` (`routes/seat.ts:293`), which runs
|
||||
`continueFreshStartup` (`seat-lifecycle-service.ts:395`): it re-checks the
|
||||
binding, pane and runtime, then delivers the pending context to the same
|
||||
conversation without relaunching. In a restore, a pod-aware seat in this
|
||||
state reports `attention_required` (`restore-orchestrator.ts:1259`). See
|
||||
[agent-spec-and-startup.md](agent-spec-and-startup.md) for the startup side.
|
||||
- **Per-seat startup decision** — `POST /api/startup/:rigId/:logicalId`
|
||||
(`routes/startup.ts:142`) takes `resume`, `start`, `fresh` or `continue`
|
||||
for one seat. `resume` captures an eligibility-checked `auto-rehydrate`
|
||||
snapshot when none exists and goes through `launchSingleNode`.
|
||||
- **Seat handover** — `rig seat handover <seat> --source
|
||||
fresh|rebuild|fork:<id>|discovered:<id> [--dry-run]`
|
||||
(`packages/cli/src/commands/seat.ts:363`) calls
|
||||
`POST /api/seat/handover/:seatRef` (`routes/seat.ts:94`), which runs
|
||||
`SeatHandoverService.handover` (`seat-handover-service.ts:267`).
|
||||
`SuccessorSessionLauncher` (`successor-session-launcher.ts:90`) respawns
|
||||
the same pane in place, so the session name stays the same. The fresh
|
||||
source delivers a daemon-built restore packet (`buildRestorePacket`,
|
||||
`seat-handover-service.ts:1146`), which is separate from the CLI
|
||||
`rig restore-packet` below. The rebuild source primes from the seat's
|
||||
durable chain (`buildRebuildPrimingChain`, `rebuild-priming-chain.ts:22`).
|
||||
|
||||
## 5. Daemon-side restore-check and CLI restore-packet
|
||||
|
||||
### 5.1 `rig restore-check` — readiness probe
|
||||
|
||||
`GET /api/restore-check?rig=<name>&noQueue=true&noHooks=true&compact=1&ready=1`
|
||||
(`routes/restore-check.ts:205–206`). The route calls
|
||||
`createRestoreCheckService` (`:149`), which assembles a framework-free
|
||||
`RestoreCheckDeps` (`:153–201`) over existing daemon projections —
|
||||
`listRigs` from `rigRepo` (`:172–175`), `getNodeInventory` (joining
|
||||
`node_id` by `logical_id`, `:24–29`, `:176–182`), `getStartupContext` (reads
|
||||
(`routes/restore-check.ts:218–219`). The route calls
|
||||
`createRestoreCheckService` (`:150`), which assembles a framework-free
|
||||
`RestoreCheckDeps` (`:154–214`) over existing daemon projections —
|
||||
`listRigs` from `rigRepo` (`:173–176`), `getNodeInventory` (joining
|
||||
`node_id` by `logical_id`, `:25–30`, `:177–183`), `getStartupContext` (reads
|
||||
`node_startup_context`, parses `projection_entries_json` /
|
||||
`resolved_files_json` / `startup_actions_json`, `:47–141`), `hasSnapshot`
|
||||
/ `getLatestSnapshot` from `snapshotRepo` (`:186–192`), `probeQueueStore`
|
||||
(`:155–167`), `getClaudeActivityHookEvents` (`:168–171`), and
|
||||
`probeDaemonHealth` (self-evident: "We're inside the daemon — if this route
|
||||
is responding, daemon is healthy", `:193–196`). It then runs
|
||||
`service.check(...)` (`:216–217`).
|
||||
`resolved_files_json` / `startup_actions_json`, `:48–142`), `hasSnapshot`
|
||||
/ `getLatestSnapshot` from `snapshotRepo` (`:187–193`), `getRestoreInputs`
|
||||
(`:194–205`), `probeQueueStore` (`:156–168`), `getClaudeActivityHookEvents`
|
||||
(`:169–172`), and `probeDaemonHealth` (self-evident: "We're inside the
|
||||
daemon — if this route is responding, daemon is healthy", `:206–209`). It
|
||||
then runs `service.check(...)` (`:229–230`).
|
||||
|
||||
`RestoreCheckService.check()` (`restore-check-service.ts:243`) layers:
|
||||
`RestoreCheckService.check()` (`restore-check-service.ts:254`) layers:
|
||||
|
||||
1. **Host checks** — `checkDaemonReachable` (probe-throw → `verdict:
|
||||
unknown`, NOT `not_restorable`, `:256–263`), `checkStateDirWritable`
|
||||
(`:265`), `checkHostInfraDeclaration` (`:266`, impl `:432`).
|
||||
2. **Rig enumeration** — `listRigs()` throw → `buildUnknown` (`:271–278`);
|
||||
`--rig` filter; unknown rig → red `rig.<name>.exists` (`:280–287`).
|
||||
3. **Per-rig checks** — `checkSnapshot` (`:294`, impl `:701`),
|
||||
`checkSpecPresent` (`:299`, impl `:1064`).
|
||||
4. **Per-seat checks** — `checkSeatReadiness` (`:315`), `checkStartupContext`
|
||||
(`:329`, impl `:789`; `unknownChecks` → `buildUnknown`, `:330–335`),
|
||||
`checkTranscript` (`:351`), `checkResumePath` (`:355`), and unless opted
|
||||
unknown`, NOT `not_restorable`, `:265–275`), `checkStateDirWritable`
|
||||
(`:277`, impl `:493`), `checkHostInfraDeclaration` (`:278`, impl `:510`).
|
||||
2. **Rig enumeration** — `listRigs()` throw → `buildUnknown` (`:281–290`);
|
||||
`--rig` filter; unknown rig → red `rig.<name>.exists` (`:292–300`).
|
||||
3. **Per-rig checks** — `checkSnapshot` (`:306`, impl `:779`),
|
||||
`checkSpecPresent` (`:311`, impl `:1142`), and
|
||||
`rig.<name>.restore-preconditions` (`checkRestorePreconditions`, `:333`,
|
||||
impl `:417`). The last runs the same `validatePreRestore` that restore
|
||||
runs (`:441`) on the inputs from `getRestoreInputs`: red on a blocker,
|
||||
yellow on a warning, green otherwise. Without a usable snapshot it is
|
||||
yellow when current-state rehydrate is eligible and red when it isn't;
|
||||
when the inputs can't be read it is yellow and the rig's rollup becomes
|
||||
`unknown` (see Verdict). The rig-status route sets
|
||||
`recoveryOnly` (`routes/rigs.ts:272`), which skips this check when every
|
||||
seat is ready (`restore-check-service.ts:332`).
|
||||
4. **Per-seat checks** — `checkSeatReadiness` (`:341`), `checkStartupContext`
|
||||
(`:355`, impl `:867`; `unknownChecks` → `buildUnknown`, `:356–361`),
|
||||
`checkTranscript` (`:377`), `checkResumePath` (`:381`), and unless opted
|
||||
out: the daemon SQLite `queue_items` availability probe `checkQueueStore`
|
||||
(`:360`, gated by `--no-queue`; impl `:910`) and `checkHooks` (`:365`,
|
||||
gated by `--no-hooks`; impl `:933`). Queue continuity is represented by
|
||||
the shared daemon store; an empty queue is valid
|
||||
(`routes/restore-check.ts:159–160`). Claude hook readiness follows the
|
||||
(`:385–389`, gated by `--no-queue`; impl `:988`) and `checkHooks`
|
||||
(`:390–394`, gated by `--no-hooks`; impl `:1011`). Queue continuity is
|
||||
represented by the shared daemon store; an empty queue is valid
|
||||
(`routes/restore-check.ts:158–161`). Claude hook readiness follows the
|
||||
persisted `claude_activity_hooks` runtime-resource selection
|
||||
(`restore-check-service.ts:957`) and the adapter's activity-relay
|
||||
projection in the seat CWD (`restore-check-service.ts:974–975`). A seat
|
||||
(`restore-check-service.ts:1034–1041`) and the adapter's activity-relay
|
||||
projection in the seat CWD (`restore-check-service.ts:1052–1053`). A seat
|
||||
that deliberately omits that resource is not applicable. In compact mode
|
||||
without `ready=1`, a seat whose readiness check is green gets only the
|
||||
startup-context check (`restore-check-service.ts:327`, `:347–349`); that
|
||||
check still counts toward the verdict (`:337–340`).
|
||||
5. **Verdict** — `buildResult` (`:1106`) aggregates: any red →
|
||||
`not_restorable`; any yellow → `restorable_with_caveats`; else
|
||||
`restorable` (`:1121–1128`); probe-uninspectable → `unknown`
|
||||
(`buildUnknown`, `:1137`). Plus a `RecoveryPlan` (`buildRecovery`,
|
||||
`:1241`) and a `RepairStep[]` packet (`buildRepairPacket`, `:1475`;
|
||||
`null` when fully restorable, `:1476`).
|
||||
startup-context check (`restore-check-service.ts:353`, `:363–375`); that
|
||||
check still counts toward the verdict.
|
||||
5. **Verdict** — `buildResult` (`:1186`) aggregates: any red →
|
||||
`not_restorable`; else any rig rollup with status `unknown` → `unknown`
|
||||
(`:1204–1205`); else any yellow → `restorable_with_caveats`; else
|
||||
`restorable` (`:1201–1210`). A rig rollup is `unknown` when its restore
|
||||
inputs could not be read (`:1530–1532`). A probe that can't be inspected
|
||||
at all also gives `unknown` (`buildUnknown`, `:1219`). Plus a
|
||||
`RecoveryPlan` (`buildRecovery`, `:1323`) and a `RepairStep[]` packet
|
||||
(`buildRepairPacket`, `:1562`; `null` when fully restorable, `:1563`).
|
||||
|
||||
The result shape is `RestoreCheckResult` (`restore-check-service.ts:144–156`):
|
||||
The result shape is `RestoreCheckResult` (`restore-check-service.ts:147–159`):
|
||||
`verdict`, `readiness`, `continuity`, `rigs[]`, `hostInfra`, `recovery`,
|
||||
`counts {red,yellow,green}`, `classCounts`, `checks[]`, `repairPacket`. With
|
||||
`compact=1` the route returns a reduced body (`routes/restore-check.ts:219–245`).
|
||||
`compact=1` the route returns a reduced body (`routes/restore-check.ts:232–259`).
|
||||
|
||||
**Honest-error design:** a daemon-probe *exception* produces
|
||||
`verdict: unknown` (uninspectable state), distinct from a daemon
|
||||
definitely-down state which is `red` / `not_restorable`
|
||||
(`restore-check-service.ts:253–263`, `checkDaemonReachable` `:393–413`). The
|
||||
(`restore-check-service.ts:265–275`, `checkDaemonReachable` `:471–491`). The
|
||||
route's catch-all returns the same `unknown`-shaped body with HTTP 500 + a
|
||||
`probe.error` red check (`routes/restore-check.ts:249–297`).
|
||||
`probe.error` red check (`routes/restore-check.ts:262–310`).
|
||||
`CheckEntry.remediationSafe` defaults to `false` (conservative —
|
||||
unclassified remediations are NOT auto-execution-safe,
|
||||
`restore-check-service.ts:19–24`, applied at `:1490`).
|
||||
`restore-check-service.ts:22–27`, applied at `:1577`).
|
||||
|
||||
CLI surface (`../cli-reference.md` `### rig restore-check`; options in
|
||||
CLI surface (`../cli-reference.md` `### restore-check`; options in
|
||||
`packages/cli/src/commands/restore-check.ts:222–227`):
|
||||
`rig restore-check [--rig <name>] [--full] [--ready] [--no-queue] [--no-hooks] [--json]`.
|
||||
Output is compact unless `--full` (`:313`). Exit codes: `0` restorable (or
|
||||
with caveats), `1` not restorable (red), `2` unknown / probe error
|
||||
(`:214–217`).
|
||||
(`:214–217`). When the daemon is down, the CLI builds a local
|
||||
`not_restorable` result and exits 1 (`:269`).
|
||||
|
||||
### 5.2 `rig restore-packet` — cross-runtime restore packet
|
||||
|
||||
@@ -265,7 +365,7 @@ CLI-side, no restore-packet daemon route. `commands/restore-packet.ts`
|
||||
(**539** lines, `wc -l < packages/cli/src/commands/restore-packet.ts`)
|
||||
implements **3** subcommands
|
||||
(`grep -c 'cmd.command(' packages/cli/src/commands/restore-packet.ts`;
|
||||
`../cli-reference.md` `### rig restore-packet`): `write [options]` (`:216`;
|
||||
`../cli-reference.md` `### restore-packet`): `write [options]` (`:216`;
|
||||
generate a packet directory from a source session or JSONL file, with
|
||||
`omitted-records` accounting; `--source-session` reads the transcript from the
|
||||
daemon's `/api/transcripts/<session>/full`, `:156`), `read <packet-dir> [--json]`
|
||||
@@ -284,6 +384,8 @@ via runtime parsers + redaction (`packages/cli/src/restore-packet/`).
|
||||
- `transport-and-transcripts.md` — transcript boundary markers written on
|
||||
restore.
|
||||
- Source roots: `packages/daemon/src/domain/{restore-orchestrator,
|
||||
snapshot-capture,snapshot-repository,checkpoint-store,active-occupant,
|
||||
restore-check-service}.ts`, `packages/daemon/src/routes/{restore-check,up}.ts`,
|
||||
`packages/cli/src/commands/{restore-packet,restore-check}.ts`.
|
||||
restore-preconditions,restore-topology,restore-attempt-receipt,
|
||||
rehydrate-eligibility,snapshot-capture,snapshot-repository,checkpoint-store,
|
||||
active-occupant,restore-check-service}.ts`,
|
||||
`packages/daemon/src/routes/{restore-check,up,snapshots}.ts`,
|
||||
`packages/cli/src/commands/{restore-packet,restore-check,restore}.ts`.
|
||||
@@ -9,14 +9,14 @@ applies-when: |
|
||||
source routing, bootstrap plan/apply, or the retained package install engine.
|
||||
siblings: [agent-spec-and-startup.md, plugin-agent-image-context-pack.md]
|
||||
prerequisite-reads: [../README.md, agent-spec-and-startup.md]
|
||||
last-verified-against-source: 254122872cf477511514979a4300b695d77cd1f7
|
||||
last-updated: 2026-10-03
|
||||
last-verified-against-source: a350c59b5a5fb37ee4a21b1026b595068b603df1
|
||||
last-updated: 2026-10-05
|
||||
---
|
||||
|
||||
# Packaging, bootstrap and bundles
|
||||
|
||||
This module describes source at main commit
|
||||
`254122872cf477511514979a4300b695d77cd1f7`. Source paths below are repository-relative.
|
||||
`a350c59b5a5fb37ee4a21b1026b595068b603df1`. Source paths below are repository-relative.
|
||||
An npm CLI artifact and a `.rigbundle` have different builders and consumers; neither
|
||||
source verification nor archive integrity establishes that a daemon has adopted an artifact.
|
||||
|
||||
@@ -59,30 +59,90 @@ from live state.
|
||||
| Pod-aware rig spec | `packages/daemon/src/domain/pod-bundle-assembler.ts`: `PodBundleAssembler.assemble()` produces schema-version-2 metadata, vendors resolved agent trees, and rewrites agent references to local paths. |
|
||||
| Legacy rig spec | `packages/daemon/src/domain/bundle-assembler.ts`: `LegacyBundleAssembler` (imported as `BundleAssembler` by the route) collects the referenced legacy packages and produces the legacy manifest shape. |
|
||||
|
||||
The pod builder collects culture, docs and startup material, preserves file bytes and available
|
||||
mode metadata, and skips vendoring the terminal sentinel as an agent. Required document
|
||||
collection failures stop assembly; missing declared skills can instead produce warnings.
|
||||
The pod builder collects culture, docs and startup material, preserves file bytes, and skips
|
||||
vendoring the terminal sentinel as an agent. A copied file keeps its source mode with owner
|
||||
read and write added. Required document collection failures stop assembly; missing declared
|
||||
skills can instead produce warnings, which are also written into the provenance notes.
|
||||
Inspect the builder's collection helpers when changing these distinctions.
|
||||
|
||||
Both route branches can consume a source-root `bundle.yaml` to carry declared skills,
|
||||
plugins, workflow specs, context packs and agent images into staging before computing
|
||||
integrity. Manifest types, validation and serialization are in
|
||||
plugins, workflow specs, context packs, agent images and preconditions into staging before
|
||||
computing integrity. Manifest types, validation and serialization are in
|
||||
`packages/daemon/src/domain/bundle-types.ts`. Provenance and compatibility are metadata;
|
||||
provenance is not an author signature.
|
||||
|
||||
Preconditions are carried as data. Commands containing shell operators produce a warning,
|
||||
inspect reports the block, and the behaviour view lists it under needs. The install and
|
||||
bootstrap code do not reference preconditions.
|
||||
|
||||
Two more inputs are accepted only for a pod-aware spec; the legacy branch refuses them with
|
||||
a 400:
|
||||
|
||||
- `--context-pack <dir>` (`vendorContextPackDir()` in
|
||||
`packages/daemon/src/domain/bundle-carried-context-pack.ts`) carries a pack's
|
||||
`manifest.yaml` and its declared files to `context-packs/<manifest name>/`. The pack may
|
||||
sit outside the rig folder.
|
||||
- `--project-dir <dir>` (`vendorProjectDir()` in
|
||||
`packages/daemon/src/domain/bundle-carried-project.ts`) carries a folder with a
|
||||
`project.yaml` to `project/`.
|
||||
|
||||
Create also refuses content it must not ship. `computeIntegrity()` in
|
||||
`packages/daemon/src/domain/bundle-integrity.ts` throws on sensitive paths (`.env` files,
|
||||
`*.pem`, `*.key`, `*.p12`, `credentials.*`, `tokens.*`, `.git/` and `node_modules/`).
|
||||
`assertShippableSubstance()` in `packages/daemon/src/domain/agent-resolver.ts` runs on
|
||||
vendored content and on the final staging tree, and refuses text containing
|
||||
`substrate/shared-docs/` and files declaring `taxonomy: lore`.
|
||||
|
||||
### Configurations and package identity
|
||||
|
||||
A bundle folder may declare alternate team configurations in `configurations.yaml` beside
|
||||
`rig.yaml` (`packages/cli/src/lib/bundle-configuration.ts`). `rig bundle configurations
|
||||
<spec>` lists them. `rig bundle create --preset <name>` or `--seat <member=runtime>` resolves
|
||||
one and stages a copy of the rig folder in a temporary directory with the chosen runtimes and
|
||||
profiles, so the author's folder is never changed.
|
||||
|
||||
`packages/daemon/src/domain/bundle-identity.ts` defines the two identity values:
|
||||
|
||||
- `configurationId()`: every member's `pod.member=runtime`, sorted and joined with `,`.
|
||||
Create computes it from the normalized spec and returns a 400 if the client sent a
|
||||
different one.
|
||||
- `packageDigest()`: SHA-256 over the sorted `integrity.files` entries (coverage label
|
||||
`openrig.package-digest/v1`). It covers every packaged file's bytes; it does not cover
|
||||
`bundle.yaml`, the junk files the integrity walk skips, or file modes.
|
||||
|
||||
Create and inspect responses carry the source, configuration ID, package digest, archive
|
||||
hash and assembler version.
|
||||
|
||||
### Archive checks
|
||||
|
||||
`packages/daemon/src/domain/bundle-archive.ts` owns `pack()` and `unpack()`;
|
||||
`packages/daemon/src/domain/bundle-integrity.ts` owns the per-file integrity map.
|
||||
|
||||
Packing sorts entries, uses portable tar metadata and a fixed tar timestamp, and writes a
|
||||
sibling SHA-256 file. Unpacking checks that digest, rejects archive links and unsafe entry
|
||||
paths before extraction, requires `bundle.yaml` and its integrity section, and verifies
|
||||
the extracted file inventory. Hashes establish consistency with the supplied manifest and
|
||||
digest; they do not authenticate whoever supplied both.
|
||||
Packing requires a `.rigbundle` output path, sorts entries, uses portable tar metadata, a
|
||||
fixed tar timestamp and gzip level 9, and writes a sibling SHA-256 file. Unpacking checks that
|
||||
digest, rejects archive links and unsafe entry paths before extraction, requires
|
||||
`bundle.yaml` and its integrity section, and verifies the extracted file inventory. Hashes
|
||||
establish consistency with the supplied manifest and digest; they do not authenticate
|
||||
whoever supplied both.
|
||||
|
||||
The inspect handler unpacks through this same path before reporting the appropriate manifest
|
||||
shape. A syntactically valid manifest alone is not an inspected archive.
|
||||
The inspect handler does not call `unpack()`, because it must report a broken bundle rather
|
||||
than refuse it. It reports a missing or mismatched digest as `digestValid: false`, applies the
|
||||
same unsafe-entry pre-scan (`collectUnsafeArchiveEntries()`) and extraction filter, and runs
|
||||
`verifyIntegrity()` to report an `integrityResult`. A syntactically valid manifest alone is
|
||||
not an inspected archive.
|
||||
|
||||
Inspect also returns a behaviour view, `describeBundleBehaviour()` in
|
||||
`packages/daemon/src/domain/bundle-behaviour.ts` (schema `openrig.bundle-behaviour/v1`). It
|
||||
reads the extracted archive only, through `bundle-behaviour-inspect.ts` (regular UTF-8 files,
|
||||
at most 1 MiB each and 8 MiB in total, opened without following links). It lists the team,
|
||||
each seat's permission posture, the files seats are told to read, what else runs, where
|
||||
content is written, literal outside addresses, needs, and what can't be known before launch.
|
||||
A schema-1 archive gets `state: not_generated`. Posture comes from the member's or rig's
|
||||
`permission_policy`: `builtin:yolo` is full bypass and `builtin:auto` is auto, both by launch
|
||||
flag; `builtin:locked`, `builtin:standard` and `builtin:open` are configuration; a policy file
|
||||
is read from the archive or marked unresolved; no policy means the product default. The CLI
|
||||
prints the view before `rig bundle install` and `rig up` as diagnostics only; it never changes
|
||||
the request or the exit code.
|
||||
|
||||
## Source routing and bootstrap
|
||||
|
||||
@@ -95,13 +155,14 @@ extensionless paths also have content detection.
|
||||
|
||||
| Input | Consumer |
|
||||
|---|---|
|
||||
| Existing rig name | Existing-rig restore path. |
|
||||
| Existing rig name | Existing-rig restore path; unarchived rigs of that name are preferred. |
|
||||
| Single rig spec or bundle | `BootstrapOrchestrator.bootstrap()` in `packages/daemon/src/domain/bootstrap-orchestrator.ts`. |
|
||||
| Topology manifest | `MultiRigLauncher`, with a single-rig bootstrap or remote-up leaf per entry. |
|
||||
|
||||
Topology plan mode is rejected. Placement belongs on the individual topology entries; a
|
||||
top-level host flag is rejected. The selected topology parser/route also rejects nested
|
||||
topologies and existing-rig-name entries. This is narrower than the single-rig `up` surface.
|
||||
topologies, existing-rig-name entries and `.rigbundle` entries: topology entries are spec
|
||||
paths only. This is narrower than the single-rig `up` surface.
|
||||
|
||||
Bootstrap distinguishes pod-aware and legacy specs, and inspects a bundle manifest to choose
|
||||
`PodBundleSourceResolver` or `LegacyBundleSourceResolver` from
|
||||
@@ -115,7 +176,10 @@ the created rig identity rather than claiming every member launched.
|
||||
|
||||
### Durable install target
|
||||
|
||||
Bundle apply requires an explicit target root at the HTTP boundary. For a pod bundle,
|
||||
Bundle apply requires an explicit target root at the HTTP boundary. The CLI supplies the
|
||||
current directory as the default for `rig up <file>.rigbundle` and for a GitHub link through
|
||||
`rig up` or `rig bundle install`. `rig bundle install` with a local `.rigbundle` sends no
|
||||
default, so apply without `--target` gets a 400. For a pod bundle,
|
||||
`materializePodBundle()` copies the verified extraction into that durable root before
|
||||
instantiation, so local agent references and relative working directories survive removal
|
||||
of the extraction directory. It checks destination conflicts before copying, preserves
|
||||
@@ -125,6 +189,30 @@ Pod bundles containing service definitions are refused by bootstrap; their servi
|
||||
requires a stable spec directory. Direct spec bootstrap has separate service prelaunch
|
||||
handling. See [agent-spec-and-startup.md](agent-spec-and-startup.md) for instantiation.
|
||||
|
||||
## GitHub folder links and the bundle check
|
||||
|
||||
`packages/cli/src/lib/bundle-source.ts` lets `rig up`, `rig bundle create`, `inspect` and
|
||||
`install` take a GitHub folder link (`isGitHubBundleLink()`). Only a credential-free
|
||||
`https://github.com/owner/repo[/tree/<ref>/<folder>]` link is accepted. The ref is resolved to
|
||||
the longest matching branch or tag, or taken as a full commit hash. The import:
|
||||
|
||||
- needs a healthy local daemon whose `selfHostId` matches the local origin;
|
||||
- runs Git with credential helpers, hooks and non-HTTPS protocols disabled;
|
||||
- shallow-fetches the exact commit into `bundle-imports/` under the OpenRig home, requires a
|
||||
`rig.yaml` in the folder, and refuses symlinks or agent and package references that resolve
|
||||
outside the checkout;
|
||||
- posts the folder to `/api/bundles/create` with the source recorded in provenance, refuses a
|
||||
response without a package digest or with a different source commit, and writes
|
||||
`source.json` and `build.json` in the import folder.
|
||||
|
||||
`rig bundle check <folder>` (`checkBundleFolder()` in `packages/cli/src/lib/bundle-check.ts`)
|
||||
is an advisory, file-read-only check against `openrig.bundle-standard/v1`; it makes no daemon,
|
||||
launch or provider call. Its rules are `pod_aware_rig`, `readme_in_docs`, `referenced_files`,
|
||||
`portable_agents`, `minimum_versions`, `configurations` and `credential_paths` (a filename scan
|
||||
bounded at 10,000 entries that skips `.git`, `node_modules` and symlinks). `readme_coverage`
|
||||
and `embedded_secrets` are always reported as not checked, and host-resolved resources are
|
||||
reported as checked at launch (`host_resources`). Any finding sets exit code 1.
|
||||
|
||||
## Install checks and content routing
|
||||
|
||||
The install handler in `packages/daemon/src/routes/bundles.ts` obtains a source-path
|
||||
@@ -137,22 +225,36 @@ It is not a complete agent, port or filesystem collision audit; a missing rig na
|
||||
no name comparison. Target-file conflicts are handled separately by materialization.
|
||||
Skipping these prechecks does not skip archive validation in bootstrap.
|
||||
|
||||
After a completed bundle install, the route separately unpacks and routes declared content:
|
||||
Declared content is routed by `routeBundleContents()` in
|
||||
`packages/daemon/src/domain/bundle-content-routing.ts`, which unpacks the bundle once and
|
||||
routes each kind independently. When it runs depends on the bundle schema:
|
||||
|
||||
| Manifest content | Destination selected by the route |
|
||||
- **Pod-aware (schema 2):** bootstrap runs it as `PodRigInstantiator`'s prelaunch hook, after
|
||||
the rig record exists and before any seat launches, so every seat's first turn can see the
|
||||
content.
|
||||
- **Legacy (schema 1):** there is no hook, so the install route routes after bootstrap, and
|
||||
only when the status is `completed`.
|
||||
|
||||
| Manifest content | Destination |
|
||||
|---|---|
|
||||
| `skills` | The OpenRig `packages` cache, stripping the legacy `packages/` prefix; this does not import a complete harness skill into the managed skill catalog. |
|
||||
| `plugins` | The OpenRig `plugins` root, through local plugin references. |
|
||||
| `workflow_specs` | `workflows/` beneath resolved `workspaceSpecsRoot`; unresolved settings skip this routing. |
|
||||
| `workflow_specs` | `workflows/` beneath the resolved `workspaceSpecsRoot`. If that setting is empty, this kind is recorded as a routing failure. |
|
||||
| `context_packs` | The configured `context.root`, matching context-library discovery. |
|
||||
| `agent_images` | The OpenRig `agent-images` root; declarations address image directories containing a manifest. |
|
||||
| `project` | Registered in the workspace catalog (`workspace.catalog_path`) with the rig associated, and its files placed under `workspace.projects_root/<id>` (`registerBundleProject()` in `packages/daemon/src/domain/workspace/project-registration.ts`). A conflict changes nothing and is a routing failure. |
|
||||
|
||||
The individual implementations are the `bundle-skills-router.ts`,
|
||||
`bundle-plugins-router.ts`, `bundle-workflow-specs-router.ts`,
|
||||
`bundle-context-packs-router.ts` and `bundle-agent-images-router.ts` files under
|
||||
`packages/daemon/src/domain/`. The route treats these post-bootstrap calls as best effort:
|
||||
their failure does not roll back or turn an already completed rig install into failure.
|
||||
Inspect their returned results and warnings separately from bootstrap success.
|
||||
Context packs are never merged into an installed pack of the same name: an identical one is
|
||||
reported as `already_installed`, a different one is kept unchanged as `kept_existing`. After
|
||||
routing, the live context library rescans, and a routed pack it can't load becomes a routing
|
||||
failure.
|
||||
|
||||
The per-kind implementations are the `bundle-skills-router.ts`, `bundle-plugins-router.ts`,
|
||||
`bundle-workflow-specs-router.ts`, `bundle-context-packs-router.ts` and
|
||||
`bundle-agent-images-router.ts` files under `packages/daemon/src/domain/`. Routing is best
|
||||
effort in both cases: the hook always lets the launch continue, and a failure never rolls back
|
||||
or fails the install. Failures appear in the result's `routingFailures` and `warnings`, in a
|
||||
`route_bundle_contents` stage for the hook, and in the install audit record.
|
||||
|
||||
Install audit records are appended best effort to `bundle-audit.jsonl` under the OpenRig
|
||||
home; the history endpoint reads that audit. Bootstrap run state and action journals are
|
||||
@@ -168,9 +270,12 @@ bootstrap calls `PackageInstallService` in
|
||||
The source chain is package resolution/manifest validation, planning and conflict policy,
|
||||
then apply/verification with repository records:
|
||||
|
||||
- `packages/daemon/src/domain/package-resolver.ts` and `package-manifest.ts`.
|
||||
- `resolvePackage()` in `packages/daemon/src/domain/package-resolve-helper.ts`, which reads
|
||||
and validates through `package-manifest.ts`. `package-resolver.ts` supplies the types; its
|
||||
`PackageResolver` class has no production caller.
|
||||
- `packages/daemon/src/domain/install-planner.ts`, `conflict-detector.ts` and
|
||||
`install-policy.ts`.
|
||||
`install-policy.ts`. The planner refuses any planned destination outside the install
|
||||
target.
|
||||
- `packages/daemon/src/domain/install-engine.ts`, `install-verifier.ts`,
|
||||
`install-repository.ts` and `package-repository.ts`.
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ applies-when: |
|
||||
agent-image capture/fork/protection, or Claude guided-compaction behavior.
|
||||
siblings: [packaging-bootstrap-bundles.md, agent-spec-and-startup.md]
|
||||
prerequisite-reads: [../README.md, agent-spec-and-startup.md]
|
||||
last-verified-against-source: 254122872cf477511514979a4300b695d77cd1f7
|
||||
last-updated: 2026-10-03
|
||||
last-verified-against-source: a350c59b5a5fb37ee4a21b1026b595068b603df1
|
||||
last-updated: 2026-10-05
|
||||
---
|
||||
|
||||
# Content libraries and compaction
|
||||
|
||||
This module describes source at main commit
|
||||
`254122872cf477511514979a4300b695d77cd1f7`. Source paths below are repository-relative.
|
||||
`a350c59b5a5fb37ee4a21b1026b595068b603df1`. Source paths below are repository-relative.
|
||||
Context packs, plugins and agent images have filesystem-backed content and daemon-side
|
||||
discovery. Their consumers can read database identity, mutate files, deliver messages or
|
||||
launch sessions; the whole layer is not a read-only catalog.
|
||||
@@ -30,10 +30,13 @@ the entry ID is `context-pack:<ref>`.
|
||||
|
||||
### Discovery and mutation
|
||||
|
||||
Startup in `packages/daemon/src/startup.ts` configures the shipped context-pack root,
|
||||
the resolved `context.root` and its system root, followed by an existing workspace-local
|
||||
`.openrig/context-packs` root. The library scans recursively, stops descending when it
|
||||
finds a pack manifest, and does not follow directory symlinks.
|
||||
Startup in `packages/daemon/src/startup.ts` configures the shipped context-pack root (when
|
||||
it exists), the resolved `context.root` and its `system` root, followed by the
|
||||
workspace-local `.openrig/context-packs` root. The workspace root is registered even before
|
||||
it exists, so a folder created later is found by the next sync; it is left out only when it
|
||||
is the same path as one of the other two. The library scans recursively, stops descending
|
||||
when it finds a pack manifest, skips `.tmp-add-*` staging folders, and does not follow
|
||||
directory symlinks.
|
||||
|
||||
The first configured root to encounter a physical pack owns that directory. For distinct
|
||||
physical packs with the same ref, the later root wins. This distinction prevents overlapping
|
||||
@@ -66,7 +69,9 @@ Situation composition selects fresh atoms for `fresh`, fresh plus handover atoms
|
||||
selection is Claude or Codex, with `any` atoms applicable to either. Required atoms join
|
||||
the selection; missing dependencies or runtime-incompatible dependencies fail composition.
|
||||
The output is ordered by atom order, then ID. Profile-only atoms are excluded from ordinary
|
||||
situation selection.
|
||||
situation selection, but the dependency closure doesn't check that flag, so one can still join
|
||||
as a dependency of a selected atom. Post-compaction composition skips an absent
|
||||
`seat:RECAP.md` and reports it as skipped instead of failing.
|
||||
|
||||
The profile route resolves configured tree sources through
|
||||
`packages/daemon/src/domain/context-packs/profile-source-resolver.ts`.
|
||||
@@ -85,11 +90,42 @@ reports overage and drop candidates without silently truncating the profile. Ato
|
||||
`/api/context-packs`. Its library handlers cover list, sync, compose, read/delete by ref,
|
||||
preview, pieces, addressed reads and profile composition.
|
||||
|
||||
`packages/cli/src/commands/context.ts` implements `rig context`, including
|
||||
`get`, `profile`, `compose`, `add`, `rm` and `sync`, plus work-install,
|
||||
trace, source and recap operations. The library read/compose routes do not themselves send
|
||||
a pack to a running seat. Delivery and installation consumers have their own behavior;
|
||||
inspect the selected CLI verb before treating a context command as read-only.
|
||||
`packages/cli/src/commands/context.ts` implements `rig context`, including `list`, `show`,
|
||||
`preview`, `get`, `profile`, `compose`, `add`, `rm` and `sync`, plus `work-install`, `trace`,
|
||||
`source inspect|update` and `recap-write`. The library read/compose routes do not themselves
|
||||
send a pack to a running seat. Several verbs write: `add` installs into `context.root` (from
|
||||
a folder, a URL, or Git with `--git`), `source update` fetches and merges a Git-installed pack
|
||||
and resyncs, `work-install --apply-skills` reconciles skill projections, and `recap-write`
|
||||
writes a seat recap. Inspect the selected CLI verb before treating a context command as
|
||||
read-only. `rig context work-install` resolves a project position and its declared context
|
||||
without the daemon; [workspace-primitive.md](workspace-primitive.md) covers project
|
||||
resolution.
|
||||
|
||||
On a profile read, the route adds each piece's `sha256` and provenance, with a warning when a
|
||||
piece's bytes come from outside the granted folder. When rig and seat are given, it adds the
|
||||
seat's `LEARNED.md` as an implicit atom.
|
||||
|
||||
### System World, seat recaps and skill loadouts
|
||||
|
||||
`packages/daemon/src/domain/system-world.ts` reads the System World manifest (schema
|
||||
`openrig.system-world/v0alpha1`): an id, a version, the context packs every seat starts from,
|
||||
and system skills. The `context.system_world` setting selects the default manifest at
|
||||
`system/system-world.yaml` under `context.root`, disables it, or names another manifest file.
|
||||
The default (`openrig-default`) names the `onboarding-width` and `world-public` packs and no
|
||||
skills; it is seeded when missing.
|
||||
|
||||
`rig context recap-write --rig <rig> --seat <seat> --file <file>` writes the seat's
|
||||
`RECAP.md` through `packages/daemon/src/domain/context-packs/seat-recap-store.ts`. The previous
|
||||
recap is kept byte-for-byte under `recap-superseded/`. A recap that isn't addressable Markdown
|
||||
is refused; the other content checks are advisory.
|
||||
|
||||
`packages/daemon/src/domain/skill-catalog.ts` resolves a seat's skill loadout from the
|
||||
catalog at `skills.root` (`catalog.yaml`, schema `openrig.skill-catalog/v1`) and the system,
|
||||
topology and project selections. It projects skills into `<cwd>/.claude/skills` or
|
||||
`<cwd>/.agents/skills` and tracks what it owns in
|
||||
`<cwd>/.openrig/skill-loadouts/<runtime>.json`. Each projection reports `current`, `missing`,
|
||||
`shadowed`, `stale` or `conflicting`. `rig skill loadout|audit` reads and reconciles it, and
|
||||
launch applies it per seat.
|
||||
|
||||
## Agent images and forks
|
||||
|
||||
@@ -128,9 +164,18 @@ contribute protection reasons. The route applies the guard to deletion/pruning u
|
||||
`force` is explicit. Prune defaults to dry-run. Its protection view depends on the supplied
|
||||
spec roots; it is not an inventory of every reference anywhere on the host.
|
||||
|
||||
The guard keys its result by image name, not version: every version of one name gets the
|
||||
status computed for the last-listed version, carrying that version's image ID. The delete
|
||||
route looks up by image ID, so on delete only that last-listed version of a name is matched,
|
||||
and its other versions are deleted without `force` even when pinned. `rig agent-image prune`
|
||||
sends a dry run unless `--force` is given, and `--force` also overrides the guard, so a
|
||||
guarded real prune is reachable only through the HTTP route.
|
||||
|
||||
The routes mount at `/api/agent-images` and expose list/sync, snapshot/fork,
|
||||
entry/preview, pin/unpin, delete and prune.
|
||||
`packages/cli/src/commands/agent-image.ts` owns the image library commands.
|
||||
`packages/cli/src/commands/agent-image.ts` owns the image library commands. The top-level
|
||||
`rig fork` verb (`packages/cli/src/commands/fork.ts`) drives `POST /fork`; `--keep-image`
|
||||
also captures a pinned image.
|
||||
|
||||
## Plugins
|
||||
|
||||
@@ -153,7 +198,9 @@ harness loading a plugin.
|
||||
|
||||
The service constructor is not read-only: it retires obsolete refocus hook registrations
|
||||
from the older bundled plugin and rejects duplicate vendored hook registrations across
|
||||
providers. Cached plugin versions are excluded from that duplicate-provider check.
|
||||
providers. Cached plugin versions are excluded from that duplicate-provider check. Startup
|
||||
constructs the service while building the daemon's dependencies and doesn't catch that
|
||||
rejection, so a duplicate vendored hook stops daemon creation.
|
||||
|
||||
`packages/daemon/src/routes/plugins.ts` exposes GET-only inspection at `/api/plugins`:
|
||||
list, detail, used-by and file list/read. The file routes use the discovered plugin root as
|
||||
@@ -167,11 +214,14 @@ At this pin, the route's source filter accepts vendored, Claude-cache and Codex-
|
||||
`ensureVendored()` seeds absent plugins or advances an older numeric manifest version.
|
||||
Equal/newer installed versions preserve installed bytes; equal versions can reconcile file
|
||||
modes on byte-identical files. Missing target version authority is preserved rather than
|
||||
overwritten; invalid or conflicting manifest versions are errors.
|
||||
overwritten; invalid or conflicting manifest versions are errors. Advancing a version writes
|
||||
changed files but never deletes files the new version dropped.
|
||||
|
||||
`ensureSkillGlobally()` projects a named plugin skill using a vendor-version marker.
|
||||
An existing unversioned global skill remains externally owned. Startup wires this service
|
||||
and its filesystem implementation; inspect that wiring when changing projection roots.
|
||||
and its filesystem implementation; inspect that wiring when changing projection roots. At
|
||||
startup only `openrig-core` is vendored, and it is projected globally as `openrig-skills` into
|
||||
`~/.claude/skills` and `~/.agents/skills`.
|
||||
|
||||
`ensureLatest()` performs local vendoring before `attemptAutoFetch()`. The latter
|
||||
requests a release asset with a bounded timeout and logs failures; even its successful
|
||||
@@ -190,11 +240,40 @@ and the manual-compaction route consumers.
|
||||
`packages/daemon/src/domain/user-settings/settings-store.ts` resolves the
|
||||
`policies.claude_compaction.*` settings; automatic compaction defaults to disabled.
|
||||
|
||||
`maybeAutoCompact()` applies the delivery guard when available. Its implementation
|
||||
requires Claude Code, observed usage and a valid integer threshold. Above-threshold
|
||||
automatic work additionally checks enablement, deduplication, cooldown and the
|
||||
already-triggered latch. It sends prep, then compact, and queues the below-threshold
|
||||
continuation: turn boundary, restore prompt, then compliance prompt.
|
||||
`maybeAutoCompact()` applies the delivery guard when one exists and the runtime is Claude
|
||||
Code. Its implementation requires Claude Code, observed usage and a valid integer threshold
|
||||
(default 80). Above-threshold automatic work additionally checks enablement, the
|
||||
post-restore cooldown (10 minutes), the deduplication window (60 seconds) and the
|
||||
already-triggered latch; an active manual attempt also blocks it.
|
||||
|
||||
`/compact` is held behind a preparation attempt:
|
||||
|
||||
1. The first above-threshold tick starts an attempt with its own ID, the seat's occupant
|
||||
generation, a restore-map path and a completion marker, then sends only the prep prompt.
|
||||
An unknown occupant generation stops the attempt at once.
|
||||
2. Later ticks return `preparation_pending` until the attempt's `RESTORE-MAP.md` exists and
|
||||
ends with that exact marker. The map path is under the seat's working directory
|
||||
(`.openrig/compaction/preparation/<session>/<attempt>/`, with a `.gitignore` of `*`) when
|
||||
that is writable, otherwise under `compaction/preparation/` in the OpenRig home.
|
||||
3. Only then is `/compact` sent, with the map path appended to the compact instruction.
|
||||
4. The continuation then runs below the threshold: turn boundary, restore prompt, then a
|
||||
read-depth audit prompt (each item marked `FULL`, `PARTIAL` or `NOT_READ`).
|
||||
|
||||
An automatic attempt has a 25-minute ceiling that starts once the prep is delivered. A stale
|
||||
occupant generation, the policy becoming disabled, or the deadline passing stops it. A prep
|
||||
that clearly wasn't sent is retried at most three times; an uncertain prep or `/compact` is
|
||||
never replayed.
|
||||
|
||||
The restore prompt names a restore-pending marker at
|
||||
`compaction/restore-pending/<session>.json` in the OpenRig home, which the `openrig-core`
|
||||
PreCompact hook writes. A per-seat `compaction/post-compact-extra/<session>.md` is preferred
|
||||
over the global extra file, and an extra file that declares a different seat is refused.
|
||||
|
||||
`rig compact <session>` (`packages/cli/src/commands/compact.ts`) runs the same sequence
|
||||
manually through `/api/compaction/trigger`, `cancel` and `state`, behind the bearer token.
|
||||
`--skip-map` skips the map requirement once, `--cancel` ends the preparation, and `--state`
|
||||
shows the attempt, its expected map and its deadline. A manual attempt has 120 seconds for
|
||||
preparation and the idle wait.
|
||||
|
||||
The below-threshold continuation checks policy enablement too, except for an explicitly
|
||||
operator-initiated manual sequence. A known occupant-generation mismatch invalidates the
|
||||
@@ -202,7 +281,8 @@ queued stages; an unknown generation does not prove a match. Failed or retained
|
||||
do not advance the stage. The manual trigger shares this continuation while having its
|
||||
own initiation checks.
|
||||
|
||||
Stage, deduplication and cooldown maps are in memory. Startup supplies live generation
|
||||
Stage, deduplication, cooldown and preparation-attempt maps are in memory; the restore maps
|
||||
are files. Startup supplies live generation
|
||||
resolution and a post-restore callback that records a managed-width receipt, so the wider
|
||||
operation also has persistent effects. Do not infer durable lifecycle completion from
|
||||
a library read or one successfully queued message.
|
||||
|
||||
Reference in new issue
Block a user