Forward-fix on the daemon-skill-discovery slice (1482aef): when a
profile's resources.skills entry references a name whose dirname
matches a structurally-rejected SKILL.md, the resolver now surfaces
the precise rejection reason ("rejected — <reason> at <path>")
instead of the bare "not found in resource pool" error. Operators
see exactly which file and which check failed when a drop-in skill
isn't valid.
Behavior:
- profile-resolver captures discovery.rejected from
discoverSkillsForRuntime and indexes by directory basename.
- After resolveProfileUses returns errors, "Profile uses skills:
\"<id>\" not found in resource pool" is replaced with "Profile
uses skills: \"<id>\" rejected — <reason> (at <path>)" when the
basename matches a known rejection.
- Match is basename-only, so an unrelated rejected SKILL.md (e.g.,
~/.claude/skills/foo/) does NOT contaminate the error for an
unrelated missing reference (e.g., a profile asking for "bar"
still gets the standard not-found message).
Tests:
- 2 new integration tests in profile-resolver.test.ts:
- structural-rejection reason surfaces when basenames match
(broken SKILL.md at ~/.claude/skills/broken-skill/ + profile
referencing "broken-skill" → error mentions "rejected" +
"frontmatter" + the directory path)
- unrelated rejected SKILL.md does NOT pollute an unrelated
missing-skill error (foo broken / bar missing → bar gets
standard "not found" without "rejected")
Verification:
- npx tsc --noEmit on packages/daemon/tsconfig.json: clean
- npx vitest run skill-discovery.test.ts + profile-resolver.test.ts:
39/39 PASS
Sibling forward-fix (substrate-only, no commit needed): the slice
ack.md and walk.md substrate artifacts have been scrubbed for
attribution-style residue and reconciled to a single canonical
precedence statement (rig-local agent.yaml > rig-bundled cwd >
spec-install-dir > runtime-specific user library > shared
~/.openrig/skills/), matching the listScanRoots implementation and
the walk packet's verification hooks.
Refs: substrate slice ack + walk at substrate/shared-docs/openrig-work/missions/bug-fix/slices/daemon-profile-resolver-skill-discovery/.
SC-29 EXCEPTION #7 scope unchanged from 1482aef; this is a refinement
of the same exception, not a new one.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes the gap where the profile resolver only saw skills declared in
agent.yaml's resources.skills + imports — not skills present at the
runtime-truth filesystem locations the harness already loads from.
Operators can now drop any structurally-valid skill at the
user-library or rig-bundled paths and have it resolve through profile
validation, not just at runtime.
Behavior:
- New module packages/daemon/src/domain/skill-discovery.ts walks 5
scan roots per runtime (claude-code targets ~/.openrig/skills/,
~/.claude/skills/, <cwd>/.claude/skills/, <specInstallDir>/skills/;
codex targets ~/.openrig/skills/, ~/.agents/skills/,
<cwd>/.agents/skills/, <specInstallDir>/skills/).
- Each candidate directory must contain a SKILL.md with a
---delimited YAML frontmatter block, non-empty `name` +
`description` strings, and a non-empty body. The validator's
question is "would the runtime actually load this," not "is this ID
in the daemon shared bundle."
- Rejections surface as { path, reason } records so callers can show
clear errors when an operator drops a malformed SKILL.md.
- Profile resolver augments its resource pool with discovered skills
AFTER buildResourcePool, respecting rig-local-wins precedence: a
skill ID declared in agent.yaml's resources.skills (or imports)
beats any same-id discovered skill. Among discovery roots, the
most-specific path wins (rig-bundled at cwd > spec-install-dir >
runtime-specific user library > shared user-spec library).
- ResolutionContext gains an optional homedir field that defaults to
os.homedir() in production; tests inject a fixture root.
- Runtime + cwd computation moved earlier in resolveNodeConfig so the
filesystem scan targets the right paths before resolveProfileUses
queries the augmented pool.
Tests:
- 18 unit tests in skill-discovery.test.ts cover frontmatter
acceptance/rejection (6), per-runtime path scanning including
cross-runtime exclusion (8), structural rejection (2), and the
SkillResource shape contract (2).
- 5 integration tests in profile-resolver.test.ts cover discovered-
only resolution at ~/.claude/skills/, rig-bundled cwd discovery,
rig-local resources.skills precedence over a same-id discovered
skill, codex runtime path scanning, and the preserved
skill-not-found error path.
Verification:
- npx tsc --noEmit on daemon / cli / ui: all clean
- npx vitest run on daemon suite: 3141 pass, 1 pre-existing failure
(rsi-v2-starter-spec-content non-qa-done assertion last touched in
5bdda4f, unrelated to this slice)
Refs: substrate slice ack + walk at substrate/shared-docs/openrig-work/missions/bug-fix/slices/daemon-profile-resolver-skill-discovery/.
SC-29 EXCEPTION #7 declared (substantive daemon code change adding a
filesystem scan + structural validator + resource-pool augmentation;
not allowlist-only and not additive payload field; new code path).
Ledger position confirmed by checking #5 + #6 declared in c7c9751
project-observability merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Remove the bundled rsi-v2-hot-potato workflow spec and its recipe-pinning tests while keeping the workflow runtime, hot-potato primitives, and user-file workflow paths intact.
Keep daemon builds working when no built-in workflow specs are bundled by replacing the shell cp glob with an empty-directory-safe copy script.
Dogfood evidence: /Users/wrandom/code/substrate/shared-docs/openrig-work/missions/recursive-self-improvement-v2/dogfood-evidence/release-0.3.0-slice-01-rsi-recipe-staging-20260510T024342/REPORT.md
Adds context percentage and token totals to topology table and terminal views using the existing node inventory context payload. Keeps pod table scoping aligned with pod pages and refreshes inventory polling with graph cadence.
Verification: focused UI tests, UI build, lint, and test:repo pass.
Integrates the shared graphics broaden pass after guard and VM verification.
Scope: UI-only graphics marks, topology/live-node icon cleanup, project metadata iconography, and
related tests.
Verification before merge: guard-3 advisory clean, velocity-qa VM pass, focused UI tests, lint, UI
build, and diff-check clean.
Adds a build-time mirror that copies the daemon's bundled skills from
packages/daemon/specs/agents/shared/skills/ to skills/_canonical/, so
the skill set is publicly visible at the repo root rather than buried
inside packages/daemon/specs/. Hand-authored top-level files
(README.md, CHANGELOG.md, LICENSE) live alongside _canonical/ and
survive every mirror run via strict-ownership of the _canonical/
subtree only.
Mechanism: node script + drift-detect baked into the existing
test:repo pipeline. Matches scripts/check-docs-guard.mjs +
check-docs-guard.test.mjs convention.
scripts/mirror-skills.mjs — rsync-based copy + --check mode
scripts/mirror-skills.test.mjs — unit tests on the parser + smoke
invariants (source/target shape
parity; excluded patterns absent)
package.json scripts:
mirror-skills run the mirror
mirror-skills:check drift-detect (exit 1 if stale)
test:repo now runs mirror-skills:check after the existing
scripts/*.test.mjs + check-docs-guard pass
Filtering is path-based (current product source has no metadata for
metadata-based filtering); excludes feedback.md, evals/, .DS_Store,
*.local.md. The --check mode uses --checksum so mtime-only drift
(after git checkout or cp) is content-stable and does not register
as a false positive — only byte-level changes count.
Initial mirror landed: 27 SKILL.md files across core/, pm/, pods/,
process/, plus 3 uncategorized at the top level
(claude-compact-in-place/, mental-model-ha/, rig-architect/).
skills/CHANGELOG.md initialized with the bootstrap entry per
operating-the-skill-library v1.9 §8 rule "skills/CHANGELOG.md"
(append-only per curation cycle close).
Structure is git-subtree-split-ready: a future
`git subtree split --prefix=skills HEAD` produces a freestanding repo
when skill-only contributors warrant the split.
Mechanism comparison and rationale at
substrate/shared-docs/openrig-work/lab/skill-authoring-techniques/
findings/skills-hub-mirror-mechanism-comparison.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Reusable graphics/icon layer (RuntimeMark + runtime-brand + tool-brand utilities) with first production placements across topology graph/table/terminal views, LiveNodeDetails, RigNode, AuthorAgentTag in For You, HybridTopologyNodes runtime badge, and TerminalPreviewPopover terminal mark. Branch was rebased onto project-observability-elite-pm tip preserving observability Phase H portal/positioning/wrapping behavior; backup ref project-graphics-package-pre-rebase preserved at pre-rebase tip. Establishes the graphics layer; broader use across project metadata cards/pills deferred to follow-up slice.
Closes the production-reachability gap where transcripts.lines and
transcripts.poll_interval_seconds were resident in ConfigStore +
SETTINGS_VALID_KEYS but never reached the spawned daemon process. The
rotation hook only consulted shell env vars, so
`rig config set transcripts.lines 500` followed by `rig daemon
restart` had no observable effect on the rotation cadence.
Wire-up:
- StartOptions (cli/daemon-lifecycle.ts) extends with optional
transcriptsLines + transcriptsPollIntervalSeconds.
- buildDaemonEnv emits OPENRIG_TRANSCRIPTS_LINES and
OPENRIG_TRANSCRIPTS_POLL_INTERVAL_SECONDS only when the opts are
defined; explicit opts override any inherited base-env value, same
shape as the existing OPENRIG_PORT / OPENRIG_DB / OPENRIG_TRANSCRIPTS_*
override pattern.
- The `rig daemon start` command path reads
config.transcripts.lines and config.transcripts.pollIntervalSeconds
from the resolved ConfigStore and threads them through startDaemon
→ buildDaemonEnv → daemon process env, where
getTranscriptRotationOptionsFromEnv inside the rotation module
picks them up at session-start.
Tests:
- 3 new buildDaemonEnv unit tests:
- emits OPENRIG_TRANSCRIPTS_LINES + OPENRIG_TRANSCRIPTS_POLL_INTERVAL_SECONDS
when opts provided.
- omits both env vars when opts undefined (rotation falls back to
its own defaults).
- explicit opts override inherited OPENRIG_TRANSCRIPTS_* from base
env (file-stored ConfigStore wins over stale shell vars).
- 1 new ConfigStore roundtrip test:
ConfigStore.set('transcripts.lines', '500') +
set('transcripts.poll_interval_seconds', '5') → resolve() returns
500 / 5 (proves the file-stored values reach the daemon-launch
read site).
- VALID_KEYS exhaustive-list test refreshed to include the 2 V1
pre-release keys plus the previously-stale Phase 4 / Phase 5
additions, matching the equivalent fix in the daemon settings-
store test.
Verification:
- npx tsc --noEmit on packages/cli/tsconfig.json: clean
- npx vitest run daemon-lifecycle + config-store-extended: 85/85 PASS
SC-29 EXCEPTION #4 scope unchanged: the env-var emission path is the
existing OPENRIG_TRANSCRIPTS_* mechanism, just with two more allowed
keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replaces the legacy `tmux pipe-pane` transcript-capture mechanism
(infinite-growth file per session) with a periodic
`tmux capture-pane -t <session> -p -S -<lines>` shellout that
atomically overwrites the transcript file each tick. File size stays
bounded by trailing line count + line-byte ceiling, not by session
duration.
New module packages/daemon/src/domain/transcript-rotation.ts:
- startTranscriptRotation(adapter, session, outputPath, opts)
registers a per-session timer that ticks immediately + every
pollIntervalMs.
- stopTranscriptRotation(session) clears a timer (idempotent).
- getTranscriptRotationOptionsFromEnv() reads the new tunables
(env > defaults; file config flows through the daemon settings-
store at startup).
- Each tick reads any pre-existing SESSION BOUNDARY lines from the
output file and prepends them to the captured content so the
restore-orchestrator's pre-launch boundary marker survives across
rotation overwrites.
- Timers .unref() so they don't keep the daemon process alive on
their own.
Two ConfigStore allowlist additions (lockstep daemon settings-store
+ CLI config-store):
- transcripts.lines (number; default 1000)
- transcripts.poll_interval_seconds (number; default 2)
SC-29 EXCEPTION #4 declared in pre-release ACK §5; same allowlist
shape as Phase 4 + Phase 5 prior exceptions.
Call-site swaps:
- node-launcher.ts: inline pipe-pane invocation replaced with
startTranscriptRotation + getTranscriptRotationOptionsFromEnv.
The launch-warning path narrows to "transcript directory creation
failed"; per-tick failures are silent (next tick retries).
- transcript-capture.ts: startTmuxTranscriptCapture now starts the
rotation timer instead of pipe-pane.
- rig-teardown.ts: stopTranscriptRotation called for each live
session before killSession so capture-pane stops poking dead
targets.
transcript-store.writeBoundaryMarker now mkdir's its parent on
demand so a fresh rig directory is no longer a fail case (the
restore orchestrator writes the marker before the launcher's
ensureTranscriptDir runs).
Tests:
- New packages/daemon/test/transcript-rotation.test.ts: 11 tests
(capture invocation contract / atomic file write / overwrite-not-
append / timer lifecycle + idempotent replace / env-override +
defaults including legacy RIGGED_* fallback).
- node-launcher.test.ts: transcript-integration cases now assert on
getActiveRotationCount + dir-creation-failure warning instead of
pipe-pane.
- transcript-routes.test.ts: lazy-start cases assert rotation timer
registered.
- self-attach-service.test.ts / claim-service.test.ts: transcript
capture assertions point at rotation count.
- restore-orchestrator.test.ts: removed the pipe-pane-failure
warning expectation (rotation failures are best-effort silent);
boundary marker assertion still applies.
- transcript-store.test.ts: writeBoundaryMarker filesystem-error
case rewritten to use a parent-is-a-regular-file collision now
that the deeply-nested-nonexistent case auto-mkdirs.
- user-settings-routes.test.ts + user-settings-store.test.ts:
expected key-count and key-list updated to include the 2 new
transcripts.* keys plus the previously-stale Phase 4/5 keys.
Verification:
- npx tsc --noEmit on all 3 package configs: clean
- npx vitest run on the daemon suite: 3110 pass, 1 pre-existing
failure unrelated to this slice (rsi-v2-starter-spec-content
workflow terminal-exits assertion)
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Wraps every local rig send body with a header / body / reply-hint
envelope so the recipient pane shows who the message is from, who it
is for, and the exact command shape needed to reply. Resolves the
sender's session from OPENRIG_SESSION_NAME / RIGGED_SESSION_NAME and
falls back to a "<unknown sender>" marker when neither is present.
Envelope shape:
From: <sender>
To: <recipient>
---
<body verbatim>
---
↩ Reply: rig send <sender> "..."
Cross-host sends (--host) intentionally do NOT wrap locally — the
remote rig wraps when it runs the same command, and double-wrapping
would nest envelopes. The body inside the dashes is the original
text verbatim, so multi-line input passes through unchanged.
New unit test packages/cli/test/send-header.test.ts covers header
presence, body preservation, empty-body edge case, sender fallback,
and recipient-string preservation. Existing send.test.ts assertion
updated to match the wrapped POST payload.
Verification:
- npx vitest run send.test.ts send-header.test.ts: 13/13 PASS
- npx tsc --noEmit --project packages/cli/tsconfig.json: clean
SC-29 EXCEPTION #4 continues for this slice.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Renames the managed-block markers that the daemon writes into user-managed
files (CLAUDE.md / AGENTS.md) from "BEGIN/END RIGGED MANAGED BLOCK" to
"BEGIN/END OpenRig MANAGED BLOCK". The marker carries the canonical
product name now that the package and CLI binary use the openrig
identity.
Backwards-compat for files written by prior installs:
- stripManagedBlocks recognizes both legacy (RIGGED) and current
(OpenRig) marker patterns so uninstall/cleanup paths handle both
transitional states.
- mergeManagedBlock detects legacy blocks for a package id when
deciding whether to replace; the replacement always emits the
OpenRig form so user files migrate forward in place.
- conflict-detector.hasExistingBlock returns true for legacy markers
too, so re-applying a guidance package over a pre-rename file
doesn't quietly add a duplicate block.
- install-engine.applyGuidanceMerge falls back to legacy marker
search when the OpenRig markers aren't present, then writes the
OpenRig form in place of the legacy block.
- install-verifier accepts either marker form when checking that a
guidance install's BEGIN/END markers are intact.
Test fixtures and assertions across 8 daemon tests updated to expect
the new marker form. Production write paths emit OpenRig exclusively;
production read paths recognize both forms.
Verification:
- npx tsc --noEmit --project packages/daemon/tsconfig.json: clean
- npx vitest run on the 9 affected test files: 178/178 PASS
SC-29 EXCEPTION #4 (declared in pre-release CLI/daemon ACK §5):
release-blocker product-feature change, not allowlist-only. Existing
allowlist exceptions #1 (Phase 4 advisor/operator) + #2 (Phase 5
feed.subscriptions) + additive #3 (polish-slice-4 graph payload token
telemetry) untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes the dead-code auto-expand bug surfaced by the design-reviewer
walk on abb154e. Five-point evidence chain identified the root cause:
1. routes.tsx topology routes are SIBLING parented to rootRoute,
NOT nested. /topology/rig/$id mounts RigScopePage; it does NOT
mount HostMultiRigGraph.
2. HostMultiRigGraph held `expanded` as local useState — reset on
every mount when operator returned to /topology.
3. The in-component activeRigId useEffect could only fire while
HostMultiRigGraph itself was mounted. Direct-URL entry at
/topology/rig/$id never had a chance to set state because the
component wasn't mounted at that path.
4. topology-overlay-context.tsx previously carried only ExplorerMode;
no rig-expanded persistence layer existed.
5. Live walk confirmed: /topology/seat → /topology/rig → /topology
returned to expanded=[] (all rigs collapsed).
Plus zero auto-expand test coverage in p5-2-host-multi-rig-graph.
FIX (option (a) per design-reviewer recommendation):
- Lift `expandedRigs: Map<string, boolean>` into TopologyOverlayProvider
alongside the existing ExplorerMode state. New context fields:
expandedRigs (read), setRigExpanded(rigId, expanded), toggleRig(rigId).
- Auto-expand useEffect runs at PROVIDER scope (always mounted under
AppShell) — reads pathname via useRouterState, parses active rigId
via NEW exported parseActiveRigId helper, and dispatches
setRigExpanded(rigId, true) on every rig-scoped URL pattern.
Fires regardless of which scope page is currently in the center, so
direct-URL navigation to /topology/rig/$id (RigScopePage) updates
the context state; when the operator returns to /topology,
HostMultiRigGraph mounts and reads the persisted state.
- HostMultiRigGraph now consumes via useTopologyOverlay() — replaces
the local useState + the orphaned in-component useEffect. Drop both.
NEW packages/ui/test/p5-2-bounce-rig-expand-persistence.test.tsx
(14 tests):
- parseActiveRigId pure-fn covers all three rig-scoped URL shapes
(seat / pod / rig) + URL-encoded rigIds + null at /topology root
+ null at non-topology routes.
- Provider auto-expand useEffect: route initial-entry at /topology/rig
/seat /pod each set expandedRigs[rigId]=true verified via a
ContextProbe child that exposes ctx.expandedRigs as testid attribute
(HostMultiRigGraph NOT mounted in those tests — proves the effect
fires at provider scope, the dead-code bug fix).
- /topology root pathname does NOT auto-expand any rig (default-all-
collapsed preserved).
- HostMultiRigGraph reads from context: when context's expandedRigs
has rigId=true on mount, the rig group renders expanded; default
context (empty Map) → all rigs render collapsed.
- Source-assertion guards: HostMultiRigGraph imports useTopologyOverlay
+ has no `useState<Map<string, boolean>>` local state (regression
guard against the dead-code path returning); topology-overlay-context
exposes the four context fields + the parseActiveRigId export +
contains useRouterState + parseActiveRigId call (the auto-expand
wiring).
UPDATED packages/ui/test/p5-2-host-multi-rig-graph.test.tsx:
- withQueryClient wrapper now wraps in TopologyOverlayProvider so
HostMultiRigGraph's useTopologyOverlay() returns a real toggleRig
(not the no-op default). Required for the existing collapse-toggle
test to actually flip state.
- ritual #9 coupled-literal scan redirected: useState<Map<...>>
initializer assertion now reads from
topology-overlay-context.tsx (where the state was lifted to);
collapsed: !p.isExpanded literal still in HostMultiRigGraph and
still asserted there.
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest p5-2-bounce-rig-expand-persistence: 14/14 PASS
- vitest p5-2-host-multi-rig-graph: 16/16 PASS (existing tests still
pass after wrapper + literal-scan updates)
- vitest full UI suite: 706/708 PASS (1 pre-existing library-review
baseline failure UNCHANGED; 1 skipped)
- pnpm -F @openrig/ui build: clean (1.91s)
SC-29 posture: UNCHANGED — UI-only this slice. Daemon byte-equivalent
beyond Phase 4 + Phase 5 declared exceptions.
Scope discipline: bounded to Item 6 only. The 7 PASS items from the
design-reviewer original audit (rendering / lazy-fetch / cross-rig
prefixing / vellum primitive grammar / collapse-toggle / option (a)
layout / click navigation) all stay as-shipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Unparks P5.1-4 with explicit V1 scope expansion to include the
rig-collapse affordance. The /topology landing page becomes a
figma-style canvas for fleet management at scale: by default ALL rigs
appear as collapsed vellum cards on a single ReactFlow canvas; click a
card body to expand it inline; click the "→" arrow on the rig name to
drill into the rig page.
Scope = 8 dispatch items bundled into one canvas component:
NEW packages/ui/src/components/topology/HostMultiRigGraph.tsx:
- P5.2-1 composition: ReactFlow canvas with merged nodes/edges across
N rigs; nodeTypes registers rigGroup + podGroup + rigNode; controls
pinned bottom-right with vellum styling.
- P5.2-2 useQueries pattern: single hook call regardless of rig count
(P0-1 anti-pattern guarded; tests assert no .map(useRigGraph) loop).
Per-rig graph fetch is enabled only when the rig is expanded —
scales to 100+ rigs without upfront fan-out (counts come from a
single /api/ps fetch).
- P5.2-3 cross-rig node-ID prefixing via prefixRigData helper:
every node carries `data.rigId`; IDs become `${rigId}::${nodeId}`;
parentId for react-flow parent/child also prefixed.
- P5.2-5 rig-collapse default + click-toggle: state held as
Map<rigId, boolean>; default empty Map (all collapsed); rig body
click toggles; multiple rigs expand independently; per-session.
- Auto-expand rule (parity with Phase 5.1 TopologyTreeView): on
/topology/seat/$rigId/* or /topology/pod/$rigId/* or
/topology/rig/$rigId, the matching rig auto-expands on mount and
on route change.
- P5.2-7 click handler reads rigId from `node.data.rigId` (NOT
closure): agent click → useNavigate seat URL; pod group click →
useNavigate pod URL; rigGroup body click → no-op at the canvas
level (handled by RigGroupNode's own onClick → onToggle).
- P5.2-8 per-rig features: cmux focus / placement / edge activity
thread through node.data.rigId by construction.
NEW packages/ui/src/components/topology/RigGroupNode.tsx:
- P5.2-4 ReactFlow node-type renderer for rigGroup. Vellum tactical
composition per Phase 1 primitive grammar: 1px outline-variant
border + RegistrationMarks at 4 corners + paper-press hard-shadow +
font-mono uppercase rig name + counts subline + StatusPip.
- Body click → onToggle(rigId) (provided by HostMultiRigGraph).
- Drill-in "→" Link to /topology/rig/$rigId on the rig name; its
onClick e.stopPropagation() prevents the body's toggle from also
firing. Figma frame-name vs frame-body convention.
- Collapsed visual: card body with counts + status pill.
- Expanded visual: thin counts strip (header/border) acts as the
bounding-box frame; children render inside via react-flow
parent/child positioning.
NEW packages/ui/src/lib/multi-rig-layout.ts:
- P5.2-6 layout helpers (option (a) per-rig + outer offset per ACK §2):
* prefixRigData(rigId, nodes, edges) — cross-rig prefixing + data.rigId
* computeBounds(nodes) — bounding box for an expanded rig
* packRigGroups(rigBounds, viewportWidth) — greedy row-fill grid
- Constants: COLLAPSED_RIG_WIDTH=280, COLLAPSED_RIG_HEIGHT=120,
RIG_HEADER_HEIGHT=60, RIG_PADDING=16, RIG_GUTTER_X=48, RIG_GUTTER_Y=48.
REFACTORED packages/ui/src/components/topology/ScopePages.tsx:
- HostScopePage graph view-mode replaces the prior placeholder card
with `<HostMultiRigGraph />`. Mobile graph→table degradation pattern
preserved (effectiveActive gate via useShellViewport per P5-9).
NEW packages/ui/test/p5-2-host-multi-rig-graph.test.tsx (16 tests):
- multi-rig-layout pure-fn tests: prefixRigData prefixes IDs +
threads data.rigId; two rigs with overlapping internal IDs have
no collision after prefixing (ritual #9 coupled-literal scan);
parentId preserved + prefixed; packRigGroups row-fill + wrap;
computeBounds covers empty + multi-node cases.
- HostMultiRigGraph reachability (ritual #6): mounts cleanly with N
rigs from /api/ps; default ALL collapsed; rig body click toggles
collapse state; drill-in "→" Link rendered separately from body
(stopPropagation contract guarded by source-assertion); empty rig
list renders honest empty-state.
- Source-assertion guards: HostMultiRigGraph imports useQueries from
@tanstack/react-query and calls it (positive); no .map(useRigGraph)
rules-of-hooks anti-pattern (ritual #8 negative); no useNodeSelection
(Phase 5.1 retired); RigGroupNode contains 1px outline-variant +
hard-shadow + RegistrationMarks (vellum primitive grammar guard);
drill Link contains e.stopPropagation() (ritual #9).
- Default-collapsed coupled-literal scan (ritual #9): useState<Map<...>>
initializer is `() => new Map()` (empty map → all rigs default to
collapsed via expanded.get(rigId) ?? false); collapsed boolean is
`!p.isExpanded` (single source of truth for collapse semantic).
UPDATED packages/ui/test/p5-mobile-polish.test.tsx:
- Topology graph degradation tests: the prior `topology-host-graph-
placeholder` testid no longer exists at any viewport (P5.2 removed
the placeholder). Updated assertions to look for `host-multi-rig-
graph` (or empty-state) at >= lg viewport; both placeholder and
multi-rig canvas absent at <lg (table degradation path unchanged).
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest p5-2-host-multi-rig-graph: 16/16 PASS
- vitest full UI suite: 692/694 PASS (1 pre-existing library-review
Send-to-seat baseline failure UNCHANGED; 1 skipped)
- pnpm -F @openrig/ui build: clean (1.93s)
SC-29 posture: UNCHANGED — UI-only this slice. Daemon byte-equivalent
beyond Phase 4 + Phase 5 declared exceptions.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V1 founder-walk surfaced a missing graph view at the pod scope
(/topology/pod/$rigId/$podName previously rendered a placeholder
without the live graph render). Per Phase 5.1 ACK §6 implementation
plan, pod-scope graph was the "investigate; park if new-build" item.
Investigation showed it can ride on the existing RigGraph data
(single rig) with a small filter prop — NOT new build. Shipping inline.
EXTENDED packages/ui/src/components/RigGraph.tsx:
- New optional `podScope?: string` prop. When set, the rawNodes /
rawEdges streams from useRigGraph(rigId) are filtered to:
* podGroup/group container nodes whose data.podNamespace OR data.podId
matches the pod name
* agent nodes whose data.podNamespace / data.podId / inferPodName
falls in the pod
* edges whose source AND target are both kept
- Cast inline to RigNodeShape / RigEdgeShape since useRigGraph's hook
data is typed as unknown[] (matching existing downstream pattern).
REFACTORED packages/ui/src/components/topology/ScopePages.tsx:
- PodScopePage default tab moved from "table" → "graph" so the new
graph view-mode is the landing surface.
- Graph view-mode: <RigGraph rigId={rigId} podScope={podName} ... />.
Pod-scoped subset renders with same paper-grid + tactical aesthetic.
- Mobile graph→table degradation pattern replicated from HostScopePage
+ RigScopePage (P5-9): at <lg viewport, graph effectively renders
table with degraded hint; useShellViewport gate.
- Table + Terminal + Overview view-modes unchanged.
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest rig-graph.test.tsx: 61/61 PASS (existing tests unaffected by
podScope addition since tests don't pass the prop)
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V1 founder-walk surfaced ambiguity on the topology rig overview tab:
the tab rendered placeholder text ("Existing rig spec detail page
mounted here in phase five polish") which made it unclear whether
the rig spec surface was intended to mount or whether the tab was
not yet implemented at all.
NEW RigOverviewTab inline component in ScopePages.tsx:
- Mounts existing canonical RigSpecDisplay (the same component that
renders /specs/rig/$id detail).
- Sources data via useSpecLibrary("rig") + useLibraryReview, matching
by rig name (per LibraryReview.tsx pattern).
- Loading state: font-mono "Loading rig spec…".
- No-match: EmptyState with hint to author at /specs.
- Multiple-match: EmptyState with disambiguation hint.
- Review-failed: EmptyState graceful degrade.
- Wrapped in 'topology-rig-overview' testid + 'topology-rig-overview-'
prefix for consistent test selectors.
REMOVED placeholder block ("Existing rig spec detail page mounted
here in phase five polish") and its 'topology-rig-overview-placeholder'
testid — replaced by the live mount.
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- (visual walk needed at VM)
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V1 founder-walk surfaced two gaps in the topology table view:
- Missing per-row CMUX launcher (the table view had no quick-launch
affordance equivalent to the graph view's node-click cmux focus)
- Row click did not navigate to the agent detail page (table behaved
as a read-only grid rather than a clickable surface)
NEW actions column with per-row CmuxButton component:
- Uses existing useCmuxLaunch hook (Phase 4) — POSTs to
/api/rigs/$rigId/nodes/$logicalId/focus.
- onClick stopPropagation so the row's navigate handler doesn't fire
alongside the cmux launch (cmux is the explicit action).
- Tactical aesthetic: 1px outline-variant border, font-mono uppercase
tracking, hover:bg-stone-100/60.
- enableSorting=false on the actions column.
ROW click handler navigates to /topology/seat/$rigId/$logicalId (parity
with graph node click + Explorer tree click + (P5.1-2) the unified
seat-detail navigation contract). Cursor-pointer on rows for affordance.
NEW packages/ui/test/p5-1-table-cmux.test.tsx (4 tests):
- Actions column renders with CMUX button per row.
- CMUX button click → POST to /api/rigs/.../focus (mock fetch).
- CMUX button stopPropagation prevents row navigation.
- Row click (away from action) → navigate(/topology/seat/$rigId/$logicalId)
(vi.mock useNavigate at file scope; spy assertion).
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest p5-1-table-cmux: 4/4 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V1 founder-walk surfaced a rendering bug: pod names display with
the first 2-3 characters truncated (e.g., "discovery" rendering as
"covery", "planning" as "anning") across topology surfaces.
Root cause: lib/display-name.ts displayPodName called shortId(podId, 6)
which returns the LAST 6 chars of any string — designed for 26-char
ULIDs (random tail more distinguishable than timestamp head) but
WRONG for human-readable pod namespaces. "discovery" (9 chars) → last
6 = "covery"; "planning" (8 chars) → "anning"; "kernel" (6 chars)
unchanged because shortId returns id verbatim when len ≤ 6.
Fix: displayPodName returns podId verbatim (no shortId call). Pod
IDs in OpenRig are namespace strings ("discovery" / "planning" /
"kernel" / "product-lab" / etc.), not ULIDs.
shortId import dropped from display-name.ts; the helper is still
exported for actual ULID display surfaces (rig IDs, qitem tails)
elsewhere in the codebase.
NEW packages/ui/test/p5-1-pod-name-truncation.test.ts (12 tests):
- "discovery" → "discovery"
- "planning" → "planning"
- "kernel" / "orch" / "product-lab" / multi-hyphen pass-through
- null/empty → "ungrouped" fallback preserved
- Source-assertion (ritual #9): displayPodName does NOT call shortId
in code (comment-stripped grep)
- inferPodName + displayAgentName neighbor-function preserved
(split-on-dot semantics unchanged)
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest p5-1-pod-name-truncation: 12/12 pass
- vitest full UI suite: 672/674 PASS (pre-existing library-review
baseline failure UNCHANGED; 1 skipped)
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V1 SHIP GATE BOUNCE — 2 P0 fixes from design-reviewer ship-gate walk.
P0-1 — /topology at 375x812 mobile crashed to error boundary
("Cannot read properties of undefined (reading 'length')").
Root cause: TopologyTableView called useNodeInventory inside
`scopedRigs.map((r) => ({ ..., inv: useNodeInventory(r.id) }))` — hooks
in a variable-count loop. At desktop the table mounted only after the
user clicked the table tab (rigs already resolved → consistent hook
count from first render). At mobile, P5-9 effectiveActive degrades
graph→table at FIRST render, mounting the table BEFORE useRigSummary
resolves: scopedRigs starts [] → no hooks called → rigs resolves to
[N] → next render calls N useNodeInventory hooks → React detects hook
count change → crash.
Fix: switched to `useQueries({ queries: scopedRigs.map(...) })` from
React Query — single hook call regardless of array length, hook order
stable across renders. Added `fetchNodeInventory` helper inline (the
useNodeInventory hook is now unused by this file). Memo dependency
also tightened from JSON.stringify-of-data to direct
[scopedRigs, inventoryResults] references.
P0-2 — P5-4 NodeDetailPanel orphan whitespace not actually fixed
(SharedDetailDrawer 38rem chrome correct; inner panel pinned to
viewport-right with `absolute inset-y-0 right-0 z-20 w-80`, leaving
~288px / 30% orphan whitespace inside the drawer).
Root cause: NodeDetailPanel's root <aside> still carried legacy
self-pinning class string from the pre-refactor era when it was a
right-sidebar overlay. P5-4 migrated the DrawerSelection discriminator
('node' → 'seat-detail') but did NOT rewrite NodeDetailPanel's own
positioning. SeatDetailViewer wrapped it but couldn't override the
absolute self-pin from the parent.
Fix: switched the root <aside> from
`absolute inset-y-0 right-0 z-20 w-80 border-l border-stone-300/25 ...`
to `relative w-full h-full ...` — fill-parent layout. Verified via
grep `<NodeDetailPanel`: the only production caller is SeatDetailViewer
(P5-4 retired all other paths), so the legacy absolute positioning had
no remaining consumer that depended on it. Background + backdrop
preserved; border-l dropped because parent VellumSheet already supplies
the canonical 1px outline-variant left edge.
NEW packages/ui/test/p5-ship-gate-bounce.test.tsx (3 regression guards):
- P0-1: TopologyTableView mounts cleanly when /api/rigs/summary has not
yet resolved + then resolves to [3 rigs]; pre-fix this sequence
crashed with rules-of-hooks. Source-assertion (ritual #9): useQueries
imported + called; legacy `inv: useNodeInventory` pattern absent.
- P0-2: NodeDetailPanel.tsx source has `relative w-full h-full` on root
<aside>; legacy `absolute inset-y-0 right-0` + `w-80` patterns absent
from code (comments preserved as historical record).
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest p5-ship-gate-bounce + p5-mobile-polish + p5-topology-terminal +
drawer-primitives + node-detail-panel: all pass
- Full vitest suite: 679/681 PASS (1 pre-existing library-review
Send-to-seat baseline failure UNCHANGED; 1 skipped)
- pnpm -F @openrig/ui build: clean
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Per universal-shell.md L119-L144 mobile / narrow-viewport degradation.
V1 mobile bottom nav = For You / Project / Topology only (NOT Talk;
Talk slots are V2 deferred per L144 — when web terminal ships).
NEW packages/ui/src/components/AppShell.tsx :: MobileBottomNav:
- Fixed bottom-0 left-0 right-0; lg:hidden so desktop never sees it.
- 3 slots: For You (Sparkles icon), Project (Folder), Topology (Network).
- Active state: data-active="true" on the slot whose route prefix
matches pathname; tactical font-mono uppercase 9px label.
- Vellum aesthetic (`.vellum` background + 1px outline-variant top
border) consistent with the rail.
ADDED main content pb-14 lg:pb-0 so mobile bottom nav doesn't cover
scroll content (clearance for the ~52px bottom nav at lg breakpoint
0..1024px).
NEW packages/ui/src/hooks/useShellViewport.ts (per code-map AFTER tree):
- Standalone hook so non-AppShell surfaces can degrade gracefully on
mobile without prop-drilling. Same WIDE_LAYOUT_BREAKPOINT (1024px)
as AppShell — kept in lockstep.
- Returns { isWideLayout, innerWidth }; reacts to resize events.
REFACTORED packages/ui/src/components/topology/ScopePages.tsx:
- HostScopePage + RigScopePage: at <lg viewport, graph view-mode
effectively renders the table view per universal-shell.md L143.
effectiveActive = !isWideLayout && active === "graph" ? "table" : active.
- Tab nav still shows graph as the user's selected mode; resize-to-wide
reactivates the graph automatically (effectiveActive recomputes).
- Small italic hint "Graph view degrades to table on narrow viewports."
(testid: topology-mobile-graph-degraded) prepends the table.
- Pod scope unchanged (graph was placeholder in Phase 3 anyway).
UPDATED packages/ui/test/app-shell.test.tsx (SC-1 strict count):
- Filter `nav, aside` query by lg:hidden so MobileBottomNav (mobile-only)
doesn't count toward desktop chrome total. SC-1's "exactly 2 left
chromes on desktop" is about VISIBLE chromes; raw element count was
too broad.
NEW packages/ui/test/p5-mobile-polish.test.tsx (9 tests):
- useShellViewport: isWideLayout=true at >=1024 / false at <1024 /
reacts to resize events.
- MobileBottomNav: 3 slots present; advisor/operator/talk explicitly
ABSENT (negative-assertion ritual #8 + source-assertion ritual #9
on the MobileBottomNav slot definitions); active route highlighted;
lg:hidden class present.
- Topology graph degradation: at <lg /topology renders table + degraded
hint; at >=lg /topology renders graph placeholder (no degradation).
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest full UI suite: 676/678 PASS (1 pre-existing library-review
Send-to-seat baseline failure UNCHANGED; 1 skipped)
- pnpm -F @openrig/ui build: clean
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes Phase 4 P4-6 deferral. Wires the For You subscription affordance
from a static visual list (Phase 3) into live ConfigStore-backed toggles
that filter the feed. Per for-you-feed.md L130-L151 and SC-16
(subscriptions are settings-shaped, NOT feed-shaped — primary UX of
/for-you stays the FEED in the center; subscriptions live in the
explore sidebar as a small on-demand list).
SC-29 EXCEPTION DECLARED in Phase 5 ACK §5 DRIFT P5-D2 (orch-authorized
at GO retry; same scope as Phase 4 ConfigStore exception). 5 new
allowlist keys added LOCKSTEP daemon settings-store.ts + cli
config-store.ts; NO schema migrations / new endpoints / event types.
Daemon byte-equivalent SC-29 hard line preserved otherwise.
NEW ConfigStore allowlist keys (defaults per for-you-feed.md L144-L151):
- feed.subscriptions.action_required → true (FORCED ON; cannot be
disabled via UI toggle per L145 — load-bearing human-gate items)
- feed.subscriptions.approvals → true
- feed.subscriptions.shipped → true
- feed.subscriptions.progress → true
- feed.subscriptions.audit_log → false (verbose; opt-in)
NEW packages/ui/src/hooks/useFeedSubscriptions.ts:
- useFeedSubscriptions() reads the 5 keys from /api/config (via existing
useSettings) and exposes { state, toggle, isMutating, unavailable }.
- toggle(key) writes via existing useSetSetting; action_required is not
in the toggle key union (forced ON).
- isCardKindSubscribed(kind, state) maps FeedCardKind → subscription
state field for Feed.tsx filtering.
NEW packages/ui/src/components/for-you/SubscriptionToggleList.tsx:
- Settings-shaped row list per for-you-feed.md L134-L140.
- 5 rows; action-required is forced ON with "forced ON" label (no
interactive button); other 4 render aria-checked toggle buttons that
call toggle() on click.
- Settings unavailable (legacy v0.2.0 daemon) → renders defaults +
CLI-fallback hint pointing at `rig config set feed.subscriptions.<kind>
true|false`.
REFACTORED packages/ui/src/components/Explorer.tsx:
- For You surface body delegates to <SubscriptionToggleList />; replaces
the static visual list from Phase 3.
REFACTORED packages/ui/src/components/for-you/Feed.tsx:
- Feed cards filtered by subscription state FIRST (via
isCardKindSubscribed), THEN sliced to HISTORY_LIMIT, THEN lens-filtered.
- action-required cards always pass (forced ON); observation cards pass
only when audit_log is ON; etc.
NEW packages/ui/test/p5-subscription-toggle.test.tsx (6 tests):
- isCardKindSubscribed contract (action-required always; observation
gated by auditLog; shipped gated by shipped).
- SubscriptionToggleList renders 5 rows; action-required has no
toggle button (forced ON); others have interactive buttons.
- Toggle click POSTs to /api/config/feed.subscriptions.audit_log with
flipped value "true".
- Settings unavailable (404) renders CLI hint + defaults.
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- pnpm -F @openrig/daemon exec tsc --noEmit: clean
- pnpm -F @openrig/cli exec tsc --noEmit: clean
- vitest full UI suite: 654/656 PASS (pre-existing library-review
Send-to-seat baseline failure UNCHANGED; 1 skipped)
- pnpm -F @openrig/ui build: clean
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes founder-noticed coupling bug: graph node click previously rendered
NodeDetailPanel "full-width with empty whitespace on left" because the
legacy 'node' DrawerSelection branch routed NodeDetailPanel directly past
the drawer chrome's 38rem width-coupling. Migration option (a) per Phase 5
ACK §6: remove the parallel 'node' kind from the union and migrate every
callsite to the canonical 'seat-detail' kind (Phase 4) which renders via
SeatDetailViewer wrapping NodeDetailPanel inside the canonical 38rem
VellumSheet chrome.
Changes:
- SharedDetailDrawer.tsx: 'node' kind removed from DrawerSelection union;
if (selection.type === 'node') routing branch deleted; NodeDetailPanel
no longer imported directly (reaches drawer only via SeatDetailViewer).
- AppShell.tsx useNodeSelection: backward-compat alias coerces to/from
'seat-detail' instead of 'node'; preserves the (selectedNode,
setSelectedNode) API for RigGraph consumers without breaking imports.
- Explorer.tsx: 5 spots (PodBranch seat-leaf isSelected + onClick;
ungrouped pod seat-leaf isSelected + onClick; RigBranch auto-expand
predicate) all switch from 'node' to 'seat-detail'.
- rig-graph.test.tsx: existing "click node -> setSelection({type:'node'})"
test updated to assert canonical 'seat-detail' shape.
- NEW test/node-selection-migration.test.tsx (5 source-assertion tests
per ritual #8 negative-assertion): 'node' kind absent from union;
routing branch absent; NodeDetailPanel direct-import absent;
useNodeSelection alias uses 'seat-detail'; Explorer.tsx file-wide free
of '{ type: "node"' literal (DiscoveryPlacementTarget.kind === "node"
is a SEPARATE namespace and is preserved).
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest run on app-shell + rig-graph + drawer-primitives +
node-selection-migration: 118/118 PASS
- Branch shell-redesign-v1-attempt-3-phase-5 off main f1b91ff
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
guard-3 PROCESS-BLOCKER on 4932f5d surfaced 3 legitimate gaps; this is the
fix-on-top commit. Two gaps live entirely in substrate (ACK file edits;
ACK §4 actual_at_4932f5d + deferred_to_phase_5 blocks added; ACK §5 drift
disclosures DRIFT P4-D1 site-of-use deferral + P4-D2 unlisted modifications
recorded honestly); only this test file is product-code work.
NEW packages/ui/test/drawer-primitives.test.tsx (17 tests, all passing):
- Each viewer renders without crashing with canonical-shape props
(QueueItemViewer, FileViewer, SubSpecPreview, SeatDetailViewer; the
last via vi.mock on NodeDetailPanel to bypass deep query/router deps)
- Each trigger fires setSelection on click with the correct
DrawerSelection discriminator + payload (qitem | file | sub-spec |
seat-detail kinds via DrawerSelectionContext.Provider with vi.fn spy)
- SharedDetailDrawer routes selection.type to the correct viewer
testId; selection=null returns null (SC-6 default-closed contract)
Closes ritual #6 reachability proof gap for the 4 viewers + 4 triggers
that have no live consumers at 4932f5d (P4-2 site-of-use wiring deferred
to Phase 5 per DRIFT P4-D1).
Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- pnpm exec vitest run test/drawer-primitives.test.tsx: 17/17 pass
- pnpm -F @openrig/ui build: built in 1.86s
- Pre-existing library-review.test.tsx Send-to-seat failure unchanged
(baseline 24db2b0; not Phase 4 related)
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
guard-3 PROCESS-BLOCKER on Phase 3 bounce-fix at 400477b. Two surgical
adds satisfy the gate:
ISSUE 1 — Negative-assertion regression test wasn't committed.
ACK §4 said the non-topology overlay-leak negative assertion (ritual
#8) would land in topology-view-mode.test.tsx; the agent-browser walk
proof was evidence-of-the-moment but not a CI-runnable test. guard-3
needs the test in CI to consider ritual #8 satisfied.
Fix: new describe block in test/topology-view-mode.test.tsx —
"Class B negative assertion: non-topology routes never get vellum
overlay (ritual #8)":
- 5 routes via it.each (`/`, `/project`, `/specs`, `/for-you`,
`/settings`) assert container.querySelectorAll(
'[data-explorer-mode="overlay"]').length === 0.
- 1 positive companion: `/topology` graph mode DOES carry
data-explorer-mode='overlay' on the Explorer aside.
All 6 tests pass in vitest. topology-view-mode.test.tsx: 18/19 PASS
(1 skipped seat-scope brittle from Phase 3, unrelated). Full suite:
640/642 PASS (added 6 tests; pre-existing library-review failure
preserved).
ISSUE 2 — Durable founder-walk record missing for 400477b seat-fix.
The phase-3-bounce-walk.md packet still pinned 7aa1aac; the seat-fix
verification only lived in /tmp transient handoff. Need a permanent
record co-located with the rest of the walk doctrine.
Fix: appended "FOLLOW-ON COMMITS" section to
founder-walks/phase-3-bounce-walk.md with:
- 400477b — seat-detail double-padding fix: full root cause (CSS-var
inheritance through legacy WorkspacePage), fix description, founder
verification at /topology/seat/$rigId/discovery.intake-router 1440x900,
classification of inner-panel HTTP 404 as data-availability not layout.
- Briefer one-paragraph entries for the other follow-ons that landed
during the same walk session: 6f86a5c (tabs-only shell), 84557e5
(tab breathing), 210c322 (rig graph zero-height), ed08ff2 (lighter
vellum + collapsed toggle), bbe6250 (floating toggle + vellum rail
+ lighter icons), and this process-gap fix itself.
Walk packet now reflects the actual branch tip composition; future
readers can trace each refinement back to its commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder direction 2026-05-06 — three refinements:
1. Collapsed Explorer: floating toggle, no whitespace column
Previous: collapsed Explorer rendered as a 12px aside with bg-background;
the whitespace column read as empty UI clutter.
Fix: when collapsed, render ONLY a floating chevron-right button
positioned fixed at left=3.5rem (just past the rail edge) +
top=5.5rem. No aside container, no background column. Tabs and
canvas reflow to fill (anchorLeft drops to 3rem = rail-only).
data-explorer-collapsed='true' attribute on the floating button for
runtime probing.
2. Rail with vellum surface
Previous: rail used `bg-background` (solid paper-cream).
Fix: rail now uses .vellum class (rgba(255,255,255,0.4) +
backdrop-blur(8px)) — same translucent treatment as the
topology-graph Explorer overlay. Paper-grid dots visible through
the rail surface. Reads as a paper sheet layered over the canvas
per universal-shell.md L48 sheets-of-vellum aesthetic.
3. Icons lighter line weight
Previous: lucide icons rendered at default strokeWidth=2 (heavy
line). Read as too dense against the vellum-translucent rail.
Fix: strokeWidth=1.25 on all rail destination + chat icons + the
Explorer toggle chevrons. Closer to 1px architectural drafting
feel matching the 1px ghost border doctrine.
- Widened RailIconSpec.icon ComponentType to accept strokeWidth.
- Lighter hover bg (stone-200/60 instead of stone-200) so hover
reads gentler against vellum.
Visual verification at /topology 1440x900:
- Rail: vellum-translucent; paper-grid dots show through; icons lighter.
- Explorer expanded: paper-grid dots show through; vellum overlay light.
- Click chevron-left: Explorer collapses; ONLY a floating chevron-right
button visible at rail edge; no aside column; tabs reflow to anchor=3rem;
canvas extends fully.
- Click floating chevron: Explorer re-expands.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder direction 2026-05-06 — two fixes:
1. Vellum overlay too dense
- Bounce-fix #2 used .vellum-heavy (rgba(255,255,255,0.7) +
blur(16px)) — too opaque vs the baseline Phase 2 explorer which
was rgba(250,249,245,0.035) supports-bg(0.018) + blur(14px) —
extremely transparent.
- Switched topology-graph overlay to .vellum (rgba(255,255,255,0.4)
+ blur(8px)) — lighter, paper-grid visible through it, tree text
readable. Reads coherent with baseline transparency. Lighter
shadow (rgba 0.06 vs 0.08).
2. Collapsed Explorer unreachable without page refresh
- Previous: desktopMode='hidden' applied lg:hidden to entire aside,
hiding the chevron toggle along with tree contents. Once
collapsed, no in-app affordance to re-expand.
- Fix: collapsed state is now a thin 12px (w-12) column at left=48px
(past the rail) showing only the chevron-right toggle. Clicking
re-expands. SurfaceBody (tree contents) is conditionally
unmounted in collapsed state for clean appearance. data-explorer-collapsed
attribute added for runtime probing.
- Toggle button:
* collapsed: chevron-right, centered (left-1/2 -translate-x-1/2)
* expanded: chevron-left, right-edge (right-2)
- AppShell anchor offset accounts for collapsed stub: explorerAnchorLeft
is 6rem (rail 3 + stub 3) when explorerVisible && !desktopExplorerOpen,
21rem when fully open, 3rem when no explorer for destination.
Visual verified at /topology 1440x900:
- Default: vellum overlay reads lighter; tabs + canvas + paper-grid
show through Explorer's translucent surface.
- Click chevron-left: Explorer collapses to 12px stub; chevron flips
to right-arrow at center; tabs/canvas extend to take the freed width.
- Click chevron-right: Explorer re-expands.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder reported: clicking a rig in Explore navigates to /topology/rig/$rigId
graph tab but the canvas is empty.
Confirmed not a feature gap — RigGraph IS mounted (React Flow + 21
nodes verified via probe) but the parent container collapsed to 0
height, hiding the rendered graph.
Root cause: ScopeShell content area was `flex-1 min-h-0 overflow-auto`
but NOT a flex container, so the inner graph wrapper's `flex-1` had
nothing to flex against. min-h-[400px] only enforced 400px on the
wrapper but RigGraph (React Flow) renders at h-full of its parent and
needs a defined-height ancestor chain.
Fix:
- ScopeShell content area: add `flex flex-col` so children can use
flex-1 to fill remaining vertical space.
- Graph branch wrapper: `flex-1 min-h-0 relative` (replaces
`flex-1 min-h-[400px] relative`); inherits height from the now-flex
parent and lets React Flow size correctly via h-full.
Visual verification at /topology/rig/01KQEQPN4MQJN0DHBM5CQ0N8D7:
- React Flow rect: width 1392, height 787 (was 0)
- 21 nodes rendered: orch_pod / velocity_pod / review_pod / watcher_pod
/ redo_pod / design_pod / redo3_pod with their internal agents,
context %, edges
- OPENRIG-VELOCITY rotated watermark visible
- Paper-grid background extends around the graph
- View-mode tabs still anchored at fixed left position
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder direction 2026-05-06: remove the horizontal line border that
spanned across the entire tablist; keep the underline ONLY under the
active tab; give the tabs more breathing room.
Changes (TopologyViewModeTabs):
- Removed `border-b border-outline-variant` from tablist wrapper.
- Removed `-mb-px` from buttons (no longer needs to overlap the
parent's border).
- Removed `px-3` from buttons (gap-6 between tabs handles spacing
cleanly without per-button horizontal padding).
- Increased button padding to `py-3` (was `py-2`) for vertical breath.
- Increased flex `gap-6` (was `gap-1`) for horizontal breath between tabs.
- Active state still carries `border-b-2 border-stone-900` — only the
selected tab has an underline; the rest float over the canvas.
Result: tabs read as "GRAPH TABLE TERMINAL" floating on the
paper-grid with the active item underlined; no horizontal line bar
across the canvas.
Tests + visual verified at /topology 1440x900.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder direction 2026-05-06 (with reference mockup): the topology
scope shell should render JUST the view-mode tabs anchored to the
right of the Explorer — no "TOPOLOGY · SEAT" eyebrow, no big
"DISCOVERY.INTAKE-ROUTER" title, no border-bottom + bg-background
header section. Tabs over paper-grid canvas; scope identity reads from
the URL + Explorer tree active state.
Changes:
- ScopeShell now renders just the tabsNav with marginLeft anchored to
--header-anchor-offset (preserves the fixed-anchor behavior across
graph / table / terminal switches per Class B).
- Removed: SectionHeader eyebrow, h1 title, border-b underline + bg-background
on the wrapper div.
- Kept: relative z-30 (tabs stay above Explorer overlay in graph mode),
px-6 + pt-4 padding for breathing room, marginLeft anchor.
- The eyebrow/title props are still typed (now optional) for
potential future revival but not consumed.
Visual verification at 1440x900:
- /topology: just GRAPH / TABLE / TERMINAL tabs over paper-grid; tabs
at fixed anchor.
- /topology/seat/...: just DETAIL / TRANSCRIPT / TERMINAL tabs;
LiveNodeDetails renders its own H1 below as part of the detail panel
content (separate concern; not part of the scope shell).
- Tab fixed-anchor preserved across view-mode switches.
Tests: 47/48 PASS in app-shell + topology-view-mode test files (1
skipped seat-scope brittle); no regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder reported: node detail page (e.g.,
/topology/seat/$rigId/$logicalId) was pushed too far right at desktop
1440x900. The 'DISCOVERY.INTAKE-ROUTER' detail panel rendered far right
of the Explorer's right edge instead of anchored at the workspace
left offset.
Root cause: double-padding from Class B's `paddingLeft:
var(--workspace-left-offset)` on <main>, combined with legacy
WorkspacePage (consumed by LiveNodeDetails) ALSO reading the same
--workspace-left-offset via `lg:pl-[var(--workspace-left-offset,0px)]`.
Net effect: <main> padded by anchor (21rem); WorkspacePage inside
LiveNodeDetails added another 21rem on top → content shifted ~42rem
right (about half the viewport).
Fix: reset --workspace-left-offset and --workspace-right-offset to 0px
on the inner div that wraps {children} inside <main>. This way:
- <main> reads the OUTER var (set on <main> via workspaceStyle) →
applies its own paddingLeft.
- Children (legacy WorkspacePage consumers + Phase 3 destinations)
read the INNER var (overridden to 0) → no double padding.
Phase 3 destinations (Dashboard / TopologyTreeView / etc.) don't use
the var directly; they were already fine. The fix only neutralizes
double-padding for legacy children inheriting WorkspacePage layout.
Visual verification at /topology/seat/$rigId/discovery.intake-router
1440x900: detail panel anchored just past the Explorer's right edge
(at the workspace left offset = 21rem); sub-tabs IDENTITY/AGENT_SPEC/
STARTUP/TRANSCRIPT also anchored. (Inner panel renders an HTTP 404
because /api/inventory route on shipped daemon doesn't have entries
for synthetic test seat ID — separate data-availability issue, not
layout.)
Tests: 634/636 PASS unchanged (no regression; pre-existing
library-review + skipped seat-scope).
Build: ✓ in 1.99s.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder walk on Phase 3 (commit 5720ca7) approved structurally; surfaced
5 bug fixes + 1 design refinement. Triple-gate verdicts: review-lead
APPROVE, design-reviewer DONE, guard-3 PROCESS-BLOCKER (resolved by
this commit's permission preflight in ACK §4).
Branch: shell-redesign-v1-attempt-3-phase-3-bounce on top of 5720ca7
(driver judgment to build on Phase 3 destinations).
CLASS A — 5 bug fixes:
A1 — topology landing tab content auto-load
- Verified at agent-browser: graph placeholder DID render on first
navigation (Dashboard → /topology); 25 rigs visible in tree.
- Founder's perceived "empty" was Class B Explorer-covers-content
layout — Class B fix below resolves as side effect (graph canvas
now extends edge-to-edge under translucent Explorer; placeholder
visible regardless of Explorer state).
A2 — Settings tab HTTP 404 root cause + soft empty-state
- Root cause: shipped daemon (npm v0.2.0) doesn't expose /api/config
yet; route lands at v0.3.0+. Per SC-29 daemon byte-equivalent — NO
daemon patch.
- Vite proxy verified configured (vite.config.ts L7-L20). Per dispatch
guidance, escalate before patching daemon.
- UI fix: SettingsTab error rendering softened. When error contains
"404": render honest empty-state pointing at CLI as canonical edit
path ("rig config get/set/reset"). Otherwise: existing error message
in cleaner empty-state styling. No more raw "could not load HTTP 404"
red text.
A3 — Log/RecentActivity bodies show "—" instead of message text
- New shared `lib/format-event-payload.ts` with smart fallback chain:
payload.summary → body → detail → message → title → text →
description → compact key=value preview (skipping noise keys
ts/created_at/audit_pointer) → JSON.stringify(payload) truncated.
- Applied in SettingsCenter LogPanel + Dashboard RecentActivity.
- 9 tests in test/format-event-payload.test.ts covering each fallback +
noise-key skip + truncation.
A4 — Status tab cmux block restored (regression fix)
- Original SystemPanel.tsx L52-L131 had cmux query +
`data-testid="system-cmux-status"`. My Phase 3 SettingsSystemStatusPanel
extraction missed it.
- Restored: useQuery({queryKey:["daemon","cmux"]}) +
fetchCmux from /api/adapters/cmux/status. Renders via StatusPip
primitive (active/warning/info per cmuxAvailable tri-state).
- New testid: status-cmux. Side-effect: also restored daemon health
fetch ordering for daemonConnected → cmuxAvailable resolution.
A5 — Project workspace label hardcoded "openrig-work"
- New hook `useWorkspaceName`: reads workspace.root from useSettings
(with defensive guards for malformed/missing settings shape — fixes
/project route test failure when settings endpoint returns []).
- Returns basename of root, or null when unset/unreachable.
- ProjectTreeView: live label OR honest empty-state ("No workspace
connected" + Link to /settings) when name is null.
- WorkspaceScopePage: live title OR full empty-state EmptyState card
with action to /settings.
- Chains with A2: when daemon doesn't expose /api/config, both flow to
empty-state pointing at /settings (which itself shows the CLI hint).
CLASS B — fixed-anchor view-mode tabs + selective vellum overlay:
Per dispatch + universal-shell.md L48 ("sheets of vellum paper layered
together") canon outcome.
- NEW: components/topology/topology-overlay-context.tsx —
TopologyOverlayContext + Provider + useTopologyOverlay hook. Two
modes: "overlay" | "opaque". Default opaque so non-topology surfaces
always opaque.
- AppShell.tsx — refactored:
* Outer `AppShell` wraps SpecsWorkspaceProvider + TopologyOverlayProvider
around `AppShellInner` (the actual chrome + state).
* AppShellInner reads explorerMode via useTopologyOverlay; computes
isTopologyOverlay = mode==="overlay" && surface==="topology".
* NEW CSS variables on <main>:
- --explorer-anchor-left = 21rem (rail 3 + explorer 18) when
desktop+open, else 3rem.
- --header-anchor-offset = anchor when isTopologyOverlay (so the
ScopeShell header keeps the tab bar at fixed anchor even when
canvas extends behind), else 0rem.
- workspaceLeftOffset: 0 in overlay mode (canvas extends behind
Explorer); anchor in opaque mode.
* `<main>` gets explicit padding-left: var(--workspace-left-offset)
so all destinations clear the Explorer.
* `<main>` carries data-explorer-mode attr for runtime + test probing.
- Explorer.tsx — overlayMode prop + grammar:
* "overlay" branch: vellum-heavy class (rgba(255,255,255,0.7) +
backdrop-blur(16px) per globals.css L118-L122) + z-30 + heavier
shadow. Sheets-of-vellum-layered aesthetic.
* "opaque" branch: existing Phase 2 background. z-20.
* data-explorer-mode attr on the <aside>.
* 1px outline-variant border preserved (border weight doctrine).
- topology/ScopePages.tsx — useOverlayForActiveTab(active) helper:
* useEffect sets overlay mode = "overlay" when active==="graph" else
"opaque". Cleanup on unmount → "opaque" so non-topology destinations
don't inherit overlay state.
* Applied in HostScopePage / RigScopePage / PodScopePage. SeatScopePage
(detail/transcript/terminal — no graph) doesn't use the helper.
* ScopeShell header gets marginLeft: var(--header-anchor-offset) so
the tab bar stays at the anchor in overlay mode (canvas extends
behind, but the header section is still positioned at the anchor).
* Header z-30 + bg-background so it stays on top of the Explorer
overlay even though Explorer is also z-30 (same z-index, header
paints later in document order).
Visual verification (agent-browser at 1440x900):
- /topology graph mode: Explorer translucent vellum over canvas;
paper-grid extends edge-to-edge UNDER Explorer; tabs at fixed
anchor; "HOST GRAPH" placeholder visible centered in canvas.
data-explorer-mode === "overlay".
- /topology table mode: clicked the tab (no longer blocked by Explorer
overlay — fixed-anchor header is z-above); Explorer reverts to
opaque; tabs at SAME left position as graph mode (no jump). 122 of
122 agents in tanstack table with RUNNING StatusPip badges.
data-explorer-mode === "opaque".
- /project: data-explorer-mode === "opaque"; querySelectorAll
('[data-explorer-mode=overlay]').length === 0. Non-topology
destinations NEVER get overlay (negative-assertion ritual #8 ✓).
OUT-OF-SCOPE preserved per dispatch: Phase 4 (drawer viewers + triggers
+ AuthorAgentTag cmux + Advisor/Operator wiring + RigDetailPanel
retire + subscription toggles wire) and Phase 5 (mobile polish + slice
tab piping + filesystem mission discovery + topology terminal
pagination + final dogfood). Daemon untouched per SC-29.
Verification:
- tsc --noEmit: clean
- pnpm test: 634/636 PASS (1 skipped seat-scope; 1 pre-existing
library-review unrelated; my Phase 3 /project failures from
useWorkspaceName lack-of-defensive-guard FIXED)
- pnpm build: ✓ in 1.99s
- agent-browser walk: 6 destinations + 3 topology view-modes verified;
vellum overlay only on topology graph; tabs at fixed anchor.
ETA realized: ~115 min (within 90-120 dispatch estimate; Class B took
the most time as anticipated).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
guard-3 caught a real bug missed in bounce-fix #2: VellumSheet wide preset
was calibrated 45rem → 38rem, but AppShell.tsx workspaceRightOffset still
reserved 45rem of right padding via the --workspace-right-offset CSS
variable when drawer is open. Net effect: open-drawer surfaces a 7rem
(112px) layout gap between the drawer (38rem) and the right edge of the
center workspace's content.
Fix:
- AppShell.tsx workspaceRightOffset open-drawer literal "45rem" → "38rem".
- SharedDetailDrawer.tsx stale lg:w-[45rem] comment → lg:w-[38rem]
(with calibration-date reference per content-drawer.md L9).
- New regression test in test/app-shell.test.tsx (CSS-source-assertion
per pseudo-element-paint test contract / discipline ritual #7):
* "VellumSheet wide preset and AppShell workspaceRightOffset use the
SAME literal" — extracts widthClass.wide width from VellumSheet
source and workspaceRightOffset open-drawer value from AppShell
source via regex; asserts equal.
* "no live 45rem string in chrome source" — scans AppShell, drawer,
and vellum-sheet sources; for any line containing "45rem" verifies
it's inside a // line comment OR a /* ... */ block comment context
(historical calibration notes are allowed; live class/value strings
are not). Permanent guard against the same coupling drift.
Verification:
- tsc --noEmit: clean
- pnpm test: 599/600 PASS (added 2 new tests; pre-existing
test/library-review.test.tsx > Send-to-seat modal still failing
unrelated to this scope; verified pre-Phase-1 via git stash)
- pnpm build: ✓ in 1.94s
- Runtime probe (drawer not mounted by default per SC-6) confirms
static-source coupling test is the right approach; computed-style of
CSS variables in jsdom is too brittle to depend on.
Permission preflight caveat acknowledged: bounce-fix #2 §6 fields
writable_root_coverage and approval_layer_probe were authored as
prose/boolean rather than the exact path+covered_by and command+result
subshape per velocity-permission-preflight schema. Orch accepted the
field-level close-out; will sharpen to the exact subshape on next
preflight authoring (likely Phase 3 dispatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder walk on Phase 2 commit 5f53d31 surfaced one structural addition:
desktop top bar with brand mark, symmetric with mobile. universal-shell.md
L11–L65 updated upstream — new "Desktop top bar" section L40–L53 makes
the top bar an explicit canonical region (h-14, OPENRIG brand left,
right-slot reserved, fixed, doesn't scroll with center).
Approach (a) per dispatch — single source of truth across viewports:
AppShell.tsx:
- Renamed testid app-mobile-topbar → app-topbar (top bar is now universal,
not mobile-only).
- Removed lg:hidden from <header> so the SAME element renders at all sizes.
- Hamburger button keeps its own lg:hidden (mobile-only).
- Renamed brand-home-link-mobile → brand-home-link (single Link element).
- Added right-slot with minimal V1 'localhost' env indicator
(font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant);
hidden on narrow viewports (sm:flex) to preserve mobile space.
SharedDetailDrawer.tsx:
- Drawer fixed positioning adjusted: 'top-0 right-0 z-30 h-full' →
'top-14 right-0 bottom-0 z-30' so drawer starts below the top bar.
bottom-0 anchor avoids needing a calc() expression for height.
Existing positioning unchanged — Rail / Explorer / Center are inside the
inner <div className="flex flex-1 min-h-0 relative"> below the header,
positioned relative to that flex parent (NOT viewport), so adding the
top bar to the flex column automatically pushes them down by h-14 with
no further offsets needed.
Tests (test/app-shell.test.tsx):
- Updated SC-8 describe block (split into Top bar + Mobile rail collapse).
- Added 5 top-bar tests:
* top bar renders at desktop (single source of truth — no lg:hidden)
* brand link visible at desktop with href='/' (Dashboard)
* right-slot env indicator present (V1 = 'localhost')
* hamburger button is mobile-only (lg:hidden) — preserved Phase 2 behavior
* legacy app-mobile-topbar / brand-home-link-mobile testids GONE
- Existing SC-1 strict count test unchanged and still passes (top bar is
a horizontal region, NOT a left chrome).
Verification gates:
- tsc --noEmit: clean
- pnpm test: 597/598 PASS (single failure is pre-existing
test/library-review.test.tsx > Send-to-seat modal; unrelated)
- pnpm build: ✓ in 1.96s
- agent-browser walk at 1440x900 /topology:
* document.querySelectorAll('nav, aside').length === 2 (SC-1 unchanged)
* data-testid='app-topbar' present
* brand-home-link textContent = 'OPENRIG'; href = '/'
* topbar-env-indicator textContent = 'localhost'
* Top bar visible above rail+explore+center; thick black bottom border;
paper-grid preserved.
ETA realized: ~25 min (within 25–30 estimate).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Founder direction 2026-05-04: "respect the provider; no funny business;
needs to be extremely reliable; ergonomic." Implementation = Option 3
of the design space. We respect Claude's project-dir-scoped session
storage by ensuring the rendered Use-as-starter snippet has the right
cwd baked in. The daemon does NOT override cwd at fork dispatch.
The fix shape is INTERIM and bounded to Use Case A — clone a productive
seat in the same dir. Use Case B (greenfield from primed context in a
new project dir) needs the IDEAL FORK functionality via JSONL session-
file context injection — that work continues in the lab folder
(openrig-work/lab/session-context-tools/) and is NOT in scope here.
Trigger E in mode-2/PROGRESS.md Deferred Follow-Ons tracks Use Case B.
Changes:
- AgentImageManifest type: optional sourceCwd field (camelCase TS,
source_cwd in YAML). Lockstep snake/camel name across manifest
schema + library service + parser + snippet generator + tests +
this commit message + future skill mentions.
- AgentImageEntry type: sourceCwd: string | null on the runtime
surface. null when manifest predates Finding 2 (back-compat).
- manifest-parser.ts: reads source_cwd / sourceCwd from YAML; pre-
Finding-2 manifests parse with sourceCwd: undefined.
- resume-token-discovery.ts: DiscoveryResult extended with nodeCwd:
string | null. Single nodes-table query reads runtime + cwd
together (no extra round-trip). Shared by SnapshotCapturer + any
future fork route.
- snapshot-capturer.ts: captures source seat's cwd into manifest
sourceCwd via the discovery result. nodeCwd null → manifest field
omitted (preserves back-compat fixture shape).
- agent-image-library-service.ts install(): writes source_cwd to
manifest.yaml when present. toEntry() surfaces sourceCwd verbatim
from manifest into the AgentImageEntry runtime shape (null when
absent).
- agent-image-routes.ts buildStarterSnippet(): when entry.sourceCwd
is set, prepends `cwd: <source_cwd>` ahead of the session_source
block in the rendered YAML. Operator pastes verbatim, fork starts
in source's dir, Claude's project-dir-scoped session storage
works because the jsonl file lives there. When sourceCwd is null
(pre-Finding-2 manifest), snippet renders without the cwd line —
current behavior preserved exactly. Resume-token redaction at the
HTTP boundary unchanged (sourceCwd is not sensitive; flows through
redactResumeToken's spread).
- useAgentImageLibrary.ts: AgentImageEntry UI shape extended with
sourceCwd: string | null. LibraryReview surfaces a "source cwd"
line so operators see WHERE the parent session was created (not
just WHO). Pre-Finding-2 manifests render "(unknown — pre-
Finding-2 manifest)" honestly.
NO daemon-side cwd override at fork dispatch. The rigspec-instantiator
agent_image branch never reads sourceCwd; it computes nothing from it.
If operator manually changes cwd: in their rig.yaml after pasting the
snippet → fork runs in the new dir → Claude returns "no conversation
found" → operator gets honest error per architecture.md § Resume
honesty. (The snippet's cwd line IS the contract: paste verbatim or
take the consequences.)
Tests (10 new in packages/daemon/test/agent-image-source-cwd.test.ts):
Snapshot capturer:
- captures source seat's cwd into manifest source_cwd
- omits source_cwd when source node has no recorded cwd (legacy)
Snippet generator:
- rendered snippet includes cwd line + cwd precedes session_source
- back-compat: snippet omits cwd line when manifest has no
source_cwd (current behavior preserved)
- library entry surfaces sourceCwd verbatim (null for pre-
Finding-2)
Manifest parser back-compat:
- accepts source_cwd (snake_case)
- accepts sourceCwd (camelCase)
- manifests without source_cwd still parse (back-compat)
Operator-override safety:
- rigspec-instantiator does NOT mutate member.cwd from agent_image
manifest (negative grep against the source — fails if a future
patch adds the override path)
- snippet generator emits cwd ONLY from manifest source_cwd (no
inferred fallback)
Test results:
Daemon: 3084 passed + 1 skipped (3074 baseline + 10 new). 1 skipped
is the OPENRIG_REAL_CLAUDE_INTEGRATION lane from PL-016 hardening
v0+1.
CLI: 851/854 — 3 pre-existing failures match main baseline; no CLI
surface changes (snippet renderer is daemon-side; CLI prints
response verbatim).
UI: 567/567 — no new UI test files; existing LibraryReview tests
pass through the additive sourceCwd line without regression.
TSC clean across daemon/cli/ui.
NOT in scope (explicit per founder dispatch):
- daemon-side cwd override (rejected — "respect the provider")
- session jsonl import/copy (rejected — couples to Claude internals)
- codex multi-runtime forwarding (separate scope; this is claude-
code-specific until codex parity is named)
- Use Case B greenfield-from-primed-context-in-new-dir (lab work
via openrig-work/lab/session-context-tools/; tracked at Trigger E)
Permission posture: fleet-ok throughout.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three fixes from review-lead's live e2e dogfood of PL-016 Phase A on
2026-05-04. Founder-authorized hardening pass; no scope creep beyond
the named findings.
Finding 1 (P1 defect) — claude-code adapter resume-token poll loop:
packages/daemon/src/adapters/claude-code-adapter.ts. Fork branch
called captureResumeToken IMMEDIATELY after Enter; real Claude
needs 1-3s to write the new fork session-name file under
~/.claude/sessions/. Result: EVERY claude-code agent_image fork
was silently broken since PL-016 merge — captureResumeToken always
returned undefined against a real binary, the fork branch always
errored "could not capture new post-fork session id". Mocked-fs
tests passed because the fixture returned the token instantly.
Fix: pollForResumeToken(name, attempts, delayMs) helper mirroring
verifyResumeLaunch's cadence (12 × 500ms = 6s ceiling). Error
message names the ceiling so operators can correlate timing.
Test gap fix: NEW packages/daemon/test/claude-code-adapter-fork-
poll.test.ts covers (a) deferred-write success path (3 sleeps then
capture), (b) immediate-success short-circuit (zero sleeps), and
(c) exhaustion path (12 attempts, 11 sleeps, structured error
with poll ceiling). Plus an env-flag-gated lane
(OPENRIG_REAL_CLAUDE_INTEGRATION=1) reserved for the real-binary
regression test — skipped in CI; operator runs against productive
rig when explicitly enabled.
Finding 3 (P3 UX nit) — rig agent-image create --version flag:
packages/cli/src/commands/agent-image.ts. Commander.js intercepts
--version as the global version flag (prints CLI version + exits 0
silently); the per-command --version was unreachable. Renamed to
--image-version for the per-command name; preserves the global
--version surface. Test (NEW packages/cli/test/agent-image.test.ts):
asserts --image-version 2 propagates as body.version=2 on the
snapshot POST AND that the create command does NOT register a
--version option (catches future re-introductions of the
collision).
Finding 4 (P3 data quality) — fork_count on failed launches:
packages/daemon/src/domain/rigspec-instantiator.ts +
agent-image-library-service.ts. recordConsumption was called BEFORE
startupOrchestrator.startNode, so failed launches polluted
forkCount with intent-not-success noise. Two-phase fix: pre-launch
recordConsumption(id, { incrementForkCount: false }) bumps
lastUsedAt only (records intent regardless of outcome — operators
see "this image was tried"); post-launch recordConsumption(id,
{ incrementForkCount: true }) only runs when startupResult.ok===true.
recordConsumption signature extended with opts object; legacy
positional (() => Date) form preserved for back-compat.
Tests: 2 new lib-level cases on agent-image-library-service.test.ts
({ incrementForkCount: false } bumps lastUsedAt only; legacy
positional form unchanged); NEW packages/daemon/test/agent-image-
fork-count-gate.test.ts spies the library across an instantiator
flow with a failing adapter (assert: 1 call, false) AND a
succeeding adapter (assert: 2 calls — pre-launch false, post-launch
true).
Test results:
Daemon: 3029 passed + 1 skipped (3022 baseline + 7 new + 1 skip
for OPENRIG_REAL_CLAUDE_INTEGRATION lane).
CLI: 851/854 (3 pre-existing failures match main baseline:
CLI dist build / config env / preflight env — NOT introduced
by this slice).
UI: 559/559 (no UI surfaces touched).
TSC clean across daemon/cli/ui.
Identifier-drift discipline: --image-version flag name is the only
new identifier; locked at audit; no other CLI/docs cite the old
--version per-command flag (the only callers were the create
verb's own action handler).
Permission posture: fleet-ok throughout. No host-touching beyond
the worktree, no provider auth, no destructive lifecycle operations.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PL-014 Item 6 follow-up to fce486c (Items 1-5 + 7).
Closes the strategic gap: context_packs become declarative at agent
launch time. With this fold-in, an agent.yaml can declare:
```yaml
startup_files:
- kind: context_pack
name: rsi-v2-priming
version: 1
```
…and the rig-instantiator resolves the pack against the daemon-side
ContextPackLibraryService, assembles the bundle, writes it to
<rigRoot>/.openrig/resolved-context-packs/<name>-<version>.md, and
hands it off to the existing startup-orchestrator pipeline as a
normal kind: file entry. Composes with PL-016 fork (rail #6) per
founder direction: fork from agent_image baseline → declaratively
layer context_packs at boot → seat self-reach via CLI verbs already
shipped at fce486c.
Type extensions
- StartupFile (domain/types.ts) gains optional kind: "file" |
"context_pack" + contextPackName?: string + contextPackVersion?:
string. Default kind = "file" for back-compat.
- ResolvedStartupFile (domain/runtime-adapter.ts) mirrors the same
optional fields so the instantiator's expandContextPacks helper
can rewrite entries in place.
Parser + validator (domain/startup-validation.ts)
- normalizeStartupBlock: kind: context_pack entries synthesize a
stable path (.openrig/resolved-context-packs/<name>-<version>.md)
+ default version "1" when omitted + default deliveryHint
"send_text" (pack lands as one paste in the live seat) — operator
can override delivery_hint for pre-launch filesystem delivery.
- validateStartupFile: rejects unknown kind values + missing name
on kind: context_pack; preserves the safe-path check for
kind: "file" entries.
Resolver (domain/rigspec-instantiator.ts)
- New PodInstantiatorDeps.contextPackLibrary (optional).
- expandContextPacks helper called from both buildPodResolvedStartupFiles
and buildResolvedStartupFiles before resolveAutoHints. Looks up the
pack via ContextPackLibraryService.getByNameVersion, assembles the
bundle, writes to <rigRoot>/.openrig/resolved-context-packs/, and
rewrites the entry as kind: "file" pointing at it.
- Honest errors: missing pack → "Context pack '<name>' v<version> not
found in library. Run 'rig context-pack list' to see what's
installed." Missing library service (older daemon path) →
"ContextPackLibraryService is not wired."
Wiring (startup.ts)
- ContextPackLibraryService construction hoisted before
PodRigInstantiator so the same instance is shared across the
/api/context-packs/* routes AND the materialize path. Single source
of truth — operator-installed pack visible to both surfaces
immediately after a sync.
Identifier-drift discipline
- "context_pack" locked as the kind: value (matches the manifest
filename, the library entry id prefix `context-pack:`, and the
CLI verb family `rig context-pack`). v0+1 won't drift.
Tests
- daemon: 2971/2971 (15 new in context-pack-startup-files.test.ts:
6 normalizer cases + 5 validator cases + 4 expandContextPacks
cases including missing-pack honest error, missing-library
honest error, and back-compat with kind: file entries).
- cli: 842/842 unchanged.
- ui: 543/543 unchanged.
- tsc clean across all three packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PRD: slices/rig-context-composable-injection-v0/IMPLEMENTATION-PRD.md.
New typed primitive parallel to skills + workflow_specs.
Filesystem-canonical at ~/.openrig/context-packs/<name>/ +
workspace-local .openrig/context-packs/<name>/. NO new SQLite tables,
NO new migrations, NO new event types.
Daemon (Items 1, 2, 5)
- New domain/context-packs/{context-pack-types, manifest-parser,
context-pack-library-service, bundle-assembler}.ts.
- Manifest schema: name + version + purpose + files[{path, role,
summary?}] + estimated_tokens? — guards against ../-escapes,
absolute paths, unsupported suffixes (.md/.markdown/.yaml/.yml/.txt).
- Library service walks discovery roots in workspace > user_file >
builtin order (last-wins on collision); workspace-surface
reconciliation via re-scan (operator filesystem edit always wins).
- Bundle assembler concatenates files with `## File: <path> (role:
<role>)` framing into ONE coherent paste-ready string. Missing
files surfaced as warning instead of hard fail. Token estimate via
the existing chars/4 heuristic.
- New /api/context-packs/{library, library/sync, library/:id,
library/:id/preview, library/:id/send} routes. send invokes
SessionTransport.send (one paste, NOT N sends — matches PRD § Item
5 "single coherent priming injection"); supports dryRun for the
UI's preview-before-send flow.
CLI (Item 7)
- New `rig context-pack` family: list / show / preview / sync / add /
send. add installs from a local directory under
~/.openrig/context-packs/, refuses symlinks (parallel to `rig specs
add` contract), and triggers a daemon sync after install.
UI (Items 3, 4)
- SpecsPanel adds 6th filter chip "context-packs" + PACK badge +
per-row summary (files-count + token estimate).
- LibraryReview dispatches to a new ContextPack review variant for
context-pack:* ids: header + 4-card summary grid + per-file list
with role/size/token estimate (MISSING marker for absent files) +
source-path display + "Send to seat" modal with running-session
picker + Dry-run / Send buttons + bundle preview pre-block.
- New useContextPackLibrary / useContextPackPreview /
useContextPackSend / useContextPackSync hooks.
Item 6 carve-out (NAMED v0+1 trigger)
- AgentSpec startup_files extension (kind: context_pack as a startup-
file source) — DEFERRED. PRD § Bounce Conditions allows this with
named v0+1 trigger; founder dispatch was hands-off and didn't carve
it out explicitly. Operator can still send packs to a fresh seat
via `rig context-pack send` from a startup script or interactive
command — the headline operator value (typed primitive + library +
review + send) ships fully without it.
v0+1 NAMED trigger: operator dogfood reports declaring kind:
context_pack in agent.yaml startup_files would be friction over
invoking rig context-pack send manually after launch.
PRD § from_addresses extension is gated by PL-022 markdown-addressing
(PARKED) — out of v0 per dispatch.
Cross-CLI-version drift handling
- UI useContextPackLibrary returns [] on 503 + Array.isArray guard
on the body so older daemons without the route surface as "no
packs in library" rather than crashing the SpecsPanel.
Tests
- daemon: 2956/2956 (12 manifest-parser + 11 library-service + 5
bundle-assembler + 11 routes = 39 new)
- cli: 842/842 (9 new context-pack subcommand tests)
- ui: 543/543 (2 new SpecsPanel filter tests + 2 new LibraryReview
context-pack variant tests)
- tsc clean across all three packages
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PRD: slices/token-context-usage-surface-v0/IMPLEMENTATION-PRD.md.
Surfaces the already-shipped context_usage daemon primitive across
six places: topology graph header, rig ps CLI, /context dashboard
route, Steering health gates click-through, whoami runtimeContext
block, and honest cross-CLI-version drift fallback.
Auto-compact OUT per founder safety stop directive — surface
visibility only, no auto-compaction triggers.
Daemon (Item 4 — Codex/Claude Code runtimeContext)
- whoami-service.ts WhoamiResult extends with runtimeContext block:
Codex (threadId/conversationId/estimatedTokens — threadId null at
v0 since pid-keyed lookup needs more plumbing; NAMED v0+1 trigger),
Claude Code (resumeToken from sessions.resume_token + estimatedTokens
+ lastSampledAt), terminal/unknown → null. Honest degradation when
data unavailable.
CLI (Item 2 — rig ps --nodes CTX column + filter)
- ps.ts NodeEntry extended with contextUsage block (passed through
from /api/rigs/:id/nodes; daemon already populates).
- ALLOWED_FILTER_KEYS adds contextUsage.percent (numeric) +
contextUsage.state (enum critical|warning|low|unknown — lockstep
with daemon's computeContextHealthSummary tier vocab).
- parseFilter extended with comparator parsing for numeric keys
(>=, >, <=, <, =); three-part error on non-numeric values + bogus
states + numeric op on non-numeric key.
- New CTX column in human output (5-char "<percent>%" or "??") plus
--fields contextUsage projection.
- Final canonical filter syntax locked at audit per identifier-drift
discipline: contextUsage.percent>=N (not =N).
UI (Items 1, 3, 5)
- New ContextUsageRing component: hollow ring parallel to PL-019's
filled activity dot. Two signals at the same scale (working vs
context-pressure). Tier thresholds lockstep with daemon (≥80
critical, ≥60 warning, else low; unknown → dotted gray).
- RigNode header gets the ring next to the activity dot.
- New /context dashboard route + Gauge sidebar tab + ContextWorkspace
component: header summary (total + tier counts), filter chips
(tier/runtime/rig), per-seat list sorted descending by percent,
refresh button, "Compact cmd" button copies "rig compact-plan
--rig <rig>" (read-only at v0; compact-trigger from UI is NAMED
v0+1 trigger per PRD).
- New useContextFleet hook composes /api/ps + per-rig /nodes with
15-second refetch interval.
- HealthGatesPanel: context-tier counts now click-through to /context
filtered by tier; "Open dashboard →" link added; uses Tanstack
router Link with search params validated by validateSearch on
the contextRoute.
Cross-CLI-version drift (Item 6)
- ContextWorkspace surfaces honest "Could not load … use rig context
--json from terminal" when /api/ps errors.
- contextUsage block on NodeEntry is optional; older daemons that
don't emit it leave the CTX column "??" (the unknown placeholder)
and contextUsage.* filters return false (operator can use
contextUsage.state=unknown to find them).
NO new SQLite schema, migrations, or event types.
Tests
- cli: 836/836 (10 new in ps-pl012-context-usage.test.ts; 1 existing
test updated to match the new --filter error message)
- daemon: 2903/2903 (4 new whoami runtimeContext cases)
- ui: 546/546 (10 new ContextUsageRing tests + 6 new
ContextWorkspace tests)
- tsc clean across all three packages
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PL-018. PRD: slices/preview-terminal-v0/IMPLEMENTATION-PRD.md.
Settings (Item 1, lockstep with cli ConfigStore + daemon SettingsStore)
- New keys: ui.preview.refresh_interval_seconds (3), ui.preview.max_pins
(4), ui.preview.default_lines (50). Final canonical names locked at
audit per identifier-drift discipline. RiggedConfig.ui.preview shape
exposed.
Daemon
- New domain/preview/preview-rate-limiter.ts: 1-second per-(session,
lines) cache window so multiple pinned panes for the same seat
collapse to one tmux capture without throttling legitimate polling.
- New GET /api/rigs/:rigId/nodes/:logicalId/preview?lines=N route +
parallel GET /api/sessions/:sessionName/preview alias for surfaces
with sessionName but no rigId/logicalId. Wraps the existing
SessionTransport.capture; rate-limited; returns structured 503/404/
409/502 with hints on the unhappy paths.
- startup.ts wires PreviewRateLimiter; server.ts wires it into context.
UI (Item 1 + Item 2 + Item 3)
- New PreviewPane component: live preview content + Pin/Unpin button +
honest unavailable fallback. Polls at ui.preview.refresh_interval_seconds.
- New SessionPreviewPane: session-keyed variant for Loop State / Topology
tab compositions (no Pin button at v0; pin from drawer).
- New PreviewStack: pinned-preview side rail mounted in AppShell;
self-hides when no pins. Cap enforced from ui.preview.max_pins.
- New preview-pin-store + usePreviewPins (useSyncExternalStore-backed,
no Zustand dep needed for ~50 lines).
- New useNodePreview / useSessionPreview hooks consume settings for
refresh interval + default lines.
- NodeDetailPanel gets a Preview section above Actions (CMUX launcher
button already shipped at line 181 — Item 2 reuses it; the new code
is light composition, not a rewrite).
- LoopStatePanel rows become click-to-toggle buttons that expand inline
SessionPreviewPane below the row (single-row-open at a time).
- TopologyTab seat list: each session name becomes a click-to-toggle
button that expands inline SessionPreviewPane below it.
Cross-CLI-version drift (Item 4)
- 404/409/502/503 from the daemon route map cleanly to a structured
"Preview unavailable: <reason>" UI panel that suggests `rig capture
<session>` from terminal as a fallback.
Identifier-drift discipline
- ui.preview.* keys NOT pre-added in user-settings-v0 (audit-confirmed
not on main); added here at PL-018 ship time as the consumer slice.
PRD § Item 1's draft list of future-slice settings is correctly
applied at-ship (not at-author) per memory feedback_next_available_n
_drift_in_parallel_slices.
NO new SQLite schema, migrations, or event types.
Tests
- daemon: 2917/2917 (6 new in preview-rate-limiter.test.ts + 12 new in
preview-routes.test.ts; user-settings tests updated for 15-key set).
- cli: 833/833 (7 new ui.preview.* tests in config-store-extended.test.ts).
- ui: 539/539 (9 new in preview-pane.test.tsx covering PreviewPane,
PreviewStack, pin store cap).
- tsc clean across all three packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PRD § Item 1 added workspace.slices_root as a typed setting key with
env > file > default resolution via SettingsStore. PRD § Scenario B
explicitly required: set workspace.slices_root via UI/CLI, restart,
Slice Story View consumes the new path.
Driver wired files.allowlist + progress.scan_roots through SettingsStore
correctly, but the slice indexer's startup wiring at startup.ts:706
still read OPENRIG_SLICES_ROOT directly via readOpenRigEnv. Result:
operators setting workspace.slices_root via CLI or UI saw the setting
in /api/config (source: file) but Slice Story View kept reading the
empty env-var slot — Scenario B was unfulfillable.
Fix: prefer the legacy short OPENRIG_SLICES_ROOT env var if explicitly
set (preserves backward-compat with existing dogfood/test daemons that
inherit it from their startup env), otherwise fall back to the
SettingsStore-resolved workspace.slices_root. SettingsStore is
constructed locally in the slices block to avoid restructuring the
larger startup ordering (the User Settings v0 wiring block constructs
its own SettingsStore later — both reads are cheap; same singleton
config.json read).
Verified post-fix: setting workspace.slices_root via UI Edit form +
restart → /api/slices returns 90 slices from founder's substrate
layout instead of "slices_root_not_configured" 503.
Surfaced during the User Settings v0 headed-browser dogfood at be2a239
(review-lead owned the fix-and-retest end-to-end with full autonomy
per dispatch).
Caveat (not addressed in this fix): the steering composer
(daemon/src/domain/steering/steering-composer.ts) still reads
OPENRIG_STEERING_WORKSPACE / OPENRIG_STEERING_PATH /
OPENRIG_ROADMAP_PATH / OPENRIG_DELIVERY_READY_DIR env vars directly
inside the composer (not via SettingsStore). Setting workspace.steering_path
via CLI/UI doesn't flow to the steering surface. Same gap pattern;
larger fix (composer needs to accept settings + reload-on-change).
v0+1 named trigger: founder dogfood reports steering surface
ignoring workspace.steering_path setting.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
WorkflowTopologyGraph used a BFS depth-assignment with a `< d+1`
relaxation condition that re-queues nodes whenever a longer path
to them is discovered. Cyclic specs (e.g. rsi-v2-hot-potato has a
qa → discovery loop edge) trigger an infinite loop:
discovery=0 → ... → qa=4 → discovery=5 (re-queued because 0 < 5)
→ ... → qa=N → discovery=N+1 (forever)
Result: the WorkflowTopologyGraph useMemo callback never returned;
the React render hung indefinitely; the /specs/library/workflow:NAME:V
review page was unreachable. The hang surfaced during the v0 headed-
browser dogfood when navigating to the rsi-v2-hot-potato review pane.
Switch to standard BFS visit-once semantics: only assign a child's
depth on first visit. Produces the same shortest-path-from-entry
depth assignment for acyclic specs and terminates correctly on
cyclic specs (the loop edge gets drawn at the previously-assigned
depth, which is exactly what the layout wants).
Surfaced during the Workflows-in-Spec-Library v0 headed-browser
dogfood at ff531ac (review-lead owned the fix-and-retest end-to-end
per feedback_qa_owns_dogfood_fix_loop). 5 library-review tests still
pass (existing fixtures are acyclic; recommend a cyclic-spec fixture
test in v0+1).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Item 2's per-level Priority Rail Rule visualization classifies STEERING.md as
the `steering` level ("Constraint" label), but the indexer's tree walk only
included files matching `PROGRESS.md`. Result: the classifier had a level
that never appeared in the rendered tree because STEERING.md never made it
into the file list.
Extend the tree walk to also pick up STEERING.md. The row-counts +
parsing pipeline is filename-agnostic and treats it the same as any
other rail markdown.
Surfaced during the OSR v0 headed-browser dogfood at 299b4a0 (review-lead
owned the fix-and-retest end-to-end per feedback_qa_owns_dogfood_fix_loop).
Existing 12 progress-indexer tests pass unchanged; no daemon test had
exercised STEERING.md vs PROGRESS.md filename in fixture data.
Caveat: visual top-most-node ordering (per PRD § Item 2) is not addressed
here — files still sort alphabetically by relPath. The Constraint level
chip is correct; visual first-row positioning would require sorting by
`[level.order, relPath]` in the ProgressWorkspace render and is left as
a v0+1 polish nit.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Operator Surface Reconciliation v0 — synthesizes PL-006 Operator
Status / Steering Surface + PL-008 Progress.md Project Management View
+ PL-009 Markdown Viewer / File Browser / Editor against shipped
Slice Story View v0+v1 + UI Enhancement Pack v0. One ambitious
vertical with six items; HEADLINE is the one-screen composed steering
surface (PL-006 v0 claim). NO new schema, NO migrations, NO new
event types — composes existing surfaces + adds small new routes
(steering composition, health-summary adapters) + adds small new UI
panels and extensions.
Item 1 — Steering surface (HEADLINE):
- New /steering route + Steering Link (Compass icon) in AppShell.
- New SteeringWorkspace.tsx with six panels rendered as a single
scrollable viewport.
- A. PriorityStackPanel — verbatim STEERING.md via MarkdownViewer
(hideRawToggle so the panel chrome doesn't compete) + last-modified
timestamp.
- B. RoadmapRailPanel — PL-XXX checklist + railItemCode extraction +
next-unchecked marker on first non-done row.
- C. InMotionPanel — composes PL-005 Mission Control's
/api/mission-control/views/recently-active filtered to spec-related
qitems (body/destination heuristic; PRD § Item 1C "driver picks
the matching strategy").
- D. LaneRailsPanel — per-lane (delivery-ready/mode-{0..3}) top-N
items + active/blocked/done health badges + next-pull marker.
- E. LoopStatePanel — cross-rig roll-up of recent qitems by seat via
the same recently-active view.
- F. HealthGatesPanel — node + context summaries via new
/api/health-summary/* routes; Stat badges color-coded (running →
emerald, warning → amber, attention/critical → red bold).
- New daemon route GET /api/steering returns priority-stack + roadmap
+ lane-rails in one payload (in-motion / loop-state / health
fetched separately by the UI to keep the composer narrow + testable).
- New SteeringComposer (domain/steering/steering-composer.ts) reads
STEERING.md + roadmap PROGRESS.md + delivery-ready/mode-*/
PROGRESS.md from a configured workspace root. Per-section overrides
trump workspace defaults. Reuses UI Enhancement Pack v0's
ProgressIndexer for parsing so the lane health badges match the
Progress workspace's checkbox semantics exactly.
- New /api/health-summary/{nodes,context} routes (domain/steering/
health-summary.ts) wrap getNodeInventory + raw context_usage
aggregation. Daemon-side, not CLI shell-out — same data, cheaper.
- Phone-friendly content model 9 fields preserved through PL-005
view consumers.
Item 2 — Priority Rail Rule visualization (PL-008 differentiator):
- New components/progress/priority-rail-rule.ts:
classifyPriorityRailLevel maps PROGRESS.md path → STEERING /
mission / lane / slice / intermediate via regex set; getPriority
RailLevelStyle returns label + chip class; computeNextPullLine
finds first non-done, non-blocked checkbox row per Priority Rail
Rule semantics (workstream-continuity convention).
- ProgressWorkspace.tsx extended: per-file level chip in section
header (Constraint / Mission / Lane / Slice / Cursor); next-pull
marker on the row that first qualifies for each lane file;
level-aware data-rail-level attribute on each section.
Item 3 — YAML schema validation in Files browser:
- FilesWorkspace.tsx extended: detectSpecKind matches
rig.yaml / agent.yaml basenames; SpecValidationPanel invokes the
existing /api/specs/review/{rig|agent} endpoint via the new
useSpecReview hook + renders inline validation results above the
YAML view. Non-spec YAML files don't trigger the panel.
Item 4 — Markdown viewer raw/rendered toggle:
- MarkdownViewer.tsx extended: header-bar 2-button toggle (rendered
default ⇄ raw); raw mode renders content as monospace `<pre>`
with full Markdown source visible; frontmatter metadata header
still renders in raw mode. New hideRawToggle prop suppresses the
toggle for callers (e.g. PriorityStackPanel) that don't want the
chrome.
Item 5 — File-read truncation marker:
- daemon/src/routes/files.ts /read extended: caps returned content
at FILE_READ_TRUNCATION_BYTES (1 MB per PRD § Item 5; founder Q6
option b); response includes truncated / truncatedAtBytes /
totalBytes fields when capping. Hash is computed over the FULL
file so edit-mode atomic-write conflict detection stays honest
even when the read was truncated.
- FilesWorkspace.tsx: TruncationMarker component renders an
amber-bordered banner above any truncated file body with
"use external editor for full content" hint.
Item 6 — PROGRESS.md lint warnings (PL-008 Phase B):
- computeLintWarnings (in priority-rail-rule.ts) ships 3 of 4 PRD
rules at v0:
- rule 1 long-row: row > 160 chars → per-row warning;
- rule 2 missing-tree: parent file with no Tree/Hierarchy/
Topology heading → file-scope warning;
- rule 4 qitem-no-label: row body matches bare qitem id pattern
→ per-row warning.
Rule 3 (narrative-mixed) ships active-but-quiet at v0 — the
ProgressIndexer drops non-checkbox / non-heading lines so the
heuristic can never fire; explicit known limitation recorded in
comment with v0+1 promotion path (teach the indexer to retain
narrative lines).
- ProgressWorkspace.tsx extended: header gets Show-lint toggle
(default on); per-file lint count badge + file-scope warning list
rendered below header; per-row warnings render inline next to
their row.
PRD pivots / scope decisions recorded in commit + handoff:
- env-var pivot inherited from UI Enhancement Pack v0 memory:
OPENRIG_STEERING_WORKSPACE + OPENRIG_STEERING_PATH /
OPENRIG_ROADMAP_PATH / OPENRIG_DELIVERY_READY_DIR for non-canonical
layouts. ConfigStore's strict VALID_KEYS doesn't admit dynamic
family keys; env-var is the established MVP pattern.
- In-motion + loop-state panels fetch PL-005 views separately rather
than the steering composer making internal HTTP calls — keeps
the composer narrow + testable + reuses the existing PL-005 view
contract verbatim.
- Lint rule 3 (narrative-mixed) ships active-but-quiet pending
ProgressIndexer extension for narrative-line retention; recorded
with explicit v0+1 promotion path in priority-rail-rule.ts comment.
Verification:
- daemon: 2846/2846 (was 2796 baseline at af4e220 + 50 new across 3
test files: 12 steering-composer + 5 health-summary + 3
files-truncation, plus ~30 test rebalancing through
createFullTestDb usage).
- ui: 518/518 (was 478 baseline + 40 new across 3 test files: 17
priority-rail-rule + 5 markdown-viewer-raw-toggle + 8
steering-panels, plus rebalancing).
- cli: untouched by this slice; baseline 800.
- tsc clean for daemon + cli + ui.
Boundaries preserved per PRD § No-Touch Surfaces:
- All PL-004 schema (queue_*, workflow_*, watchdog_*) — read-only.
- All PL-005 surfaces (Mission Control views, verbs, action log) —
read-only consumer; no modifications.
- PL-019's RigNode / Explorer / graph-projection / edge animation
— untouched.
- Slice Story View v0's tabs (Story / Decisions / Tests-Verification
/ Topology) — untouched.
- Slice Story View v1's spec-driven rendering — untouched.
- UI Enhancement Pack v0's /progress core walk + /files core route
+ MarkdownViewer core render + edit mode atomic-write contract —
read-only consumer + small additive extensions only; core write
contract unchanged. Acceptance tab from Slice Story View v0+v1
also untouched (item 1A in PRD scope reused PROGRESS-tree
rendering, not Acceptance).
- Existing 33+ migrations on main — untouched. v0 adds NO migrations.
- Existing event bus types — no extensions.
- AppShell entries other than the new Steering Link — untouched.
- STEERING.md / roadmap/PROGRESS.md / delivery-ready/*/PROGRESS.md /
mission PROGRESS.md — read-only consumer.
Operator setup hint for QA dogfood (recorded in handoff):
export OPENRIG_FILES_ALLOWLIST="workspace:/Users/wrandom/code/substrate/shared-docs/openrig-work,hub:/Users/wrandom/code/projects/openrig-hub"
export OPENRIG_PROGRESS_SCAN_ROOTS="workspace:/Users/wrandom/code/substrate/shared-docs/openrig-work"
export OPENRIG_STEERING_WORKSPACE="/Users/wrandom/code/substrate/shared-docs/openrig-work"
rig daemon restart
Hard-gate (operator dogfood per PRD audit row 14) is QA-owned per
feedback_qa_owns_dogfood_fix_loop.md. Worktree retained for guard
inspection.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The Files workspace edit-mode conflict UX was broken: on a 409,
useFilesWrite invalidated the read query unconditionally; the
re-fetched read fired the editor's useEffect, which silently
overwrote the operator's draft AND cleared the conflict banner
that had just been set. Net effect: operator clicks Save against
a stale mtime/contentHash → daemon correctly returns 409 → UI
silently swaps the textarea content for the new server content
and shows no error. The operator's edits are lost without warning.
Two-part fix:
- useFilesWrite no longer invalidates queries on a conflict result.
The read query stays at the operator's last-known mtime/contentHash
so the conflict banner can render and the draft survives long
enough for the operator to act on it.
- The conflict banner now includes an explicit Refresh button that
invalidates the read query manually. Clicking it triggers the
existing useEffect which loads the new server content and clears
the conflict.
Also tightened the banner copy to match the actual Refresh semantics
(the operator's draft is replaced on Refresh; copy elsewhere first
if re-application is needed) — the previous copy promised
preservation that the implementation didn't honor.
Surfaced during the UEP v0 headed-browser dogfood at 1a12230 on
the conflict scenario in PRD § Item 4 (review-lead owned the
fix-and-retest end-to-end per feedback_qa_owns_dogfood_fix_loop).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
FileWriteService's DEFAULT_AUDIT_FILE used process.env.HOME directly,
so isolated dogfood/test daemons (with custom OPENRIG_HOME) wrote
audit rows back to the host's ~/.openrig/file-edit-audit.jsonl —
breaking state isolation.
Wire startup.ts to pass auditFilePath built from the resolved
OPENRIG_HOME constant, matching how ContextUsageStore + ClaudeCodeAdapter
already isolate their per-instance state.
Surfaced during the UEP v0 headed-browser dogfood at 1a12230 (review-lead
owned the fix-and-retest end-to-end per feedback_qa_owns_dogfood_fix_loop).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
UI Enhancement Pack v0 — operator gets a substantially better UI for
browsing + viewing + editing workspace canon files. Four items ship as
one ambitious vertical because they share infrastructure (allowlist +
path-safety daemon-side; markdown rendering UI-side). NO new schema,
NO migrations, NO new event types.
Item 1A — Acceptance tab enhancement (Slice Story View v0 extension):
- Checkbox pills (rounded with status icon: ✓ done / ◯ active /
⚠ blocked) instead of raw [ ]/[x] syntax.
- 4-filter chip row (All / Active / Done / Blocked; default All).
- Click-to-expand row detail panel with prominent file:line citation.
- v0's PROGRESS.md parser still drives the row list; this slice
enhances the rendering layer only.
Item 1B — Top-level Progress browse view:
- New /progress route + Progress sidebar Link (CheckSquare icon)
alongside the existing Slices entry.
- Daemon: new domain/progress/progress-indexer.ts walks
OPENRIG_PROGRESS_SCAN_ROOTS (env-configured, comma-separated
name:path pairs), parses PROGRESS.md files with checkbox-hierarchy
awareness ([ ]/[x]/[~] checkboxes + ## / ### / #### headings +
YAML frontmatter strip + 2-space-indent depth + skip-dirs for
node_modules/.git/.worktrees/dist), and returns aggregate counts
per file + workspace-wide.
- New routes/progress.ts: GET /api/progress/tree returns
{ files, aggregate, scannedRoots } or 503 with structured
setup hint when no scan roots are configured.
- New ProgressWorkspace.tsx with two-pane layout: tree pane (per-
file collapsible sections with checkbox pills + status icons) +
inline detail panel on row click. Filter row (status: all/active/
done/blocked) + free-text search across rows. Read-only at v0
(edit is item 4 territory).
Item 2 — Markdown viewer expansion:
- New components/markdown/MarkdownViewer.tsx + SyntaxHighlight.tsx —
written from scratch (~500 lines total, no new dependencies). Per
the audit: no markdown / highlighting libs are installed in the UI
package; the inline parser keeps the bundle weight unchanged.
- Handles: YAML frontmatter (rendered as metadata header above body),
headings (# / ## / ### / ####), bullet + ordered lists with
indentation depth, fenced code blocks with per-language syntax
highlighting, inline code with backticks, links + images with
relative-src resolution against assetBasePath, tables (| col |
syntax with header + body rows + zebra striping), bold (**) and
italic (*) inline emphasis.
- Per-language tokenizer covers ts/tsx/js/jsx, py/python, bash/sh,
sql, yaml/yml, json. Token classes: keyword (violet), string
(emerald), comment (stone-500 italic), number (amber), tag (sky),
punctuation (stone-400). Unknown languages render as plain text
in the dark code panel.
- Item 2 mermaid carve-out activated: NO mermaid library bundled at
v0. Mermaid code blocks render as a labeled placeholder with a
disabled '[render mermaid] (v0+1)' button. NAMED v0+1 trigger:
"operator dogfood reports the click-to-render flow is friction"
— recorded in commit + handoff per PRD § Item 2.
- DocsTab.tsx refactored to consume MarkdownViewer for .md files;
non-.md files keep the v0 plain-`<pre>` fallback. Slice Story
View v0's docs UX is preserved + extended.
Item 3 — File browser (allowlisted, daemon-enforced path-safety):
- Driver pivot from PRD's suggested ConfigStore key family
(files.allowlist.<name>): the existing ConfigStore uses a strict
VALID_KEYS allowlist that doesn't admit dynamic key families
cleanly. Switched to env-var approach:
OPENRIG_FILES_ALLOWLIST=name1:/abs/path,name2:/abs/path. Same
delimited-pair shape as OPENRIG_PROGRESS_SCAN_ROOTS for
consistency. Fail-closed default: empty/unset → routes return
503 with structured setup hint.
- New domain/files/path-safety.ts: shared resolver with three
guarantees — '..' segment rejection (literal path-component, not
substring; foo..bar.md is allowed), absolute-path-as-relative
rejection, and symlink-realpath check ensuring no resolved file
escapes the canonical root via path.sep boundary (prevents the
/foo/bar matching /foo/bar-other false-positive).
- New routes/files.ts: GET /roots, GET /list, GET /read,
GET /asset, POST /write. Path-safety errors map to structured
4xx codes (root_unknown / path_invalid / path_escape /
not_a_file / not_a_directory / stat_failed) so the UI can
render specific error messages.
- /asset infers Content-Type from extension (image/png /
image/jpeg / image/svg+xml / video/mp4 / etc.) for embedded
image rendering in MarkdownViewer.
- New FilesWorkspace.tsx + useFiles.ts hooks: two-pane layout
(allowlist root selector + breadcrumbs + directory tree on left;
file content panel on right). Auto-selects first root; resets
path + selection on root change. Up-directory affordance.
Content-type-aware rendering: .md → MarkdownViewer; text-like →
SyntaxHighlight; image → inline `<img>` via /api/files/asset;
download-only types (.zip, .pdf, .mp4) → download link;
unknown → text fallback.
Item 4 — Optional edit mode (atomic write + JSONL audit):
- Per PRD's recommended landing posture: lightweight `<textarea>`
edit (no CodeMirror dependency) + daemon write route +
audit JSONL. The atomic-write contract is the load-bearing
primitive; the UI shell is intentionally minimalist.
- New domain/files/file-write-service.ts: writeAtomic({...}):
resolves path-safety → re-stats target → re-hashes via SHA-256 →
rejects with WriteConflictError carrying current{Mtime,
ContentHash} on mismatch (409) → writes to temp file in same
directory → fsync → atomic rename → re-stats for new mtime/
contentHash → appends one JSONL row to ~/.openrig/file-edit-
audit.jsonl with ts, actor, root, path, prev/new mtime + hash,
byteCountDelta. Audit-write failure surfaces as a separate
error code (write succeeded; audit append did not — operator
can re-derive from filesystem state). PRD's no-clobber-on-
concurrent-edit invariant pinned by tests.
- POST /api/files/write integrated into routes/files.ts; conflict
path returns 409 with the daemon's current state for the UI
to surface a Refresh affordance.
- FilesWorkspace edit mode: header-bar Edit toggle (default off);
toggling on flips the right pane to a `<textarea>` editor with
Save / Cancel / status indicator (draft (unsaved) / no changes /
saved). Save submits POST /write with last-known mtime +
contentHash; 409 conflict surfaces inline with current
daemon-side mtime; Cancel discards the draft and re-reads.
- Driver carve-out NOT invoked: edit mode ships as the PRD's
recommended in-scope item. NAMED v0+1 trigger left available
for future scope-down if dogfood reveals friction (per
PRD § Item 4 Driver carve-out).
AppShell + routes wiring:
- New header Links: Progress (CheckSquare icon) + Files (Folder
icon) alongside Slices (Layers). Surface-title resolver
recognizes /progress + /files paths.
- routes.tsx: register progressRoute (/progress) + filesRoute
(/files) on the existing root tree.
PRD reinterpretations recorded:
- Item 3 allowlist source: env-var (OPENRIG_FILES_ALLOWLIST) instead
of ConfigStore key family — driver pivot to fit existing
ConfigStore's strict VALID_KEYS allowlist.
- Item 1B scan roots: env-var (OPENRIG_PROGRESS_SCAN_ROOTS) for
consistency with the same delimited-pair shape.
- Item 2 mermaid carve-out activated per PRD: NO mermaid bundled;
placeholder + disabled button + named v0+1 trigger.
Verification:
- daemon: 2796/2796 (was 2750 baseline at 6097f4e + 46 new across
3 new test files: 18 files-path-safety + 15 files-routes + 12
progress-indexer + 1 from setup). Path-safety tests pin '..'
segment rejection vs filename-substring, absolute-path
rejection, symlink-escape rejection, env-var fallback chain
(empty-string treats as missing for legacy fallback). Files-
routes tests pin 503 graceful, list+read+asset round-trip,
atomic-write success + 409 conflict path, audit JSONL append.
Progress indexer tests pin checkbox state parsing (active/done/
blocked), heading depth derivation, indent depth derivation,
frontmatter strip, skip-dirs, max-depth, aggregate counts.
- ui: 478/478 (was 458 baseline + 20 new across 2 test files:
13 markdown-viewer + 7 acceptance-tab-enhancement). Markdown
tests pin frontmatter rendering, headings/lists/code/inline-
code/links/images/tables/bold/italic, mermaid carve-out
placeholder. Acceptance tests pin pill rendering, filter row,
filtering behavior, expand/collapse, v0-baseline preservation.
- cli: untouched by this slice; baseline 800.
- tsc clean for daemon + cli + ui.
Boundaries preserved:
- Worktree-only on ui-enhancement-pack-v0; main untouched at 6097f4e.
- NO new schema, NO migrations, NO new event types.
- Slice Story View v0's StoryTab / DecisionsTab / TestsVerificationTab
/ TopologyTab — untouched. Acceptance + Docs tabs extended only.
- v1 in-flight (separate worktree at 7bb892e): non-overlap on
load-bearing files; AcceptanceTab will need a clean merge when
v1 lands (v1 adds CurrentStepPanel ABOVE the checkbox list; this
slice extends the checkbox list rendering — surgical merge).
- PL-019 surfaces (RigNode/Explorer/graph-projection/edge animation)
— untouched.
- PL-004 Phase A/B/C/D — read-only consumers; no modifications.
- PL-005 Mission Control — untouched.
- ConfigStore (cli/src/config-store.ts) — untouched per the
env-var pivot.
- All migrations on main — untouched.
- AppShell entries other than the new Progress + Files Links —
untouched.
Operator setup hint for QA dogfood (orch routes to QA):
export OPENRIG_FILES_ALLOWLIST="workspace:/Users/wrandom/code/substrate/shared-docs/openrig-work,hub:/Users/wrandom/code/projects/openrig-hub"
export OPENRIG_PROGRESS_SCAN_ROOTS="workspace:/Users/wrandom/code/substrate/shared-docs/openrig-work"
rig daemon restart
Then:
- Header → Slices/Progress/Files (3 sidebar Links).
- /progress: tree of PROGRESS.md across the workspace with
filter + search + click-to-expand detail.
- /files: allowlist roots + directory tree + content panel
(markdown rendered via MarkdownViewer; code via
SyntaxHighlight; images inline; download-only fallback for
zip/pdf/video).
- /files edit mode: toggle Edit → save STEERING.md inline →
audit row appears in ~/.openrig/file-edit-audit.jsonl →
concurrent edit from a separate terminal triggers 409
Conflict with Refresh affordance.
Hard-gate (operator dogfood per PRD audit row 12) is QA-owned
per feedback_qa_owns_dogfood_fix_loop.md. Worktree retained for
guard inspection.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Slice Story View v1 — when a workflow_instance is bound to a slice, the
Slice Story View renders spec-driven UI: Topology shows the workflow_spec
graph, Story uses spec-defined phase tagging from workflow_step_trails,
Acceptance shows the live frontier + allowed next steps. Same UI works
for any spec, not just RSI v2. Four dimensions ship together as one
ambitious vertical. NO new schema, NO migrations, NO new event types —
consumes already-shipped workflow_specs / workflow_instances /
workflow_step_trails.
Daemon (4 new files + 3 edits):
- domain/workflow/slice-workflow-binding.ts (NEW): findSliceWorkflowBinding
resolves workflow_instance(s) touching a slice's qitem set via UNION
of two signals — workflow_step_trails.prior_qitem_id/next_qitem_id IN
slice qitems (historical) + workflow_instances.current_frontier_json
LIKE %qitem% (live). Returns most-recent-by-created_at as primary;
exposes additionalInstanceIds for "+N more" UI hint per PRD's multi-
instance carve-out. Graceful on malformed current_frontier_json
(parses to empty array).
- domain/workflow/slice-workflow-projection.ts (NEW): three projection
helpers consuming WorkflowSpec + bound instance state:
* projectSpecGraph — nodes per step + edges from each step's
next_hop.suggested_roles resolved to step ids; isCurrent /
isEntry / isTerminal flags; isLoopBack detected by declared step
order. v1 carve-out per audit row 6: every edge has
routingType="direct" because Phase D's WorkflowStepSpec has no
routing_type field — the routing-type metadata becomes v2+
enrichment.
* projectPhaseDefinitions — one entry per spec step; canonical id
= step.id; label = step.actor_role.
* projectCurrentStep — current_step_id resolves against spec;
returns step.objective + allowed_exits + allowed_next_steps
(next_hop suggested roles → step ids); null when current_step_id
is null (terminal) or doesn't resolve.
- domain/slices/slice-detail-projector.ts (EDITED):
* StoryEvent.phase: was hardcoded enum
"discovery"|"product-lab"|"delivery"|"lifecycle"|"qa"|"other",
now `string | null` (open-ended; spec step.id when bound + qitem
maps to a workflow_step_trail; null otherwise).
* v0's classifyPhase() heuristic (which inferred phase from session
name substrings) is REMOVED. Per PRD § Write Set Sketch:
"v0 hardcoded phase taxonomy in StoryTab.tsx is REMOVED (replaced
by spec-driven or ungrouped)" — this is the one v0 surface v1
modifies, by deletion. Hardcoded phase tags for non-qitem events
("discovery" for doc.edited; "qa" for proof_packet.emitted) also
removed; those events are now phase=null (untagged).
* SliceDetailPayload extended with workflowBinding +
story.phaseDefinitions + acceptance.currentStep + topology.specGraph.
All v1 fields are null when no instance is bound (v0 fallback).
* project() resolves binding once, fetches the bound spec from
WorkflowSpecCache, builds a qitem→step trail map for phase
tagging, then composes the four v1 dimensions into the payload.
* Graceful degradation: when constructed without workflowSpecCache,
v1 fields stay null even when an instance is bound (v0 mode).
- src/startup.ts (EDITED): pass workflowRuntime.specCache to
SliceDetailProjector.
UI (1 new file + 4 edits):
- hooks/useSlices.ts (EDITED): SliceDetail extended with v1 types —
WorkflowBindingPayload, PhaseDefinition, CurrentStepPayload,
SpecGraphPayload (nodes + edges) — plus the new fields on the
story / acceptance / topology sub-objects.
- components/slices/tabs/StoryTab.tsx (EDITED): phase chip color
palette is now assigned by the spec's declared phase index
(emerald/amber/violet/sky/rose/fuchsia/cyan/orange cycle) — v0's
hardcoded RSI-v2 phase color map is GONE. Untagged events
(phase=null) render with the neutral stone palette + "untagged"
label. When phaseDefinitions is null (unbound slice), event phase
ids fall through to the chip label verbatim (graceful for
previously-bound-then-unbound slices).
- components/slices/tabs/AcceptanceTab.tsx (EDITED): renders Current
Step panel ABOVE the v0 PROGRESS.md checkbox list when
acceptance.currentStep is non-null. Panel shows step id + role +
objective + allowed exits + allowed next steps + hop count + status.
v0 checkbox view + progress bar + closure callout all unchanged.
- components/slices/tabs/TopologyTab.tsx (EDITED): renders spec graph
panel ABOVE the v0 per-rig listing when topology.specGraph is
non-null. Per-step node + outgoing edges with isCurrent / isEntry /
isTerminal badges; loop-back edges styled with ↺ glyph + "loop"
badge; forward edges with → glyph. Spec name + version surfaced in
the aggregate header. v0 per-rig listing unchanged.
- components/slices/SliceStoryView.tsx (EDITED): pass phaseDefinitions
to StoryTab.
PRD audit reinterpretations recorded:
- Row 6 (routing_type carve-out activated): Phase D's WorkflowStepSpec
has only `next_hop?: { mode, suggested_roles }` — no routing_type
field. PRD's explicit carve-out applies: dimension #4 ships as
default-styled solid edges only with routingType="direct" baked in.
Routing-type metadata becomes v2+ enrichment.
Verification:
- daemon: 2781/2781 (was 2750 baseline at 6097f4e + 30 new v1 tests
across 3 new test files: 9 binding + 16 projection + 5 e2e
projector. 1 v0 route test updated to include the new
workflowBinding key in the payload key-list assertion).
- ui: 468/468 (was 458 baseline + 10 new across 1 new test file:
3 StoryTab + 3 AcceptanceTab + 4 TopologyTab).
- cli: untouched by this slice; baseline 800.
- tsc clean for daemon + cli + ui.
Boundaries preserved per PRD § No-Touch Surfaces:
- v0's DecisionsTab, DocsTab, TestsVerificationTab — unchanged.
- v0's slice indexer + GET /api/slices list endpoint + sidebar — unchanged.
- v0's PROGRESS.md acceptance parsing — unchanged.
- PL-019's RigNode / Explorer / graph-projection / routes/rigs / ps
--filter / edge animation — unchanged. (v1 doesn't compose with
PL-019 inside the spec graph at v0-of-v1; that's a v2+ enhancement
per the spec graph's list-based renderer.)
- PL-004 Phase D's workflow-runtime / workflow-spec-cache /
workflow-projector / workflow-instance-store / workflow-step-trail-log
— read-only consumer; no modifications.
- All existing migrations on main — untouched. v1 adds NO migrations.
- All event bus types — no extensions. v1 reads existing events; does
not emit new ones.
The one v0 deletion: classifyPhase() heuristic + hardcoded RSI-v2
phase taxonomy in projector + StoryTab — replaced by spec-driven
trail mapping (when bound) or null (when unbound). Recorded
explicitly in commit per PRD § Write Set § No-Touch Surfaces.
Operator dogfood readiness (QA-owned per
feedback_qa_owns_dogfood_fix_loop.md):
1. Instantiate RSI v2 starter via `rig workflow instantiate
--spec-path <pkg>/dist/builtins/workflow-specs/rsi-v2-hot-potato.yaml
--root-objective "v1 dogfood" --created-by-session <my> ...`
2. Trigger queue activity touching the resulting workflow_instance's
qitems (any `rigx queue handoff` between the spec's roles).
3. Open the Slices sidebar, click the slice that intersects those
qitems → all six tabs should render the v1 enrichment:
- Topology: spec graph with 5 RSI v2 steps + loop-back edge
- Story: events grouped by Discovery/Product Lab/Delivery/
Lifecycle/QA spec phases (NOT v0 hardcoded names)
- Acceptance: Current Step panel above the PROGRESS.md
checkbox list with allowed exits + next steps
4. Open an unbound slice (no workflow_instance touches its qitems)
→ fallbacks: Topology shows per-rig listing, Story renders
ungrouped chronological, Acceptance shows checkbox list only.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
RSI v2 starter v0 — the canonical Discovery → Product Lab → Delivery →
Lifecycle → QA hot-potato loop ships as a built-in workflow_spec
out-of-box. Three components, single coherent vertical. NO new
schema, NO migrations, NO new event types — composes PL-004 Phase D's
existing workflow runtime + cache + reconciliation contract.
Component 1 — canonical RSI v2 spec file:
- packages/daemon/src/builtins/workflow-specs/rsi-v2-hot-potato.yaml
- 5 roles (discovery-router, product-lab-planner, delivery-driver,
lifecycle-router, qa-tester) with skill_refs + preferred_targets.
- 5 steps in loop order (discovery → product-lab → delivery →
lifecycle → qa); routing hints via next_hop.suggested_roles. Only
the qa step permits `done` as a terminal exit (no-follow-on
acceptance) — non-qa steps must handoff/waiting/failed. qa loops
back to discovery via next_hop on the followup-signal path.
- coordination_terminal_turn_rule: hot_potato (matches founder default).
- loop_guards.max_hops: 30 (conservative ceiling against routing-table
churn; normal loops close in 5-10 hops).
- Schema follows packages/daemon/src/domain/workflow-types.ts strictly:
top-level `workflow:` key, `id` (not `name`), `objective` (not
`purpose`), `roles` mapping (not array), step `actor_role` (not
`role`). PRD's draft content used field names that didn't match
Phase D's parser — re-authored against the actual schema.
Component 2 — startup loader:
- packages/daemon/src/domain/workflow/starter-spec-loader.ts
- loadStarterWorkflowSpecs walks builtinDir, parses each .yaml/.yml
via WorkflowSpecCache.parseWorkflowSpec, and seeds via
cache.readThrough(absPath) — but ONLY when no row exists for the
same (name, version). Idempotent on repeated startup.
- Workspace-surface reconciliation preserved: when an operator has
authored a competing spec at a workspace path with the same
(name, version), the loader SKIPS rather than clobbering. Test
pins this behavior end-to-end.
- defaultBuiltinSpecsDir() resolves from the loader file's own
import.meta.url (../../builtins/workflow-specs from src/dist root)
so it works in both dev (running from src/) and prod (running
from dist/). Daemon package.json build script copies the .yaml
file from src/builtins/workflow-specs/ to dist/builtins/workflow-specs/.
- Wired into startup.ts after WorkflowRuntime construction. Errors
are collected into the result for diagnostic logging (console.warn);
startup is NOT blocked by a malformed bundled spec.
- Surfaces the resolved builtinDir path on AppDeps.workflowBuiltinSpecsDir
so the routes layer can compute per-row isBuiltIn flags.
Component 3 — `rig workflow specs` subcommand + indicator:
- New WorkflowSpecCache.listAll() ordered by (name, version).
- New GET /api/workflow/specs route returning { specs: [...] } with
per-row isBuiltIn computed from sourcePath being strictly under
workflowBuiltinSpecsDir (path.sep boundary check rules out the
`/foo/bar-other` matches `/foo/bar` false-positive). Mounted
BEFORE /:instance_id catchall per Phase A R1 SSE route-order
lesson — pinned by a dedicated test.
- New `rig workflow specs [--json]` CLI subcommand. Sibling of the
existing `rig workflow validate / list / instantiate` — NOT a new
top-level command (PRD bounce condition was specifically against
`rig workflow starter-list`-style additions; `rig workflow specs`
is the natural complement to the existing instance-listing
`rig workflow list`).
- Human output: `<name> v<version>` with optional `(built-in)` suffix
+ indented purpose excerpt (120 chars) + source path.
- JSON output: full row with isBuiltIn boolean for agents.
PRD audit-row reinterpretations recorded in commit + handoff:
- Row 6: `rig workflow list` lists INSTANCES, not specs. Adding
`rig workflow specs` (sibling subcommand, not top-level) respects
the bounce condition while extending the workflow surface naturally.
- Row 2: PRD draft spec content used incompatible field names; the
shipped spec follows Phase D's actual parser shape strictly.
Verification:
- daemon: 2750/2750 (was 2723 baseline at 66796c2 + 27 new RSI-v2
cases across 3 new test files: 13 spec-content + 7 starter-loader +
7 specs-route).
- cli: 797/800 — same 3 pre-existing env-probe failures (preflight
+config) reproduce on baseline; no regression. Driver did not
modify those code paths.
- tsc clean for daemon + cli + ui.
- npm run build copies the YAML to dist/builtins/workflow-specs/
(verified post-build: rsi-v2-hot-potato.yaml present in dist/).
Boundaries preserved:
- Worktree-only on rsi-v2-starter-workflow-spec-v0; main untouched at 66796c2.
- NO new migrations, NO new schema, NO new event types.
- PL-004 Phase D's workflow_specs table + workflow-spec-cache.ts +
workflow-runtime.ts contracts are the destination — extended via
`WorkflowSpecCache.listAll()` (additive) and one new GET route
(additive); no existing surface modified.
- Workspace-surface reconciliation contract honored end-to-end:
loader skips already-cached (name, version) so operator overrides
are preserved.
- NO auto-instantiation on rig creation (PRD bounce); operator
instantiates explicitly via existing Phase D verb.
- NO multi-spec starter library; v0 ships ONE starter
(rsi-v2-hot-potato). Future starters are separate slices.
- NO production-shop language. NO new top-level CLI command.
- Slice Story View v0, PL-005, PL-019, PL-004 Phase A/B/C surfaces
all read-only.
Operator setup hint for QA dogfood:
- After `pnpm install` + `npm --workspaces run build`, start the daemon
and run:
rig workflow specs --json | jq '.specs[] | {name, version, isBuiltIn}'
Expect: rsi-v2-hot-potato v1 with isBuiltIn=true.
- Validate the spec:
rig workflow validate dist/builtins/workflow-specs/rsi-v2-hot-potato.yaml
Expect: passes Phase D's validator (no errors).
- Instantiate it:
rig workflow instantiate \\
--spec-path dist/builtins/workflow-specs/rsi-v2-hot-potato.yaml \\
--root-objective "smoke test" \\
--created-by-session <my-session> \\
--entry-owner-session <intake-router@my-rig>
Expect: workflow_instance created; current_step_id="discovery".
(Note: Phase D's instantiate verb takes specPath, not name+version
per the route signature — operator points it at the bundled spec
file directly.)
Hard-gate (operator dogfood per PRD audit row 7) is QA-owned per
feedback_qa_owns_dogfood_fix_loop.md. Worktree retained for guard
inspection.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Slice Story View v0 — operator clicks a slice in /slices and sees the
entire RSI lifecycle (Story / Acceptance / Decisions / Docs / Tests /
Topology) in one tabbed page. Single ambitious vertical; six tabs
shipped (none deferred). Read-only: NO new schema, NO migrations, NO
new event types — indexes + composes already-shipped data.
Daemon:
- New domain service `domain/slices/slice-indexer.ts`: walks the
configured slicesRoot, parses YAML-lite frontmatter, maps status to
active/done/blocked/draft, joins to queue_items by slice-name body
match + rail-item body match (token-aware), discovers
dogfood-evidence proof packets via TOKEN matching (slice tokens
with -vN stripped must all appear in dir name; handles real-world
shape `pl005-phase-a-mission-control-queue-observability-20260504`
even though slice folder uses suffix form), classifies markdown /
screenshots / videos / traces, and emits a normalized SliceRecord.
In-memory cache with 60s TTL.
- New `domain/slices/slice-detail-projector.ts`: per-tab payload
projector covering all six tabs. Story stitches queue_items create
events, queue_transitions append-only log, mission_control_actions
verbs (filtered to slice qitem chain), slice doc mtimes, and proof
packet emission events into one chronological narrative with phase
tags. Topology groups affected seats by parsed rig name (no new
topology endpoint — Topology tab links through to existing
/api/rigs/:rigId/graph).
- New `routes/slices.ts`:
GET /api/slices?filter=all|active|done|blocked list endpoint
GET /api/slices/:name full per-tab payload
GET /api/slices/:name/proof-asset/<rel> binary serving for
screenshots / videos /
traces with content-type
detection + path-traversal
guard + aggressive cache
GET /api/slices/:name/doc/<rel> markdown content for
Docs tab (lazy on click)
Route-order discipline per Phase A R1 lesson: literal `/` and
`/proof-asset/*` and `/doc/*` register BEFORE bare-param `/:name`
catchall to avoid shadowing.
- Server.ts wires slicesRoutes; startup.ts constructs the indexer +
projector from OPENRIG_SLICES_ROOT + OPENRIG_DOGFOOD_EVIDENCE_ROOT.
Unset env returns 503 + `slices_root_not_configured` setup hint
(graceful — no daemon-restart-debug-loop).
UI:
- New /slices and /slices/$name routes mounted on the existing root
route tree.
- New `components/slices/SliceStoryView.tsx` two-pane surface: left
filter row (All / Active / Done / Blocked; default Active) + slice
list, right-pane tab nav (Story / Acceptance / Decisions / Docs /
Tests / Topology) with conditional view rendering. Slice rows
show status glyph, name, rail-item, qitem count, "proof" indicator
when dogfood-evidence exists, age in days.
- Six tab views in `components/slices/tabs/`:
- StoryTab: chronological event list with phase-color tags
(discovery=amber, product-lab=violet, delivery=emerald,
lifecycle=sky, qa=rose, other=stone) + click-to-expand JSON
detail view.
- AcceptanceTab: progress bar + checkbox list with file:line
source citations + "Goal Met" closure callout when slice
status=done.
- DecisionsTab: mission_control_actions timeline with verb /
actor / search filters + before/after JSON snapshot disclosure.
- DocsTab: file tree (left) + lazy markdown viewer (right);
pre-renders content as preformatted text (Markdown viewer
composes with PL-009 when that ships).
- TestsVerificationTab (founder-named LOAD-BEARING): per-proof-
packet section with rendered primary markdown, MANDATORY inline
`<img>` tags for screenshots, MANDATORY `<video controls>`
players for videos, trace download links, pass/fail badge with
semantic color, additional-markdown disclosure. Gracefully
handles empty videos array (real-world: QA hasn't captured one
yet).
- TopologyTab: read-only consumer of PL-019's graph payload —
groups slice seats by rig with totalSeats aggregate + click-
through to main topology surface (per PRD: "this tab is
read-only; clicking a node from here takes the operator to
the regular topology / node-drawer surface"). v1 will inline
a filtered RigNode subgraph; v0's list-based view satisfies
"see the seats" intent without re-mounting React Flow inside
the slice context.
- AppShell header: new Slices Link (Layers icon) navigates to
/slices; surface-title resolver recognizes /slices.
- New hooks `useSlices`, `useSliceDetail`, `useSliceDoc` +
`proofAssetUrl()` helper. Surface 503 indexer-unavailable as a
structured `unavailable` error so the UI renders the env-config
setup hint instead of a raw error toast.
Audit-row 6 amendment recorded in commit + memory: real
dogfood-evidence layout differs from PRD proposal — directories
named `<prefix>-<slice-name>-<date>/` with `*.md` at top + optional
`screenshots/*.png` + optional `headed-browser/screenshots/` +
`phone-screenshots/`. NO `videos/` or `traces/` subdirs exist yet —
indexer + UI handle absence gracefully. Tests cover the .mp4/.webm
classification path so the pipeline is ready when QA captures.
Boundaries preserved:
- Worktree-only on `slice-story-view-v0`; main untouched at ca4f02e.
- Base = PL-019 commit ee39768 (per dispatch); two follow-up QA-fix
commits on PL-019 worktree (favicon, deep-link context) are
unrelated to slice-story-view and intentionally not pulled in.
- Phase B worktree (cd071b0/a3780a6) untouched; surface non-overlap
total per audit row 2.
- NO new migrations. NO new event types. workflow_instances entity
model deferred to v1 per founder direction (PRD bounce condition).
- Read-only at v0; no acceptance/decisions/docs editing surface.
- NO production-shop language. NO Mission One reference's separate
Build/Commits/Tests pipeline tabs (those concepts fold into the
Tests/Verification tab as proof-packet content per PRD).
Verification:
- daemon: 2664/2664 (was 2625 baseline at PL-019 base + 39 new
PL-slice cases across 2 new test files: 21 slice-indexer + 18
slices-routes).
- ui: 448/448 (was 432 baseline + 16 new: 10
tests-verification-tab + 6 slice-story-view-surface).
- cli: 797/800; same 3 pre-existing env-probe failures (preflight +
config resolver) reproduce on baseline — unrelated to this slice.
- tsc clean for daemon + cli + ui.
Hard-gate row 10 (headed-browser dogfood with operator confirming
all 6 tabs render correctly on PL-005 Phase A as the test slice;
Tests tab specifically: screenshots render inline + video player
visible/playable) is QA-owned per
feedback_qa_owns_dogfood_fix_loop.md. Worktree retained for guard
inspection.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PL-019 Topology Activity Indicators v0 — operator sees at-a-glance
which agents are working, what qitems they're on, and how the topology
is moving. Single ambitious vertical; six items shipped in one batch.
Item 1 (CLI convenience): rig ps --nodes --filter agentActivity.state=<state>
extends the --filter allow-list with a nested key + enum-validated value.
--active is sugar for agentActivity.state=running. Combining --active
with --filter is rejected. Three-part error on invalid value
(what failed / what's allowed / what to do). Fixes a pre-existing gap
where bare 'agentActivity' was in ALLOWED_FILTER_KEYS but applyNodeFilter
didn't actually evaluate it (silent no-op).
Item 2 (graph node color): RigNode dot now driven by agentActivity.state
via a new shared activity-visuals helper. Restrained palette per orch
design: running = warm green/teal (emerald-500) + subtle 1.8s opacity
pulse; needs_input = static amber-500 (the eye-catcher); idle = calm
slate-400; unknown = desaturated stone-300. No backwards-compat shim
for the old startup-status dot color — getStartupStatus* helpers
deleted. ATTN/FAILED badges retained as a separate signal (startup-state
answers a different question than activity-state). Stale-sample badge
when sampledAt > ~30s threshold.
Item 3 (Explorer indicator): NodeActivityIndicator added next to the
existing startup-status icon on every Explorer row. Same palette as
item 2. 'Owns active work' bullet appears when currentQitems is
non-empty; tooltip lists short ULID tail + body excerpt per qitem.
Item 4 (graph payload): InventoryOverlay carries agentActivity through
to RFNodeData; routes/rigs.ts attaches activity via attachAgentActivity
at graph-payload time so UI consumers don't need a separate fetch.
Item 5 (active-qitem read-side join): loadCurrentQitemsForSessions
helper exported from routes/rigs.ts joins queue_items WHERE state =
'in-progress' AND destination_session IN (...) ORDER BY ts_updated DESC.
Cap of 3 qitems per node (multi-claim is rare); body excerpted at 80
chars for phone-friendly surfaces. Same enrichment layered onto
routes/sessions.ts node-detail. Cheap by virtue of the existing
idx_queue_items_destination_state index.
Item 6 (edge animation): useTopologyEdgeActivity hook subscribes to
/api/events SSE and tracks recent topology traffic by directed
(source, dest) pair across queue.created, queue.handed_off, and
qitem.fallback_routed. Edges between matched seats get the
edge-recent-traffic class for ~30s sustained emphasis (1.5x weight +
brighter) and edge-just-fired for a one-shot 1.2s pulse on each
firing. mission_control.action_executed is intentionally not animated
at v0 because the event payload lacks a destination session pair.
Reduced-motion media query suppresses both animations.
Boundaries preserved: NO new schema, NO migrations, NO new event
types — v0 consumes the agentActivity primitive shipped at types.ts
and subscribes to events that already exist on PL-004 / PL-005 buses.
Mission Control surfaces (PL-005) and queue-repository (PL-004) are
read-only. PL-005 Phase B surfaces in the parallel worktree are
untouched.
Verification:
- daemon: 2625/2625 pass (was 2607 baseline + 18 new PL-019 cases:
6 graph-projection-pl019, 9 rigs-routes-pl019-current-qitems, plus
pre-existing rigs/sessions tests now exercise the new graph-payload
enrichment via createFullTestDb extended through migration 025).
- ui: 432/432 pass (added 18 activity-visuals + 8 edge-activity
normalize cases + 3 RigNode activity tests; updated 2 RigNode tests
+ 4 RigGraph SSE tests to find the rig-events EventSource by
?rigId= URL since RigGraph now opens two SSE connections).
- cli: 797/800 pass; the 3 failures (preflight env-probes, config
resolver) reproduce on the unchanged Phase A baseline so they are
pre-existing and unrelated to PL-019.
- tsc clean for daemon + cli + ui.
- cli-reference.md updated to document --active and the new
agentActivity.state filter key.
Hard-gate row 9 (headed-browser dogfood with operator-confirmed
legibility on a real rig with multiple active seats) is QA-owned per
feedback_qa_owns_dogfood_fix_loop.md and feedback_velocity_proof_ownership_split.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
One ambitious vertical per orch ratification 2026-05-04:
"Mission Control runs from my phone over tailnet, pings me when
something needs me, and the legacy dashboard is gone."
Audit-first verdict (12 rows; 5 hard-gates ALL GREEN except QA-owned
row 12 + the substrate-side directory deletion deferred to operator
authorization — see below):
Row 1: GREEN Phase B worktree clean at ca4f02e
Row 2: GREEN Phase A migration is 037 (orch-corrected from packet's
draft 033; mission_control_actions table itself
unchanged)
Row 3-7: GREEN Phase A surfaces present + unmodified
Row 8: HARD-GATE GREEN — daemon refuses non-loopback bind when
bearer empty (assertBindAuthInvariant in index.ts
before serve())
Row 9: HARD-GATE GREEN — constant-time compare via
crypto.timingSafeEqual (constantTimeEqual helper)
Row 10: HARD-GATE GREEN — operator notification mechanism choice
recorded: ntfy.sh DEFAULT per planner brief; webhook
alternate adapter shipped; operator selects via env
OPENRIG_NOTIFICATIONS_MECHANISM=ntfy|webhook|none
(default none — unconfigured daemon stays operational)
Row 11: HARD-GATE PARTIAL — openrig-repo references to
services/dashboard removed (architecture.md updated
to past-tense without the literal path; grep -r
returns zero in openrig). Substrate-repo directory
rm -r shared-docs/control-plane/services/dashboard/
DEFERRED to explicit operator authorization: the
shared-docs repo is dirty (ahead 22 from origin
with multiple in-flight orch/founder/planner edits)
and a destructive cross-repo deletion from the
velocity-driver seat without explicit approval would
conflict with the just-recorded promotion-incident
memory's "report-blocker-don't-route-around"
protocol. See driver-to-delivery-orch handoff for
the explicit ask.
Row 12: N/A — QA-owned headed-browser dogfood per dispatch packet
Items shipped (4 of 5; item 5 substrate-side partial per row 11 above):
Item 1 — Remote-bind + bearer-token middleware:
- middleware/auth-bearer-token.ts: constantTimeEqual via
crypto.timingSafeEqual (no early-return on byte mismatch);
authBearerTokenMiddleware (401 + 3-part error body on missing
or mismatched token); isLoopbackBind (127.x / localhost / ::1
→ loopback; everything else including 0.0.0.0 + tailnet IPs
→ non-loopback); assertBindAuthInvariant (HARD-GATE startup
check called from index.ts before serve()).
- Mounted on POST /api/mission-control/action and POST
/api/mission-control/notifications/test (write verbs gated;
reads remain UNgated per planner brief default — bearer is
for write integrity, not view confidentiality).
- createDaemon plumbs bearerToken through DaemonOptions →
AppDeps.missionControlBearerToken → missionControlRoutes opts.
- Read from env OPENRIG_AUTH_BEARER_TOKEN (no legacy alias —
new in Phase B).
Item 2 — Push notifications:
- notification-adapter-types.ts: shared NotificationAdapter +
NotificationPayload + NotificationDeliveryResult contracts.
- notification-adapter-ntfy.ts: ntfy.sh adapter (POST to topic
URL with Title/Click/Tags headers; sanitized title); ~70 lines.
- notification-adapter-webhook.ts: generic webhook adapter
(POST JSON body with stable schema_version=1 shape so operators
can route through Slack/Discord/Telegram/their own infra);
~70 lines.
- notification-dispatcher.ts: subscribes to event-bus; dispatches
on human-gate qitem arrival (mandatory trigger; detected via
queue.created event + tier='human-gate'); optional dispatch on
mission_control.action_executed when includeVerbCompletion=true
(off by default per planner brief). Best-effort delivery;
failure does NOT interrupt the underlying action (PRD invariant).
Dedup by (qitem, trigger-kind, mechanism) so distinct triggers
about the same qitem don't suppress each other. Emits
mission_control.notification_sent / _failed.
- Wired in startup.ts via env config:
OPENRIG_NOTIFICATIONS_MECHANISM=ntfy|webhook|none (default none),
OPENRIG_NOTIFICATIONS_TARGET=<topic url|webhook url>,
OPENRIG_NOTIFICATIONS_INCLUDE_VERB_COMPLETION=1 (default off).
- Optional POST /api/mission-control/notifications/test endpoint
(bearer-gated) sends a synthetic notification so the operator
can verify their setup before relying on it.
Item 3 — Mobile-responsive proof: surface ready for QA-led
headed-browser dogfood per dispatch packet (QA owns proof
end-to-end per feedback_qa_owns_dogfood_fix_loop.md).
Item 4 — GET /api/mission-control/audit + AuditHistoryView UI:
- audit-browse.ts: read-only filter+paginate over
mission_control_actions. Filters: qitem_id, action_verb,
actor_session, since/until time range. Limit clamped to
[1, 200] (default 50). Pagination cursor returns nextBeforeId
(null when no more rows). ORDER BY acted_at DESC, rowid DESC
(deterministic insertion-order tiebreak per
feedback_ulid_tiebreaker_nondeterministic memory; ULID
action_id same-ms tail is non-deterministic). Cursor uses
`WHERE rowid < (SELECT rowid FROM mission_control_actions
WHERE action_id = ?)` so opaque action_id cursors map to
monotonic rowid filter.
- GET /api/mission-control/audit route registered BEFORE
/views/:view-name catchall (route-order discipline per
Phase A R1 lesson). Read-only; no mutation surface.
- Product UI:
- useMissionControlAudit hook: TanStack Query against /audit.
- AuditHistoryView.tsx: 4-dim filter UI + paginated result
table; prev/next buttons; read-only.
- MissionControlSurface adds "audit-history" tab as 8th tab
(UI-side union extends Phase A's 7 daemon view names; daemon
MISSION_CONTROL_VIEWS enum unchanged — Phase A surface
no-touch).
Item 5 — services/dashboard/ retirement (PARTIAL):
- openrig-repo: ZERO references remaining (verified via
grep -rn "services/dashboard" across .md/.ts/.tsx/.js/.json).
docs/as-built/architecture.md updated: removed literal path,
reworded as past-tense ("An earlier prototype that informed
Mission Control's view/verb vocabulary was retired in PL-005
Phase B; git history preserves it as evidence").
- substrate-repo (shared-docs/control-plane/services/dashboard/
directory): NOT deleted in this commit. The shared-docs repo
is dirty (ahead 22 + many uncommitted M files from in-flight
orch/founder/planner edits). Per the just-recorded promotion
incident memory, destructive cross-repo deletions on a dirty
collaboration repo from the velocity-driver seat without
explicit operator approval are NOT routed around — they are
escalated. The handoff packet asks delivery-orch / human to
authorize the substrate-side rm + commit (or direct the
driver to perform it under explicit grant).
3 new RigEvent types added to types.ts (existing PL-005 events
unchanged):
- mission_control.notification_sent
- mission_control.notification_failed
Verification:
- tsc clean for daemon + cli + ui.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory:
- packages/daemon: 2664 / 2664 pass (was 2607 at base + Phase A;
+57 new Phase B daemon tests across 5 new test files:
auth-bearer-token (15), notification-adapters (8),
notification-dispatcher (8), audit-browse (11),
mission-control-routes-phase-b (15)).
- 5/5 deterministic loop on audit-browse pagination test (was
flaky against ULID action_id DESC; now deterministic with
rowid DESC).
Boundaries preserved (Phase B):
- Worktree-only on phase-b-mission-control-queue-observability;
main untouched at ca4f02e.
- NO new migrations (Phase B reads existing mission_control_actions
from Phase A migration 037).
- Phase A's 4 mission-control/ services + 7 views + 7 verbs +
fleet-cli-capability + write-contract + read-layer all
UNMODIFIED beyond additive route inserts.
- Phase A's 3 mission_control.* events untouched; Phase B adds 2
additive events.
- PL-006 steering controls absent. No new managed app. No native
mobile. No push-notification multi-target routing.
- No new CLI commands at v0 (Mission Control remains product UI
inside existing shell).
- NO OAuth/SSO/per-user routing/role-based perms/token rotation
(single static bearer per planner brief MVP context).
- NO archive policy / rotation for mission_control_actions.
- NO production-shop language ("graceful migration", "cutover",
"deprecation period", "feature flag for rollback", "version
negotiation") in code comments or commit message.
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions, no remote push, no
tag, no publish, no Lifecycle adoption, no live host daemon /
topology / provider-auth touch.
Per dispatch: hand back to delivery-orch-lead so QA can run the
headed-browser dogfood on the operator's actual phone (Item 3 +
Hard-Gate Row 12). substrate-side dashboard directory deletion
ask included in handoff packet.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Repairs guard BLOCK on R0 commit 33e01bd:
R0's MissionControlFleetCliCapability defaulted probeRig to a no-op
that always reported staleCliCount=0 + no unsupported fields, and
startup.ts didn't inject a real probe. Result: production
/api/mission-control/cli-capabilities reported zero drift even when
the audit-row-5 case (recoveryGuidance not in CLI v0.2.0 allow-list)
was present. The 4-sub-clause graceful-degradation claim was only
proven through test injection, not through the production route.
R1 fix:
- mission-control-fleet-cli-capability.ts adds three new exports:
- LOCAL_CLI_NODE_FIELDS_AT_0_2_0: daemon-side mirror of the CLI's
ALLOWED_NODE_FIELDS at the workspace v0.2.0 (sourced from
packages/cli/src/commands/ps.ts:79-96; CLI does not export the
set so the daemon mirrors it explicitly).
- LOCAL_CLI_VERSION_LABEL: build-time version label ("0.2.0").
- makeLocalCliCapabilityProbe(opts?): production-default probe
factory. Returns a probeRig function that compares
MISSION_CONTROL_DESIRED_FIELDS against the daemon-mirrored
allow-list and reports unsupported fields. Per-rig honesty:
each rig is probed individually; in v1 single-host topology all
rigs share the local CLI so they all report the same drift,
which is the honest outcome for the audit-row-5 case.
- startup.ts wires makeLocalCliCapabilityProbe() into the
MissionControlFleetCliCapability constructor's probeRig parameter.
The 4 sub-clauses (per-field availability, per-rig honesty,
once-per-session-per-rig logging, "rigs running stale CLI"
indicator) are now exercised through the production path, not
just an injected unit seam.
R1 regression tests (4 new total):
- mission-control-fleet-cli-capability.test.ts (3 new):
- "PRODUCTION-WIRED probe (makeLocalCliCapabilityProbe):
recoveryGuidance NOT in CLI allow-list → reports drift on every
rig" — uses the production factory (not a fake probeRig);
asserts staleCliCount=2 (both seeded rigs), degradedFields
contains "recoveryGuidance" but NOT "agentActivity"
(audit-row-5 ground truth verified at the production seam).
- "agentActivity IS in LOCAL_CLI_NODE_FIELDS_AT_0_2_0
(audit row 5 ground truth)" — pinpoint assertion on the source
mirror.
- "production probe: an extended (hypothetical future) CLI
allow-list with recoveryGuidance reports zero drift" —
regression that proves graduation path: when the CLI surfaces
the desired field, the production probe stops reporting drift
automatically (no source change required).
- mission-control-routes.test.ts (1 new):
- "PRODUCTION-WIRED ROUTE: /cli-capabilities reports
recoveryGuidance drift in JSON payload + per-row
cliDriftDetected" — END-TO-END proof through the route layer
(not an injected unit seam): builds an app with the production
probe wired (matching startup.ts), hits
/api/mission-control/cli-capabilities, asserts the JSON payload
exposes drift to UI consumers (staleCliCount > 0,
degradedFields includes "recoveryGuidance",
every row has cliDriftDetected=true and cliVersionLabel="0.2.0").
Verification:
- tsc clean for daemon + cli.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2607 / 2607 pass (was 2603 in PL-005 Phase A R0;
+4 R1 regressions: 3 fleet-cli-capability + 1 routes-level).
- daemon + cli builds clean.
- UI infra (PostCSS) failures identical to baseline; not Mission
Control regressions.
Boundaries preserved (R1):
- Worktree-only on phase-a-mission-control-queue-observability;
main untouched.
- Smallest-honest-patch: 1 daemon file extended (3 new exports
+ 1 helper factory) + 1 startup wiring line + 4 new test cases.
No other surface touched.
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions, no remote push, no
tag, no publish, no Lifecycle adoption, no live host daemon /
topology / provider-auth touch.
The honest-equivalent design: the daemon mirrors the CLI's
v0.2.0 ALLOWED_NODE_FIELDS set rather than shelling out per rig.
v1 single-host topology makes per-rig CLI shelling unnecessary
overhead; future graduation can replace the static mirror with a
live introspection per rig if multi-host fleets become a v0
shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Implements Phase A per ratified slice contract; founder Q1-Q5 (2026-05-03)
applied verbatim:
Q1 Dashboard graduation: 7 views + 7 verbs on day one.
Q2 PL-004 Phase B dependency: read PL-004 daemon-backed view layer
as source-of-truth (filesystem fallback only for graceful degradation).
Q3 Human queues: first-class product concept (HumanSeatCard).
Q4 Recent ships: 10 by default.
Q5 Action audit: daemon-backed SQLite table with archive-policy hooks
(no JSONL mirror unless dogfood discovers concrete durability need).
Audit-first results (15 rows; per IMPL.md):
Row 1: GREEN (migrations 023-036 present; PL-005 numbered 037, NOT
the packet's provisional 033 — Phase D shipped 033-036).
Row 2: GREEN (db/connection.ts + db/migrate.ts unchanged).
Row 3: GREEN (PL-004 domain unmodified at HEAD).
Row 4: GREEN (UI shell at packages/ui/src/{AppShell,routes}.tsx).
Row 5: YELLOW (agentActivity in CLI allow-list; recoveryGuidance NOT
in 0.2.0 allow-list; not a blocker — exactly the cross-CLI-
version drift the 4-sub-clause graceful degradation contract
is designed to handle).
Row 6: GREEN (POC services/dashboard available as vocabulary).
Row 7: GREEN (stream surface readable: stream_items + .jsonl).
Row 8: GREEN (human-seat first-class design encoded).
HARD-GATES 9-15: ALL GREEN (Q1-Q5 encoded; daemon has Coordination
Primitive bundle; explicit OPENRIG_DB pattern; no in-flight
PL-004 revision; no migration test; cross-CLI-drift handled
by spec; 9-field model preserved).
Migration:
- 037_mission_control_actions: append-only audit. 7-verb enum at
application layer (matches Phase C/D pattern). FK to queue_items.
Indexes on (acted_at DESC, action_verb), (qitem_id, acted_at DESC),
(actor_session, acted_at DESC).
Daemon domain (4 new files, 1 new subdirectory):
- mission-control-action-log: append-only writer; verb-specific
required-field validation (annotate→annotation; hold/drop→reason).
API surface lacks update/delete/remove (contract enforcement).
- mission-control-write-contract: LOAD-BEARING. Atomic 7-verb
coordinator. Each verb is one db.transaction: queue mutation via
Phase D's QueueRepository.updateWithinTransaction (preserves Phase A
hot-potato closure validation; emits queue.updated) + audit row +
persisted mission_control.action_executed event. The 4-step handoff
shape (source-update + destination-create + opt-in best-effort
notify + audit append) is verified atomic; notify failure does NOT
roll back durable mutations (PRD invariant). Failure-injection
rollback test included.
- mission-control-read-layer: maps 7 views to source-of-truth paths.
PL-004 daemon-backed services as primary; filesystem only for
graceful degradation. recently-active delegates to PL-004 Phase B
ViewProjector.show("recently-active"). recent-observations reads
PL-004 Phase A stream_items via StreamStore.
- mission-control-fleet-cli-capability: per-rig CLI capability cache
+ fleet roll-up. Implements 4 sub-clauses of PRD § Runtime/Source
Drift Acceptance (per-field availability, per-rig honesty,
once-per-session-per-rig logging, "rigs running stale CLI"
indicator).
Routes (1 new file):
- routes/mission-control.ts: GET /views (list), GET /views/:view-name,
POST /action, GET /sse + /watch, GET /cli-capabilities. SSE/literal
paths mounted BEFORE /views/:view-name catchall per PL-004 Phase A
R1 SSE route-order discipline.
3 new RigEvents added to types.ts (additive; existing events untouched):
- mission_control.action_executed
- mission_control.cli_drift_detected
- mission_control.view_refreshed
Wiring:
- server.ts: AppDeps extended; context-set; route mounted.
- startup.ts: Mission Control services constructed after PL-004
Phase D wiring so all daemon-backed coordination surfaces are
available.
Product UI (NEW Mission Control surface inside existing shell):
- MissionControlSurface.tsx: top-level container with tab nav across
7 view names. Mounted at /mission-control route in routes.tsx.
- 7 view components (My queue, Human gate, Fleet, Active work, Recent
ships, Recently active, Recent observations). Most wrap shared
GenericListView; FleetView combines CliDriftIndicator + per-row
MissingFieldPlaceholder; MyQueueView combines HumanSeatCard +
GenericListView with verb actions enabled.
- CompactStatusRow.tsx: 9-FIELD PHONE-FRIENDLY CONTENT MODEL ATOM.
NON-NEGOTIABLE per PRD § Acceptance Criteria item 1. Renders
compact / expanded densities; UI may collapse but JSON preserves
all 9 fields (rig/mission name, current phase, active/idle/
attention/blocked/degraded, next-action, pending-human-decision,
read-cost, last-update, confidence/freshness, evidence link).
- VerbActions.tsx: shared 7-verb sub-component with per-verb
required-field input and structured error display.
- HumanSeatCard.tsx: first-class human-seat rendering (founder Q3).
- CliDriftIndicator.tsx: fleet-level + per-row drift surfacing.
- 3 React Query hooks (useMissionControlView, useMissionControlAction,
useMissionControlCliCapabilities).
Tests (5 new daemon test files; 42 cases):
- mission-control-action-log.test.ts (8 cases): append-only contract,
verb-specific required-field validation, JSON encoding,
listForQitem/listForActor filters, no update/delete API.
- mission-control-write-contract.test.ts (10 cases): all 7 verbs
drive correct closure shape; ATOMIC ROLLBACK regression
(handoff with invalid destination rolls back source close + audit
+ new qitem); annotate has no queue mutation; route tags new packet.
- mission-control-read-layer.test.ts (7 cases): 7-view structural
coverage; 9-FIELD MODEL preserved across all 7 views (PRD
non-negotiable assertion); my-queue filters human-gate to operator;
recent-ships caps at 10 (founder Q4); active-work priority sort;
recent-observations reads from stream_items.
- mission-control-fleet-cli-capability.test.ts (7 cases): 4
sub-clauses of graceful degradation including once-per-session-
per-rig log dedup + per-rig honesty + queue-state activity summary.
- mission-control-routes.test.ts (10 cases): 7 endpoint round-trips
+ R1 SSE route-order regression + literal /views not shadowed.
Verification:
- tsc clean for all 3 packages (daemon, cli, ui).
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2603 / 2603 pass (was 2561 at base; +42 new
Mission Control daemon tests).
- UI test suite has 26 pre-existing failures from PostCSS/Tailwind
module resolution infra in this fresh worktree; CONFIRMED identical
at HEAD baseline (d15ca1f) via git stash + re-run before commit
(8 failed / 26 failed test files / 273 passed both with and
without Mission Control changes). Not a Mission Control regression;
documented as a worktree node_modules infrastructure observation.
- daemon + cli builds clean. UI vite build also fails on the same
PostCSS infra issue (tsc step succeeds; vite step fails). Same
pre-existing infra artifact, not a Mission Control regression.
Boundaries preserved:
- Worktree-only on phase-a-mission-control-queue-observability;
main untouched at d15ca1f.
- PL-004 surfaces consumed READ-ONLY beyond the orch-ratified
extension points (Phase D's QueueRepository.updateWithinTransaction
+createWithinTransaction + maybeNudge visibility, all of which
were added in PL-004 Phase D R1 and are already on main).
- POC services/dashboard filesystem state untouched (vocabulary
reference only; no migration/cutover ceremony).
- POC ~/.openrig/stream/<date>.jsonl untouched (read-only).
- PL-006 steering controls absent.
- No new managed app; no native mobile; no push notifications.
- No new CLI commands (Mission Control is product UI inside existing
shell).
- localhost-only auth at v0 (matches existing dashboard posture).
- Existing 36 PL-004 migrations untouched. Phase A adds ONE new
migration 037.
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions, no remote push, no
tag, no publish, no live runtime adoption.
CLI shim drift caveat preserved: installed rig reports 0.2.0 but some
command help/output still carries legacy rigx wrapper behavior while
daemon APIs are green. Mission Control consumes daemon APIs directly,
so this caveat does not affect the Phase A surface, but is recorded
per dispatch packet instruction.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Repairs QA-found nondeterminism on R3 commit 3e32b0a flagged in:
shared-docs/openrig-work/missions/recursive-self-improvement-v2/dogfood-evidence/
pl004-major-release-battle-test-3e32b0a-20260503T160531Z/
PL004-major-release-battle-test-QA-packet.md
Root cause: WorkflowStepTrailLog.listForInstance() ordered same-
timestamp closed rows by trail_id DESC. trail_id is a ULID; ULIDs
encode time in their leading bytes but the trailing random component
is independent across two ULIDs generated in the same millisecond.
That makes ULID DESC ordering non-deterministic when closed_at ties,
which the R2 resume regression hit on fast machines (3 close events
within the same ms produced unstable trail[0] reads).
Fix (one line; ports the QA-applied dirty patch verbatim):
workflow-step-trails ORDER BY closed_at DESC, rowid DESC
SQLite implicit rowid is monotonically assigned in INSERT order on
tables that don't declare WITHOUT ROWID. Migration
035_workflow_step_trails uses an unmodified TEXT PRIMARY KEY, so the
implicit rowid column is present and stable. rowid DESC is therefore
deterministic insertion-order.
Verification:
- Audit: only file dirty in worktree was the QA-applied
workflow-step-trail-log.ts ordering change. No other source diff.
- Tight loop: 30/30 runs of "R2 resume regression: handoff → waiting
on step N → done on same qitem closes step N (not N+1)" passed
with explicit OPENRIG_HOME + OPENRIG_DB=:memory: temp env.
Pre-fix, this test was QA-flagged as nondeterministic.
- Focused suites: workflow-projector (14), workflow-routes (13),
workflow-cross-loop-three-step (1), workflow-step-trail-log (7) —
all 35/35 pass.
- Full daemon vitest baseline: 2560 / 2560 pass.
- Per-package CLI watchdog + workflow tests: 15 / 15.
- tsc clean for both packages.
Boundaries preserved (R4):
- Worktree-only on phase-d-coordination-primitive-system; main untouched.
- Smallest-honest-patch: 1 line (the QA-applied dirty fix verbatim).
No other surface touched.
- No-touch surfaces unchanged: Phase A non-extension queue surfaces
beyond the orch-ratified Phase D extension points, Phase B
project/classifier/view, Phase C beyond ratified extension points,
Lifecycle v2, migrations 001-033 + 035-036, POC source.
- No primary checkout used as proof source.
- No cleanup, adoption, CAS promotion, reset, checkout, clean, push,
tag, publish, or Lifecycle handoff performed.
- No production DB, host daemon, live topology, provider credentials,
or preserved VM clone touched.
VM blocker noted but out of scope for this commit. Per dispatch packet:
release closeout remains blocked until both this source repair is guard-
accepted AND the required disposable VM proof is completed (or VM/
operator blocker accepted by orch).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Repairs guard BLOCK on R2 commit 1c94719:
R2 added the WorkflowProjectorError(exit_not_allowed) domain error but
the workflow routes errorResponse fall-through left the new code
mapping to HTTP 500 internal-server-error, dishonest at the public
surface (HTTP API + rig workflow project CLI).
R3 fix:
- routes/workflow.ts errorResponse: add err.code === "exit_not_allowed"
→ HTTP 400. Structured details (stepId, attemptedExit, allowedExits)
are already preserved by the existing
`{...(err.details ?? {})}` spread.
R3 regression added in workflow-routes.test.ts:
- "POST /project surfaces exit_not_allowed as 400 with structured
details + no side effects": instantiates a workflow on the produce
step (allowed_exits=[handoff]), captures pre-state via the public
surface, attempts exit=done, asserts:
- res.status === 400 (NOT 500)
- body.error === "exit_not_allowed" (NOT "internal_error")
- body.message contains "produce"
- body.stepId === "produce"
- body.attemptedExit === "done"
- body.allowedExits === ["handoff"]
- Post-rejection instance state via GET /:instance_id matches
pre-state (currentFrontier, currentStepId, status all unchanged)
- GET /:instance_id/trace returns trail === []
Verification:
- tsc clean for daemon and cli.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2560 / 2560 pass (was 2559 in Phase D R2; +1
R3 route regression).
- packages/cli: 15 / 15 watchdog + workflow tests pass; pre-existing
CLI failures unchanged.
Boundaries preserved (R3):
- Worktree-only on phase-d-coordination-primitive-system; main untouched.
- Smallest-honest-patch: 1 line added to error mapping + 1 test added.
No domain change; no migration change; no other surface touched.
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions, no remote push, no
tag, no publish.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Repairs guard BLOCK on R1 commit bf28cc2:
Blocker 1 — waiting/resume semantics still wrong (trail-based inference):
R1 preserved the closed packet on currentFrontier when exit=waiting,
but the next project() call inferred current step from "last trail row
+ 1," skipping the parked step. POC contract is to extract step_id
from the current packet binding, not from trail order.
R2 fix — durable current_step_id binding on workflow_instances:
- Migration 034_workflow_instances extended IN PLACE with
current_step_id TEXT column. (In-place because Phase D has not
been promoted past base 1a3b313; same pattern guard accepted for
Phase C R1's actionable/last_actionable_at columns on 031.)
- WorkflowInstance.currentStepId field; rowToInstance reads it.
- WorkflowInstanceStore.create() accepts currentStepId; updateFrontier
accepts currentStepId of "preserve" | "clear" | <stepId>.
- WorkflowRuntime.instantiate() sets current_step_id to the entry
step at instance creation time.
- WorkflowProjector.project() replaces inferCurrentStep (trail-based)
with resolveCurrentStep (reads instance.currentStepId). Falls back
to entry step ONLY when current_step_id is null (defense-in-depth).
- Frontier-update transitions:
handoff → current_step_id = nextStep.id
waiting → current_step_id preserved (resume on same packet
still on same step)
done / failed → current_step_id cleared (terminal)
Resume regression added (canonical "drive→park→resume on same id" per
skill lesson feedback_resume_after_parked_state_regression):
- "R2 resume regression: handoff → waiting on step N → done on same
qitem closes step N (not N+1)": instantiate three-step workflow,
handoff produce → review, waiting on review qitem (status=waiting,
current_step_id="review", frontier preserves review qitem), then
done on the SAME review qitem. Asserts: trail's last entry has
stepId="review" (NOT "finalize"); workflow ends as completed
because no successor projected; NO trail row written for "finalize"
(would have indicated a step skip).
Blocker 2 — projector did not enforce step.allowed_exits at runtime:
R1 left allowed_exits as a validation-only decoration. The R1 done test
encoded the wrong behavior (projecting exit=done on a step that only
allows handoff and asserting closure). R2 makes allowed_exits a runtime
contract that fires BEFORE any side effect.
R2 fix:
- WorkflowProjector.project() validates input.exit ∈
currentStep.allowed_exits when the step declares allowed_exits.
Throws WorkflowProjectorError {code: "exit_not_allowed",
details: {stepId, attemptedExit, allowedExits}} BEFORE queue close,
trail append, frontier update, or event persist. No side effects
on rejection.
R2 regression tests added:
- "R2 allowed_exits enforcement (positive): exit included in
step.allowed_exits succeeds": handoff on a step that allows handoff
succeeds and returns the next step.
- "R2 allowed_exits enforcement (negative): disallowed exit throws
exit_not_allowed with NO side effects": done on a step that allows
only handoff throws WorkflowProjectorError(exit_not_allowed) with
structured details; queue state, instance frontier, current_step_id,
trail count, and event count are ALL unchanged after the rejection.
Test fixture changes:
- Added PERMISSIVE_SPEC fixture for closure-shape unit tests
(project(done), project(waiting), project(failed), instance-not-active
rejection). The main 3-step SPEC has allowed_exits=[handoff] on the
entry step; tests that deliberately exercise non-handoff closures on
the entry packet now use PERMISSIVE_SPEC's "act" step which allows
all four exits. R1 tests that incorrectly assumed the produce step
accepted any exit are corrected.
Skill lesson filed at memory/feedback_resume_after_parked_state_regression.md
per guard's call-out: every preservation repair (waiting, blocked, parked,
restore-resumed) needs a "drive→park→resume on same id" regression that
asserts the resumed action targets the same logical entity that was
parked, not an inferred-from-side-effect successor.
Verification:
- tsc clean for daemon and cli.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2559 / 2559 pass (was 2556 in Phase D R1; +3
R2 regressions: resume + allowed_exits positive + allowed_exits
negative).
- packages/cli: 15 / 15 watchdog + workflow tests pass; pre-existing
CLI failures unchanged.
Boundaries preserved (R2):
- Worktree-only on phase-d-coordination-primitive-system; main untouched.
- Migration 034 extended in place (Phase D unpromoted; matches
accepted Phase C R1 in-place 031 extension pattern).
- No-touch surfaces unchanged: Phase A non-extension queue surfaces
beyond the orch-ratified Phase D extension points, Phase B
project/classifier/view, Lifecycle v2, migrations 001-033, 035-036,
POC source.
- workflow-keepalive still reads ONLY from SQLite.
- Workflow runtime PROJECTS on closure; does NOT GATE closure (Phase A
hot-potato strict-rejection remains the closure authority).
- allowed_exits enforcement at projection complements (does not
replace) the validator's spec-level allowed_exits check.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Repairs guard BLOCK on R0 commit 55bdcbb:
Blocker 1 — projection bypassed Phase A queue closure/audit contract:
R0's WorkflowProjector.project() did a direct UPDATE on queue_items
that skipped closure_reason / closure_target / handed_off_to / blocked_on
metadata, queue_transitions append, and queue.updated event. R1 routes
all closure through a new Phase A primitive:
- QueueRepository.updateWithinTransaction(): same closure validation +
UPDATE + transition log + queue.updated event as update(), but
composes inside the caller's outer db.transaction (caller responsible
for post-commit notifySubscribers). update() is now factored through
a private updateInTransactionalContext() helper; behavior unchanged
for existing callers.
- QueueUpdateInput.handedOffTo + .blockedOn: optional new fields that
persist the canonical pointer columns. update SQL extended with
COALESCE for both. Backward-compatible with all existing callers.
- workflowExitToQueueClosure() helper translates each workflow exit
into the Phase A queue closure shape:
handoff → state=handed-off, closure_reason=handed_off_to,
closure_target+handed_off_to=<next-owner>
waiting → state=blocked, closure_reason=blocked_on,
closure_target+blocked_on=<blocker>
done → state=done, closure_reason=no-follow-on
failed → state=done, closure_reason=denied
- WorkflowProjector.project() now resolves the next owner BEFORE
closure (so closure_target is set), calls
queueRepo.updateWithinTransaction(), then optionally
queueRepo.createWithinTransaction(). All inside one outer
db.transaction. Phase A hot-potato strict-rejection invariants
(state=done requires closure_reason; handed_off_to/blocked_on/
escalation also require closure_target) apply unchanged — workflow
projection cannot land state=done without a valid closure_reason.
Blocker 2a — exit=failed treated as completed:
R0 had a frontier-empty branch that always set status=completed.
R1 makes exit=failed a distinct terminal state:
- status=failed, completedAt stamped
- emits workflow.failed (NOT workflow.completed)
- failed event carries the reason from input.resultNote
- workflow.failed and workflow.completed are now mutually exclusive
Blocker 2b — waiting frontier bug:
R0 removed the closed packet from currentFrontier even on exit=waiting.
That broke workflow-keepalive's owner resolution: a single-packet
waiting workflow had no owner to wake. R1 PRESERVES the closed packet
on currentFrontier when exit=waiting; workflow-keepalive can still
resolve the blocked owner via Phase A queue_items destination.
R1 regression tests (4 new in workflow-projector.test.ts):
- "project(handoff) persists Phase A queue closure metadata + transition
row + queue.updated event": asserts closure_reason=handed_off_to,
closure_target=<next-owner>, handed_off_to=<next-owner>; queue_transitions
row with closure_reason; queue.updated event emitted.
- "project(done) without exposed closure overrides defaults closure_reason
to no-follow-on": asserts default closure_reason path.
- "project(waiting) uses Phase A blocked state + closure_reason=blocked_on
+ blocked_on column": asserts blocked-state Phase A shape.
- "project(failed) sets workflow status=failed AND emits workflow.failed
(NOT completed)": asserts the formerly-merged failed-vs-completed split.
The existing waiting frontier test renamed + actually asserts
currentFrontier preservation:
- "project(waiting) sets status=waiting AND preserves the closed packet
on currentFrontier (workflow-keepalive resolvability)": asserts
projected.instance.currentFrontier still contains the closed packet
AND re-fetched instance state agrees.
Verification:
- tsc clean for daemon and cli.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2556 / 2556 pass (was 2552 in Phase D R0; +4
R1 regressions in workflow-projector.test.ts).
- packages/cli: 15 / 15 watchdog + workflow tests pass; pre-existing
CLI failures unchanged from prior baseline.
Boundaries preserved (R1):
- Worktree-only on phase-d-coordination-primitive-system; main untouched.
- QueueRepository.updateWithinTransaction is a NEW method; existing
update() unchanged (factored through new private helper).
- QueueUpdateInput optional fields handedOffTo + blockedOn are
additive; existing callers unaffected.
- No-touch surfaces unchanged: Phase A non-extension queue surfaces
beyond the orch-ratified Phase D extension points (createWithinTransaction
+ updateWithinTransaction + maybeNudge visibility), Phase B
project/classifier/view, Lifecycle v2, migrations 001-032,
POC source. Phase C already-extended surfaces unchanged from R0.
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions, no remote push,
no tag, no publish.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds the Workflow Runtime primitive from PL-004 Phase D:
Migrations (033-036):
- 033_workflow_specs: cached read-through from markdown/YAML spec
files. UNIQUE on (name, version); source_hash content-invalidation;
workspace-surface reconciliation (operator edits win at next read).
- 034_workflow_instances: live instance state. status enum
(active/waiting/completed/failed); current_frontier_json; hop_count;
last_continuation_decision_json. Survives daemon restart from SQLite.
- 035_workflow_step_trails: append-only history. FK to workflow_instances
and queue_items (next_qitem_id, prior_qitem_id). Append-only at
API surface (no update/delete methods).
- 036_watchdog_policy_enum_extension: documenting no-op. Phase C uses
application-layer enum enforcement (PHASE_D_POLICIES array), so
extending the enum to include workflow-keepalive needs no DDL.
Migration exists for audit-trail integrity per IMPL row 17.
Domain (8 new files; load-bearing transactional-scribe):
- WorkflowSpecCache: read-through cache via the `yaml` package.
Re-caches in place on hash miss (UPDATE same spec_id).
- WorkflowValidator: role resolution + step uniqueness + allowed-exits
+ optional seat liveness. Returns structured ValidationResult.
- WorkflowInstanceStore: CRUD on workflow_instances; updateFrontier
composes inside outer caller-managed transactions.
- WorkflowStepTrailLog: append-only writer; pattern mirrors Phase A
queue-transition-log + Phase C watchdog-history-log.
- WorkflowProjector: LOAD-BEARING. Implements transactional-scribe.
A single db.transaction holds: closing the current packet, creating
the next-step packet (via Phase A QueueRepository extension point),
appending the trail entry, updating instance frontier+status, and
persisting workflow events. Either everything commits or everything
rolls back. Lost handoffs are impossible by design.
- WorkflowRuntime: facade over the above; operations validate +
instantiate (creates instance + entry qitem in one transaction) +
project (delegates to projector) + continue (idempotent inspector).
- policies/workflow-keepalive: TypeScript port of POC
`lib/policies/workflow-keepalive.mjs`. SOURCE-OF-TRUTH SWITCHED
from markdown to SQLite per audit row 18 (LOAD-BEARING). Reads
workflow_instances directly; resolves frontier owners via queue_items.
Phase A surface extension (orch-ratified per IMPL § Driver Handoff):
- QueueRepository.createWithinTransaction(): smallest-honest extension
that lets workflow-projector compose queue creation inside its outer
transaction without breaking notify/nudge post-commit semantics.
create() now factored through createInTransactionalContext() helper;
behavior unchanged for existing callers.
- QueueRepository.maybeNudge(): visibility raised to public so
workflow-projector can fire nudges after its outer transaction
commits.
Phase C surface extension (orch-ratified per IMPL § Write Set):
- WatchdogJobsRepository: PHASE_C_POLICIES → PHASE_D_POLICIES (extends
enum with workflow-keepalive); policy_deferred_to_phase_d rejection
removed. PHASE_C_POLICIES retained as deprecated alias.
- WatchdogPolicyEngine: instance-level policy registry +
additionalPolicies in deps (was static module-level Map). Allows
workflow-keepalive registration at startup with db handle injected.
Phase D events (6 new RigEvent types):
- workflow.instantiated, workflow.step_closed,
workflow.next_qitem_projected, workflow.completed,
workflow.failed, workflow.routing_table_changed
- 20 existing PL-004 events untouched.
Routes (1 new file, route-order discipline preserved):
- POST /api/workflow/{validate,instantiate,project}
- GET /api/workflow/list
- GET /api/workflow/sse + /watch (mounted BEFORE /:instance_id)
- GET /api/workflow/:instance_id and :/:instance_id/trace
- POST /api/workflow/:instance_id/continue
CLI:
- rig workflow validate/instantiate/project/list/show/trace/continue
- 7 subcommands; matches POC `bin/rig-workflow` shape.
Wiring:
- startup.ts: WorkflowRuntime constructed from queueRepo + db + bus;
workflow-keepalive policy injected into WatchdogPolicyEngine via
additionalPolicies. Phase D services start with daemon; recover
from SQLite on restart.
Test changes:
- 8 new daemon test files (~67 new test cases): workflow-spec-cache
(9), workflow-validator (9), workflow-instance-store (7),
workflow-step-trail-log (7), workflow-projector (7 incl. transactional-
scribe rollback regression), workflow-keepalive-policy (8 incl.
SQLite-source-only LOAD-BEARING assertion), workflow-routes (12 incl.
SSE route-order regression guard), workflow-cross-loop-three-step (1
PRD acceptance criteria load-bearing case).
- 1 new CLI test file: workflow.test.ts (8 cases).
- Phase C registration-rejection assertions REPLACED with positive
registration-accept (the orch-ratified Phase C test surface change):
watchdog-jobs-repository.test.ts, watchdog-routes.test.ts,
cli/test/watchdog.test.ts.
Verification:
- tsc clean for daemon and cli.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2552 / 2552 pass (was 2492 in Phase C R2; +60
new workflow tests).
- packages/cli: 15 / 15 watchdog + workflow tests pass; pre-existing
CLI failures (host-registry, config, specs, preflight, client
--version) unchanged from Phase C R2 baseline.
Boundaries preserved:
- Worktree-only on phase-d-coordination-primitive-system; main
untouched at 1a3b313 (Phase C R2).
- No-touch surfaces unchanged: Phase A stream/queue/inbox/outbox/
transitions tables + non-extension repository methods, Phase B
project/classifier/view, Lifecycle v2, migrations 001-032,
POC workflow-runtime-v0/, POC services/watchdog/lib/policies/
workflow-keepalive.mjs, progress-rail (Phase E remains
markdown-authoritative for v1).
- Workflow runtime PROJECTS on closure; does NOT GATE closure
(Phase A hot-potato strict-rejection remains the closure authority).
- One-hop continuity is the load-bearing default; multi-hop chaining
deferred (PRD § Risks "Workflow runtime over-engineered").
- workflow-keepalive reads ONLY from SQLite (audit row 18 LOAD-BEARING).
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions, no remote push, no
tag, no publish, no live runtime adoption (per human steering
2026-05-03 for the Coordination Primitive feature).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Repairs guard BLOCK on R1 commit dd9820e:
Blocker 1 (discriminating scheduler regression for scan_interval_seconds):
R1 added the right isDue() shape but proved it only with an engine-only
active-wake test that used equal interval values (interval=30,
scan=30). A scheduler regression to interval_seconds-only would have
passed it. R2 adds four discriminating tests in
packages/daemon/test/watchdog-scheduler.test.ts:
- isDue uses scan_interval_seconds (=30) over interval_seconds (=600):
due after 45s elapsed.
- isDue uses scan_interval_seconds (=600) over interval_seconds (=30):
NOT due after 45s elapsed.
- runTickNow does NOT invoke policy nor write history during the
scan_interval_seconds not-due window (drives the scheduler boundary,
asserts deliver callback never called AND no history row written AND
last_evaluation_at unchanged).
- runTickNow DOES invoke policy when scan_interval_seconds elapsed even
with large interval_seconds (the inverse: confirms scan_interval can
be smaller than interval and still trigger).
Lesson filed at memory/feedback_poc_regression_must_discriminate.md:
when porting POC regressions, preserve discriminating param values and
exercise the layer under review.
Blocker 2 (artifact-pool YAML/frontmatter parity):
R1 used a local key/value parser that treated any unquoted colon in a
value as malformed. Real frontmatter contains ISO timestamps
(claimed_at: 2026-05-03T05:15:25Z) and URLs
(source_url: https://...) that would be wrongly excluded. R2 swaps
in the `yaml` package (already a daemon dep) so:
- Valid YAML scalars containing colons parse cleanly and the artifact
is included.
- Only true YAML parse failures (broken: value: still broken) cause
exclusion when include_malformed_frontmatter=false.
- ScannedArtifact.frontmatter widened from Record<string, string> to
Record<string, unknown> because YAML loaders return heterogeneous
scalars (string, Date, number, etc.).
- statusFromFrontmatter() coerces non-string status scalars (Date,
number) to string so include_statuses comparisons stay stable.
- sourceKeyFor() coerces non-string key_field scalars similarly.
Three new POC-parity regression tests in
packages/daemon/test/watchdog-policies.test.ts:
- includes ready artifacts with ISO timestamp frontmatter.
- includes ready artifacts with URL frontmatter.
- includes ready artifacts with quoted-string frontmatter containing colons.
Plus the existing malformed-frontmatter exclusion test (broken: value:
still broken) continues to pass: that fixture is genuine invalid YAML.
Verification:
- tsc clean for daemon and cli.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2492 / 2492 pass (was 2485 in R1; +7 new
watchdog tests for R2: 4 scheduler + 3 YAML-parity).
- packages/cli: 7 / 7 watchdog tests pass.
Boundaries preserved (R2):
- Worktree-only on phase-c-coordination-primitive-system; main untouched.
- No-touch surfaces unchanged: Phase A/B/Lifecycle/migrations 001-030/
workflow runtime/progress-rail/POC services-watchdog state.
- workflow-keepalive still rejected at register with policy_deferred_to_phase_d.
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions, no remote push, no
tag, no publish.
- Three policy enum values unchanged: periodic-reminder,
artifact-pool-ready, edge-artifact-required.
- R1 in-place migration extension (actionable + last_actionable_at on
031) unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Repairs guard BLOCK on commit 48c0267:
1. scan_interval_seconds vs active_wake_interval_seconds (blocker 1):
- Scheduler isDue uses scan_interval_seconds when set, falling back
to interval_seconds. Mirrors POC engine.mjs:38-46.
- Policy engine enforces active_wake_interval_seconds as a re-delivery
throttle: when state.actionable was already true AND wake-window
not elapsed since last_fire_at, returns quiet skip with reason
active_wake_not_due (does NOT call delivery, preserves last_fire_at).
Mirrors POC engine.mjs:49-64 and :243-263.
- New columns on watchdog_jobs: actionable INTEGER NOT NULL DEFAULT 0,
last_actionable_at TEXT. Engine sets these on every meaningful
evaluation; clears on skip; preserves last_actionable_at across
continued-actionable windows. Migration extended in-place since
031 has not yet been promoted past Phase B HEAD.
- Ported POC active-wake regression (tests/watchdog-active-wake-interval.test.sh)
into daemon test as a 6-tick state-machine regression.
2. edge-artifact-required POC contract (blocker 2):
- Spec uses context.source / context.target (singular pool specs)
not source_pools / target_pools. Each may have path or paths.
- Edge-satisfaction is body-match: a downstream artifact satisfies
a source iff target.raw INCLUDES the source key. NOT a frontmatter
field match. Mirrors POC edge-artifact-required.mjs:11-12.
- Target scan overrides include_statuses to [] (POC override) so any
downstream state counts as 'exists'.
- Delivery target is job.target.session (top-level), not from context.
- Label key is context.edge_label (POC parity).
- Daemon test ports the POC pilot-a body-reference scenario.
3. Artifact-pool scanner POC parity (blocker 3):
- Default ignores README.md and .DS_Store always applied.
- Configured pool.ignore_names extends default ignores.
- pool.recursive=true descends into subdirectories.
- Malformed-frontmatter excluded unless pool.include_malformed_frontmatter.
- Raw content preserved on every artifact (used by edge-artifact-required
body-match).
- ENOENT-tolerant pool path.
- artifact-pool-ready and artifact-pool-helpers tests cover all 5 POC
scanner behaviors.
Engine state-machine alignment to POC:
- PolicyJob shape uses {target: {session}, message?, context}. Engine
parses spec_yaml top-level target + message blocks and falls back to
{session: registered targetSession} when spec lacks target.
- Policies all read job.target.session for delivery target and
job.message OR context.message for periodic-reminder.
- PolicyEvaluation send action returns target as object {session: ...}
to match POC contract (engine extracts session string for delivery).
- QUIET_SKIP_REASONS set (not_due, no_actionable_artifacts,
no_missing_edge_artifacts, active_wake_not_due) suppresses both
history record AND event emit. Loud skips still record + emit.
Mirrors POC shouldAppendHistory (engine.mjs:99-112).
Verification:
- tsc clean for daemon and cli.
- Per-package vitest baseline under explicit OPENRIG_HOME +
OPENRIG_DB=:memory: temp env:
- packages/daemon: 2485 / 2485 pass (was 2471; +14 new watchdog
tests covering POC contract + active-wake state machine).
- packages/cli: 7 / 7 watchdog tests pass.
Boundaries preserved:
- Worktree-only on phase-c-coordination-primitive-system branch;
main untouched.
- No-touch surfaces (Phase A stream/queue/hot-potato, Phase B
project/classifier/view, Lifecycle v2, migrations 001-030, workflow
runtime, progress-rail code, POC services/watchdog state) unchanged.
- workflow-keepalive still rejected at register with
policy_deferred_to_phase_d; not added as a product policy module.
- No fleet sweep, no protected-host mutation, no external spend, no
destructive lifecycle, no VM-boundary actions.
Docs updated in architecture.md and cli-reference.md to reflect the
quiet-skip parity + scan/wake cadence semantics. codemap entries from
48c0267 still describe the same files.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes QA verdict revise-required from delivery-orch-lead per qitem-20260503024929-ea365d2b + handoff packet substrate/rigs/openrig-velocity/state/delivery-orch/pl004-phase-a-r1-sse-route-fix-driver-handoff-20260503.md.
Foundation: openrig fd4fb30 (PL-004 Phase A R1). Branch: phase-a-coordination-primitive-system (worktree-scoped; main NOT touched).
ROOT CAUSE:
The R1 commit fd4fb30 mounted GET /watch and GET /sse routes AFTER the bare-param catchalls (GET /:qitemId on queue.ts:278; GET /:streamItemId on stream.ts:69). Hono dispatches GET /api/queue/sse to the /:qitemId handler (with qitemId="sse"), which calls QueueRepository.getById("sse") → null → returns 404 with `{"error":"qitem_not_found"}`. Same problem on stream side: GET /api/stream/sse → /:streamItemId handler → 404 `{"error":"stream item not found"}`.
The QA proof caught this; my R1 test was inadequate (HEAD comparison; both /watch and /sse returned the same 404 status, so the "they match" assertion held while both were broken).
FIX:
1. Moved sseHandler definition + `app.get("/watch", sseHandler)` + `app.get("/sse", sseHandler)` BEFORE the bare /:qitemId GET in routes/queue.ts (now mounted alongside /whoami, /list, /overdue — matching the existing literal-first convention with explicit comment).
2. Same fix in routes/stream.ts: SSE routes now mounted BEFORE GET /:streamItemId.
3. Removed the duplicate handler definition + mounts at end of both files.
NEW LIVE-GET TESTS (replacing inadequate HEAD comparison):
queue-routes.test.ts (3 cases under "R1 SSE route — live GET reaches the SSE handler"):
- GET /api/queue/sse → 200 + content-type: text/event-stream (handler reached)
- GET /api/queue/watch → 200 + content-type: text/event-stream (handler reached)
- GET /api/queue/sse does NOT return qitem_not_found (route-order regression guard)
stream-routes.test.ts (3 cases):
- GET /api/stream/sse → 200 + content-type: text/event-stream
- GET /api/stream/watch → 200 + content-type: text/event-stream
- GET /api/stream/sse does NOT return stream-item-not-found (regression guard)
Each test cancels the response body (`res.body?.cancel()`) so the long-lived SSE stream releases cleanly. The content-type assertion proves the SSE handler is reached (vs the JSON 404 the catchall would emit).
RE-VERIFICATION COMMANDS (all green; node v22.22.1):
Focused (under explicit temp-rooted OPENRIG_HOME + OPENRIG_DB):
- `OPENRIG_HOME=/tmp/pl004-r1-sse-final-d OPENRIG_DB=/tmp/pl004-r1-sse-final-d/openrig.sqlite npx vitest run test/queue-repository.test.ts test/queue-routes.test.ts test/coordination-integration.test.ts test/inbox-handler.test.ts test/hot-potato-enforcer.test.ts test/stream-routes.test.ts test/stream-store.test.ts test/outbox-handler.test.ts` → 93/93 PASS (89 R1 baseline + 4 new SSE route-order regression tests).
- daemon tsc --noEmit + npm run build: exit 0.
Compiled-output runtime live-GET check (the canonical anti-regression proof):
```
node -e '
... boot Hono app + queueRoutes() + streamRoutes() from dist/ ...
for (const path of ["/api/queue/sse", "/api/queue/watch", "/api/stream/sse", "/api/stream/watch"]) {
const res = await app.request(path);
console.log(path, "→ status:", res.status, "ctype:", res.headers.get("content-type"));
await res.body?.cancel();
}
'
```
Output (all 4 paths):
- /api/queue/sse → status: 200 ctype: text/event-stream
- /api/queue/watch → status: 200 ctype: text/event-stream
- /api/stream/sse → status: 200 ctype: text/event-stream
- /api/stream/watch → status: 200 ctype: text/event-stream
Honest per-package baselines (NO env override):
- daemon: 2334/2334 PASS / 0 FAIL (R1 baseline 2330 + 4 new).
- cli: 757/761 PASS / 4 FAIL named pre-existing flakes (chatroom wait timing, config-store env pollution, two preflight host-port). NO new regressions; matches prior baseline.
DB ISOLATION:
All test runs used explicit temp-rooted OPENRIG_HOME + OPENRIG_DB under /tmp/pl004-r1-sse-*. No daemon launched. No production DB opened.
BOUNDARIES PRESERVED:
- Worktree-only changes; main checkout `/Users/wrandom/code/projects/openrig` HEAD unchanged at ab822eef throughout. Verified pre and post.
- 4 files changed (all in packages/daemon/{src,test}/routes-and-tests). No CLI changes; no domain changes.
- No host daemon mutation. No host credential mutation. No live rig topology mutation.
- No standing Codex auto-review or approval-heavy loop.
- Normal posture approvals_reviewer=user honored throughout.
LESSON LEARNED (filed inline; carry into future PR review discipline):
HEAD-method tests against SSE/streaming routes can pass while the underlying route-resolution is broken. Test discipline for streaming routes: GET the route, assert response.status + content-type header, then cancel the body to release the stream. Hono's app.request returns the Response synchronously even when the body is a stream; checking headers without consuming body avoids hangs.
HANDOFF RECOMMENDATION: ready-for-qa-proof.
QA's sustained proof loop now has a green-for-real SSE surface; can resume the cross-loop dogfood + Velocity-pair cross-rig validation + reissued VM proof per the original R1 dispatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes adjudication revise-required from delivery-orch-lead@openrig-velocity
per qitem-20260503021500-49b0db82 + handoff packet
substrate/rigs/openrig-velocity/state/delivery-orch/pl004-phase-a-revision-driver-handoff-20260503.md.
Foundation: openrig d8d8ee9 (Phase A R0). Branch: phase-a-coordination-primitive-system (worktree-scoped; main NOT touched).
PRIOR REVIEW INPUTS (all four corroborate the R1 deliverable list):
- Codex independent review (revise-required): qitem-20260503020120-ef9b04db / artifact pl004-phase-a-independent-codex-review-20260503.md
- Claude independent review (clean-with-followups): qitem-20260503020117-0efbff1d / artifact pl004-phase-a-independent-claude-review-20260503.md
- Empirical QA (proof-corroborated-with-caveats): pl004-phase-a-empirical-qa-20260503.md
- Model-diverse blocker packet (blocked-pending-adjudication): coordination-primitive-system-phase-a-d8d8ee9-model-diverse-review-blocker-20260503.md
- Adjudication: orch-lead@openrig-velocity-claude → revise-required.
R1 SCOPE DELIVERED (4 ratified Phase A gaps):
1. Topology-backed validateRig wired in startup.ts (closes Codex finding 1; closes Claude FU2-rebound):
- packages/daemon/src/startup.ts:131 now constructs QueueRepository with `validateRig: topologyValidateRig` that splits `<member>@<rig>` and rejects rigs absent from rigRepo.findRigsByName(). Bare ids (no @) also rejected (no canonical rig binding).
- packages/daemon/src/domain/queue-repository.ts: `create()` and `handoff()` already called the hook; new `handoffAndComplete()` also enforces it.
- Route + repo + CLI rejection coverage: queue-routes.test.ts adds 4 R1 tests (unknown-rig 400 on create / on handoff / on handoff-and-complete + accept-known-rig); queue-repository.test.ts already had the hook test.
2. Default nudge wiring + persistence (closes Codex finding 2):
- QueueRepository now takes optional `transport: QueueNudgeTransport` (minimal interface; concrete impl is SessionTransport). After successful create / handoff / handoff-and-complete commits, fires `transport.send(destination, "Queue handoff: <qitemId> - check your queue.", { verify: true })` and persists last_nudge_attempt + last_nudge_result via existing recordNudgeAttempt(). recordNudgeAttempt() is no longer dead code.
- Opt-out: `nudge: false` in the input skips the call entirely. CLI exposes `--no-nudge` on create / handoff / handoff-and-complete. Default behavior is nudging.
- InboxHandler.absorb() passes `nudge: false` when creating the absorbed qitem (the inbox surface already had its own delivery via the original drop; second nudge on absorb would be duplicative).
- Nudge failure is recorded as `failed:<reason>` in last_nudge_result; the underlying queue mutation is NOT unwound (operator can retry the wake separately).
- Post-construction transport attachment: new `attachTransport()` setter lets startup.ts wire the transport AFTER both QueueRepository and SessionTransport are constructed (SessionTransport depends on agentActivityStore which depends on eventBus, so it's later in the dep graph).
- 5 R1 tests in queue-repository.test.ts: nudge default + opt-out + failure-recording + handoff nudges + attachTransport post-hoc wiring.
3. Missing route/CLI surfaces (closes Codex finding 3):
- POST /api/queue/:qitemId/handoff-and-complete (new route) — variant of handoff() that closes source as state=done (terminal) instead of state=handed-off (intermediate). Same atomic close+create + chain_of_record + default-nudge contract. Mirrored on the CLI as `rig queue handoff-and-complete <qitemId> --from --to [--body --note --priority --tier --tags --no-nudge --json]`.
- GET /api/queue/whoami (new route; mounted before /:qitemId so literal path wins) — returns {session, asDestination:{pending,inProgress,blocked,recent[]}, asSource:{total}} for the queried session. Read-only. CLI: `rig queue whoami --session <s> [--recent-limit N --json]`.
- GET /api/stream/sse and GET /api/queue/sse — Phase A contract route names per IMPL § Routes (which named /api/stream/sse). Mounted as a SECOND alias alongside the existing /watch handler in both stream.ts and queue.ts (same handler function dual-mounted; either path emits the identical event stream). Documented as "compat alias" in code comments. No behavior change for existing /watch consumers.
- 3 R1 tests in queue-routes.test.ts (whoami GET success + missing-session 400 + handoff-and-complete success + handoff-and-complete missing-fromSession). 1 R1 test in stream-routes.test.ts (sse mounted alongside watch). 2 R1 tests in queue-repository.test.ts for whoami + handoff-and-complete repository methods.
4. CLI tests for queue + stream (closes Codex finding 3 + Claude FU8):
- NEW packages/cli/test/queue.test.ts (9 cases): registration check + create POST shape + --no-nudge passthrough + update done-without-closure_reason error rendering with validReasons + handoff-and-complete POST shape + whoami query params + unknown-rig 400 surfacing + handoff --no-nudge + list filter params.
- NEW packages/cli/test/stream.test.ts (8 cases): registration check + emit POST shape + --id idempotent passthrough + list filter + show + archive + non-2xx exit on emit + 404 exit on show.
- Pattern mirrors compact-plan.test.ts: vi.mock daemon-lifecycle, inject clientFactory with stubbed HTTP client. No real daemon. No DB. No network.
ASYNC CASCADE (mechanical):
- QueueRepository.create() / handoff() / new handoffAndComplete() are now async (they `await` the post-commit nudge call). Routes + InboxHandler.absorb() updated to await. Test files updated:
* queue-repository.test.ts: every it() callback async; every repo.create()/repo.handoff() awaited; .toThrow() patterns refactored to .rejects.toThrow() where appropriate.
* inbox-handler.test.ts: same async-cascade for inbox.absorb() callers.
* queue-routes.test.ts: route handler at /inbox/:inboxId/absorb now awaits InboxHandler.absorb().
- InboxHandler.absorb() is now async; the queueRepo.create() call inside still happens BEFORE the inbox UPDATE transaction (Claude FU1 atomicity concern from prior review remains as a separate followup; in-scope fix would be to wrap both in a single SAVEPOINT-nested transaction).
RE-VERIFICATION COMMANDS (all green; node v22.22.1):
Focused (under explicit temp-rooted OPENRIG_HOME + OPENRIG_DB):
- daemon: `OPENRIG_HOME=/tmp/pl004-r1-final-d OPENRIG_DB=/tmp/pl004-r1-final-d/openrig.sqlite npx vitest run test/queue-repository.test.ts test/queue-routes.test.ts test/coordination-integration.test.ts test/inbox-handler.test.ts test/hot-potato-enforcer.test.ts test/stream-routes.test.ts test/stream-store.test.ts test/outbox-handler.test.ts` → 89/89 PASS (68 baseline + 21 R1).
- cli: `OPENRIG_HOME=/tmp/pl004-r1-final-c OPENRIG_DB=/tmp/pl004-r1-final-c/openrig.sqlite npx vitest run test/queue.test.ts test/stream.test.ts` → 17/17 PASS (NEW; no prior CLI tests for queue/stream).
- daemon tsc + build: exit 0; cli tsc + build: exit 0.
Compiled-output runtime checks:
- node import dist/domain/queue-repository.js → QueueRepository: function; attachTransport: function; handoffAndComplete: function; whoami: function.
- node import dist/index.js → queue subs: claim,create,fallback,handoff,handoff-and-complete,inbox-absorb,inbox-deny,inbox-drop,inbox-pending,list,outbox-list,outbox-record,overdue,show,transitions,unclaim,update,whoami; stream subs: archive,emit,list,show.
Full per-package suites (NO env override; honest baseline):
- daemon: 2330/2330 PASS / 0 FAIL.
- cli: 757/761 PASS / 4 FAIL named pre-existing flakes (chatroom wait timing, config-store env pollution, two preflight host-port). NO new regressions; matches prior PL-004 review baseline.
DB ISOLATION (per dispatch boundary):
- All test runs used explicit temp-rooted OPENRIG_HOME + OPENRIG_DB under `/tmp/pl004-r1-*`.
- No daemon launched. No production DB opened. The d8d8ee9 prior incident (5 additive tables in production DB) remains inert per orch decision; this commit does not interact with production state.
BOUNDARIES PRESERVED:
- Worktree-only changes; main checkout `/Users/wrandom/code/projects/openrig` HEAD unchanged at ab822ee throughout. No merge / rebase / cherry-pick / commit on main. Verified pre and post.
- 12 files changed (10 modified + 2 new), all in packages/daemon/{src,test} and packages/cli/{src/commands,test}.
- M2/M3/M4 surfaces (Restore-Packet vertical) NOT touched.
- Velocity prior-art .mjs unchanged.
- No host daemon mutation. No host credential mutation. No live rig topology mutation. No baseline VM mutation.
- No production DB cleanup attempted (per dispatch boundary).
- No standing Codex auto-review or approval-heavy loop (per safety stop).
- Normal posture approvals_reviewer=user honored throughout.
HANDOFF RECOMMENDATION: ready-for-qa-proof.
Driver-side focused verification passed (89 + 17 = 106 focused tests under explicit temp DB isolation + compiled-output runtime checks). QA-side sustained proof loop (cross-loop dogfood from disposable VM + Velocity-pair cross-rig validation + nudge persistence under real SessionTransport) belongs to velocity-qa per the velocity SOP correction (driver fast + implementation + focused verify; QA owns sustained VM/dogfood/E2E/runtime proof loops).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes M2c-Daemon dispatch qitem-20260502020626-8cd2b7a6. Final sub-chunk of M2; M2 marshal-acceptance gate evaluated as a whole at this commit.
Predecessors:
- M2a R2 ACCEPT: openrig 5e3a... (schema-validator + JSON schema asset).
- M2b ACCEPT: openrig bdf4cf6 (runtime parsers + redaction + omitted-records).
- M2b R2 ACCEPT: openrig 5c87145 (Claude parser nested tool_use/tool_result).
- M2c-CLI ACCEPT: openrig e5de3ab (packet-writer atomic emission + write action).
- M2c-CLI R2 ACCEPT: openrig a8401f6 (--source-jsonl provenance fix; honest fallback).
AUTH PRIMITIVE (NAMED):
The new GET /api/transcripts/:session/full route uses NO session-scoped authorization. Per orch decision approved-option-a-open-local-transcript-full-read-with-redaction (escalation qitem-20260502020833-68e4eca3), the route adopts the existing tail/grep posture: open route within the daemon's local-trust boundary (127.0.0.1:7433 default), with route-level redaction as the protective primitive. Audit confirmed the daemon middleware stack (server.ts:207-254) has no authentication layer; the only auth-bearing route in the daemon is activity.ts:8-27 (bearer/header token). No caller-identity primitive exists for transcripts today.
ORCH-REFRAMED TEST PLAN: the originally-requested session-scoped denial test (case b in dispatch) was reframed by orch to session-resolution failure (404 unknown session), since no caller-identity primitive exists. A future Product Lab signal should propose a coherent transcript-read auth policy across tail/grep/full — that work is OUT OF SCOPE for M2c-Daemon and is filed as a follow-up signal in this closeout.
ROUTE PATH: GET /api/transcripts/:session/full (consistent with /:session/tail and /:session/grep naming).
DAEMON SIDE (packages/daemon/):
1. NEW packages/daemon/src/domain/transcript-redaction.ts (35 lines):
- TRANSCRIPT_SECRET_PATTERNS array: 5 regexes mirroring CLI's redaction.ts:31-37 (sk-*, gh[pousr]_*, github_pat_*, Bearer *, base64≥40). Patterns duplicated rather than imported because @openrig/cli is not a daemon dependency (and adding it would invert the package graph). Drift between these arrays is a defect; reviewers should diff them on any change.
- redactTranscriptContent(text): pure function applying the v0 policy.
2. EXTEND packages/daemon/src/domain/transcript-store.ts:
- readFull(rigName, sessionName): string | null — returns raw file contents (no terminal-cleanup heuristics; callers handle as needed). Returns "" for empty, null for missing/error. Imports readFileSync from node:fs.
3. EXTEND packages/daemon/src/routes/transcripts.ts:
- GET /:session/full route at L222-244. Resolves session via existing resolveSessionToRig (inherits 404 unknown / 409 ambiguity). Calls transcriptStore.readFull → applies redactTranscriptContent BEFORE c.json serialization. Per Quality Lesson v9: the wire payload MUST be already redacted; client-side redaction is not relied upon.
- Disabled-store check matches tail/grep behavior (404 with explicit "Transcripts are disabled" guidance).
- Missing-file response: 404 with explicit "No transcript for '<session>'" guidance (mirrors tail/grep wording).
DAEMON ROUTE TESTS (packages/daemon/test/transcript-routes.test.ts):
6 new test cases (4 dispatch-required + 1 ambiguity bonus per orch's "if easy" + 1 missing-file companion):
- transcript-routes.test.ts:270-283 — (a) authorized success: existing session returns 200 with full content.
- transcript-routes.test.ts:285-296 — (b) session-resolution failure (orch reframe of denial): unknown session returns 404 with "not found" + "rig ps" guidance.
- transcript-routes.test.ts:298-325 — (c) route-level redaction: 4 synthetic credential patterns (sk-*, ghp_*, github_pat_*, Bearer *) planted in fixture; assertion that none appear in wire payload AND [REDACTED] marker present AND surrounding context preserved.
- transcript-routes.test.ts:327-336 — (d-disabled) transcripts disabled: 404 with "disabled" guidance.
- transcript-routes.test.ts:338-351 — (d-missing) known session but no file: 404 with "No transcript" guidance.
- transcript-routes.test.ts:353-372 — ambiguity 409 across rigs (matches tail/grep precedent; orch easy-bonus case).
CLI FINAL M2 REGRESSION (packages/cli/test/restore-packet.test.ts):
6 new test cases:
- M2c-Daemon final regression — Velocity 4-pack round-trip via CLI: 4 cases, one per Velocity prior-art role (driver, guard, planner, tester). Loads each role's restore-summary.json from field-notes/2026-04-27-velocity-claude-from-codex-restore/, locates the original Codex JSONL via source_jsonl field, runs --source-jsonl through createProgram().parseAsync (Quality Lesson v12 surface), passes --source-session-id-override + --source-rig-override matching the reference packet's source_session (Codex JSONLs carry UUID-shaped rollout ids, not <seat>@<rig> shape — the operator-supplied canonical name in the prior-art .mjs is mirrored via overrides). Asserts: schema validation, source_session_id == reference source_session, source_rig from @ split, source_cwd matches reference, redaction_policy_id == "openrig-v0", omitted_classes non-empty (Velocity packets have many reasoning_records and function_call_output records). Driver case parses 16,455 JSONL lines → 1,218 extracted messages, matching the original .mjs run exactly. Tests gated on file presence; skip with console.warn when source JSONLs absent (other-host portability).
- M2c-Daemon final regression — CLI-surface redaction + omitted round-trips: 2 cases.
- --source-jsonl with credential-pattern fixture: synthetic sk-* in assistant message → emitted transcript-latest.md does NOT contain the secret AND contains [REDACTED]; summary.omitted_classes contains "redacted_secrets"; redaction_policy_id == "openrig-v0".
- --source-jsonl with M2b R2 nested Claude content (tool_use + tool_result inside top-level assistant/user records — Quality Lesson v11 reproducer): emitted summary.omitted_classes contains both function_call_output and raw_tool_outputs; provenance fields preserved from per-record sessionId; touched-files inventory captures path from omitted tool_result. Exercises Quality Lesson v11 + v12 simultaneously via CLI surface.
PROVENANCE PARITY WITH VELOCITY PRIOR ART:
Driver: 1218 extracted messages (matches original .mjs exactly).
Guard: 1031, Planner: 602, Tester: 854 (no original-run reference cached but counts realistic).
All four packets validate against v0 schema; all four preserve source_session_id + source_rig + source_cwd from operator overrides matching the reference packets.
RE-VERIFICATION COMMANDS (all green):
cd packages/cli:
- npx vitest run test/restore-packet.test.ts → 86/86 PASS (75 baseline + 5 R2 + 6 M2c-Daemon final regression).
- npx tsc --noEmit → exit 0.
- npm run build → exit 0; dist/schemas/*.json present.
cd packages/daemon:
- npx vitest run test/transcript-routes.test.ts → 17/17 PASS (11 baseline + 6 new full-read).
- npx tsc --noEmit → exit 0.
- npm run build → exit 0.
Compiled-output check (Quality Lesson v10 + v12):
- node -e 'await import("./dist/routes/transcripts.js")' → transcriptRoutes: function ✓
- node -e 'await import("./dist/domain/transcript-redaction.js")' → redactTranscriptContent: function; sample input "token sk-FakeAbCdEfGhIjKlMnOpQr seen" → "token [REDACTED] seen" ✓
Per-package honest baselines (Quality Lesson v8):
- packages/cli (npx vitest run --root packages/cli): 730 total, 726 PASS / 4 FAIL named flakes (chatroom wait timing, config-store env pollution, two preflight host-port).
- packages/daemon (npx vitest run --root packages/daemon): 2242 total, 2242 PASS / 0 FAIL.
No regression vs M2c-CLI R2 baseline (CLI: 720→726 = +6 new; daemon: 2236→2242 = +6 new).
M2 MARSHAL-ACCEPTANCE GATE (per IMPL § M2 line 142-152, evaluated end of M2c-Daemon):
- M1 contract approved → DONE.
- restore-packet write emits valid packets matching contract § 1 shape (4 required + optional transcript.md) for BOTH adapters → DONE.
- Velocity JSONL round-trip green for ALL FOUR role packets at provenance + omitted + redaction + schema fields → DONE (4 CLI tests).
- Mock-daemon --source-session round-trip green (exercising new full-read route shape) → DONE (preserved at restore-packet.test.ts:1088-1148; updated for R2 with --source-rig-override).
- Mutual-exclusion + auto-detect tests still green → DONE (M2a regression preserved).
- Atomic emission verified (validation failure → no partial directory) → DONE (preserved at restore-packet.test.ts:943-963 + 928-941).
- Redaction + omitted-record filtering verified end-to-end → DONE (new CLI redaction test + Velocity 4-pack + daemon route test c).
- Daemon route tests for full-read route landed and green (4 cases: auth + redaction + disabled/missing) → DONE (6 cases shipped — 4 required + 2 bonus per orch).
- Closeout names auth primitive + cites daemon route tests by file:line → DONE (above).
- No regression in existing CLI / daemon test suites → DONE.
- Posture declared full QA/testing window → DONE.
BOUNDARY DISCIPLINE:
- ONLY 5 files changed (verified via git diff --cached --stat):
packages/cli/test/restore-packet.test.ts (+235)
packages/daemon/src/domain/transcript-redaction.ts (NEW, +35)
packages/daemon/src/domain/transcript-store.ts (+25/-1; readFull addition)
packages/daemon/src/routes/transcripts.ts (+49)
packages/daemon/test/transcript-routes.test.ts (+121)
- M3+ surfaces UNTOUCHED (read/validate stubs remain M3-pending; live host walk M4-pending; VM proof M5-pending).
- Velocity prior-art .mjs at field-notes/2026-04-27-velocity-claude-from-codex-restore/tools/codex-jsonl-to-restore-packet.mjs UNCHANGED.
- Existing tail/grep routes UNCHANGED (per orch — no retrofit in this chunk).
- Codex/Claude parser, redaction (CLI), runtime-detect, omitted-records, types, schema-validator, M2a CLI shell mutual-exclusion, M2c-CLI packet-writer, R2 deriveProvenance: ALL PRESERVED.
- Unrelated dirty work (docs/*, ps.ts, codex-runtime-adapter, daemon SKILL.md) NOT staged.
- No auth tokens, device codes, or real credentials in fixtures (Quality Lesson v9). Synthetic patterns in redaction tests use "Fake" prefix to make non-credential intent unambiguous.
- No transcript content in daemon stdout/stderr (route returns JSON via c.json; no console.log of content).
PRODUCT LAB FOLLOW-UP SIGNAL (per orch directive):
A future slice should propose a coherent transcript-read auth policy across /:session/tail, /:session/grep, and /:session/full. Today these are all open routes within the local-trust boundary; route-level redaction is the only protective primitive. Discovery should examine: (1) whether to add session-token bearing at peer-handshake time, (2) whether to repurpose the activity-hook bearer-token primitive for transcript reads, (3) whether to factor a daemon-wide auth middleware that gates by route classification. This signal will be filed as a durable artifact in the dogfood-ready or work-discovery pool by the velocity-driver after this commit lands.
CUMULATIVE QUALITY-LESSON CARRY-FORWARD (v1-v9 + v10-v12 candidates):
- v6, v7 refined, v8 (honest baseline match per-package), v9 (no transcript content / credentials in logs / fixtures).
- v10 (asset packaging): packet-writer + daemon route handler + daemon redaction module ALL importable from compiled dist/.
- v11 (provider transcript parser nested-content coverage): exercised in regression suite (CLI-surface omitted-record round-trip with nested Claude shape).
- v12 (writer-internal round trips not enough for CLI source adapters): exercised in regression suite (Velocity 4-pack round-trip + redaction round-trip + nested-content round-trip ALL via createProgram().parseAsync).
Posture: full QA/testing window (matches dispatch + IMPL § M2 line 152).
Dispatch: qitem-20260502020626-8cd2b7a6.
Stop-and-flag: qitem-20260502020833-68e4eca3 (orch decision approved-option-a).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes guard BLOCK on M2c-CLI at e5de3ab. Per dispatch
qitem-20260502014948-af2d4713 + BLOCK artifact at
substrate/rigs/openrig-velocity/state/orch/restore-packet-M2c-cli-guard-checkpoint-block-2026-05-02.md.
Bug (R1 / e5de3ab):
- `--source-jsonl` adapter wrote the JSONL FILE PATH into
restore-summary.json.source_session_id (not the parsed session id).
- packet-writer.ts:174-175 silently derived source_rig from sourceSessionId
via @ split, masking the bug as `source_rig: "unknown"` for any value
without @ shape.
- Schema-valid but semantically WRONG; exactly the source-provenance
drift the restore-packet standard exists to prevent. Writer-internal
tests bypassed the parse-to-write provenance path entirely.
Fix:
1. CLI adapter (commands/restore-packet.ts): after parseCodexJsonl /
parseClaudeTranscript, derive canonical (source_session_id, source_rig)
honestly via new deriveProvenance() helper.
- For --source-jsonl: prefer parsed structured.sessionMeta?.sessionId
over the file path; fail with explicit operator guidance if absent
and no override flag supplied.
- source_rig: <seat>@<rig> regex split; fail if shape doesn't match
and no override flag supplied. No silent "unknown" fallback.
2. New flags --source-session-id-override and --source-rig-override give
operators an explicit honest path when source JSONL has no session_meta
(e.g., raw test fixtures, mid-session captures) or session id is bare.
3. packet-writer.ts: WritePacketOptions adds required `sourceRig: string`.
Writer no longer parses sourceSessionId — that derivation moved to the
adapter where it belongs.
Tests (TDD red-then-green; 5 new R2 cases at the CLI surface using
createProgram().parseAsync with synthetic JSONL fixtures, exercising the
parse-to-writePacket adapter path that 75/75 was bypassing):
- guard reproducer: --source-jsonl with session_meta sets
source_session_id from parsed id and source_rig from <seat>@<rig> split.
- --source-jsonl with NO session_meta and NO overrides fails with
explicit operator guidance referencing both override flags.
- --source-jsonl with NO session_meta + both overrides succeeds with
overridden provenance.
- --source-jsonl with bare-id session_meta fails without
--source-rig-override (no silent unknown).
- --source-jsonl with --source-session-id-override overrides parsed
session_meta id (override-wins precedence).
Updates to existing tests:
- M2c-CLI atomic-emission, Velocity-shape round-trip, M2b R2 nested-content
fixtures pass `sourceRig:` explicitly to writePacket() (now required).
- Mock-daemon --source-session round-trip uses --source-rig-override
for bare "src-session" fixture and asserts source_rig is honored.
Re-verification (all green; honest baseline match per Quality Lesson v8):
- npx vitest run test/restore-packet.test.ts → 80/80 pass (75 baseline + 5 R2).
- npx tsc --noEmit → exit 0.
- npm run build → exit 0; dist/schemas/restore-summary.schema.json present.
- Compiled-output guard reproducer (BLOCK artifact verbatim) →
source_session_id="velocity-driver@openrig-velocity",
source_rig="openrig-velocity", validation_valid=true.
- Full CLI suite → 720/724; 4 failures match named flake baseline
(chatroom wait timing, config-store env pollution, two preflight
host-port). No regression vs 715/719 + same 4 flakes pre-R2.
M2c-Daemon surfaces (packages/daemon/src/routes/transcripts.ts +
packages/daemon/test/transcript-routes.test.ts) NOT touched per orch
sub-chunking. Velocity prior-art .mjs UNCHANGED. M3 read/validate stubs
preserved. No auth tokens or credentials in fixtures (Quality Lesson v9).
Quality Lesson v12 (NEW; filed at this dispatch): writer-internal round
trips are not enough for CLI source adapters — slices that claim adapter
functionality MUST include at least one compiled-output CLI-path
provenance check that exercises the actual command surface
(createProgram().parseAsync against synthetic JSONL with realistic
session_meta). Filed inline at this commit; will be formalized at M2
marshal-acceptance close.
Posture: full QA/testing window (matches M2 declared posture).
Dispatch: qitem-20260502014948-af2d4713.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Guard BLOCKED M2b at openrig bdf4cf6: Claude transcript parser silently
dropped `tool_use` parts (inside assistant content) and `tool_result`
parts (inside user content) without counting them against the omitted-
record-class enum, leading to all-zero `omittedCounts` on real Claude
transcripts where every assistant turn typically carries a `tool_use`
and most user turns carry a `tool_result`. M1 contract § 5 requires
`omitted_classes` to enumerate the classes ACTUALLY excluded from
transcript output; the previous parser would emit a contract-violating
all-zero summary at M2c.
R2 fix in src/restore-packet/claude-transcript-parser.ts:
- Replace `textFromClaudeMessage` with `walkClaudeContent` returning
`{ text, toolUseCount, toolResultCount, toolPaths }`. The walker
inspects each content part once, branching on `part.type`:
- `"text"` → contribute to user-visible text.
- `"tool_use"` → increment `function_call_output` counter; serialize
`part.input` and route to path extraction.
- `"tool_result"` → increment `raw_tool_outputs` counter; flatten
`part.content` (string OR array of `{ type: "text" }` parts) and
route to path extraction.
- The kept-message branch now records nested-part omissions BEFORE
deciding whether to drop a record without visible text. A user turn
that's pure tool_result (no visible text; common in real transcripts)
no longer emits a kept message but DOES count its tool_result against
the omitted-class counter and contributes the tool_result content to
the path inventory.
- Path extraction from omitted nested parts mirrors the Codex parser's
behavior of walking function_call args / custom_tool_call inputs for
paths even though the records themselves are omitted from messages.
TDD discipline (per memory feedback_tdd_scope): 8 new tests authored to
fail FIRST. Test commit: 7 fail, 1 pass (a happy-path baseline). Then
the parser fix made all 8 pass. The committed state is the green state.
Tests added (8 new in `M2b R2 Claude parser nested-content-part handling`
describe block):
- guard reproducer fixture (the exact reproducer from the BLOCK
artifact): assistant with tool_use + text, user with tool_result
→ omittedCounts.function_call_output=1, raw_tool_outputs=1, paths
contains "/Users/wrandom/code/projects/openrig-hub".
- counts each tool_use part in a multi-tool assistant turn (tool_use*2 +
text → function_call_output=2; path "/Users/wrandom/code/x.md"
extracted from input).
- counts each tool_result part in a multi-result user turn
(tool_result*2 → raw_tool_outputs=2; both paths extracted).
- mixed text + tool_use: visible text kept; tool_use counted.
- tool_use with non-path input: counted but contributes no paths
(the `cmd: "echo hello"` doesn't match the /Users/wrandom/ or
recognized-prefix patterns).
- tool_result with non-path content: same — counted but no paths.
- tool_result with array content shape: walks the array's text parts
for paths.
- kept text-only assistant turn: counters stay at zero (no regression
on the simple-text path).
Re-verification (per dispatch's required-fix list):
- npx vitest run test/restore-packet.test.ts → 63/63 pass (55 prior +
8 new).
- npx tsc --noEmit → exit 0.
- npm run build → exit 0; dist/schemas/restore-summary.schema.json
copied; dist/restore-packet/claude-transcript-parser.js emitted with
the fix.
- Compiled-output runtime check using the guard's exact reproducer:
node -e 'import(...claude-transcript-parser.js).then(...)'
→ output: `function_call_output: 1 raw_tool_outputs: 1 paths_count: 1`.
Confirms the fix is reachable from compiled JS, not just source TS.
- Full CLI suite re-run: 703/707 pass. M2b baseline was 695/699; R2 adds
8 tests, all green → 703/707. Same 4 pre-existing environmental flakes:
- test/chatroom.test.ts > Chatroom wait detects newly arriving post-start
message (10s timing flake)
- test/config.test.ts > DaemonClient config integration > ConfigStore
non-default host/port (env pollution)
- test/preflight.test.ts > rig preflight --json (host port 7433 in use)
- test/preflight.test.ts > rig preflight check marks (host port 7433 in use)
All four touch zero files in this R2 commit. No regression.
Scope (R2 stays narrow per dispatch):
- Codex parser path-inventory shape NOT changed (the Claude walker
mirrors Codex behavior; no Codex parser regressions).
- Velocity prior-art .mjs FROZEN reference UNCHANGED.
- M2c surfaces (packet-writer.ts, atomic emission, daemon full-read
transcript route, daemon route tests) NOT touched.
- restore-packet write stays M2a-deferred error stub.
- restore-packet read + validate stay M3-pending stubs.
- M3/M4/M5/M6 surfaces NOT touched.
- No openrig source changes outside the two R2 files.
- No daemon code changes.
- No unrelated dirty work staged.
Quality lessons applied:
- v6 boundary isolation: R2 stays inside packages/cli/src/restore-packet/.
- v7 refined: R2 cites M2b R1 BLOCK artifact in this commit message; does
NOT extend M2b R1 grant.
- v8 honest baseline: full CLI suite re-run; named flakes baseline-checked.
- v9 synthetic tokens only: all R2 fixtures use synthetic strings.
- v10 schema-packaging gap: schema-validator self-contained pattern
unaffected; R2 modules are pure TypeScript with no runtime asset
loading.
- v11 candidate (NEW from this BLOCK): "provider transcript parser
nested-content-part coverage." Parser acceptance tests for provider
transcript adapters MUST cover provider-native nested tool-call /
tool-result / inline-tool-use shapes — not only top-level synthetic
omitted records. R2 applies this lesson: 8 new focused tests cover
Claude's nested-content-part shape AND the guard reproducer fixture
is asserted byte-for-byte.
Posture: full QA/testing window (per dispatch).
Source qitem: qitem-20260502011841-fb53d7fd (M2b R2 dispatch).
Guard verdict that triggered this: rigs/openrig-velocity/state/orch/restore-packet-M2b-guard-checkpoint-block-2026-05-02.md.
Per orch decision (Path B chunked M2; M2b chunk after M2a R2 ACCEPT at 40d0724):
five new sub-modules under packages/cli/src/restore-packet/ + extended test
coverage. Velocity prior-art `.mjs` borrowed STRUCTURALLY only (translated to
TypeScript); the .mjs file at field-notes/.../tools/codex-jsonl-to-restore-packet.mjs
remains FROZEN (unchanged).
Sub-modules:
- NEW src/restore-packet/types.ts: shared TypeScript types — ExtractedMessage,
OmittedRecordClass enum (4 values per contract § 5), PathCount, TypeCounts,
OmittedCounts, SessionMeta, StructuredTranscript, SourceRuntime. Both
parsers emit StructuredTranscript; downstream packet-writer (M2c) consumes.
- NEW src/restore-packet/redaction.ts: openrig-v0 policy implementation.
SECRET_PATTERNS const exports the 5-pattern list verbatim from Velocity
prior art `:34-48` (sk-*, gh[pousr]_*, github_pat_*, Bearer *, base64 ≥40).
redact(text) replaces matches with `[REDACTED]`. hasSecretPattern(text) is
the boolean probe used by the parsers' redaction-counter.
- NEW src/restore-packet/omitted-records.ts: per-record-class classifier.
classifyCodexRecord maps Codex `payload.type` to one of the 4 omitted
classes or `kept`; classifyClaudeRecord does the same for Claude `type`
values. OmittedCounter accumulates per-class counts and exposes
activeClasses() for the summary's `omitted_classes` field.
- NEW src/restore-packet/runtime-detect.ts: detectRuntime(content) →
"codex" | "claude-code" | null. Uses content shape (Codex `type:
"response_item"|"session_meta"|"compacted"` markers vs Claude `type:
"user"|"assistant"`). Per dispatch directive "Do NOT use file extensions
or filenames as the detection key. Detect by content shape." — verified
by negative tests with filename-shaped strings.
- NEW src/restore-packet/codex-jsonl-parser.ts: parseCodexJsonl(content) →
StructuredTranscript. Translates Velocity `:81-160` to TypeScript:
session_meta capture, compacted counter, response_item dispatch (kept
user-visible messages get redacted + path-extracted; tool calls have
paths extracted then omitted; reasoning records counted as omitted).
Malformed JSONL lines skipped silently (matches Velocity behavior).
- NEW src/restore-packet/claude-transcript-parser.ts: parseClaudeTranscript(
content) → StructuredTranscript. NEW work; no prior art. Emits the
SAME shape as the Codex parser. Walks Claude transcript JSONL: captures
sessionMeta from first record carrying cwd + sessionId; user/assistant
records become messages (with redaction); attachment records go into
raw_tool_outputs; other meta records go into reasoning_records.
compactedCount is 0 (Claude transcripts don't emit compaction records).
Tests (37 new tests added to packages/cli/test/restore-packet.test.ts;
total 18 M2a + 37 M2b = 55):
- 7 redaction tests (each pattern + non-credential round-trip + pattern
list size).
- 9 omitted-records tests (classifier branches per runtime + counter
accumulation + active-classes ordering).
- 7 runtime-detect tests (codex via response_item / session_meta;
claude via user/assistant; null on empty/garbage/ambiguous; explicit
"no filename heuristic" negatives).
- 7 codex-jsonl-parser tests (session_meta + messages; reasoning +
function_call + custom_tool_call filtering; compacted counter; redaction
in content; malformed line skip; path extraction with frequency sort).
- 6 claude-transcript-parser tests (user + assistant; sessionMeta capture;
attachment → raw_tool_outputs counter; redaction; compactedCount 0;
shape conformance with codex parser via Object.keys equality).
- 1 interaction test (Codex parse → redact → omitted-records counter
chain produces a structured representation with all 3 codex omitted
classes counted AND the redacted_secrets counter incremented for the
user message that had a credential pattern).
TDD discipline applied per memory feedback_tdd_scope: tests reference
modules that didn't exist → import failures → modules implemented to
green. The committed state is the green/passing state.
Re-verification (per dispatch's required-fix list):
- npx vitest run test/restore-packet.test.ts → 55/55 pass.
- npx tsc --noEmit → exit 0.
- npm run build → exit 0; dist/restore-packet/{types,redaction,
omitted-records,runtime-detect,codex-jsonl-parser,claude-transcript-parser}.js
emitted; dist/schemas/restore-summary.schema.json copied.
- Compiled-output runtime check: node -e import each compiled module →
detectRuntime, redact, parseCodexJsonl, parseClaudeTranscript all
loadable as functions.
- Full CLI suite re-run (per Quality Lesson v8 — never silently downgrade):
695/699 pass. Baseline at M2a R2 was 658/662; M2b adds 37 tests, all
passing → 695/699. Failure pattern UNCHANGED:
- test/chatroom.test.ts > Chatroom wait detects newly arriving post-start
message (10s timing flake)
- test/config.test.ts > DaemonClient config integration > ConfigStore
non-default host/port (env pollution)
- test/preflight.test.ts > rig preflight --json (host port 7433 in use)
- test/preflight.test.ts > rig preflight check marks (host port 7433 in use)
All four pre-existing environmental flakes touch zero files in this M2b
commit. No regression.
Boundary discipline (per dispatch — M2b does NOT touch):
- packet-writer.ts (M2c).
- Atomic write-via-tempdir-then-rename (M2c).
- Daemon full-read transcript route extension (M2c).
- Daemon route tests (M2c HARD DELIVERY FOLLOW-UP).
- restore-packet write actual implementation: stays the M2a-deferred
error stub. UNCHANGED.
- restore-packet read + validate: stay M3-pending stubs. UNCHANGED.
- M3/M4/M5/M6 surfaces.
Quality lessons applied:
- v3 distinct test summary prefixes: assertion failures use ASSERTIONS_
prefixes where applicable; test-runner reports its own PASS/FAIL.
- v4 marshal-layer wire-shape: shape-conformance test asserts both
parsers expose identical top-level keys and identical omittedCounts
enum keys (the 4 contract values).
- v5 test-naming-vs-instantiation: tests invoke parseCodexJsonl /
parseClaudeTranscript / detectRuntime / redact / classify* / OmittedCounter
directly, NOT mock-spy.
- v6 boundary isolation: M2b stays inside packages/cli/src/restore-packet/;
daemon/test code untouched.
- v7 refined: M2b does NOT extend M2a R2 grant; this commit cites the
separate M2b grant via the dispatch packet ID.
- v8 honest baseline: full CLI suite re-run; named flakes baseline-checked.
- v9 no auth tokens: all fixtures use synthetic tokens (sk-AbCdEf...,
ghp_AbCdEfGh..., etc. — clearly synthetic, NOT real provider tokens).
- v10 schema-packaging gap (M2a R2 lesson): M2b sub-modules are pure
TypeScript (no runtime asset loading), so the M2a R2 packaging issue
doesn't apply here. The schema embed + drift-catcher pattern from M2a
R2 stands.
Posture: full QA/testing window (per dispatch).
Source qitem: qitem-20260502010558-463ff419 (M2b dispatch).
Parent: M2a R2 ACCEPT at openrig 40d0724 (qitem-20260502005113-24bf9de8 closure).
Guard BLOCKED M2a at de2dbd4 with a build/packaging gap: schema-validator
read `restore-summary.schema.json` from disk via import.meta.url, but tsc
does not copy non-TS files into dist. Compiled validator hit ENOENT at
runtime in the guard's reproducer (raw `npx tsc --outDir <cache> --rootDir
src` + `node -e "import(...).then(m => m.validateRestoreSummary({}))"`).
R2 fix (combined approach 2 + approach 1 from guard verdict):
(1) src/restore-packet/schema-validator.ts: schema is now embedded as a
typed TS const RESTORE_SUMMARY_SCHEMA (verbatim from contract § 2.1 and
the canonical .json file). Validator uses the const directly; no
import.meta.url file read at runtime. tsc emits the const into compiled
JS, so `tsc` alone (no copy step) leaves a working validator.
(2) src/schemas/restore-summary.schema.json: kept as the canonical source
for downstream IDE / tooling consumption. Unchanged content.
(3) test/restore-packet.test.ts: added a drift-catcher test that loads
the JSON file and asserts `RESTORE_SUMMARY_SCHEMA` (the TS const) equals
it byte-for-byte. Prevents the two forms from diverging silently. Test
count 17 → 18.
(4) packages/cli/package.json: build script changed from `tsc` to
`tsc && mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/`.
This is approach 1 from the guard verdict — keeps the JSON Schema
reachable in `dist/schemas/` for downstream tooling AFTER `npm run build`.
Note: the guard's reproducer uses raw `tsc` (not `npm run build`), so the
copy step is for distribution only; the runtime correctness comes from
approach 2 (the embedded TS const).
Re-verification (per guard's required-fix list):
- `npx vitest run test/restore-packet.test.ts` → 18/18 pass (M2a focused
suite + drift-catcher; M2a's prior 17 still green plus the new test).
- `npx tsc --noEmit` → exit 0; no type errors.
- Guard's reproducer:
rm -rf node_modules/.cache/openrig-cli-m2a-r2-build-check
npx tsc --outDir node_modules/.cache/openrig-cli-m2a-r2-build-check --rootDir src
node -e "import('node_modules/.cache/openrig-cli-m2a-r2-build-check/restore-packet/schema-validator.js').then((m) => { const r = m.validateRestoreSummary({}); console.log('valid:', r.valid, 'errors:', r.errors.length); }).catch(...)"
→ output: `valid: false errors: 21` (21 required-field violations for
empty `{}` summary, exactly as expected from the schema's 21-required-
field set). NO ENOENT.
Full CLI suite re-run (per Quality Lesson v8 — never silently downgrade):
- npx vitest run (full suite) → 658/662 pass.
- M2a baseline was 657/661; R2 adds the drift-catcher test, so totals
shift by +1/+1. Failure pattern UNCHANGED:
- test/chatroom.test.ts > Chatroom CLI > chatroom wait detects a newly
arriving post-start message (10s timing flake)
- test/config.test.ts > DaemonClient config integration > ConfigStore
resolves non-default host/port from config file (env pollution from
prior test runs)
- test/preflight.test.ts > Preflight CLI > rig preflight --json prints
structured result (host port 7433 in use; real daemon running)
- test/preflight.test.ts > Preflight CLI > rig preflight prints check
marks and exits 0 on pass (same host port 7433 issue)
All four are pre-existing environmental flakes (host has a real daemon
on port 7433; chatroom timing flake; env pollution between test runs).
None of the four failures touch any file in this R2 commit. No
regression introduced.
Scope (R2 stays narrow per dispatch):
- `restore-packet write` still a M2a shell that enforces mutual-exclusion
+ required-source behavior + returns the M2a deferred-implementation
error. UNCHANGED.
- `restore-packet read` + `restore-packet validate` still return M3-pending
errors. UNCHANGED.
- M2b parsers + redaction + omitted-records + packet-writer + daemon
route extension + daemon route tests NOT touched.
- No openrig source changes outside the three R2 files.
Posture: full QA/testing window (per dispatch's required-posture line).
Source qitem: qitem-20260502005113-24bf9de8 (M2a R2 dispatch).
Guard verdict: rigs/openrig-velocity/state/orch/restore-packet-M2a-guard-checkpoint-block-2026-05-02.md.
Per orch decision (Path B chunked M2; M2a chunk):
- NEW packages/cli/src/schemas/restore-summary.schema.json — JSON Schema
draft-07 for restore-summary.json, derived byte-for-byte from M1 contract
§ 2.1 at substrate/openrig-work/specs/restore-packet/SCHEMA.md. 21 required
fields (16 standard + 5 v1-promoted-to-v0-required) plus the optional
full_transcript object. Nested required-shapes for bounded_latest_transcript
(with const bound: 120), touched_files, and durable_pointers (4 required
arrays). additionalProperties: false at the root.
- NEW packages/cli/src/restore-packet/schema-validator.ts — TypeScript
validator using ajv 8.18.0 + ajv-formats 3.0.1 (added to deps). Loads the
schema file once (module-level cache), compiles into an ajv validator,
and returns per-field ValidationError records (field, value, rule,
severity) per contract § 8 reporting shape. M3 validate command + M2c
generator-side emit-time self-check both consume this module.
- NEW packages/cli/src/commands/restore-packet.ts — commander command shape
per IMPL § M2 line 105-141. Three subcommands: write / read / validate.
M2a implements write's mutual-exclusion gate (--source-session vs
--source-jsonl: exactly-one-required) and the missing-target check via
commander requiredOption. read + validate are M2a stubs that report an
M3 deadline; their CLI surfaces are registered now so the command tree
is discoverable.
- EDIT packages/cli/src/index.ts — register restorePacketCommand after
restoreCheckCommand (alphabetical: restore < restore-check < restore-packet
per re-anchor finding); wire RestorePacketDeps into ProgramDeps.
- EDIT packages/cli/package.json — add ajv ^8.18.0 + ajv-formats ^3.0.1 to
dependencies. Both already hoisted in monorepo node_modules at the same
versions; this commit declares them honestly.
- NEW packages/cli/test/restore-packet.test.ts — 17 Tier 1 tests:
- 11 schema-validator accept/reject tests covering each required-field
family (source_session_id required; source_runtime enum; source_cwd
pattern; bounded_latest_transcript.bound const 120; durable_pointers
nested required arrays; redaction_policy_id enum; source_trust_ranking
minItems; generated_at format; additionalProperties: false; optional
full_transcript accepted; valid baseline accepted).
- 6 CLI shell tests: subcommands present (read/validate/write); write
mutual-exclusion both supplied; write neither supplied; --target
missing; read M2a stub registered; validate M2a stub registered.
TDD discipline (per memory feedback_tdd_scope): test file authored before
schema-validator.ts and restore-packet.ts (failing import), then
implementations brought the suite to green. Committed state is the green
state.
Test runs:
- packages/cli vitest run test/restore-packet.test.ts → 17/17 pass.
- packages/cli vitest run (full suite) → 657/661 pass; the 4 failures are
the pre-existing environmental flakes from prior session (preflight x2:
port 7433 in use; chatroom wait: 10s timeout flake; config-store: env
pollution). All four touch zero files in this M2a commit. No regression.
Posture declared: full QA/testing window. Out of scope for M2a (per orch
chunked decision): codex-jsonl-parser / claude-transcript-parser /
runtime-detect / redaction / omitted-records / packet-writer (M2b);
daemon route extension + daemon route tests (M2c); read/validate command
implementations (M3); M4-M6 proof work.
Source qitem: qitem-20260501232012-e660c0ce (Restore-Packet vertical M2).
Orch chunked-M2 decision: arrived as inline reply on the scope-check flag
qitem-20260501232237-7748b7d6.
Guard blocked M2 R1 (commit 2f8f269) with four findings — all in tests;
the source patch in `rigspec-instantiator.ts` is correct. M2 R2 lands
the four test patch rows from IMPLEMENTATION.md § M2 Revision 2.
Patch rows:
- M2-R2-1: replace mockClaudeAdapter / mockCodexAdapter with real
ClaudeCodeAdapter / CodexRuntimeAdapter; mock at the tmux + fsOps
boundary. The real `deliverStartup` now fires the real `guidance_merge`
branch and the per-seat CLAUDE.md (claude) / AGENTS.md (codex) ends
up containing the starter content inside a managed-block envelope.
- M2-R2-2: tighten POST /api/up positive to apply mode + assert STARTER
reaches startup-orchestrator via `node_startup_context.resolved_files_json`
SQLite roundtrip; add credential-bearing fixture failed-scan negative
that confirms the launch refuses with a clear error AND no
startup_context row is persisted (load-bearing Finding 2 contract at
the route boundary). Schema-composition rejection cases (fork+starter_ref,
terminal+starter_ref) remain plan-mode tests since they fail upfront.
- M2-R2-3: CLI `rig up --plan --json` now asserts that resolved starter
contents reach plan output. The daemon's mocked plan response carries
a `resolve_starter` stage with `detail.starterContent`; the CLI's
--json branch (commands/up.ts:175-181) renders the full daemon
response, so starter resolution is visible to operators. Path A from
the M2 R2 dispatch — no CLI source change.
- M2-R2-4: instantiator test gains two assertions:
(a) continuityOutcome stays "fresh" when starter is the only continuity
source (asserts startNode call args carry no resumeToken/forkSource/
rebuildArtifacts AND the resolved promise carries
continuityOutcome="fresh");
(b) STARTER layer survives `node_startup_context.resolved_files_json`
JSON roundtrip with the registry path / appliesOn / deliveryHint
shape intact.
Test-app helper extended with optional `podInstantiatorFsOps` and now
exposes `startupOrchestrator` so apply-mode route tests and spy-based
assertions work without monkey-patching.
No source-code changes: the rigspec-instantiator.ts patch from M2 R1
is unchanged and correct.
Marshal-acceptance:
- daemon vitest: 143 files / 2236 tests pass.
- cli up.test.ts: 28/28 pass.
- 4 other CLI failures (preflight x2, chatroom-wait, config-store)
are pre-existing environmental flakes — host has a real daemon on
port 7433 — and pre-date M2 / M2 R2.
Wire AgentStarterResolver into pod-rig instantiation and prepend a STARTER
layer in `resolvedStartupFiles`, so an agent member with `starter_ref` flows
its starter artifacts through the existing `deliverStartup` adapter seam
without any adapter-side code change. Resolver-throw aborts the launch with
a clear startup error and no `deliverStartup` call (load-bearing
credential-safety contract — Finding 2).
Source changes:
- packages/daemon/src/domain/rigspec-instantiator.ts: in `launchExistingAgentMember`,
invoke the resolver when `member.starterRef` is set; on
`AgentStarterCredentialScanFailedError` / missing-entry / malformed-yaml,
return `{status: "failed"}` before any adapter `deliverStartup` runs.
STARTER artifacts are prepended ahead of `dedupedResolvedFiles` to form
the new STARTER layer at the front of the layer chain.
New tests:
- agent-starter-instantiator.test.ts (5 tests): resolver invoked + STARTER
prepended; resolver-throw aborts launch with no `deliverStartup` call;
missing entry aborts; absent starterRef = no STARTER layer; rebuild
composition allowed.
- agent-starter-adapter-claude.test.ts (1 test): claude-code adapter
receives STARTER artifacts at front of `deliverStartup`.
- agent-starter-adapter-codex.test.ts (1 test): codex adapter receives
STARTER artifacts at front of `deliverStartup`.
Updated tests:
- up-route.test.ts: 3 new M2 cases (positive plan-mode wiring proof,
fork+starter_ref rejected per Finding 1, terminal+starter_ref rejected
per secondary cleanup). Required threading optional `upRouterFsOps`
through `createTestApp` so the route can read a real fixture spec.
- pod-rigspec-instantiator.test.ts: M1 forward-compat smoke now feeds the
resolver a fixture registry entry (M2 actively reads the registry; M1
test was a passthrough).
- packages/cli/test/up.test.ts: smoke proving `rig up <fixture-with-starter_ref> --plan`
forwards the spec path to the daemon and surfaces "planned".
Marshal-acceptance:
- daemon vitest: 143 files / 2233 tests pass (M1 R2 baseline 2223 + 10
new M2 tests).
- cli up.test.ts: 28/28 pass. Other CLI failures (preflight, chatroom,
config-store) are environmental — host has a real daemon running on
port 7433 — and pre-date M2.
Revision 2 of M1. Closes three guard-blocked findings against commit
eb4acf2. All within M1 envelope; no M2/M3/M4/M5 scope expansion.
Finding 1 (R2-1): RigSpecCodec.serialize() dropped starterRef
====================================================================
M1 R1 normalized snake-case `starter_ref` input into `member.starterRef`
but the canonical pod-aware serializer at rigspec-codec.ts:48-68 had no
emission branch for it. The forward-compat smoke at
pod-rigspec-instantiator.test.ts:657-671 was therefore a false proof —
the YAML it passed to instantiate dropped `starter_ref:` silently.
Patch:
- rigspec-codec.ts: insert `if (m.starterRef) member["starter_ref"] = { name: m.starterRef.name };`
after the session_source emission branch.
- rigspec-codec.test.ts: 3 new tests covering
- serialize → parse → validate → normalize roundtrip with starter_ref
asserting wire-shape (`yaml.toContain("starter_ref:")`) AND
seed-equality (`member.starterRef === seed`);
- starter_ref + session_source.mode="rebuild" both survive (composition
allowed per Finding 1 of M1 R1);
- specs without starter_ref do not emit a spurious starter_ref: line.
- pod-rigspec-instantiator.test.ts: forward-compat smoke now asserts
`expect(yaml).toContain("starter_ref:")` and `expect(yaml).toContain("fixture-starter")`
before instantiation — closes the false-proof gap.
Finding 2 (R2-2): ExpansionPodFragment + buildSyntheticSpec drop starterRef
==========================================================================
The slice packet's component 1 named expansion pass-through, but
ExpansionPodFragment.members had no `starterRef?` field and
RigExpansionService.buildSyntheticSpec at rig-expansion-service.ts:135-154
had no `starter_ref` emission branch. An expansion request couldn't
carry starterRef through the same route that already carried sessionSource.
Patch:
- types.ts (ExpansionPodFragment.members near line 743): add
`starterRef?: StarterRefSpec` mirroring `sessionSource?: SessionSourceSpec`.
- rig-expansion-service.ts (member emission near line 153): insert
`...(member.starterRef ? { starter_ref: { name: member.starterRef.name } } : {})`
after the session_source emission spread.
- rig-expansion-service.test.ts: 2 new tests
- positive: spy on podInstantiator.materialize to capture the synthetic
YAML; assert it contains `starter_ref:` + the starter name when an
expansion request declares starterRef on a member;
- negative regression: absent starterRef does not leak `starter_ref:`
into synthetic YAML.
Finding 3 (R2-3): AgentStarterCredentialScanFailedError leaks match-line content
================================================================================
At lines 256/265/273 of agent-starter-resolver.ts, the refusal reason
string included `(line: ${JSON.stringify(line)})` — echoing the matched
line content from the malicious file. Once M2 wires this resolver into
`rig up`, error flows would surface credential-bearing lines into logs/
API responses. Hard refusal must identify file + line number + refusal
code WITHOUT echoing secret content.
This closes the v0 wrapper diagnostic-`match`-field caveat at the
daemon-tier productization layer. The wrapper's caveat was operator-
focused; the daemon resolver's audience is broader (logs, API, dashboards)
and demands tighter redaction.
Patch:
- agent-starter-resolver.ts: replace `JSON.stringify(line)` echo in all
three error sites with the literal phrase `(content redacted)`.
Preserved: refusal code (`credential_path_disallowed` /
`credential_content_disallowed`); line number; file path. Comment
added at site 1 explaining the redaction rationale + cross-link to
the v0 wrapper caveat.
- agent-starter-resolver.test.ts: 2 new tests + extensions to 3
existing M1 R1 negative tests
- all 3 R1 negatives now also assert `reason.toContain("content redacted")`
AND `reason.not.toContain("<fixture-secret-string>")` AND
`message.not.toContain("<fixture-secret-string>")` for path,
content, and uppercase-marker fixtures;
- new: sk- token-shaped fixture's reason MUST NOT contain the token
substring;
- new: error reason still preserves non-sensitive diagnostics
(refusal code regex match + `line N` regex match + filePath
presence).
M1 R2 marshal-acceptance gate (per IMPL § M1 Revision 2):
- [x] R2-1 + R2-2 + R2-3 tests green INCLUDING codec roundtrip
wire-shape assertion + expansion YAML wire-shape assertion +
resolver redaction negative tests. ✓
- [x] All M1 R1 tests still green (no regression). ✓
- [x] No regression in existing rigspec-codec.test.ts (was 5; now 8 — +3 R2-1),
rig-expansion-service.test.ts (was 14; now 16 — +2 R2-2), or daemon
full suite. ✓
- [x] Posture: full QA/testing window (preserved). ✓
Verification:
- Focused (6 test files): 109/109 pass (agent-starter-schema 12,
agent-starter-resolver 16, rigspec-schema 33, rigspec-codec 8,
rig-expansion-service 16, pod-rigspec-instantiator 24)
- Daemon full regression: 2223/2223 pass (was 2216 at M1 R1; +7 net new
R2 tests across the three findings)
- 140 test files (no new files in R2; all extensions to existing tests)
STOP-AND-ESCALATE clean:
- No M2/M3/M4/M5 surface touched (instantiator launchExistingAgentMember
unchanged; runtime adapter deliverStartup unchanged; CLI up.ts
unchanged; route up.ts unchanged)
- No starter_ref + session_source.mode="fork" composition accepted
(Finding 1 of R1 still enforced via existing tests)
- Credential-scan still THROWS (Finding 2 of R1 still enforced; R2-3 tightens the diagnostic
surface)
- No M5 shrinkage; M5 contract preserved
- No fleet-ok posture for any milestone past M2
Closeout feedback for proof packet — Product Lab feeder-loop quality
lesson v4: "claimed-surface-vs-implemented-surface cross-check at
planning stage." Same family as the prior misses (codex pod-aware,
agent-starter inspect, summary-line-prefix). Pattern: contract names a
surface (here: codec roundtrip, expansion pass-through, productized
error redaction) but implementation doesn't enumerate the surface
literally; the writer/reader assume "of course it's covered." Mitigation:
planner cross-checks each contract phrase against the file:line
implementation table BEFORE driver dispatch; reviewer cross-checks each
test claim against the wire-shape assertion (a green smoke that doesn't
assert the wire shape is a false proof).
Out-of-slice (preserved untouched):
- packages/cli/src/commands/ps.ts, packages/cli/test/ps.test.ts,
docs/as-built/cli-reference.md (concurrent-agent dirty work)
- packages/daemon/src/adapters/codex-runtime-adapter.ts +
packages/daemon/test/codex-runtime-adapter.test.ts (preserved Codex
TOML config-fragment dedup work flagged in prior slices)
- Untracked CLAUDE.md at openrig root (substrate/host overlay)
R2 commit staged ONLY the 8 in-slice files via explicit pathspec.
Slice rails:
substrate/shared-docs/openrig-work/missions/recursive-self-improvement-v2/work-discovery-and-slice-shaping/slices/knowledge-context-agent-starter-v1-vertical/
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
M1 milestone of the agent-starter-v1-vertical slice. Schema + resolver
scaffolding only; M2 wires the resolver into the launch path; M3/M4
are real-runtime e2e proofs; M5 is disposable Tart VM proof.
THIS COMMIT IS M1 ONLY. M2/M3/M4/M5 surfaces are explicitly NOT touched
per delivery-orch directive ("marshal-acceptance gate at M1 must pass
before M2 dispatches separately").
What M1 ships (6 files, +361 / -1):
=================================
Schema (3 files):
- packages/daemon/src/domain/types.ts: new `StarterRefSpec` interface +
`starterRef?: StarterRefSpec` field on `RigSpecPodMember` after
`sessionSource?`. Doc-comment notes: artifact-seeded fresh-launch
context; composes additively with `session_source`.
- packages/daemon/src/domain/rigspec-schema.ts: new
`validateStarterRef()` invocation after `validateSessionSource` at
line 308, plus the function itself enforcing four rules:
1. malformed `name` rejection (must be alphanumeric + optional
`_`/`-`; path-traversal guard);
2. terminal runtime + `starter_ref` rejection (analogous to
existing terminal `session_source` rejection at line 321 —
terminal `deliverStartup` is a no-op so a starter has nothing
to seed);
3. **`starter_ref` + `session_source.mode="fork"` REJECTION**
(per slice review-independent finding 1 — the v1+ "Real
native-fork-from-registered-thread-id starter proof" trigger
covers that composition);
4. `starter_ref` + `session_source.mode="rebuild"` ACCEPTED
(additive: both apply on `fresh_start`; rebuild artifacts and
starter artifacts compose independently in the launch pipeline).
Plus new `normalizeStarterRef` helper invoked at line 758 of the
pod-aware normalization map.
Resolver (1 new file):
- packages/daemon/src/domain/agent-starter-resolver.ts (NEW): new
`AgentStarterResolver` class with `resolveStarter(name)` returning
`{ files: ResolvedStartupFile[], registryPath }`. Plus
`AgentStarterCredentialScanFailedError` class. Resolver semantics:
- Constructor lookup chain (via static helper):
`opts.registryRoot` > `env[envVarName]` > `homeDirRoot` (if
exists) > `substrateFallback`. Default env var name is
`OPENRIG_AGENT_STARTER_ROOT`; default home subpath is
`~/.openrig/agent-starters/`; default substrate fallback is
the v0 prototype location.
- `resolveStarter(name)`:
* THROWS on invalid name (path-traversal guard).
* THROWS on missing registry entry.
* THROWS on empty file or malformed YAML (best-effort: requires
`starter_id:` field; M1 doesn't pull in a YAML parser to keep
the dependency surface narrow — M2 can swap in real parsing
when it walks the priming-pack manifest).
* **THROWS `AgentStarterCredentialScanFailedError` on a failed
no-credentials scan** (per review-independent finding 2 —
resolver does NOT return a structured "ok: false" the
orchestrator could ignore; failed scan is a hard refusal point).
* On clean scan: emits `ResolvedStartupFile[]` rooted at the
registry directory. M1 scaffolding shape: one entry per
starter (the registry YAML itself, marked
`deliveryHint: "guidance_merge"`, `appliesOn: ["fresh_start"]`).
M2 can extend the resolver to walk the priming-pack manifest's
`read_full` paths if M3/M4 e2e proof needs richer per-layer
artifacts; the type contract and refusal semantics are what
M1 locks in.
- No-credentials scan implementation mirrors v0 wrapper at
`specs/agent-starters/lib/agent-starter-helpers.sh::scan_starter_file`:
three deny families (literal credential paths;
`(credentials|auth\.json|secrets|tokens?\.json)` path regex;
case-insensitive credential-string regex including `api_key`,
`secret_key`, `bearer_token`, `sk-` token shapes, `gh[ps]_` token
shapes, `password=`/`password:`). Allowlist exception: `transcript_path`
values under `~/.claude/projects/` or `~/.openrig/transcripts/` are
accepted; same paths under any other field/key/comment are rejected.
Tests (3 files):
- packages/daemon/test/agent-starter-schema.test.ts (NEW, 12 tests):
acceptance (alone, with rebuild composition); codec roundtrip
(normalize produces `starterRef`; absent → undefined; preserved
alongside `sessionSource`); rejection (empty/non-string/non-object
name; disallowed characters; path-traversal); composition rejection
(fork; terminal runtime); forward-compat (existing specs validate).
- packages/daemon/test/agent-starter-resolver.test.ts (NEW, 14 tests):
lookup-chain branches (4 tests covering opts.registryRoot, env var,
homeDirRoot when exists, substrateFallback when home doesn't exist);
successful resolve emits `ResolvedStartupFile[]` with correct
`ownerRoot`/`appliesOn`/`required`; credential-scan refusal (THROWS
on path match; THROWS on content match; THROWS on case-insensitive
uppercase API_KEY); allowlist exception (transcript_path under
`~/.claude/projects/` accepted); error throws (missing entry; missing
`starter_id:` field; empty file; invalid name with traversal chars;
invalid name with slash).
- packages/daemon/test/pod-rigspec-instantiator.test.ts (UPDATE, +1
test): forward-compat smoke that a pod-aware spec carrying
`starterRef` on a member instantiates without error (M2 will wire
it into the launch path; M1 verifies no regression).
M1 Marshal-Acceptance Gate (per IMPL § M1):
- [x] All new schema/normalization/codec tests green INCLUDING:
- rebuild composition accepted ✓
- fork composition rejected with explicit error (Finding 1) ✓
- terminal + starter_ref rejected with explicit error ✓
- [x] All resolver fetch tests green INCLUDING:
- no-credentials negative case throws
AgentStarterCredentialScanFailedError (Finding 2) ✓
- [x] Registry-root lookup chain tests green for all four branches ✓
- [x] No regression in existing rigspec-schema.test.ts (33/33) ✓
- [x] Posture: full QA/testing window (declared) ✓
Daemon full regression: 2216/2216 passing (was 2189 + 1 skipped at
codex-auth-refusal closeout / `53e9409`; now 2216 + 0 skipped — +27
net new tests; the 1 prior skip was the documented codex auth-refusal
deferral that revision-2 closed). 140 test files (was 138; +2 new files).
STOP-AND-ESCALATE triggers (per delivery-orch verbatim) all clean:
- No M2/M3/M4/M5 surface touched (instantiator left untouched at the
`launchExistingAgentMember` site; adapter `deliverStartup` left
untouched; CLI `up.ts` left untouched; route `up.ts` left untouched)
- No `starter_ref + session_source.mode=fork` accepted (Finding 1
enforced)
- Credential-scan hard refusal preserved (Finding 2 enforced — resolver
THROWS, does not return)
- No M5 shrinkage; M5 still requires both Claude AND Codex disposable
VM proof
- No fleet-ok-posture for any milestone past M2
- Cross-runtime alignment flag from prior codex-auth-refusal slice
preserved as follow-up (NOT settled)
Greenfield confirmation (planner-verified at audit time, re-confirmed
at commit):
- `grep -rn "starter_ref|agent_starter|AgentStarter|starterRef|StarterRefSpec" packages/`
returned zero hits before this commit; M1 lands in untouched
territory.
Out-of-slice (preserved untouched):
- `docs/as-built/cli-reference.md`, `packages/cli/src/commands/ps.ts`,
`packages/cli/test/ps.test.ts` — pre-existing dirty work from
concurrent agents
- `packages/daemon/src/adapters/codex-runtime-adapter.ts`,
`packages/daemon/test/codex-runtime-adapter.test.ts` — pre-existing
Codex TOML config-fragment dedup work flagged as untouched in prior
slices
- Untracked `CLAUDE.md` at openrig root — substrate/host overlay,
preserved
M1 commit staged ONLY the 6 in-slice files via explicit pathspec.
Slice rails:
substrate/shared-docs/openrig-work/missions/recursive-self-improvement-v2/work-discovery-and-slice-shaping/slices/knowledge-context-agent-starter-v1-vertical/
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Revision 2 of the codex-auth-refusal-attention-required slice. Closes the
gap velocity-guard caught in commit 63ee206: the legacy `CodexResumeAdapter`
path was correct, but the production pod-aware path
(`CodexRuntimeAdapter.launchHarness` → `verifyResumeLaunch`) still returned
`{ok: true}` when the probe reported `attention_required`, which
`launchHarness` translated into `{ok: true, resumeToken, ...}`. The slice's
"end-to-end" claim was false for pod-aware nodes.
Four classification-routing patches, no schema/persistence/auth-credential
mutation:
1. `runtime-adapter.ts` (Row 5 + Row 8): extend
`HarnessLaunchRecovery = "retry_fresh"` → `"retry_fresh" | "attention_required"`;
add optional `evidence?: string` field to `HarnessLaunchResult.ok=false`;
append `"codex_auth_refusal"` to `ATTENTION_REQUIRED_READINESS_CODES`
(defensive, keeps readiness path semantically aligned with launch path).
2. `codex-runtime-adapter.ts:495-548` (Row 6): insert
`attention_required` branch in `verifyResumeLaunch` BEFORE the trailing
`return {ok: true}` at line 547. Mirrors `no_saved_session` shape with
`recovery: "attention_required"` + last-12-line pane evidence (verbatim
Claude shape per `claude-resume.ts:97`).
3. `startup-orchestrator.ts:184-219` (Row 7 part A): extend `StartupResult`
`ok=false` variant with `evidence?: string`; extend `fail()` to accept
evidence. Add new branch in launch-failure block: when `launchResult.recovery
=== "attention_required"`, call `this.fail(input, "attention_required",
errors, launchResult.evidence)` instead of falling through to the failed
path. NOT routed to fresh-fallback (auth-refusal is operator-recoverable,
not a stale-token signal).
4. `restore-orchestrator.ts:858-877` (Row 7 part B): HOISTED
`attention_required` check above BOTH the `isPodAware && resumeRequested`
branch (line 859) and the `isPodAware` branch (line 867). The IMPL
pointed only at lines 867-877, but the actual production path for
pod-aware Codex resume goes through line 859 first; without the hoist,
the patch was unreachable for the resume-requested case (which is the
common production scenario). Mirrors the runtime-agnostic legacy mapping
at lines 725-735 — `RestoreNodeResult` shape unchanged (already supports
`status: "attention_required"` + `attentionEvidence`).
**Broader behavior change to flag:** the hoisted check at
`restore-orchestrator.ts:858` fires for ANY pod-aware
`startupStatus: "attention_required"`, not just Codex auth-refusal. This
includes existing readiness codes (`update_gate`, `trust_gate`, `mcp_gate`,
`login_required`). Previously these were collapsed to `status: "failed"`
even though the session-row `startupStatus` was already `"attention_required"`
and the error string already said "Restore startup requires attention" — a
pinned dishonesty in the prior pod-aware mapping. The patch aligns the
RestoreNodeResult status with the already-honest startupStatus across all
attention-required readiness codes. One existing test
(`restore-orchestrator.test.ts:1478` "pod-aware Codex restore … update gate")
was updated to assert the new honest mapping; renamed from "fails when …
update gate" to "surfaces attention_required when … update gate".
**Dirty-file staging discipline applied (per packet § Dirty-Files
Provenance):** `packages/daemon/src/adapters/codex-runtime-adapter.ts` and
`packages/daemon/test/codex-runtime-adapter.test.ts` carry pre-existing
in-progress Codex TOML config-fragment dedup work (lines ~649-687 and
~925-980). Only the auth-refusal hunks (`verifyResumeLaunch` at line 520
and the new auth-refusal tests around line 854) are staged in this commit.
The config-fragment hunks remain unstaged in the working tree.
Cross-runtime alignment audit flag from revision 1 still preserved as
follow-up: Claude `looksLikeClaudeLoginPrompt` returns `failed/login_required`
(terminal); Codex `looksLikeCodexAuthRefusal` returns `attention_required`
(recoverable). Recommend separate slice to settle.
Tests:
- `codex-runtime-adapter.test.ts`: +2 cases (verifyResumeLaunch
attention_required for both Codex token-refresh-failure variants).
- `startup-orchestrator.test.ts`: +1 case (recovery: "attention_required" →
startupStatus: "attention_required" with evidence preserved; NO
fresh-fallback retry).
- `lifecycle-scenario-matrix.test.ts`: +1 case (pod-aware Codex auth-refusal
end-to-end via launchHarness → verifyResumeLaunch → orchestrator;
asserts per-node `status: "attention_required"` + `attentionEvidence`
+ rig-level `partially_restored`; launchHarness called once, no
fresh-fallback).
- `restore-orchestrator.test.ts`: 1 test updated to assert new honest mapping.
Daemon suite: 2189/2189 passing, 0 skipped (was 2185 at HEAD 63ee206;
+4 net new tests; 0 regressions).
Slice rails: substrate/shared-docs/openrig-work/missions/recursive-self-
improvement-v2/primitive-hardening/slices/codex-auth-refusal-attention-
required/
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes the deferral recorded by the lifecycle scenario matrix slice: Codex
resume against a stored OAuth token whose refresh fails (operator logged out
elsewhere, account changed, device key revoked, etc.) is alive-but-recoverable,
not terminal `failed`. Operator runs `codex login` and the seat continues —
the same recovery affordance Claude already had for resume-selection prompts.
Three source surfaces patched, all classification-routing only:
1. native-resume-probe.ts — new `looksLikeCodexAuthRefusal` helper requiring
BOTH the access-token phrase AND the operator-instruction phrase to avoid
false-positives on incidental debug output. Returns
`{status: "attention_required", code: "codex_auth_refusal"}`. Inserted in
the Codex branch right after the explicit `no_saved_session` failure check
so the explicit auth-refusal path takes precedence over the trust /
model-selection / update inconclusive gates and over `paneCommand=codex`
active-runtime detection.
2. codex-resume.ts — `verifyResume` adds `attention_required` pass-through
mirroring `claude-resume.ts:92-99`. Returns `{ok: false, code:
"attention_required", message, evidence}` where `evidence` is the last 12
lines of pane content (verbatim Claude shape).
3. restore-orchestrator.ts — Codex branch of `attemptResume` (the
non-pod-aware legacy resume path) now mirrors the Claude translation
table: `ok → resumed`, `retry_fresh → retry_fresh`, `attention_required →
attention_required` with `message+evidence`, else → `failed`. The
runtime-agnostic per-node mapping at lines 725-735 emits `status:
"attention_required"` with `attentionEvidence` for both runtimes — no
further wiring needed. The polymorphic dispatcher return type at lines
923-928 already declared all four kinds and is untouched.
NOT changed (and explicitly checked):
- restore-orchestrator.ts:725-735 per-node mapping (runtime-agnostic).
- restore-orchestrator.ts:923-928 dispatcher return type union.
- node-inventory.ts attention_required projection (covers both runtimes).
- context-monitor.ts attention_required reconciliation.
- startup-orchestrator.ts (does not gate Codex probe path).
- Schema migrations / nodes / sessions tables.
Pane patterns sourced verbatim from codex-cli 0.125.0 binary strings at
/opt/homebrew/Caskroom/codex/0.125.0/codex-aarch64-apple-darwin (driver did
not improvise patterns from training-data guesses per packet § Pattern
Research):
"Your access token could not be refreshed because you have since
logged out or signed in to another account. Please sign in again."
"Your access token could not be refreshed. Please log out and sign
in again."
Cross-runtime alignment audit flag (NOT settled in this slice — recorded as
a follow-up question for orch per packet § Out-Of-Scope Audit Flag): Claude's
`looksLikeClaudeLoginPrompt` at native-resume-probe.ts:79-85 returns
`{status: "failed", code: "login_required"}` (terminal); the new Codex
auth-refusal pattern returns `{status: "attention_required", code:
"codex_auth_refusal"}` (recoverable). Both runtimes can present a "needs
login" state. Whether Claude should align with Codex (recoverable) or Codex
should align with Claude (terminal) is a provider-auth-semantics decision
beyond a probe-pattern slice. Recommend a separate slice to settle.
Tests (3 files, 271 total insertions across source+test):
- native-resume-probe.test.ts: +5 cases (auth-refusal positive x2, both-anchors-
required negative, no_saved_session/trust_gate/active_runtime non-collision).
- codex-resume.test.ts: +2 cases (verifyResume attention_required pass-through
with evidence shape; both-anchors-required negative).
- lifecycle-scenario-matrix.test.ts: REPLACE the documented `it.skip` at line
411 with an end-to-end orchestrator test asserting Codex auth-refusal →
per-node `status: "attention_required"` + `attentionEvidence` + rig-level
`partially_restored` rollup. The 1-skipped baseline from the prior
lifecycle slice is now 0-skipped.
Daemon suite: 2185/2185 passing (was 2176/2176-ish + 1 skipped at fcfc7dc;
net +9 from this slice; deferral closed; zero regressions).
Slice rails: substrate/shared-docs/openrig-work/missions/recursive-self-
improvement-v2/primitive-hardening/slices/codex-auth-refusal-attention-
required/
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The pod-aware /launch route at sessions.ts:113-118 returns
HTTP 409 `pod_aware_launch_unsupported` for any node where
`node.podId` is non-null. RigExpansionService at lines 42-74
ALWAYS materializes via the pod-aware seam, so every node `expand`
creates has `podId` set. Therefore every retry target previously
printed by `expand`'s partial-failure output (`rig launch <rigId>
<logicalId>`) is doomed to refusal — a load-bearing identity-honesty
contradiction documented in the 2026-04-27 add-node brittleness
field note and re-confirmed empirically at HEAD ab01331.
The honest recovery path (per the field note's confirmed sequence
and the operating semantics of `rig shrink`) is:
1. Verify other seats are healthy: rig ps --filter rigName=<rig>
2. Shrink the failed new pod: rig shrink <rig> <pod-namespace>
3. Fix the spec/config issue.
4. Re-run: rig expand <rig> <pod-fragment-path>
Patch is local to packages/cli/src/commands/expand.ts. Does NOT
relax the daemon /launch route's authority boundary; does NOT
change RigExpansionService; `rig.expanded` event shape unchanged.
Lands as part of the topology-mutation-add-seat-proof-pass slice
(primitive-hardening #4 topology mutation, foundation class).
Driver Section A audit (fleet-ok): 4 PASS + 1 NEEDS-HONEST-PATCH
(this row, this commit) + 1 NEEDS-DEFERRAL (Tier 2 stretch).
Mode 1 narrowing trigger satisfied per packet § Mode And Risk Policy.
Section B disposable-rig matrix proof BLOCKED on `operator auto
window` permission posture; not improvised down to a weaker proof
class per packet stop-and-flag rule and the 2026-04-30
permission-posture policy update. Section B routes to orch for
posture grant coordination.
Tests:
- packages/cli/test/expand.test.ts: T3a updated to assert the new
shrink+re-expand recovery shape and "Failed targets:" line; T3b
replaced with explicit negative-grep on the doomed
`rig launch <rigId> <logicalId>` form.
- CLI suite: 636/640 PASS (4 pre-existing env-dependent failures
unchanged: chatroom-timing, config-file, preflight x2 — none touch
expand.ts).
- Daemon suite: 2176/2177 (1 skipped is the documented Codex
auth-refusal deferral from the prior lifecycle slice; no
regressions).
Slice rails: substrate/shared-docs/openrig-work/missions/recursive-
self-improvement-v2/primitive-hardening/slices/topology-mutation-add-
seat-proof-pass/
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Promote the `--host <id>` flag from `rig send`/`rig capture` (closed at
cdce3a6) to `rig ps` and `rig whoami`. Strictly-mechanical extension:
the shared cross-host infrastructure (`host-registry.ts`,
`cross-host-executor.ts`, `cross-host-cli-helpers.ts`) is REUSED AS-IS
with zero modifications. Only the two command files gain the wiring,
mirroring the `send.ts`/`capture.ts` shape exactly.
`rig ps --host <id>`:
- Forwards every shaping flag (`--nodes`, `--full`, `--limit`,
`--fields`, `--summary`, `--filter`, `--json`) to the remote `rig ps`
in operator-declared order; non-set flags are not pushed.
- Verbatim remote stdout passthrough on success — the remote `rig ps`
already produced the correct shape (bare-array or envelope per its
own shaping flags); we do NOT double-wrap.
- Failure surfaces the four-`failedStep` enum from the closed
cross-host-rig-commands slice (`ssh-unreachable`,
`permission-gate` with keychain-over-SSH hint,
`remote-daemon-unreachable`, `remote-command-failed`) via the shared
`emitCrossHostFailure` helper.
`rig whoami --host <id>`:
- Forwards `--node-id`, `--session`, `--json` to the remote
`rig whoami`.
- **Load-bearing identity-resolution distinction**: cross-host runs
BEFORE local `resolveIdentitySource()`. The local resolution walks
env vars and tmux pane metadata; cross-host must NOT require any of
that — the remote rig has its own daemon, tmux, and identity
context. A test asserts that `rig whoami --host vm-a --json` with no
local `--node-id`/`--session` does NOT trigger the "Cannot determine
identity" error path and DOES surface the remote rig's response.
`PsDeps` and `WhoamiDeps` interfaces extend `StatusDeps` with optional
`hostRegistryLoader` and `crossHostRun` DI seams, mirroring
`SendDeps`/`CaptureDeps` exactly. Tests inject mocks; no real ssh / no
real `~/.ssh/` reads / no real network in any test.
Out of scope per slice IMPL boundaries: modifications to the cross-
host infrastructure modules (HARD GATE row 5 verified clean —
`git diff HEAD -- host-registry.ts cross-host-executor.ts
cross-host-cli-helpers.ts` returns 0 lines); daemon source (HARD GATE 2
verified — `git diff HEAD -- packages/daemon/` returns 0 lines);
`--host` on any other CLI command; `rig host` sub-namespace; registry
shape changes; failure-mode taxonomy changes; non-SSH transports;
connection pooling; multi-hop SSH; cross-host queue routing or seat-
handover composition. The local-path (no `--host`) for both commands
is byte-identical to the prior source — verified by compat regression
tests asserting that no-host invocations do NOT call the registry
loader OR the executor (both spied; both never invoked) and by the
existing ps.test.ts (43/43) and whoami.test.ts (18/18) suites passing
unchanged.
Tests (CLI-only; no VM cycle; no daemon mutation; no real ssh):
- `packages/cli/test/cross-host-commands.test.ts` — 20 new cases (10
for `ps --host`, 10 for `whoami --host`):
- argv reconstruction with all flags forwarded in operator order;
- bare `--host vm-a` produces `["rig","ps"]` / `["rig","whoami"]`;
- cross-host runs BEFORE local identity resolution (whoami
load-bearing test);
- verbatim remote stdout passthrough on success (no
double-JSON-wrapping);
- 4 failure modes (ssh-unreachable, permission-gate with hint,
remote-daemon-unreachable, remote-command-failed);
- JSON failure envelope (top-level `failedStep`);
- help text mentions `--host`;
- compat regression: no-`--host` paths do NOT call registry loader
OR executor.
Daemon untouched. CLI regression: 636/640 PASS (the same 4 pre-
existing environment-dependent failures from prior slices unchanged).
ps.test.ts 43/43 and whoami.test.ts 18/18 unchanged. cross-host-
commands.test.ts goes from 20 to 40 passing cases. `git diff --check`
clean.
Doc patches: `cli-reference.md` adds `[--host <id>]` to both `rig ps`
and `rig whoami` Usage lines plus a Notes bullet on each describing
the cross-host contract. The `rig whoami` bullet additionally calls
out that identity resolution happens on the REMOTE rig (not locally),
which is the load-bearing distinction.
Slice: openrig-work/missions/recursive-self-improvement-v2/primitive-hardening/slices/host-ps-whoami-promotion/
Driver checkpoint: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-host-ps-whoami-promotion-driver-checkpoint-20260430.md
Guard accept: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-host-ps-whoami-promotion-guard-checkpoint-approve-20260430.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Mirror the closed `--filter` rejection pattern (`ps.ts:55,77-80`) for
the `--fields` projection: an unknown key produces an actionable error
naming the unknown key(s) and the sorted supported list, and exits
with code 1. Validation runs BEFORE any HTTP call so operator scripts
that gate on exit status see the same shape as `--filter` rejection.
Per-level allow-lists (the rig and node entries have different
schemas):
- `ALLOWED_RIG_FIELDS` — 9 keys exactly matching `PsEntry` (rigId,
name, rigName, nodeCount, runningCount, status, lifecycleState,
uptime, latestSnapshot).
- `ALLOWED_NODE_FIELDS` — 16 keys exactly matching `NodeEntry`
(rigId, rigName, logicalId, podId, podNamespace,
canonicalSessionName, nodeKind, runtime, sessionStatus,
startupStatus, restoreOutcome, lifecycleState, tmuxAttachCommand,
resumeCommand, latestError, agentActivity).
Load-bearing aliasing decision (preserved per closed `rig ps trust
and scale-safety` slice at openrig 0a9fb43): `name` is rig-level only;
`rigName` is the alias and is accepted at both levels. When an
operator types `--fields name` against a node query, the rejection
fires a level-aware `Hint: 'name' is a rig-level field; use 'rigName'
for node entries.` so the node-side rejection nudges toward the
correct alias rather than silently producing `{name: undefined}`.
Implementation:
- New constants `ALLOWED_RIG_FIELDS` and `ALLOWED_NODE_FIELDS` next
to `ALLOWED_FILTER_KEYS`.
- New `parseFields(input, allowed, level)` helper next to
`parseFilter`, mirroring its shape: split on comma, trim, drop
empties; reject empty result; reject unknown keys with quoted
key(s), `key`/`keys` plural form, sorted supported list, and the
level-aware hint.
- Validation wired up-front in the action handler (`opts.nodes`
determines level; both rig and node downstream branches use the
validated `fields` array). Bails with `process.exitCode = 1` on
rejection. The `useEnvelope` predicate is unchanged.
- Help text body lists per-level supported fields plus the
`name`-vs-`rigName` distinction and the no-nested-field caveat.
- `cli-reference.md` `--fields` line refreshed with the per-level
enumeration plus the rejection contract.
Out of scope per slice IMPL boundaries: daemon source (HARD GATE on
audit row 5 confirmed CLI is sole projection seam — daemon
`grep -n 'fields|selectFields'` returns empty); other CLI commands;
new top-level commands; nested-field projection / wildcards /
case-folding; `--host` promotion to `rig ps` (C9b is a separate
slice). The `name`/`rigName` aliasing at rig-level is preserved
unchanged from the closed 0a9fb43 contract.
Tests (CLI-only; no VM cycle; no daemon mutation):
- `packages/cli/test/ps.test.ts` — 10 new cases covering: rig-level
single-unknown rejection with sorted supported list; multi-unknown
rejection with `keys` plural; empty `--fields`; whitespace-only
after trim; node-only field at rig-level; both `name` AND
`rigName` accepted at rig-level (aliasing positive); `name` at
node-level fires `rigName` hint AND its sorted node-level
supported list does NOT contain `name` (negative regex);
bogus key at node-level; `--filter` rejection still works after
`--fields` rejection wired in (regression); combined
`--filter status=running --fields rigName,status` regression. The
pre-existing positive `ps --json --fields projects only named
fields` test at `:546` still passes for valid inputs.
- ps.test.ts: 43/43 PASS (33 existing + 10 new).
- CLI regression: 616/620 PASS — the 4 unrelated failures are
pre-existing env-dependent (chatroom timing, port 7433, preflight)
unchanged across this slice; +10 new cases land cleanly vs prior
606 baseline. `git diff --check` clean.
Slice: openrig-work/missions/recursive-self-improvement-v2/primitive-hardening/slices/fields-ux-rejection/
Driver checkpoint: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-fields-ux-rejection-driver-checkpoint-20260430.md
Guard accept: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-fields-ux-rejection-guard-checkpoint-approve-20260430.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Add Option A of the Seat Continuity and Handover primitive: a v1 narrow
MVP for `session_source: { mode: rebuild, ref: { kind: artifact_set,
value: [paths...] } }` declared at member-level in a rig spec or
`rig expand` payload. Fresh-launches the harness and seeds the running
TUI with operator-declared artifacts in trust-precedence order
(highest-trust first: rig CULTURE, role doc, handover packet, queue
files, session logs).
Identity-honesty bedrock: the rebuild seat persists with NO
`resumeToken` and `continuityOutcome: "rebuilt"` (NEVER `"fresh"`,
`"resumed"`, or `"forked"`). Verified at four layers — schema dispatch,
orchestrator threading (no `resumeToken`, no `forkSource`),
SQLite read-back (sessions.resume_token IS NULL), and negative-grep on
resolver output JSON (no `restored`/`resumed`/`snapshot`/`forked` tags).
Schema (types, rigspec-schema, rigspec-codec):
- `SessionSourceSpec` refactored from a single fork-only shape to a
discriminated union `SessionSourceForkSpec | SessionSourceRebuildSpec`
so consumers can typed-narrow on `mode`. Existing fork consumers
unchanged.
- `validateSessionSource` dispatches on `mode` and delegates to either
`validateForkSessionSource` (existing behavior, sharper "v1 fork
mode" wording) or `validateRebuildSessionSource` (new). Rebuild
rejects: terminal runtime; non-`artifact_set` ref.kind (with
cross-mode hint when the kind belongs to fork); empty/non-array
`ref.value`; empty-string entries within `ref.value`.
- `normalizeSessionSource` likewise dispatches and produces a typed
`SessionSourceSpec` for either mode.
- Codec serializer's `ref.value` slot already accepts the array shape
via the YAML library; commented for clarity.
Expansion (routes/rigs):
- `normalizeExpansionPodFragment` now reads both `mode: fork` and
`mode: rebuild` shapes from the `/api/rigs/:id/expand` body.
Resolver (NEW: session-source-rebuild-resolver):
- `resolveRebuildArtifacts(spec, opts)` loads each declared path,
preserves operator-declared trust-precedence order, records missing
paths as gaps, and refuses (with a clear error citing trust-
precedence order as guidance) only when ALL declared paths fail to
resolve. The function does NOT execute, parse, or evaluate artifact
contents — it just records that a path exists and hands metadata to
the orchestrator's existing `deliverStartup` seam. Pluggable
`exists` injection seam for unit tests.
Orchestrator (startup-orchestrator):
- `StartupResult.continuityOutcome` literal widened to include
`"rebuilt"` (the broader `ContinuityOutcome` union in `types.ts`
already included it from L2 lifecycle work).
- `StartupInput.rebuildArtifacts?: ResolvedStartupFile[]` added.
- When `rebuildArtifacts` is set, the artifacts are prepended to
`resolvedStartupFiles` so the existing partition + delivery path
handles them in operator order.
- `launchHarness` is called with NO `resumeToken` and NO `forkSource`
on rebuild — fresh-launch shape. `continuityOutcome` is set to
`"rebuilt"`.
Instantiator (rigspec-instantiator):
- Dispatches on `member.sessionSource?.mode`: `"fork"` builds the
`forkSource` opt (existing behavior); `"rebuild"` resolves artifacts
via the new resolver and passes them as `rebuildArtifacts`. If the
resolver refuses (no paths resolved), the instantiator returns
`{status: "failed", error: <resolver error>}` before calling
`startNode`.
Out of scope for Option A (per slice IMPL): the full code-direct seat
handover (Option B / Mode 3 sub-mission); `SeatHandoverService` daemon
module; `rig seat handover` / `rig occupant rebuild` CLI commands;
nodes-table schema migration (HARD GATE on audit row 4 cleared:
`continuityOutcome: "rebuilt"` already in the enum); live session-
binding mutation; cross-host work; auto-discovery of artifacts
(operator declares paths in `ref.value` in trust-precedence order);
content execution or evaluation from artifacts in the resolver.
Tests (Tier 1, in-package mocks only — no real Claude / Codex; no
host `~/.claude` / `~/.codex` / `~/.openrig` mutation; resolver real-
filesystem smoke uses `mktempSync` temp paths only):
- `test/session-source-rebuild.test.ts` — 26 cases: 10 schema accept/
reject (full Honest Refusal Matrix for rebuild + cross-mode); 1
codec roundtrip; 4 resolver unit (all-existing, gaps recorded,
all-missing fail, operator order preserved); 6 orchestrator
integration including SQLite read-back asserting NO `resumeToken`
on rebuild seat and `continuityOutcome: "rebuilt"`; 3 honest-UX
literal contract; 2 real-filesystem smoke.
- `test/session-source-fork.test.ts` — two assertion strings updated
to match the new multi-mode error wording (`"v1 supports fork
only"` → `"v1 supports fork or rebuild"`; `"v1 supports native_id
only"` → `"v1 fork mode supports native_id only"`). Pure wording
propagation, no behavior change. All 33 fork cases still PASS.
- Daemon regression: 137 files, 2146/2146 PASS.
Tier 2 (real-runtime rebuild against a disposable Tart/OpenRig VM)
is the tester's call per the corrected SOP framing — disposable-VM
proof is default-allowed; skip only with concrete substrate reason.
Human gate applies only for protected/non-disposable hosts, host
`~/.claude` / `~/.codex` mutation, unexpected external spend/service
calls, destructive lifecycle, or leaving the disposable VM boundary.
Doc patches: `cli-reference.md` adds a new "Session source declaration
(`session_source`)" subsection covering BOTH modes (fork + rebuild)
with example YAML and validation rules. `architecture.md` adds a
one-paragraph reference covering both modes alongside the existing
cross-host notes.
Slice: openrig-work/missions/recursive-self-improvement-v2/primitive-hardening/slices/seat-continuity-and-handover/
Driver checkpoint: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-seat-continuity-handover-driver-checkpoint-20260430.md
Guard accept: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-seat-continuity-handover-guard-checkpoint-approve-20260430.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Add a v0 narrow MVP for the Cross-Host Rig Commands primitive: declare
remote hosts in `~/.openrig/hosts.yaml` and route `rig send` or
`rig capture` to a declared host via single-hop ssh. CLI-side shell-out
only — the local daemon is not involved in routing, and there is no
daemon-to-daemon networking.
Load-bearing identity rule: SSH success is NOT verify success. The
remote rig is authoritative on `--verify`; its `Verified: yes/no` line
is surfaced verbatim and is never collapsed into the SSH exit code.
New modules (CLI):
- `host-registry.ts` — read-only loader/validator for `hosts.yaml`.
Schema: `hosts: [{ id, transport, target, user?, notes? }]`. v0
validates required fields, unique ids, and refuses non-`ssh`
transport with a v0-scope message. Missing-file error names the
canonical path.
- `cross-host-executor.ts` — `runCrossHostCommand(host, argv, opts)`
with pluggable spawn for testing. Returns a structured
`CrossHostResult` whose `failedStep` enum maps each dossier failure
mode to a distinct value:
* `ssh-unreachable` — ssh exit 255 / spawn error with no auth signal.
* `permission-gate` — ssh exit 255 with Permission/Keychain/host-key
pattern; includes a hint to the keychain-over-SSH field note.
* `remote-daemon-unreachable` — ssh succeeded; remote rig stderr
matched "Daemon not running" / "Failed to fetch ... from daemon".
* `remote-command-failed` — ssh succeeded; remote rig exited
non-zero for any other reason.
- `cross-host-cli-helpers.ts` — shared `emitCrossHostError` /
`emitCrossHostFailure` so `send` and `capture` produce identical
operator-actionable error formats.
Modified commands:
- `send.ts` — adds `--host <id>` option. When set, short-circuits
before the daemon path: loads registry, resolves id, reconstructs
argv (`["rig","send",session,text,...flags]`), runs via the
executor. Annotates output with `[via host=<id> (<target>)]`; JSON
output wraps `{ cross_host: {host, target}, result }`. SendDeps adds
optional `hostRegistryLoader` and `crossHostRun` injection points.
- `capture.ts` — same shape with capture's argv (`["rig","capture",
session?,...flags]` forwarding `--rig`, `--pod`, `--lines`,
`--json`).
Out of scope for v0 (per slice IMPL): non-SSH transports; `--host` on
`rig ps` / `rig whoami` / other commands; connection pooling;
multi-hop SSH; cross-host queue routing; cross-host seat handover; any
`rig host` sub-namespace; auto-generated or auto-modified `hosts.yaml`.
The daemon is untouched — `packages/daemon/` has no changes in this
slice.
Tests (Tier 1, in-package mocks only — no real ssh, no real network,
no `~/.ssh/` reads, no `hosts.yaml` outside `mktempSync` temp paths):
- `host-registry.test.ts` — 19 cases: accept/reject schema rules,
duplicate-id detection, transport refusal, malformed YAML, unknown
id discoverability, empty-registry honesty.
- `cross-host-executor.test.ts` — 18 cases: `shellQuote`; full
`classifyResult` exit-code → failedStep table; argv construction
(`-l <user>`, `ConnectTimeout=N`); happy path; **--verify honest
pass-through**; permission-gate detection; transport defense-in-
depth; stdin forwarding; custom connect timeout.
- `cross-host-commands.test.ts` — 20 cases: send + capture short-
circuit, argv reconstruction, **--verify honesty in command path**,
flag forwarding, JSON envelope shape, each of the 4 failure modes
surfacing operator-actionable errors with field-note hints, compat
regression (no `--host` ⇒ neither registry loader nor executor is
called).
Doc patches: `cli-reference.md` documents `--host` on send + capture
plus a new "Cross-host execution (`--host <id>`)" subsection (registry
shape, validation rules, four `failedStep` values, v0 out-of-scope
list). `architecture.md` adds a one-line note that `--host` is CLI-
side shell-out.
Tier 1.5 (ssh localhost loopback) skipped — not part of standard dev-
host config; recorded as a deferred follow-up. Tier 2 (real disposable
Tart VM proof against an isolated remote rig) deferred per planner
recommendation with explicit `human-gate: tart-vm-cross-host-proof`
annotation.
Slice: openrig-work/missions/recursive-self-improvement-v2/primitive-hardening/slices/cross-host-rig-commands/
Driver checkpoint: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-cross-host-rig-commands-driver-checkpoint-20260430.md
Guard accept: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-cross-host-rig-commands-guard-checkpoint-approve-20260430.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Add the v1 narrow MVP for the Session Source Fork primitive: declare
`session_source: { mode: fork, ref: { kind: native_id, value: <id> } }`
on a rigspec member to start a new managed seat from a prior native
runtime conversation source. Identity-honesty bedrock: the new seat's
persisted resume token is the NEW post-fork token; the parent token is
NEVER written onto the new seat record.
Schema (rigspec-schema, rigspec-codec, types):
- Add SessionSourceSpec interface; add sessionSource field to
RigSpecPodMember and ExpansionPodFragment.members[].
- Encode the full Honest Refusal Matrix in the schema validator: terminal
runtime + sessionSource → reject; mode != "fork" → reject; ref.kind
∈ {artifact_path, name, last} → reject with v1-scope reason; missing or
empty value → reject.
- Codec serializer emits session_source block when present; round-trip
through serialize → parse → normalize is preserved.
Expansion (rig-expansion-service, routes/rigs):
- normalizeExpansionPodFragment reads session_source / sessionSource from
/api/rigs/:id/expand body.
- buildSyntheticSpec passes session_source through into the synthetic YAML
fed to the materializer.
Runtime adapter contract (runtime-adapter):
- Add ForkSource interface; extend RuntimeAdapter.launchHarness opts with
optional forkSource. resumeToken and forkSource are documented as
mutually exclusive; the captured resumeToken in the result is the NEW
post-fork token, never the parent.
Adapters (claude-code, codex, terminal):
- claude-code: for forkSource.kind="native_id", build
`claude --permission-mode acceptEdits --resume <parent> --fork-session
--name <seat>` and capture the NEW post-fork session id via
captureResumeToken(opts.name).
- codex: for forkSource.kind="native_id", build
`codex[ -p <profile>] fork[ --add-dir ...] <parent>` and capture the
NEW thread id via captureFreshThreadId(binding). Refuse on capture
failure (no token == no honest seat).
- terminal: defensive refusal with clear error.
- All three: refuse other ref.kind values (defense-in-depth on top of
schema gate); refuse simultaneous resumeToken + forkSource.
Orchestrator (startup-orchestrator, rigspec-instantiator):
- StartupInput.forkSource threaded through to adapter.launchHarness when
no resumeToken is set; continuityOutcome literal widened to
"resumed" | "fresh" | "forked" (broader ContinuityOutcome union already
included "forked").
- rigspec-instantiator passes member.sessionSource → forkSource when
starting a new node.
Tier 1 proof: 33/33 cases in test/session-source-fork.test.ts cover schema
accept/reject matrix, codec round-trip, adapter command shape + capture +
mutual-exclusion, terminal refusal, orchestrator integration (including
SQLite read-back asserting persisted resume_token equals NEW + NOT equals
PARENT), expansion pass-through, and identity-honesty literal contract.
Daemon suite: 2120/2120 pass.
Tier 2 (real Tart VM fork against disposable ~/.claude and ~/.codex)
deferred per planner recommendation with explicit human-gate annotation.
Slice: openrig-work/missions/recursive-self-improvement-v2/primitive-hardening/slices/session-source-fork/
Driver checkpoint: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-session-source-fork-driver-checkpoint-20260430.md
Guard accept: shared-docs rigs/openrig-velocity-claude/state/orch/velocity-session-source-fork-guard-checkpoint-approve-20260430.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The rig ps section in docs/as-built/cli-reference.md predated the closed
"rig ps trust and scale-safety" slice and only documented `[--json]` and
`[--nodes]`. Source HEAD ships `--full / --limit / --fields / --summary /
--filter`, a rig-level `LIFECYCLE` column with `run`/`rec`/`stp`/`deg`/`att`
codes, a `rigName` alias, and a node-tier inventory exposing
STATUS/STARTUP/LIFECYCLE/ACTIVITY/RESTORE/ERROR side-by-side so startup
status and live runtime state can be diagnosed without a separate command.
This patch updates the doc to:
- list the full shaping flag set on both rig and `--nodes` usages;
- document the `LIFECYCLE` column and node-tier diagnostic columns;
- document the `rigName` alias and the bare-array vs envelope JSON shape
decision (envelope appears only when at least one shaping flag is set);
- document the human-output truncation budgets (50 rigs / 100 nodes) plus
the footer/narrowing instruction;
- document the `--summary` aggregate-only mode and its cross-facet caveat;
- document the `--filter` allow-list and rejection of unknown keys;
- document the `--fields` shallow-projection caveat (no nested-field
drilling);
- update exit-code 1 to include "invalid `--filter` / `--limit`".
Doc-only patch from the runtime-truth-and-inventory bedrock slice
(recursive-self-improvement-v2, primitive-hardening). Audit deliverable
and accept artifact at:
- shared-docs rigs/openrig-velocity-claude/state/orch/velocity-runtime-truth-inventory-driver-checkpoint-20260430.md
- shared-docs rigs/openrig-velocity-claude/state/orch/velocity-runtime-truth-inventory-guard-checkpoint-approve-20260430.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Builds on the Lifecycle v2 epic (L1+L2+L3+L3b) by making `rig ps` operator-
trustworthy and scale-safe. The slice covers three linked outcomes against
the same operator surface:
1. **Naming consistency.** Per-node `NodeInventoryEntry` exposes the rig's
name as `rigName`; rig-summary `PsEntry` historically used `name`. Agent
code that projected `.rigName` from `rig ps --json` saw silent
`null`/`undefined`. Fixed by adding `rigName` as an alias on `PsEntry`,
always populated and always equal to `name`. Existing consumers that
read `.name` continue to work; agent code should now prefer `rigName`.
2. **Lifecycle visibility.** L2's `lifecycleState` column was already in
the default human `rig ps` output; this slice asserts it via test and
documents it in the help text.
3. **Output budget.** Hosts with realistic agent counts (~75 nodes already
on the dev box; targeting 100+) flooded operator terminals and agent
context windows. The fix is hybrid: human terminal output truncates by
default to 50 rigs / 100 nodes with an honest footer naming actual
totals and the `--full` opt-out; JSON output preserves the bare-array
default for machine consumers but adds opt-in `--limit`/`--fields`/
`--summary`/`--filter` flags that switch to an envelope shape with
`entries`/`totalRigs`/`truncated`/`hint`. The envelope only triggers
when at least one shaping flag is specified.
Decisions resolved (per plan, ratified by orch-lead with amendments):
- A2 — alias `rigName` alongside `name` (zero JSON consumer breakage).
- B1 — keep LIFECYCLE column in default human output (already present from L2).
- C hybrid — human truncates default with footer; JSON unbounded by default;
envelope only on `--limit`/`--fields`/`--summary`/`--filter` flag use.
Per orch amendments (overriding plan where they conflict):
- **A. CLI-side shaping.** All filtering/limiting/summary/projection
happens in the CLI; no daemon query parameters added; `/api/ps` stays
bare-array with the new `rigName` alias only. `routes/ps.ts` and
`routes/rigs.ts` are NOT touched.
- **B. Guard pre-commit review.** Driver sent kickoff with `Verified: yes`,
implemented + verified, filed a pre-commit checkpoint, created a durable
guard review qitem, and waited for guard accept BEFORE committing. This
commit lands only after guard issued ACCEPT in
`velocity-slice-rig-ps-trustworthy-guard-checkpoint-approve-20260428.md`.
- **C. Smallest fixture proving budget boundary.** A 60-rig synthetic
fixture (just over the 50-budget) proves human-truncation correctness
in 35ms; the L4 proof at `fad10a9` already exercised real 100x10 scale.
Justification recorded in the test comment.
Implementation:
- `packages/daemon/src/domain/ps-projection.ts`: add `rigName: string`
field to `PsEntry` (always populated, always equal to `name`).
`lifecycleState` typing remains required (post-L2); empty rigs derive
`stopped`. `routes/ps.ts` is unchanged because `PsEntry` IS the wire
shape and `rigName` flows through automatically.
- `packages/cli/src/commands/ps.ts`: add `--full`, `--limit`, `--fields`,
`--summary`, `--filter` flags. Implement allow-list filter parser
(`status`, `lifecycleState`, `name-prefix`, `name`) with explicit
exit-1 errors for unknown keys / malformed values / non-integer
`--limit`. Implement human truncation at `HUMAN_RIG_BUDGET=50` /
`HUMAN_NODE_BUDGET=100` with honest footer. Implement JSON envelope
triggered ONLY by `useEnvelope` (any of `--limit`/`--summary`/
`--fields`/`--filter`); default `--json` and `--full --json` keep
the bare-array shape for back-compat. Help text rewritten with all
new flag examples and a note about the `rigName` alias.
Filter parser security: in-memory filtering over already-fetched arrays;
allow-list Set rejects unknown keys before any HTTP call; no SQL
injection vector.
Tests added (17 cases total: 2 daemon + 15 CLI):
- `daemon/test/ps-projection.test.ts`: `rigName` alias populated equal
to `name` on every entry; `lifecycleState` always populated even for
empty rigs.
- `cli/test/ps.test.ts`: `--json` rigName pass-through (jq `.[].rigName`
regression named in the source qitem); `--json` default bare array;
`--json --limit` envelope; `--json --fields` projection;
`--json --summary` aggregate-only; `--filter status=running`;
`--filter lifecycleState=attention_required`; filter rejects unknown
keys; filter rejects malformed value; `--limit` rejects non-integer;
60-rig fixture proves 50-budget human truncation with honest footer;
`--full` disables truncation; `--json` default bare-array preserved
with 60 rigs (back-compat); `--nodes --json` default unbounded
(back-compat); `--nodes --json --limit` envelope.
Verification:
- focused daemon: `npx vitest run test/ps-projection.test.ts test/node-inventory.test.ts`
→ 43/43 green.
- focused CLI: `npx vitest run test/ps.test.ts` → 33/33 green.
- full daemon suite: 2087/2087 (+2 from L3b baseline of 2085).
- full CLI suite: 4 pre-existing failures (port 7433 in use; chatroom
timeout; config reads env). Same set documented in L2/L3/L3b
checkpoints; verified not L3-followup regressions.
- `npx tsc --noEmit` clean on both packages.
Invariants preserved:
- **Backward compat for `rig ps --json`**: default (no flags) returns
bare array with `rigName` alias added; `name` field preserved; existing
consumers reading `.name` keep working.
- **Backward compat for `rig ps --nodes --json`**: same.
- **Envelope only on flag use**: `--full --json` keeps bare array;
envelope only when shaping flag specified.
- **No silent nulls**: `lifecycleState` typed as required + asserted in
test.
- **Truncation honesty**: footer always names actual totals + next-command
hint; never silently drops.
- **Filter parser security**: allow-list only; no injection vector.
- **L1/L2/L3/L3b invariants untouched**: no changes to reconcile,
lifecycle derivation, restore-attempt model, snapshot selection, or
any L1-L3b surface.
- **No new schema migration; no daemon query parameters; no
RestoreOrchestrator/rig-repository/node-inventory changes; no
UI/MCP/Auth-C/OAuth/account/quota/`~/.codex` changes; no host
lifecycle mutation; no kernel-rig dependency; no recurring
drain/watchdog; no retired queue-fallback revival.**
Refs:
- rigs/openrig-build/state/orch/velocity-slice-rig-ps-trustworthy-and-scale-safe-plan-20260428.md
- rigs/openrig-build/state/orch/velocity-slice-rig-ps-trustworthy-driver-checkpoint-20260428.md (pre-commit driver checkpoint)
- rigs/openrig-build/state/orch/velocity-slice-rig-ps-trustworthy-guard-checkpoint-approve-20260428.md (guard ACCEPT)
- L3b predecessor: e70694f fix(daemon,cli): rig up <rigName> falls back to manual snapshot when no auto-pre-down exists
- L3 predecessor: 66785c8 feat(daemon,cli): restore attempt id, attention_required classification, runtime-truth reconciliation
- L2 predecessor: fad10a9 feat(daemon,cli): add recoverable/degraded/attention_required lifecycle projection
- L1 predecessor: 3ad1df6 fix(daemon): classify tmux socket-absence as missing session and log startup reconcile
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Lifecycle v2 L3b builds on L3 (66785c8) by closing the rig-up-by-name +
manual-snapshot gap reproduced in the L4 VM proof at fad10a9 cycle 1. Before
L3b, an operator running `rig up <rigName>` against a rig that has only
manual snapshots got HTTP 404 with `code: "no_snapshot"` and the message
"Rig exists but has no auto-pre-down snapshot." even though `rig restore
<snapshotId> --rig <rigId>` succeeded against the same manual snapshot.
After L3b, `rig up <rigName>` consults the latest snapshot of any kind whose
metadata is sufficient for `RestoreOrchestrator.restore`'s pre-validation,
preferring `auto-pre-down` when present. The selected snapshot kind is
echoed in the API response so operators see what was used; the CLI prints
a one-line manual-fallback note when the kind is not `auto-pre-down`.
Decisions resolved (per plan, ratified by orch-lead with amendments):
A. Re-use `rig up <rigName>` (no new explicit operator command). The
404-on-manual-only behavior was a missing-implementation symptom, not a
safety feature. Adding a separate `rig recover` would force operators to
memorize a "use this command for this snapshot kind" dichotomy that PM
spec D2 warns against.
Per orch amendments (overriding the plan where they conflict):
1. **Validate against actual SnapshotData.** There is no `data.bindings[]`
field, and `Session` has no `runtime` field — runtime lives on `nodes`.
Resume tokens are NOT required (resume-tokenless rigs remain restorable).
The new helper checks the same minimum metadata
`RestoreOrchestrator.validatePreRestore` actually requires: rig with id,
nodes, edges, sessions arrays, checkpoints object; and when sessions is
non-empty each session has a non-empty `sessionName` and `nodeId`. Empty
sessions arrays are accepted (matches the validator).
2. **Candidate selection does not LIMIT 1 before validation.** The SQL
orders snapshots by `(kind = 'auto-pre-down') DESC, created_at DESC,
id DESC`, then a JS loop validates each candidate and skips invalid
rows (corrupted JSON, missing structural metadata, missing per-session
linkage). Auto-pre-down preference is preserved.
3. **Test files** follow repo conventions:
`snapshot-repository.test.ts`, `up-route.test.ts`, `rigs-routes.test.ts`,
and `up.test.ts`.
Implementation:
- `packages/daemon/src/domain/snapshot-repository.ts`: new method
`findLatestRestoreUsable(rigId): Snapshot | null`. Free helper
`isRestoreUsableSnapshotData(data)` validates structural metadata.
`findLatestAutoPreDown` and `findLatestUsableSnapshot` (L2) are
unchanged.
- `packages/daemon/src/routes/up.ts`: `restoreByRigId` calls the new
helper. Response `snapshotKind: snapshot.kind` echoed on success and on
pre-restore-validation 409 paths. 404 message updated to "Rig exists but
has no restore-usable snapshot. Start fresh with: rig up <spec-path>".
- `packages/daemon/src/routes/rigs.ts`: `/:id/up` mirrors the same
changes.
- `packages/cli/src/commands/up.ts`: when the daemon response carries
`snapshotKind` and it's not `auto-pre-down`, the CLI prints
`Restoring from manual snapshot (kind=<kind>); no auto-pre-down snapshot
available.` BEFORE the "Rig restored" line so operators see the
fallback explicitly.
Tests added (matching plan closeout checklist + orch amendment #3):
- `daemon/test/snapshot-repository.test.ts`: 10 cases for
`findLatestRestoreUsable` —
auto-pre-down preferred over manual,
manual returned when no auto-pre-down,
latest of multiple manuals (created_at DESC, id DESC),
auto-pre-down preferred over a NEWER manual,
null when no snapshot exists,
skips corrupted JSON and considers next,
skips snapshot with empty sessionName on a session,
skips snapshot with missing nodeId on a session,
null when no snapshot has restore-usable structural metadata,
accepts empty sessions array (matches validatePreRestore),
plus a regression confirming `findLatestAutoPreDown` is unchanged.
- `daemon/test/up-route.test.ts`: 3 cases — auto-pre-down preference;
manual-fallback restore returns `snapshotKind: "manual"`; 404 with
updated `restore-usable` message and no `auto-pre-down snapshot` text.
- `daemon/test/rigs-routes.test.ts`: 3 cases mirroring the above for
`/api/rigs/:id/up`.
- `cli/test/up.test.ts`: 2 cases — fallback note printed when
`snapshotKind="manual"`; NOT printed when `snapshotKind="auto-pre-down"`.
Verification:
- focused daemon: `npx vitest run \
test/snapshot-repository.test.ts \
test/up-route.test.ts \
test/rigs-routes.test.ts` → 65/65 green.
- focused CLI: `npx vitest run test/up.test.ts` → 27/27 green.
- full daemon suite: 2085/2085 (+17 from L3 baseline of 2068).
- full CLI suite: 4 pre-existing failures (port 7433 in use; chatroom
timeout; config env reads). Confirmed not L3b regressions; same set
L2/L3 documented.
- `npx tsc --noEmit` clean on both packages.
Invariants preserved:
- **Auto-pre-down preference unchanged** when both kinds are present.
- **`findLatestAutoPreDown` unchanged**; still returns auto-pre-down
only with no kind fallback (regression-checked).
- **`findLatestUsableSnapshot` (L2) untouched** — different concept
(requires resume tokens; consumed by lifecycle projection).
- **Direct `rig restore <snapshot> --rig <id>` unchanged.**
- **Fail-closed when no usable snapshot exists**: returns 404 with
actionable message, no silent fallback to incompatible snapshots.
- **No schema migration**; the existing `snapshots.kind` text column is
queried, not constrained.
- **No `RestoreOrchestrator` changes**; helper validation matches what
the orchestrator already enforces in `validatePreRestore`.
- **No UI/MCP/Auth-C/OAuth/account/quota/`~/.codex` changes**;
`routes/up.ts` and `routes/rigs.ts` /:id/up are the only routes
touched.
- **No host lifecycle mutation.**
L3b implementation accept may close L3b directly without a new VM cycle
per the plan (the L4 proof at fad10a9 already reproduced the gap; the
fix is unit/integration-test-coverable; no new runtime behavior beyond
snapshot selection). If guard requests a focused VM cycle, tester can run
one targeted L4-3 cycle.
Refs:
- rigs/openrig-build/state/orch/velocity-slice-L3b-snapshot-selection-rig-up-by-name-plan-20260428.md
- rigs/openrig-build/state/orch/velocity-conveyor-state-l4-vm-reboot-proof-fad10a9-20260428.md (cycle 1 reproduced the gap)
- L3 predecessor: 66785c8 feat(daemon,cli): restore attempt id, attention_required classification, runtime-truth reconciliation
- L2 predecessor: fad10a9 feat(daemon,cli): add recoverable/degraded/attention_required lifecycle projection
- L1 predecessor: 3ad1df6 fix(daemon): classify tmux socket-absence as missing session and log startup reconcile
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Lifecycle v2 L3 builds on L1 cold-start truth repair (3ad1df6) and L2 lifecycle
projection (fad10a9) to make restore attempts durable, interrupt-aware, and
reconcilable with later runtime truth. Four decisions resolved per the plan
packet:
1. **Attempt id source** — the persisted `restore.started` event seq IS the
attempt id. No new `restore_attempts` table; the event log already orders
restore lifecycle events durably. If a future need for in-flight status
beyond the event log appears, escalate before adding a table or a
`restore.in_progress` heartbeat.
2. **Claude resume-selection prompt** classified as
`restoreOutcome=attention_required`. Do NOT auto-select any option;
surface evidence (recent pane content) so an operator/UI can act.
Auto-selection is deferred to a follow-up policy slice.
3. **Runtime-truth reconciliation** via new
`RestoreOrchestrator.reconcileNodeRuntimeTruth(rigId, nodeId)`. Upgrades
`failed`/`attention_required` to `operator_recovered` ONLY when ALL four
visible-runtime-evidence preconditions hold:
1. tmux session exists (`tmuxAdapter.hasSession`),
2. foreground process is claude or codex (`paneCommand`),
3. resume token was used (sessions row carries non-null `resume_token`),
4. pane is at usable state (`nativeResumeProbe.assessNativeResumeProbe`
returns `resumed`, NOT a resume-selection prompt).
Reconciliation NEVER mutates or deletes the original failure event;
instead it APPENDS `restore.outcome_reconciled` carrying attempt id, node
id, from/to transition, and the four evidence fields. The terminal
post-reconciliation outcome is `operator_recovered`; producing `ready` is
forbidden.
4. **Rig-up-by-name + manual-snapshot fix is OUT OF SCOPE** for L3 and will
become a separate slice (L3b). `routes/up.ts` and `up-command-router.ts`
are not touched here.
Implementation:
- `packages/daemon/src/domain/types.ts`: extend `NodeRestoreOutcome` with
`attention_required` and `operator_recovered`; extend
`RestoreNodeResult.status` correspondingly; add `attentionEvidence` to
`RestoreNodeResult`; add `restore.outcome_reconciled` to the `RigEvent`
union.
- `packages/daemon/src/domain/native-resume-probe.ts`: new
`attention_required` status; new code `claude_resume_selection_prompt`;
detector requires both an explicit "Choose ... conversation" verb AND a
numbered/arrow option marker so similar TUI strings don't false-positive.
- `packages/daemon/src/adapters/claude-resume.ts`: when the probe returns
`attention_required`, propagate as
`{ ok: false, code: "attention_required", message, evidence }` so the
orchestrator can map to per-node `attention_required` instead of `failed`.
- `packages/daemon/src/domain/restore-orchestrator.ts`:
- `restore()` accepts optional `onAttemptStarted: (attemptId) => void`
callback fired with the persisted `restore.started` event seq.
- `attemptResume()` returns a discriminated union covering the new
`attention_required` kind.
- Legacy resume path translates `attention_required` to per-node
`RestoreNodeResult.status="attention_required"` with pane evidence.
- `rollupRestoreRigResult()` rolls `attention_required` up as
`partially_restored`.
- New `reconcileNodeRuntimeTruth(rigId, nodeId): Promise<ReconcileNodeResult>`
method implements Decision 3.
- `packages/daemon/src/domain/node-inventory.ts`: `deriveRestoreOutcome`
recognizes the new outcomes; `deriveContinuityOutcome` maps
`operator_recovered` → `resumed` and `attention_required` → null;
`deriveNodeLifecycleState` recognizes the explicit `attention_required`
outcome (alongside the L2 failed+alive proxy).
- `packages/daemon/src/routes/snapshots.ts`: restore route returns
`{ ok: true, attemptId, status: "started", rigId }` with HTTP 202 AS SOON
AS the orchestrator emits `restore.started`. Per-node restore work runs
in the background; pre-restore validation failures and other
"couldn't even start" errors return the original error mappings (404 / 409
/ 500) because no `restore.started` was emitted.
- `packages/cli/src/commands/restore.ts`: prints `Restore attempt id: <id>`
and `Status: started` on the 202 path; installs SIGINT/SIGTERM handler
that prints an honest message ("Client interrupt received; daemon-side
restore may continue. Use 'rig ps --nodes' or 'rig restore-check' to
follow progress."). Decision 1 amendment honored: the message references
ONLY existing surfaces, never a non-existent `rig events` command.
Tests added (matching plan closeout checklist):
- `daemon/test/native-resume-probe.test.ts`: 4 cases — numbered prompt
classification, › arrow variant, regression for active TUI not being
classified as attention_required, regression that bare "Choose a
conversation" mention without numbered options is not classified.
- `daemon/test/restore-orchestrator.test.ts`: 12 cases —
`onAttemptStarted` invoked with restore.started seq;
`onAttemptStarted` NOT invoked when pre-restore validation fails;
reconcile success path for both `failed` and `attention_required`
starting outcomes; audit-trail preservation; four no-op cases (one per
precondition); refusal to upgrade non-failed/attention_required outcomes;
rejection when no `restore.started` recorded; assertion that the upgrade
target is always `operator_recovered`, never `ready`.
- `daemon/test/snapshots-routes.test.ts`: existing route tests updated to
the new async contract (202 + attemptId + queryable event seq).
- `cli/test/restore.test.ts` (NEW): 4 cases — CLI prints attempt id from
202 response; SIGINT handler text references only existing surfaces and
not a fake `rig events`; 404 path; 409 pre-restore-validation-failed
path with blockers.
Verification:
- focused daemon: `npx vitest run test/restore-orchestrator.test.ts test/native-resume-probe.test.ts test/snapshots-routes.test.ts`
→ 125/125 green.
- focused cli: `npx vitest run test/restore.test.ts` → 4/4 green.
- full daemon suite: 2068/2068 green (up from 2052 L2 baseline; +16 L3 tests).
- full cli suite: 4 pre-existing failures (port 7433 in use; chatroom timeout
flake; config reads env). Confirmed not L3 regressions by stash-and-rerun
on baseline before L3 changes.
- `npx tsc --noEmit` clean on both packages.
Invariants preserved:
- Permission errors remain fail-closed (L1 unchanged).
- L2 lifecycleState projection unchanged for pre-L3 outcomes; new outcomes
flow correctly.
- `restoreOutcome=ready` is NEVER produced anywhere. The reconciler upgrade
target is locked to `operator_recovered`.
- Audit trail: original failure events are NEVER mutated or deleted; the
reconciler always APPENDS `restore.outcome_reconciled`.
- No new `restore_attempts` table; no new schema migration.
- No `rig events` CLI command added or referenced (Decision 1 amendment).
- No auto-selection on resume prompts (Decision 2).
- No `routes/up.ts` / `up-command-router.ts` changes (Decision 4: rig-up-by-name
fix is L3b).
- No CLI cancellation as a daemon protocol; ship the honest interrupt message
only.
L3 implementation accept does NOT close L3. Full closure waits on a follow-up
L4 sub-cycle (or L4-2) where tester proves D5/D6 surfaces with L3 in lane
(mirror of L1/L2).
Refs:
- rigs/openrig-build/state/orch/velocity-slice-L3-restore-attempt-state-model-plan-20260428.md
- L1 predecessor: 3ad1df6 fix(daemon): classify tmux socket-absence as missing session and log startup reconcile
- L2 predecessor: fad10a9 feat(daemon,cli): add recoverable/degraded/attention_required lifecycle projection
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Lifecycle v2 L2 builds on the L1 cold-start truth repair (3ad1df6) by giving
operators a first-class projection that distinguishes recoverable rigs/nodes
from genuinely-stopped ones. Pre-L2, a detached session row and a stopped-from-
scratch rig look identical to `rig ps` and `rig up`; the wording divergence
between "Recover from snapshot" and "Turn on" is unreachable without this state.
All new state is DERIVED — no schema migration, no new column, no auto-restore
on daemon start. Decisions per plan packet:
1. v0 derives `recoverable` from session detachment + snapshot resume metadata;
persistence deferred until performance/projection needs justify it.
2. CLI says "Recover" when the rig's latest usable snapshot can resume; says
"Turn on" when no usable snapshot exists. Help-text honesty: "Recover"
describes what `rig up` does, it does not promise success before tester L4.
3. Rig-level `degraded` = mixed live + detached on the same rig; per-node
`attention_required` = the post-L3 Claude resume-prompt proxy (failed
restoreOutcome with a still-alive tmux session).
Per-node lifecycleState (additive on NodeInventoryEntry):
attention_required > running > recoverable > detached, in priority order.
Per-rig lifecycleState (additive on PsEntry):
any attention_required → attention_required;
all running → running;
all non-running, any recoverable → recoverable;
all non-running, none recoverable → stopped;
mixed → degraded.
Implementation:
- packages/daemon/src/domain/types.ts: declare NodeLifecycleState +
RigLifecycleState; add lifecycleState to NodeInventoryEntry.
- packages/daemon/src/domain/rig-repository.ts: free function plus instance
method `findLatestUsableSnapshot(rigId)` returns the latest snapshot whose
data carries at least one non-null resume token; returns null otherwise.
- packages/daemon/src/domain/node-inventory.ts: new helper
`deriveNodeLifecycleState`; getNodeInventory loads the rig's usable
snapshot once and folds per-node state.
- packages/daemon/src/domain/ps-projection.ts: new helper
`deriveRigLifecycleState`; getEntries derives per-rig lifecycleState by
folding per-node states from getNodeInventory.
- packages/daemon/src/routes/rigs.ts: GET /api/rigs/summary enriched with
lifecycleState so CLI surfaces (rig up wording) work without a second
round trip.
- packages/cli/src/commands/ps.ts: add LIFECYCLE column to rig + node tables
with abbreviated codes (run/rec/stp/deg/att, run/rec/det/att); JSON
output passes through lifecycleState from API.
- packages/cli/src/commands/up.ts: when isRigName resolves to an existing
rig (no library spec match), fetch /api/rigs/summary and print
"Recovering rig 'X' from latest snapshot..." for recoverable, "Turning
on rig 'X'..." for stopped. JSON output is unaffected (no prefix).
Tests added (matching plan closeout checklist):
- packages/daemon/test/node-inventory.test.ts: 6 lifecycleState cases
(running, recoverable with token, detached when token absent, detached
with no snapshot, attention_required on failed+alive, exited+detached).
- packages/daemon/test/ps-projection.test.ts: 5 rig-level cases plus
deriveRigLifecycleState fold helper full-branch coverage. Also migrated
the test fixture to createFullTestDb so the full migration set (including
pods) is available — required because ps-projection now folds via
getNodeInventory.
- packages/daemon/test/rig-repository.test.ts: 5
findLatestUsableSnapshot cases (no snapshot, all-null tokens, partial
tokens, latest-of-many ordering, returns even when only some nodes have
tokens — per-node state then determines individual recoverability).
- packages/cli/test/ps.test.ts: 4 cases (rig table LIFECYCLE column with
all five abbreviated codes; rig JSON includes lifecycleState; node
table column; node JSON includes lifecycleState).
- packages/cli/test/up.test.ts: 4 wording-divergence cases (recoverable
prints Recovering, stopped prints Turning on, running prints neither,
--json output is clean).
Verification:
- npx vitest run focused (daemon node-inventory + ps-projection +
rig-repository): 62/62 green.
- npx vitest run focused (cli ps + up): 43/43 green.
- Full daemon suite: 2052/2052 green.
- Full CLI suite has 4 pre-existing failures (port 7433 in use,
chatroom timeout flaky, config reads env) — confirmed by running them
on the pre-L2 baseline. Not caused by L2.
- npx tsc --noEmit clean on both packages.
Invariants preserved:
- No persisted column added (decision #1 derive-only).
- Existing `sessionStatus`/`startupStatus`/`restoreOutcome` fields
unchanged — `lifecycleState` is additive.
- No auto-restore on daemon start; `createDaemon` reconcile loop is
unchanged from L1.
- Help text says "Recovering ... from latest snapshot" which describes
what `rig up` does without promising success before tester L4 VM proof.
- `recoverable` derivation handles null fields gracefully (snapshot exists
but token is null → node stays detached, not falsely recoverable).
- Permission errors (L1 fail-closed) propagate unchanged through the new
projection; lifecycleState only consults sessionStatus + restoreOutcome
+ snapshot data.
L2 implementation accept does NOT close L2 — full closure waits on tester
L4 VM-reboot end-to-end proof, mirror of L1.
Refs:
- rigs/openrig-build/state/orch/velocity-slice-L2-recoverable-state-safe-defaults-plan-20260428.md
- L1 predecessor commit: 3ad1df6 fix(daemon): classify tmux socket-absence as missing session and log startup reconcile
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
After a host reboot the tmux socket file at /tmp/tmux-<uid>/<name> is gone, so
`tmux has-session` exits non-zero with a transport-absent message rather than
the server/session-absent messages the existing classifier knows. The startup
reconciler caught the unknown error, recorded it silently, and skipped marking
the row detached, leaving DB rows that said `running`/`ready` against a tmux
reality where the sessions were gone.
Add `isTmuxTransportAbsentError` to the tmux adapter; classify post-reboot
socket-absence (`No such file or directory`) and `Connection refused` against
a tmux socket path as absence. Wire it into `hasSession`, `listSessions`,
`listWindows`, and `listPanes` for symmetry. Permission errors (`Permission
denied`, `Operation not permitted`, `EACCES`, `EPERM`) remain rethrown so
callers stay fail-closed; the classifier checks the permission keywords
before any absence pattern.
Capture each `Reconciler.reconcile(rig.id)` aggregate at startup and log a
compact summary line:
startup reconcile: rigs=<n> checked=<n> detached=<n> errors=<n>
Per-rig errors are written as warnings without crashing startup. Logging is
guarded so it can never throw out of `createDaemon`.
Tests:
- packages/daemon/test/tmux-adapter.test.ts — 4 new `hasSession` cases
(2 socket-absent variants, 2 permission rethrow regressions).
- packages/daemon/test/reconciler.test.ts — explicit socket-absent detach
case + explicit permission-rethrow case.
- packages/daemon/test/startup.test.ts — assertion that the compact reconcile
summary line appears in startup output.
Refs:
- openrig-work/field-notes/2026-04-28-openrig-post-reboot-tmux-restore-postmortem.md
- rigs/openrig-build/state/orch/velocity-slice-L1-cold-start-tmux-truth-repair-plan-20260428.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Cycle-2 of H46 Slice 2 per QA REJECT on safe contract.
safe now means execution safety per action, not blanket true:
- safe:false for mutating actions (daemon start, chmod, create
files, snapshot creation, queue file creation)
- safe:true for read-only/informational actions (check daemon
status, list rigs, transcript info, hook inspection guidance)
- Conservative default: omitted remediationSafe → safe:false so
new checks without explicit classification don't invite agents
to auto-execute mutating commands
blocking remains severity (must-fix-before-restore vs caveat).
Agents distinguish severity via blocking, execution safety via
safe. Both fields are independent.
Comment on CheckEntry.remediationSafe updated to document
conservative default. CLI daemon-down local result updated to
safe:false (daemon start is mutating).
Tests: omission-default regression proving omitted remediationSafe
→ safe:false; blocker test expects safe:false for daemon start;
caveat test expects safe:false for snapshot creation; route test
for non-null repairPacket with blocking field on broken fixture.
Service 37/37; route 8/8; CLI 12/12 = 57 total. Daemon 1861/1861;
CLI 471/472 (preexisting chatroom); tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
H46 Slice 2: populate repairPacket with agent-readable repair steps.
repairPacket shape: Array<{step, command, rationale, safe, blocking}>
- step: 1-indexed ordinal
- command: prose remediation action (not auto-executed)
- rationale: evidence of what was wrong
- safe: true (all Slice 2 actions are read-only suggestions)
- blocking: true for red/must-fix-before-restore; false for
yellow/caveat. Agents distinguish severity via blocking, not safe.
Packet is null when verdict is restorable (nothing to fix).
Ordering: blockers first, then caveats, both in check order.
1-indexed steps after filtering entries with empty remediation.
CLI daemon-down and 500/unknown local results also produce
repairPacket with correct blocking/safe fields and exit codes.
Human output shows repair step count + blocker/caveat breakdown.
Unknown verdict includes repairPacket with blocking:true entries
so agents can identify probe-recovery actions.
Tests: service 36/36 (+6 repair-packet); route 8/8 (+2 broken-
fixture repairPacket + blocking field); CLI 12/12 (+1 daemon-down
repairPacket + 1 500-repairPacket + 1 human repair-step output).
Full daemon 1860/1860; CLI 471/472 (preexisting chatroom);
tsc + build clean both workspaces.
TDD note: RED evidence was produced by stashing production edits
and running new tests against Slice 1 baseline (repairPacket
always null). QA contributed compatible test variants before
ownership coordination was established; absorbed intentionally.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-5 follow-up to 004e36f (not amended). r2 reproduced a
directory mtime mutation from the old write+delete probe. The
cycle-4 test only tracked exists() references, which was too weak.
This test:
- Creates a temp dir and sets OPENRIG_HOME to it
- Pins mtime to a known past value via utimesSync
- Runs RestoreCheckService.check({noQueue:true, noHooks:true})
- Asserts no .restore-check-probe file exists
- Asserts directory mtimeMs is unchanged (no filesystem mutation)
- Restores original OPENRIG_HOME in finally block
Proves the accessSync(W_OK) replacement from 004e36f is genuinely
non-mutating at the filesystem level, not just at the import level.
Service 26/26; route 6/6; full daemon 1848/1848; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-4: r2 found checkStateDirWritable() created and deleted a
.restore-check-probe file, violating H46's read-only boundary.
Replaced writeFileSync/unlinkSync with accessSync(stateDir, W_OK)
— a non-mutating permission check that throws on non-writable
without touching the filesystem.
Regression test added: service.check() does not reference any
probe file path through the exists() dep. Removed unused fs
imports (writeFileSync, unlinkSync).
Service 26/26; route 6/6; full daemon 1848/1848; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-3 of H46 Slice 1. QA found probeDaemonHealth() throw still
produced not_restorable because checkDaemonReachable() returned a
red CheckEntry on catch, and buildResult() aggregated red into
not_restorable.
checkDaemonReachable() now returns null on probe throw. Caller
short-circuits to buildUnknown() when null is returned. Definite-
down (healthy=false, negative text) still produces red/
not_restorable. Only probe exceptions (socket unavailable, etc.)
produce unknown.
Test added: probeDaemonHealth throw → verdict unknown + daemon
check red with "unable to determine" evidence.
Service 25/25; route 6/6; full daemon 1847/1847; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-2 of 4a333f2 per QA REJECT.
QA Fact 1 (missing spec/root checks): added checkSpecPresent()
with three states: rig root missing → red; rig root exists but
rig.yaml missing → yellow; both present → green. Tests added for
all three paths.
QA Fact 2 (probe error → not_restorable instead of unknown):
listRigs and getNodeInventory catch blocks now call buildUnknown()
which produces verdict: "unknown" (not "not_restorable"). Operators
can distinguish "definitely broken" from "checker couldn't inspect."
2 tests added for thrown probes expecting unknown verdict.
QA Fact 3 (hooks false-green): checkHooks() now returns yellow with
"not yet implemented (Slice 2)" evidence instead of false-green.
--no-hooks removes the check entirely; without it, hooks are
honestly classified as uninspected. Test added asserting yellow
status. Verdict aggregation test updated to use --no-hooks for
"all green" path.
Route tests fixed: removed dynamic import; use static import
of createApp directly.
Tests: service 24/24 (+6); route 6/6; CLI 11/11. Full daemon
1846/1846; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-4 of H44 Slice 2 per QA narrow test-only rejection.
3 CLI refresh tests added:
- --refresh calls /nodes?refresh=true once then normal inventory
(path tracking via requestedPaths array)
- refresh 502 failure exits 2 with "Context refresh failed" +
"Fix:" copy (hard-fail, not silent stale)
- refresh success returns same JSON semantics as non-refresh
CLI context tests now 13/13. All builds clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-3 of H44 Slice 2 per QA REJECT-AND-REVISE on a5d5fc3.
QA item 1 (daemon route tests): 4 tests added to sessions-routes
proving ?refresh=true calls pollOnce before projection, non-refresh
does not call it, refresh failure returns 502 with code
context_refresh_failed, and missing contextMonitor is handled.
QA item 2 (UI tests): 4 RigNode tests proving prominent context
number for known-fresh (red >=80, amber 60-80, green <60), stale
at 50% opacity, unknown shows '?'. 2 Explorer tests proving
context percentage indicator per agent row with amber color +
fallback to runtime label for unknown context.
All existing tests preserved. Gates: daemon 1816/1816;
CLI 456/457 (preexisting chatroom); UI 402/402 (38 files);
all builds clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
H44 Slice 2: context-usage as a first-class operator surface.
CLI: new `rig context` command with two-axis status model
(urgency x freshness) matching the shipped rigx-context wrapper.
Flags: --rig, --threshold (keeps unknown + stale visible),
--refresh (triggers daemon re-sample), --json. Sort: CRITICAL >
WARNING > stale > ok > unknown. Only low+fresh = ok; low+stale =
stale (not ok). Human table + fleet summary.
Daemon:
- FRESHNESS_THRESHOLD_MS updated from 120_000 (2min) to 600_000
(10min) per PM spec — aligns CLI/UI/daemon freshness semantics.
- contextMonitor wired through AppDeps so routes can call
pollOnce() for --refresh plumbing.
- GET /api/rigs/:id/nodes?refresh=true triggers pollOnce before
inventory projection; 502 on refresh failure (honest, non-silent).
UI:
- RigNode.tsx: context percentage enlarged from 8px badge to
prominent 16px bold number; color-coded (green <60 / amber
60-80 / red >=80); stale samples at 50% opacity; unknown shows
"?" instead of "CTX unknown".
- Explorer.tsx: context percentage indicator added per agent row;
replaces runtime label when context data available; same color
coding + stale opacity.
- useNodeInventory.ts: contextUsage type added to
NodeInventoryEntry interface.
Files: 10 across CLI/daemon/UI. No new /api/context endpoint —
reuses existing node inventory routes per spec.
Gates: daemon 1812/1812; CLI 446/447 (preexisting chatroom);
UI 396/396; tsc clean across all 3 workspaces.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-3A test-only addendum. Prior fail-closed test used a mocked
hasSession vi.fn throw. This test wires a REAL TmuxAdapter with
a mock exec that throws Permission denied for has-session commands.
Proves the actual adapter error-classification contract (known
absence vs unexpected probe failure) propagates through
classifyRunningSessions to produce rig_not_stopped without DB
mutation or pre_restore snapshot creation.
Closes the r2 review requirement for a production-adapter-backed
restore regression rather than relying on logical sufficiency
from separate adapter + orchestrator mocked tests.
Gates: restore-orchestrator 55/55; full daemon 1812/1812;
tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-3 of snapshot-restore fix. r2 found that hasSession swallowed
all errors as false, so classifyRunningSessions could never reach
its unknown/fail-closed branch with the real adapter. A permission
denied or socket error would be treated as "session absent" and
restore would proceed over a session it couldn't actually verify.
hasSession now distinguishes:
- Known absence (return false): "session not found", "can't find
session", "no server running" — session genuinely doesn't exist.
Post-crash restore proceeds for these.
- Unexpected probe failure (rethrow): permission denied, socket
error, unrecognized exec failure. classifyRunningSessions catches
the throw, classifies as unknown, and blocks restore with
rig_not_stopped. No DB mutation, no pre_restore snapshot.
Added isSessionAbsenceError() helper alongside existing
isNoServerError().
Tests added/updated:
- tmux adapter: 6 hasSession tests (exits-0 true, session-not-found
false, can't-find false, no-server false, permission-denied throws,
generic-error throws)
- restore: fail-closed test uses Permission denied error (previously
used "tmux server not running" which is a known-absence pattern)
Gates: tmux-adapter 33/33; restore-orchestrator 54/54;
snapshots-routes 20/20; startup 9/9; full daemon 1811/1811;
tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-2 revision of 44b7a3d per r2 REJECT-AND-REVISE + qa_mentor
VM RED.
r2 MUST-FIX 1 (pre_restore snapshot ordering): cycle-1 mutated DB
(marked stale sessions detached) before capturing pre_restore
snapshot, so the snapshot recorded post-mutation state. Fixed:
classifyRunningSessions reads + checks tmux WITHOUT mutating DB;
pre_restore snapshot captures while DB still has original running
state; stale sessions marked detached only AFTER snapshot.
r2 MUST-FIX 2 (latestByNode miss): cycle-1 used a per-node Map
proxy that could miss older live sessions behind newer detached
rows. Fixed: classifyRunningSessions iterates ALL sessions (not
just latest-per-node); any running session with live tmux blocks.
qa_mentor VM RED (hasSession misclassification): TmuxAdapter
hasSession relied on listSessions format-string parsing which
failed when tab delimiters were malformed. Fixed: hasSession now
uses `tmux has-session -t <name>` directly (exit-code-based; no
output parsing needed).
Tests added (4 restore + 3 tmux adapter updated):
- pre_restore snapshot preserves original running status
- older-live + newer-detached same-node blocks
- tmux-check-throws → fail-closed (rig_not_stopped, no mutation,
no pre_restore snapshot)
- tmux adapter hasSession tests use has-session command
Startup composition tests updated: mock exec throws for
has-session commands to simulate missing sessions correctly.
Gates: restore-orchestrator 54/54; snapshots-routes 20/20;
tmux-adapter 30/30; startup 9/9; full daemon 1808/1808 (128
files); tsc + build clean.
Recommend r1+r2 re-review + qa_mentor VM re-dogfood.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
After a tmux crash, DB sessions still show status='running' even
though tmux sessions are dead. RestoreOrchestrator.restore()
checked hasRunningSessions() against DB state alone, refusing
restore with 'rig_not_stopped' when the rig was actually stopped.
Add reconcileStaleRunningSessions() before the running-sessions
check: verifies each DB-running session against tmux reality via
hasSession(); marks dead sessions as 'detached'. Genuinely live
tmux sessions still block restore (inverse invariant preserved).
Fails closed: if tmux check errors, session stays running (honest
about uncertainty).
Error message updated from generic 'must be stopped' to 'has live
sessions' with concrete recovery guidance.
Tests added (3):
- Post-crash: stale DB running + tmux dead → restore proceeds
- Inverse: live tmux sessions → restore still blocks (409)
- Partial failure: one node launch fails → all snapshot nodes
present in result with logicalIds (no silent pod drops)
Route test (snapshots-routes) updated: 409 test now uses
hasSession=true to simulate genuinely live sessions.
Gates: restore-orchestrator 51/51; snapshots-routes 20/20; full
daemon 1805/1805 (128 files); tsc + build clean.
Restore semantics materially changed (reconcile-before-check):
recommend r1/r2 adversarial review + qa_mentor VM dogfood.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-2 revision of 3cafd39 per QA merge-gate rejection.
QA repro: box-wrapped Codex startup banner inner lines (blank
rows, model:, directory:) survived cleanup because
isStartupSplashLine only matched branded headers. The full
╭───╮ / │...│ / ╰───╯ box structure contains 5-7 lines; only 1
was filtered.
Extended isStartupSplashLine with two context-safe additions:
- Box-drawing-only lines (│ + whitespace + │, border rows):
filtered when the line contains box chars but is empty after
stripping them.
- model:/directory: lines INSIDE box wrappers: filtered only when
the original line has │/╭/╰/╮/╯ chars (context discriminator).
Standalone model:/directory: output without box wrappers
survives — a new preservation test confirms.
Tests: 33/33 transcript-store (+1 standalone model/directory
preservation). Full daemon 1802/1802. tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-2 revision of 3d8d7e8 per adversarial-pair synthesis.
r2 HIGH: Bash(echo:*) is not a convenience-safe pattern — echo
can write via shell redirection (echo "x" > file). Removed from
the baseline. Remaining 10 patterns are genuinely read-only Bash
commands. Comment and constant renamed from STANDARD_SAFE_BASELINE
to CONVENIENCE_BASELINE; provenance baseline marker changed from
"standard-safe" to "convenience"; test section header updated.
Code comment documents shell-redirection residual risk explicitly.
r2 MEDIUM: provenance timestamp made repeated provisioning
non-file-idempotent. Fixed by tracking whether any new patterns
were actually added; if all patterns are already present, the
write is skipped entirely (preserves existing file content +
provenance timestamp). New regression test verifies two
consecutive deliverStartup calls produce byte-identical
settings.json.
Gates: claude-adapter 27/27 (+1 file-idempotency regression);
full daemon 1799/1799; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Extend provisionRigPermissions() to inject the standard-safe
Bash baseline alongside the existing Bash(rig:*) on every rig up /
restore / launch. Addresses B1 momentum-killer where Claude Code
sessions reset to ask-all on benign read-only ops (ls, cat, tail,
head, wc, grep, rg, pwd, echo, which).
11 patterns in STANDARD_SAFE_BASELINE using Bash(cmd:*) colon-form.
Additive only — never removes existing allow entries. Provenance
marker (_openrig_provenance) written so operators can distinguish
rig-injected from human-authored entries.
Path C partial fix per PM spec: durable across OpenRig lifecycle
events (re-provisions on every touch); unknown durability across
arbitrary Claude updates (spike never concluded). Honest position
documented in architect brief.
v0 scope: no rig.yaml schema changes, no new CLI commands, no new
routes, no teardown cleanup. Same 11 patterns for every Claude
Code member (per-rig customization deferred to v0.5).
Tests added (6): baseline applied on fresh settings, additive with
existing entries, idempotent on re-run, provenance marker present,
deny/ask preserved, non-permissions keys preserved.
Gates: claude-adapter 26/26; full daemon 1798/1798; tsc + build
clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-2 revision of 638c541 per adversarial-pair + VM synthesis.
r2 MUST-FIX: npm pack excluded scripts/check-abi.mjs because the
files field only listed ['dist', 'daemon', 'ui']. Fresh installs
failed with MODULE_NOT_FOUND before any ABI logic ran. Fixed by
adding 'scripts' to the files array. New repo-level test
(scripts/check-packing.test.mjs) verifies the script IS in the
tarball; prevents silent drift.
r2 HIGH: catch block classified every native-addon-load failure as
ABI mismatch. A MODULE_NOT_FOUND error got the "native binary does
not match" headline + wrong nvm/rebuild fix. Fixed by branching on
NODE_MODULE_VERSION in the error message: ABI mismatch gets the
specific message; other failures get a generic "native addon failed
to load" with honest detail + appropriate fix commands.
Files changed:
- packages/cli/package.json (files field + already had postinstall)
- packages/cli/scripts/check-abi.mjs (error classification branch)
- packages/cli/test/check-abi.test.ts (+2 tests: MODULE_NOT_FOUND
gets generic message, EACCES gets generic message; total 10/10)
- scripts/check-packing.test.mjs (tarball-content regression)
Gates: check-abi 10/10; check-packing 1/1; CLI 446/447
(1 preexisting chatroom); daemon 1792/1792; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
npm install @openrig/cli now runs a postinstall check that fails
loudly if the Node runtime can't load better-sqlite3's native
binary. Layered on top of the engines.node constraint from c377fa9:
engines warns at npm level; this hard-blocks at install time with
a copy-paste fix command.
Two-phase check (Option C hybrid):
1. Version-range fast path: rejects odd-numbered Node majors
(21/23/25) and versions below 20 before attempting any native
load. Short-circuits with clear error + nvm install fix.
2. Native addon load: on supported versions, requires
better-sqlite3 and catches ABI mismatch (wrong
NODE_MODULE_VERSION). Reports npm rebuild fix command.
Error messages follow honest-errors principle: what failed + why
it matters + copy-paste fix (nvm install 22 or npm rebuild).
Files:
- packages/cli/scripts/check-abi.mjs (postinstall script)
- packages/cli/package.json scripts.postinstall wiring
- packages/cli/test/check-abi.test.ts (8 unit tests covering
supported versions, odd versions, old versions, ABI mismatch,
and fast-path short-circuit verification)
Gates: check-abi 8/8; CLI 444/445 (1 preexisting chatroom fail);
daemon 1792/1792; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Installing @openrig/cli under Node 25 (homebrew default) silently
succeeds at npm but the daemon fails at runtime because
better-sqlite3 native prebuilds only ship for even-numbered LTS
lines (20, 22, 24). No engines field existed in packages/cli or
packages/daemon package.json to warn on install; README said
"Node.js 20+" with no upper bound.
Added engines.node "^20 || ^22 || ^24" to:
- packages/cli/package.json (published to npm; npm warns on
mismatch when engine-strict or --engine-strict is set)
- packages/daemon/package.json (private; consistency)
- package.json (monorepo root; local dev)
Updated README Requirements to name supported LTS versions
explicitly and explain why odd releases are unsupported.
Added scripts/check-engines.test.mjs (runs under npm run
test:repo) to verify all three package.json files carry the
LTS-only constraint, preventing silent drift back to ">=20".
Postinstall ABI check deferred to separate scope (would require
VM gate for install-time validation under multiple Node versions).
Gates: check-engines 3/3; daemon 1792/1792; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The IDLE_STATUS_BAR_PATTERNS check (Codex model footer / Claude
edit-accept bar) was applied across the last 3 non-blank lines,
same as the prompt-pattern check. During short Codex work bursts,
a stale status bar from a prior idle state could remain in the
last 3 non-blank lines while the agent was actively working,
causing looksLikeMidWork() to false-negative (classify active work
as idle and let rig send through).
Split the two discriminators:
- IDLE_PROMPT_PATTERNS: still checked across last 3 non-blank
(prompt line may sit above the status bar in the visual stack)
- IDLE_STATUS_BAR_PATTERNS: now checked on the LAST non-blank line
ONLY — status bars render at the visual bottom; if anything else
appears below them in the non-blank sequence, active work has
been rendered over the stale bar
Existing stale-scrollback tests pass (idle status bar IS the last
non-blank line in genuine idle states). Short-burst false-negative
now correctly blocked (stale status bar is NOT the last non-blank
line when active work footer appears below).
Tests: 33/33 session-transport (+1 short-burst regression fixture);
full daemon 1792/1792; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Test-infrastructure improvement: add buildPaneFixture() helper and
5 realistic full-screen pane fixtures that include scrollback,
status bars, separator lines, and trailing blank padding matching
real tmux capturePaneContent output.
Tests verify 14a9170's non-blank-window approach handles real render
shapes:
- Codex active-working: scrollback + working footer + padding →
blocks (mid_work)
- Codex idle-at-prompt: stale Working in scrollback + status bar
+ padding → allows (stale-scrollback correctly classified)
- Claude Code active-working: tool output + working footer +
padding → blocks
- Claude Code idle-at-prompt: stale Working in scrollback +
edit-accept bar + padding → allows
- Codex trust-prompt full-screen: instructions + numbered choices
+ hint + heavy padding → blocks
Residual noted in test comment: if a stale Codex status bar
("gpt-5.4 ... Context [...]") from a prior idle state remains in
the last 3 non-blank lines during active work (very short work
burst), the idle discriminator false-negatives. In realistic
renders the distance is larger; fixture models that. The edge case
is a genuine gap worth a follow-up production-code refinement.
No production code changes. Test-only: +5 tests, +1 helper.
session-transport 32/32; full daemon 1791/1791; tsc clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Codex and Claude Code trust-prompt choice lines like '› 1. Yes,
continue' passed through looksLikeMidWork() because no existing
MID_WORK_PATTERNS matched numbered-choice text. Sending through
a trust prompt bypasses user consent.
Add /^[❯›]\s*\d+\.\s/m to MID_WORK_PATTERNS — matches prompt-char
+ digit + period + space (numbered choice lines from either
runtime). Interacts correctly with the idle-prompt discriminator
from 69479d9: if the trust prompt is stale scrollback with an idle
prompt below, the discriminator overrides and send proceeds; if the
trust prompt is current (no idle prompt below), the guard blocks.
Tests added (2): Codex trust-prompt block, Claude Code trust-prompt
block. Existing stale-scrollback + idle-prompt + genuine-mid-work
tests preserved (26/26 session-transport). Full daemon: 1785/1785.
tsc clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-2 revision of 85d0844. r2 found the idle-prompt override
regex /^[❯›]\s/ was too broad — it matched any line starting with
the prompt char, including active input like '❯ Working on a task.'
where "Working" should trigger the mid-work guard.
Tightened to two complementary discriminators:
- IDLE_PROMPT_PATTERNS: /^[❯›]\s*$/ (empty prompt only — prompt
char + optional whitespace + end-of-line)
- IDLE_STATUS_BAR_PATTERNS: Codex model footer (gpt-N... Context)
and Claude edit-accept bar — these only render at idle prompt,
never during active tool execution
r2 fixture A (active Claude input '❯ Working on a task.') now
correctly blocks. r2 fixture B (trust-prompt '› 1. Yes, continue')
is documented as a pre-existing gap: no MID_WORK_PATTERNS match
that content, so looksLikeMidWork was never triggered pre- or
post-fix. Trust-prompt detection would require scope expansion
per PM constraint #4; filed as separate concern. Original
stale-scrollback fixes preserved (both Codex and Claude idle
tests still pass via the status-bar discriminator).
Tests: 25/25 session-transport (5 new: 2 stale-scrollback override,
1 genuine mid-work preserved, 1 r2 fixture A active-input blocks,
1 r2 fixture B trust-prompt documented). Full daemon: 1784/1784.
tsc clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
looksLikeMidWork() checked the last 5 pane lines for patterns like
"Working" and "esc to interrupt" but did not account for the pane
having settled to an idle prompt since the working text appeared.
Stale scrollback from a completed task would trip the guard and
hard-block rig send to idle-at-prompt agents.
Add IDLE_PROMPT_PATTERNS that recognise Claude Code (❯) and Codex
(›) prompt characters. When mid-work patterns match, a second pass
checks the last 3 non-blank lines for prompt indicators. If a
prompt is found, the match is classified as stale scrollback and
the guard returns false — send proceeds without --force.
Genuine mid-work (working footer with no idle prompt below it) is
still blocked. The guard is not removed or weakened for real
interruption protection.
Tests added (3):
- Codex pane with "Working" + "esc to interrupt" above idle "›"
prompt → send proceeds (was false-positive)
- Claude Code pane with "Working…" above idle "❯" prompt → send
proceeds (was false-positive)
- Working footer present with NO prompt below → send still blocks
(regression: real protection preserved)
Gates: session-transport.test.ts 23/23; full daemon suite 1782/1782
(128 files); tsc --noEmit clean; npm run build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cycle-1 (3023de8) prevented the CLAUDE.md / AGENTS.md write but
left the adapter reporting the entry as projected (via
ProjectionResult.projected) and the file as delivered (via
StartupDeliveryResult.delivered++). r2 caught: adapter contract
lies — skipped work shows up as completed, violating the
honest-detection constraint the cycle-1 fix was meant to restore.
mergeGuidance now returns boolean (true=merged, false=rig-role skip).
projectEntry propagates the return. project()'s loop routes the
entry into the `skipped` array when mergeGuidance reports skip.
deliverStartup's guidance_merge case `continue`s past the
`delivered++` on skip so counts reflect actual writes.
Same single-chokepoint shape as cycle-1; no new special cases, still
narrow on blockId === 'rig-role'. Architect's three binding
constraints: (1) adapter-layer only (both adapters + tests); (2)
honest detection — this IS the restoration (the point of the patch);
(3) no broader-allow special case — still only rig-role.
Tests updated + added (6 cycle-1 tests + 2 new cycle-2 regression):
- projectEntry rig-role asserts `result.skipped` contains 'rig-role'
AND `result.projected` does not (each adapter)
- projectEntry non-rig-role asserts `result.projected` contains
block id AND `result.skipped` does not (each adapter — NEW
regression-on-contract)
- deliverStartup rig-role asserts `result.delivered === 0` AND
`result.failed === []` (each adapter — was ===1 in cycle-1)
Gates: vitest 40/41 in claude+codex adapter suites (1 preexisting
unrelated Codex thread-id env fail still present via stash+retest);
tsc --noEmit clean in packages/daemon. New commit on top of 3023de8
per cycle discipline, not amended.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The rig-role managed-block regenerator pairs (target-file x spec)
without seat correlation, so multi-seat pods collide into one
CLAUDE.md/AGENTS.md body. openrig-pm + corpus-pm were behaviorally
affected; openrig-build saw the on-disk symptom only (startup.files
insulated runtime).
SHAPE 1 surgical fix per plat-architect: in claude-code-adapter and
codex-runtime-adapter, refuse mergeManagedBlock when blockId is
`rig-role`. Per-seat role content travels through the send_text
startup path instead, which preserves seat identity. Skip is logged
(structured, never silent) so the collision can't land quietly.
Adapter-layer only (both adapters + their tests). No domain, route,
or UI changes. Narrow check on the single blockId; no broader
special-case.
Tests added (6):
- projectEntry + category=guidance + effectiveId=rig-role does not
write CLAUDE.md / AGENTS.md (each adapter)
- projectEntry with non-rig-role guidance still merges (regression,
each adapter)
- deliverStartup guidance_merge for path=rig-role does not write
(each adapter)
Gates: vitest 38/39 in claude+codex adapter suites (1 preexisting
unrelated Codex thread-id env fail confirmed via stash); tsc
--noEmit clean in packages/daemon.
Shape 2 (config sweep of agent.yaml rig-role references) is a
separate follow-up PR per architect scoping.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
cmux 0.63.x removed list-surfaces and agent-pids and added
list-panels / list-panes / list-pane-surfaces. The adapter hard-coded
the removed commands, so every surface.list request failed and
queryAgentPIDs misreported state.
Probe `cmux --help` at factory time; dispatch surface.list to
list-panels (modern) with a list-surfaces fallback; normalize the
panels payload to {surfaces} so CmuxAdapter.listSurfaces is unchanged.
workspace.agentPIDs uses legacy agent-pids when present, otherwise
throws CmuxSurfaceError{code:'unavailable', method}, preserving the
existing 'unavailable' vocabulary without a route-layer change.
Adapter-only scope: cmux-transport.ts + its test. No domain/route/UI
changes. cmux-transport.test.ts 24/24 green; cmux-adapter.test.ts
33/33 unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Reflects the 41 program.addCommand() registrations in
packages/cli/src/index.ts:98-138, including setup (setup.ts:383).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Forensically traced from bundle-types.ts, bundle-archive.ts, and
pod-bundle-assembler.ts. Covers:
- Archive format (.tar.gz with .sha256 sibling digest)
- Manifest schema v2 (pod-aware) with full field reference
- Security model (self-consistency, not authenticity)
- Safety guarantees (no symlinks, no traversal, digest + integrity)
- CLI surface (create, inspect, install, rig up)
- Assembly process (how agent refs are resolved, vendored, rewritten)
- Validation rules for manifest, archive safety, and assembly
- Legacy v1 compatibility note
Validated against a real bundle (specright-pm.rigbundle): manifest
fields, agent entries, integrity section all match documented schema.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Add optional docs field to RigSpec for bundled documentation
Add a top-level `docs` array field to the pod-aware RigSpec schema.
Each entry has a `path` field (safe relative path, same validation as
culture_file and startup file paths).
The engine does not consume these files for instantiation — they are
metadata for humans and agents setting up the environment before launch.
The bundle packer includes listed docs in the archive so setup
instructions travel with the distributable artifact.
Schema changes:
- types.ts: add RigSpecDoc interface and docs field to RigSpec
- rigspec-schema.ts: validate docs array with path safety checks
- rigspec-schema.ts: normalize docs into typed array
- rigspec-codec.ts: serialize docs field
Bundle changes:
- pod-bundle-assembler.ts: collect docs files into the bundle archive
Tests:
- rigspec-schema-docs.test.ts: 7 tests covering validation, rejection,
normalization, and path safety
Reference doc:
- docs/reference/rig-spec.md: document the docs field
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Fix review findings: null guard, missing-docs failure, bundle tests
Addresses all findings from R2 and orch1-peer independent reviews:
1. Guard against null/non-object entries in docs validation — docs: [null]
and docs: [42] now return clean validation errors instead of crashing
the validator (R2 #1, peer #1)
2. Bundle assembly fails loudly when a declared docs file is missing from
disk — prevents silently shipping bundles that advertise docs they
don't contain (R2 #2, peer #2)
3. Add bundle-level tests proving docs are collected and missing docs
fail assembly (R2 #3)
Tests: 5 new tests (3 schema null/primitive/string + 2 bundle positive/negative)
Full suite: 128 files, 1722/1722 passing
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Provision runtime config and disclose setup mutations
* Fix: force acceptEdits for managed sessions, don't inherit restrictive modes
deliverStartup unconditionally sets permissions.defaultMode = "acceptEdits"
for managed Claude sessions instead of preserving existing restrictive modes
via nullish coalesce. A pre-existing denyAll or other restrictive mode in
.claude/settings.local.json would previously survive provisioning, causing
managed rigs to hit prompt/block behavior in reused workspaces.
Found by: dev1-qa independent repro
Fix: change ?? "acceptEdits" to unconditional "acceptEdits" assignment
Test: regression test seeds denyAll, verifies deliverStartup overrides it
Full suite: 128 files, 1723/1723 passing
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Fix JSON normalization, disclosure copy, and README accuracy
Addresses CodeRabbit findings verified by QA:
1. readJsonObject now excludes arrays (typeof "object" includes arrays
in JS). Prevents silent property loss when settings.local.json or
.mcp.json contains a bare JSON array instead of an object.
2. provisionPermissionsAndMcps refactored to use readJsonObject,
readJsonObjectField, and readStringArray consistently instead of
raw JSON.parse + manual type checks. Same helpers used elsewhere
in the adapter.
3. Setup disclosure for .claude/settings.local.json now mentions
statusLine mutation alongside permissions (CodeRabbit item 3).
4. README clarifies that managed runtime boot performs config writes
while rig setup only discloses the paths (CodeRabbit item 6).
5. Regression test: array-valued settings.local.json and .mcp.json
are recovered to proper objects during provisioning.
Full suite: 128 files, 1724/1724 passing
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Harden setup disclosure tests against copy churn
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
- Add rig-architect SKILL.md to shared skills (specs/agents/shared/skills/rig-architect/)
- Register in shared agent.yaml so it's available via the normal skill system
- Skill teaches: design process (intent to topology), authoring workflow,
agent startup content strategy, diagnosis protocol, pattern catalog
- References ~/.openrig/reference/ docs for canonical spec format details
- Knowledge-transfer style skill, not workflow-compliance
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Move reference docs from packages/daemon/docs/reference/ to
docs/reference/ at the repo root — this is the authoring location
- Update .gitignore to track docs/reference/ alongside docs/as-built/
- Update build-package.sh to copy from docs/reference/ into the
assembled CLI package (not from inside the daemon package)
- Daemon startup still copies the bundled docs to ~/.openrig/reference/
on start for stable agent-accessible paths
Flow: docs/reference/ (edit here) → build → cli/daemon/docs/reference/
(npm package) → daemon start → ~/.openrig/reference/ (agents read here)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Add docs/reference/ inside daemon package: rig-spec.md, agent-spec.md,
agent-startup-guide.md, edge-types.md — all forensically traced from
actual parser/validator code, examples validated against rig spec validate
and rig agent validate
- Daemon startup copies bundled reference docs to ~/.openrig/reference/
on every start, keeping them in sync with the installed version
- Build script includes daemon/docs/ in the assembled CLI package
- Agents can read reference docs at a stable path (~/.openrig/reference/)
without needing CLI discovery commands
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Quick Start leads with rig setup -> rig up demo -> rig ui open.
Post-demo guidance points to orch1.lead and Open CMUX with tmux
attach fallback. Setup vs doctor distinction added with honest
best-effort phase-1 framing. Agent-driven profile-choice guidance
tells agents to ask users about core vs full before invoking.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
runSetup now resolves runDoctorChecks() including async checks and
embeds verification.checks in the result using doctor check names
and statuses as-is. ready is derived from both setup step failures
and verification fail statuses — warn/skipped alone do not flip
ready to false. Command handler builds default doctorDeps for
non-dry-run execution.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Non-interactive rig setup command with core and full profiles. Core
profile: brew check, tmux/cmux install, tmux config managed block,
verification. Full profile extends with jq and gh. --dry-run shows
plan without mutations and exits 0. --json returns structured result
with step ids and statuses. Failure in one step does not crash setup;
later steps are attempted or skipped honestly.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Rename shell cmux check to cmux_shell. Add async cmux_daemon check
that probes the running daemon's /api/adapters/cmux/status. When shell
cmux works but daemon cannot control it, report warn with mismatch
guidance. Daemon not reachable uses honest 'skipped' status. Shell
cmux missing omits cmux_daemon entirely. DoctorCheck status union
extended with 'skipped'.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add buildDaemonEnv() that scrubs terminal/GUI/session variables
(GHOSTTY_*, XPC_*, __CF*, TERM_PROGRAM, TERMINFO, CMUX_SOCKET_PATH,
CMUX_WORKSPACE, CMUX_SURFACE_ID) from the daemon process environment.
Preserves HOME, PATH, USER, SHELL, and all non-scrubbed vars. Explicit
OPENRIG_* launch values always override inherited values. startDaemon()
now uses buildDaemonEnv() instead of raw process.env spread.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Lead with value prop, screenshot, and quick start instead of sysadmin
content. Add demo rig preview, architecture overview, key concepts,
agent-managed software example, and Managed Agents comparison link.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
GET /api/adapters/cmux/status now calls connect() on every request
to perform a live capabilities probe instead of returning stale
startup-cached state. System panel label updated to 'cmux control'
with honest description reflecting current OpenRig control health.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Remove cmuxSurface gate on topology toolbar CMUX button — now renders
for all nodes with rigId. Retarget from /focus to /open-cmux. Both
unbound and bound nodes use the open-or-focus route. Flash feedback
preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Rename node detail cmux button to Open CMUX, retarget from /focus
to /open-cmux route. Button renders for all nodes including unbound.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add NodeCmuxService with openOrFocusNodeSurface() and POST /open-cmux
route. Three branches: focus existing bound surface, create new terminal
for tmux-backed nodes (attaches into tmux), create honest helper console
for external-cli nodes. All side effects checked honestly — sendText and
focusSurface failures return ok:false. Binding persisted on creation.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Update cmux transport to modern CLI shape: focus-panel, send --surface,
current-workspace, new-surface. Adapter normalizes real cmux response
payloads: workspace_id for current workspace, ref-first fallback chain
(created_surface_ref -> created_surface_id -> surface_ref -> surface_id)
for surface creation. Version logic stays in transport layer only.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Update services-readiness.ts comments to list shipped consumers
(boot-time gate, on-demand receipt capture) and mark background
monitor and rig ps env summary as deferred. Update architecture.md
with explicit runtime truth model. Update cli-reference.md to
reflect wired --volumes and note rig ps env gap.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Fix resolveMemberAgent in LibraryReview to resolve relative local:
refs against the rig source path instead of only matching the narrow
local:agents/<name> pattern. Handles both simple and deeply nested
relative refs like local:../../../agents/apps/vault-specialist.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add policyOverride option to ServiceOrchestrator.teardown() so callers
can override the persisted down policy. Route threads volumes=true as
policyOverride=down_and_volumes. Existing callers with no opts still
use persisted policy. Dead comment block removed from route.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Track probe outcome as probeStatus (fresh/stale/no_orchestrator) in
GET /api/rigs/:rigId/env response. Failed probes and null results
return stale with probeError instead of silently serving cached
receipts as fresh truth. No probeStatus when hasServices is false.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Fail early in the pod-bundle bootstrap path when the rig spec contains
a services block. Service-backed rigs require a stable source directory
and cannot be launched from .rigbundle archives. The rejection stays
inside the try/finally cleanup block so extracted temp dirs are always
cleaned up. Error uses resolve_spec stage with services_unsupported code.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add Specialist Agent summary card to LibraryReview for service-backed
rigs, showing canonical identity (vault.specialist). Update test mocks
to canonical topology and summary mentioning specialist. Non-service
rigs explicitly tested to show no specialist card.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Update secrets-manager rig to use canonical managed app convention:
pod id vault, member id specialist, agent_ref pointing to the new
vault-specialist AgentSpec. Summary explicitly mentions the Vault
specialist agent.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
New built-in vault-specialist AgentSpec at agents/apps/vault-specialist/
with Vault-specific role guidance, startup context (dev mode, proof
token, access paths), and vault-user skill (health, API auth, secret
CRUD). Default profile loads shared skills plus vault-user.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Extend env route to return surfaces from specJson. Add useRigEnv hook
with 10s polling. Add RigEnvPanel component showing overall env state
(Healthy/Degraded/Stopped/Unknown), per-service health, surface links,
View Logs, and Stop Env. Add Env tab to RigDetailPanel between Info
and Chat, shown only for service-backed rigs.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add environment tab to RigSpecDisplay gated by showEnvironmentTab prop,
only passed from LibraryReview. Tab shows compose services, health gates,
and surfaces. Add Copy Setup Prompt action for service-backed rigs with
buildSetupPrompt helper. Draft/open-file review explicitly excluded via
regression test.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add unified library section with All/Apps/Rigs/Agents filter chips.
Use single useSpecLibrary() all-entries query for the merged list.
Richer rows show type badge (APP/RIG/AGENT), stability badge
(Stable/Experimental/Community), and summary. Drafts remain in
their original Rig Specs / Agent Specs sections.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add RigSpecServicesReview to review output for service-backed rigs.
Domain extracts kind, composeFile, projectName, downPolicy, waitFor,
and surfaces from raw YAML (filesystem-free). Route layer enriches
with composePreview by reading the compose file (best-effort). Wait
target types match the canonical RigServicesWaitTarget contract.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add hasServices?: boolean to SpecLibraryEntry in both daemon and UI.
In classifySpec(), detect services block by parsing raw YAML for rig
entries. Only set when true to keep agent entries clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Move secrets-manager spec from specs root into rigs/launch/secrets-manager/
so it is naturally discoverable by the built-in library scanner. Fix
agent_ref relative path for the new depth. Update starter tests to expect
secrets-manager in the indexed rig library scan.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Built-in secrets-manager rig spec backed by HashiCorp Vault in dev mode.
Compose file with health check, wait_for HTTP health target, surfaced
URLs (UI + API) and commands. Receipt-only continuity (no checkpoint
hooks in v1). Proves the generic services substrate without special-case
code.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
T02: ComposeServicesAdapter (adapters/) for docker compose up/down/
status/logs/probe. ServiceOrchestrator (domain/) as leaf service for
boot/teardown/captureReceipt. services-readiness.ts shared evaluator
for wait targets + derived health. 21 tests.
T03: Bootstrap prelaunch hook in PodRigInstantiator.instantiate() runs
service gate between topology creation and node launch. Bootstrap
orchestrator builds hook via canonical pod-aware codec, persists
services record, boots services. Restore orchestrator boots services
before agent restore. service_boot_failed added to InstantiateOutcome
and RestoreOutcome. Startup wires ServiceOrchestrator into both
orchestrators. 4 integration tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
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.
# Prepare the machine (attempts tmux, cmux, Claude Code, Codex, tmux defaults — reports what worked)
rig setup
# Boot the conveyor starter rig (4 seats, mixed runtimes)
rig up conveyor
# Open the UI
rig ui open
```
After the conveyor rig boots, open the UI and click **intake.lead** in the topology graph. Use **Open CMUX** to jump into a terminal for that node. If cmux is not available, use the tmux attach command shown in the node detail panel instead.
## Launch Walkthrough
The launch path above has been walked end-to-end on fresh macOS VMs. The only thing your agent can't do would be the oauth logins for claude and openai and dealing with permission prompts.
### 1. Open the conveyor rig
When the UI opens, the explorer is already visible on the left. Click the `conveyor` rig to load its live topology.
### 2. Click `CMUX` on `intake.lead`
Once the topology loads, go to the `intake.lead` node and click its `CMUX` button. That opens the intake terminal directly.
> From here, give the intake agent a packet of work and watch it move through planning, build, review, and closeout.
## What It Does
OpenRig is a multi-agent harness — it manages the system that coding agents form when you run them together. Not the agents themselves, but the team they create: which sessions are running, how they relate, how to recover after a reboot, and how to stop it from becoming terminal sprawl.
- **Define** topologies in YAML (RigSpec) with pods, edges, and continuity policies
- **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.
- **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.
- **Culture**: CULTURE.md sets coordination norms for the group. Research rigs get exploratory culture. Implementation rigs get conservative, trust-but-verify culture.
## Agent-Managed Software
A rig can package actual software alongside the agents that manage it. The shipped example is `secrets-manager`: a HashiCorp Vault instance operated by a specialist agent.
```bash
rig up secrets-manager
rig env status secrets-manager
rig send vault-specialist@secrets-manager "Check Vault health and report status." --verify
```
Requires Docker for service-backed rigs.
## Requirements
- Node.js 20, 22, or 24 (even-numbered LTS releases; odd releases like 25 lack native addon prebuilds)
- tmux
Optional:
- cmux for `Open CMUX` node surface controls
- Docker for service-backed rigs and managed apps
## Setup and Troubleshooting
-`rig setup` attempts core machine preparation: tmux, cmux, Claude Code, Codex, and tmux defaults. It reports what it tried and what actually succeeded. If something fails, it gives the local agent enough context to finish the job.
-`rig setup --full` attempts a broader operator workstation setup (jq, gh) on top of core.
-`rig doctor` inspects current system health and helps diagnose problems after setup. Use it when something stops working or after machine changes.
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
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.
## 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.
-`rig env status <rig>` — inspect rig-scoped environment status for service-backed rigs and managed apps
-`rig env logs <rig> [service]` — fetch service logs
-`rig env down <rig>` — tear down the rig environment
**Managed-app-aware help and copy:**
-`rig up` help/examples explicitly include library-backed managed apps like `secrets-manager`
-`rig specs` help/list/preview text now distinguishes rigs, agents, and managed apps
-`openrig-user` now teaches the shipped managed-app model (`software + specialist`) and cross-rig specialist interaction
**Communication and history commands:**
-`rig send <session> "message" [--verify] [--force] [--host <id>]` — send to agent terminal; `--host` routes to a host declared in `~/.openrig/hosts.yaml` via single-hop ssh (CLI-side shell-out; daemon untouched). SSH success is NOT verify success — the remote rig's verify result is surfaced verbatim.
-`rig capture <session> [--lines N] [--json] [--host <id>]` — capture agent pane content; `--host` same shape as `rig send --host`.
**Session source declaration:** Member YAML may carry `session_source` to control how a new managed seat derives its starting context. v1 supports two modes: `fork` (start from a prior native runtime conversation; captures and persists a NEW post-fork token, never the parent — see cli-reference.md "Session source declaration"); and `rebuild` (fresh-launch seeded with operator-declared artifacts in trust-precedence order; the seat's `continuityOutcome` is `rebuilt` and no `resumeToken` is persisted). Both modes flow through the existing rig spec and `rig expand` member-input shapes; no new top-level command was introduced for either.
- **System drawer:** global event/activity surface replacing the old status-bar role
- Pod selection opens the rig drawer with pod section expanded (no separate pod drawer)
-Human-readable IDs are UI-only: pod labels instead of ULIDs in explorer, short ULID tails for glanceability, full IDs in detail views
- Infrastructure/terminal nodes have distinct visual treatment
-Graph shows pod grouping via React Flow group nodes, status colors (green=ready, amber=launching, red=failed, gray=stopped), and lightweight hover/runtime hints
- Explicit center-workspace routes now exist for:
-`/specs`
-`/specs/rig`
-`/specs/agent`
-`/specs/library/$entryId`
-`/rigs/$rigId/nodes/$logicalId`
High-level shape:
-`AppShell` wraps every route with a desktop rail, route-aware `Explorer`,
center workspace, shared right drawer, preview stack, and topology overlay
provider.
- Primary destinations are Dashboard (`/`), Topology (`/topology`), For You
(`/for-you`), Project (`/project`), Library (`/specs`), and Settings
(`/settings`).
- Topology uses scoped routes for host, rig, pod, and seat views:
`/topology`, `/topology/rig/$rigId`, `/topology/pod/$rigId/$podName`, and
`/topology/seat/$rigId/$logicalId`.
-Project uses scoped routes for workspace, mission, and slice:
`/project`, `/project/mission/$missionId`, and `/project/slice/$sliceId`.
-Library keeps `/specs` as the route but presents the product label
`Library`, including skills at `/specs/skills/$skillToken` and
`/specs/skills/$skillToken/file/$fileToken`.
Current UI primitives:
- Vellum primitives live in `components/ui`: `VellumCard`, `VellumSheet`,
`RegistrationMarks`, `StatusPip`, empty states, tabs, tables, buttons, and
form controls.
- Graphics primitives live in `components/graphics` and `lib`: `RuntimeMark`,
`RuntimeBadge`, `ToolMark`, `ToolBadge`, `ActorMark`, `runtime-brand.ts`, and
`tool-brand.ts`.
- Project metadata primitives live in `components/project/ProjectMetaPrimitives`
and render event badges, queue state badges, tag pills, actor chips, date
chips, flow chips, proof thumbnails, and proof packet headers.
Current topology UI:
- Host topology is a hybrid React Flow graph with soft rig frames, pod frames,
-`022_node_codex_config_profile.ts` adds Codex config-profile field on nodes.
-`023_stream_items.ts` through `027_outbox_entries.ts` add the PL-004 Phase A coordination tables (see § Coordination Layer below).
-`028_project_classifications.ts` through `030_views_custom.ts` add the PL-004 Phase B classifier + view tables.
-`031_watchdog_jobs.ts` and `032_watchdog_history.ts` add the PL-004 Phase C watchdog supervision-tree tables. `watchdog_jobs` is the canonical scheduler state (policy, target_session, scan + active-wake intervals, actionable + last_actionable_at active-wake state, lifecycle); `watchdog_history` is the append-only audit of meaningful evaluations only (loud `sent`/`skipped`/`terminal`). Quiet skip reasons (`not_due`, `no_actionable_artifacts`, `no_missing_edge_artifacts`, `active_wake_not_due`) are NOT recorded and do NOT emit `watchdog.*` events — POC parity so agents are not woken about scheduler polls. Scan cadence (`scan_interval_seconds`) is enforced by the scheduler; re-delivery cadence (`active_wake_interval_seconds`) is enforced by the policy engine and only suppresses repeated wakes during a continuously-actionable window. Phase D extends the policy enum to four values (Phase C's three plus `workflow-keepalive`); see § Workflow Runtime below.
-`033_workflow_specs.ts`, `034_workflow_instances.ts`, `035_workflow_step_trails.ts` add the PL-004 Phase D Workflow Runtime tables. Migration `036_watchdog_policy_enum_extension.ts` is a documenting no-op that records the Phase D enum extension (Phase C uses application-layer enforcement via `PHASE_D_POLICIES` array, so no DDL is needed).
-`037_mission_control_actions.ts` adds the PL-005 Phase A Mission Control action audit table. Append-only at the API surface; records every operator action through Mission Control with before/after qitem snapshots for forensic reconstruction (see § Mission Control / Queue Observability below).
### Mission Control / Queue Observability (PL-005 Phase A)
Daemon-backed Mission Control surface. Per PRD acceptance criteria: seven views, seven verbs, first-class human seats, recent-ships=10, and a daemon-backed action audit table; no old-dashboard migration/cutover.
**Seven views** (`my-queue`, `human-gate`, `fleet`, `active-work`, `recent-ships`, `recently-active`, `recent-observations`) all return rows in the load-bearing 9-field phone-friendly content model (rig/mission name, current phase, active|idle|attention|blocked|degraded, next-action, pending-human-decision, read-cost, last-update timestamp, confidence/freshness, evidence link). The model is non-negotiable across all 7 views; UI may render compact, JSON preserves all 9.
`MissionControlReadLayer` maps each view to its source-of-truth path:
-`my-queue` / `human-gate` / `active-work` / `recent-ships` query PL-004 Phase A `queue_items` via `QueueRepository`.
-`fleet` consumes the per-rig CLI capability cache + queue summary.
-`recently-active` delegates to PL-004 Phase B `ViewProjector.show("recently-active")`.
-`recent-observations` reads PL-004 Phase A `stream_items` via `StreamStore`.
Filesystem fallbacks (`~/.openrig/stream/<date>.jsonl`, raw queue file grep) are graceful-degradation aids, NOT the primary path.
**Seven verbs** (`approve`, `deny`, `route`, `annotate`, `hold`, `drop`, `handoff`) execute through the load-bearing `MissionControlWriteContract`. Each verb is one atomic daemon transaction: queue mutation via `QueueRepository.updateWithinTransaction()` (which preserves Phase A hot-potato closure validation) + audit row in `mission_control_actions` + persisted `mission_control.action_executed` event, all in one `db.transaction`. The 4-step `handoff` shape (source-update + destination-create + opt-in best-effort notify + audit-record append) is verified atomic; notify failure does NOT roll back durable mutations (PRD invariant). Failure-injection rolls back source closure + audit row + new qitem together.
**Cross-CLI-version drift** is handled per the 4 sub-clauses of PRD § Runtime/Source Drift Acceptance:
2. Per-rig capability honesty (each rig surfaces its own capabilities; mixed fleet rendered honestly).
3. Once-per-session-per-rig logging (`MissionControlFleetCliCapability` deduplicates drift events by `(rig, field)` until daemon restart).
4. Fleet-level "rigs running stale CLI" indicator (`FleetView` surfaces `staleCliCount` from `/api/mission-control/cli-capabilities`).
**Human seats are first-class product concepts**: the operator's default `human-operator@kernel` seat is rendered via `HumanSeatCard` showing identity, pending load, blocked count, and verb capabilities. NOT invisible config-layer convention.
**Mission Control is integrated product UI inside the existing shell** — a new top-level route `/mission-control` registered in `packages/ui/src/routes.tsx` mounting `MissionControlSurface`, NOT a new managed app. An earlier prototype that informed Mission Control's view/verb vocabulary was retired in PL-005 Phase B; git history preserves it as evidence.
PL-005 Phase B extends the daemon Mission Control surface with: a tailnet-bind bearer-token middleware (`packages/daemon/src/middleware/auth-bearer-token.ts` — constant-time comparison via `crypto.timingSafeEqual`; daemon refuses non-loopback bind when the bearer is empty; mounted on `POST /api/mission-control/action` and `POST /api/mission-control/notifications/test`); a notification dispatcher with two adapters (`notification-adapter-ntfy.ts` default + `notification-adapter-webhook.ts` alternate; selected via `OPENRIG_NOTIFICATIONS_MECHANISM` env; ntfy.sh recommended for the operator's phone via tailnet); a read-only audit-history browse layer (`audit-browse.ts`) over `mission_control_actions` exposed at `GET /api/mission-control/audit` with filters (`qitem_id`, `action_verb`, `actor_session`, `since`, `until`) and `(limit, before_id)` pagination cursored on SQLite `rowid` (deterministic insertion order; ULID action_id same-ms tail is non-deterministic). Two new RigEvent types (`mission_control.notification_sent`, `mission_control.notification_failed`) cover the notification audit trail. Phase B adds NO migrations — the audit table from Phase A migration `037_mission_control_actions` is the only data source. UI adds an `audit-history` tab to `MissionControlSurface` with filter UI + paginated result table; the existing 7 Phase A views and 7 verbs are unmodified.
PL-005 Phase A extends `RigEvent` with 3 new types: `mission_control.action_executed`, `mission_control.cli_drift_detected`, `mission_control.view_refreshed`. Existing 32 PL-004 events untouched. Localhost-only auth at v0 (matches existing dashboard posture; remote-bind is a separate slice).
### Workflow Runtime (PL-004 Phase D)
Daemon-native Workflow Runtime — declarative workflow specs, live instance state, append-only step trails, and the load-bearing transactional-scribe contract. The PRD § L4 operating model is owner-as-author semantically plus workflow-as-transactional-scribe mechanically.
`workflow_specs` is a read-through cache of human-authored markdown/YAML spec files. Sources are workspace-surface; the daemon caches by `(name, version)` with a content `source_hash` so that valid operator edits to the spec file win at next read (workspace-surface reconciliation). Spec authoring stays markdown-authoritative; `workflow_specs` exists for fast lookup and runtime resolution.
`workflow_instances` carries live state per running workflow: `status` (`active|waiting|completed|failed`), `current_frontier_json` (active qitem ids), `hop_count` (loop-guard counter), and `last_continuation_decision_json` (most recent close/continue decision). Instances survive daemon restart from SQLite — no filesystem reconciliation required.
`workflow_step_trails` is the append-only history of meaningful step transitions. Every closure produces one trail row pairing the prior qitem with the next qitem (null on terminal). Append-only at the API surface: `WorkflowStepTrailLog.record()` is the only writer.
The transactional-scribe contract — the load-bearing Phase D guarantee — is implemented in `WorkflowProjector.project()`. A single `db.transaction` holds: closing the current packet (state mutation on `queue_items`), creating the next-step packet (`QueueRepository.createWithinTransaction()`), recording the trail entry, updating the instance frontier + status, and persisting workflow events. Either everything commits or everything rolls back; lost handoffs are impossible by design. Post-commit, subscribers are notified and the next owner is nudged.
`workflow-keepalive` (the Phase C-deferred watchdog policy) reads `workflow_instances` directly via SQLite — never the markdown source. Eligible only when `status` is `active|waiting`; resolves frontier qitem owners by querying `queue_items`; combines with explicit observer/created_by targets; sends to the first resolved target.
Phase D extends `RigEvent` with: `workflow.instantiated`, `workflow.step_closed`, `workflow.next_qitem_projected`, `workflow.completed`, `workflow.failed`, `workflow.routing_table_changed`. Existing 20 PL-004 events are unchanged.
Phase D scope intentionally excludes multi-hop chaining (graduation per PRD § Risks "Workflow runtime over-engineered"), gate-return-sweep, and the closure-enforcement path (the daemon's transactional state remains the closure authority via Phase A's hot-potato strict-rejection; workflow runtime PROJECTS on closure, does not GATE closure). Phase E (progress-rail) remains markdown-authoritative for v1.
### Coordination tables (PL-004 Phase A)
Five host-scoped tables back the coordination primitive:
- Owns cross-pod `edges[]`, rig-level startup overlays, and `cultureFile`.
**RigServicesSpec**
- Optional `services` block on a pod-aware `RigSpec`.
- Current shipped service kind is Compose-backed env management with:
-`composeFile`
-`projectName?`
-`profiles?`
-`downPolicy?`
-`waitFor?`
-`surfaces?`
-`checkpoints?`
**RigSpecPod**
- Pod-local bounded context with `members[]`, pod-local `edges[]`, pod startup, and optional continuity policy.
@@ -329,13 +508,21 @@ Migration boundary:
- Pre-launch vs interactive delivery split: `guidance_merge`/`skill_install` happen before harness boot (filesystem); `send_text` happens after harness is ready (TUI).
- Persists replay context including resume token for future restores.
### Operator-layer types (North Star + Post-North-Star)
**EnvReceipt**
- RigEnv capture result persisted for service-backed rigs.
- Carries service status/health plus evaluated wait targets and capture timestamp.
**RigServicesRecord**
- Persisted database representation of a rig-scoped services substrate.
-`rig-repository.ts`: now persists `rig_services` rows and surfaces `hasServices` in rig summaries for runtime/UI gating
-`spec-library-service.ts`: classifies builtin/user specs and marks service-backed rigs with `hasServices`
-`spec-review-service.ts`: lifts services metadata into review models; route layer enriches with compose preview
### Runtime adapters
Three runtime adapters implement the five-method contract: projection, startup delivery, harness launch, readiness, and installed-resource listing.
@@ -429,18 +625,20 @@ Three runtime adapters implement the five-method contract: projection, startup d
- launches via `codex`, resumes via `codex resume <threadId>`
- readiness probe: polls for Codex ready indicator
**TerminalAdapter** (North Star)
**TerminalAdapter**
- no-op project/deliver/launch (shell IS the harness)
- immediate readiness (shell is ready as soon as tmux session exists)
- for infrastructure nodes: servers, log tails, build watchers
### Node inventory and operator surfaces (North Star)
### Node inventory and operator surfaces
-`node-inventory.ts`: universal node-level projection. The single source of truth for node state consumed by CLI (`ps --nodes`), UI (explorer + graph + drawer), and MCP (`rigged_rig_nodes`). Core + extended field tiers.
-`demo-rig-selector.ts`: existing-rig power-on helper — finds the right rig by name from ps summary
-`whoami-service.ts`: daemon-backed runtime identity projection used by `rig whoami` and adopted-session parity flows
-`context-usage-store.ts`: SQLite + state-dir persistence for per-node context usage
-`context-monitor.ts`: daemon-owned monitor that records context usage for runtime/operator surfaces
-`whoami-service.ts`: daemon-backed runtime identity projection used by `rig whoami` and adopted-session parity flows, now with context-usage access
### Transport and communication (Post-North-Star)
### Transport and communication
-`session-transport.ts`: communication primitives — send/capture/broadcast with session resolution (canonical + legacy names), mid-work detection, honest error reporting, pod/rig/global targeting
-`transcript-store.ts`: pipe-pane transcript management — ANSI stripping on read, boundary markers, readTail, grep. Filesystem-backed, not SQLite.
@@ -448,29 +646,40 @@ Three runtime adapters implement the five-method contract: projection, startup d
-`ask-service.ts`: context engineering evidence pack — gathers rig summary plus transcript excerpts, chat excerpts, insufficiency state, and guidance. Does NOT call an external LLM.
Six host-scoped services implementing the SQLite-canonical coordination layer. POC `rigx queue` / `rigx stream` filesystem path remains untouched; daemon `rig queue` / `rig stream` writes only to SQLite. Routes import these; services are Hono-free.
-`stream-store.ts`: L1 stream — idempotent emit (on `stream_item_id`), chronological list with cursor pagination, archive (soft).
-`queue-repository.ts`: L3 queue — create, claim/unclaim, update (general state mutator with hot-potato strict-rejection on `done`), transactional handoff (close source as `handed-off` plus create new owned qitem, single transaction), pod-fallback rerouting, overdue lookup, nudge/heartbeat tracking. Cross-rig validation hook exposed as `validateRig` constructor option.
-`queue-transition-log.ts`: append-only state transition log. Used by `queue-repository.ts` and exposed read-only on `queue_repository.transitionLog`.
-`hot-potato-enforcer.ts`: pure validator for the load-bearing API contract — `state=done` requires `closure_reason ∈ {handed_off_to, blocked_on, denied, canceled, no-follow-on, escalation}`; `handed_off_to | blocked_on | escalation` additionally require `closure_target`. Tier→SLA mapping for `closure_required_at` lives here.
-`inbox-handler.ts`: mailbox handler — authenticated drop (idempotent on `inbox_id`), absorb (promotes pending entry to a queue_item, idempotent), deny (records reason). Auth check is a pluggable constructor hook.
-`outbox-handler.ts`: sender-side outbox; idempotent record, mark delivered/failed, list. No event-bus events emitted (pure audit).
### Resume honesty
-`native-resume-probe.ts`: honest assessment of whether a harness actually resumed vs fresh-launched — probes pane content for runtime-specific indicators
-`resume-metadata-refresher.ts`: post-launch resume token capture — reads Claude conversation IDs from `.claude/` state, Codex thread IDs from SQLite state files
-`codex-thread-id.ts`: Codex-specific thread ID extraction from the Codex SQLite database
### Discovery extensions (North Star)
### Discovery extensions
-`draft-rig-generator.ts`: synthesize a candidate RigSpec YAML from discovered sessions — groups by CWD, suggests pods, sanitizes IDs
-`claim-service.ts`: adopted-session integration service — claim/bind/create-and-bind flows, tmux metadata writes, and post-claim onboarding hint
### Final Stretch 1 authoring and library surfaces
### Authoring, library, and identity surfaces
-`spec-library-service.ts`: filesystem-backed library index over builtin/user roots, classified via structured review
-`spec-review-service.ts`: canonical review model for raw YAML, library entries, and reusable display components
-`spec-library-service.ts`: filesystem-backed library index over builtin/user roots, classified via structured review and `hasServices`
-`spec-review-service.ts`: canonical review model for raw YAML, library entries, reusable display components, and managed-app env details
-`whoami-service.ts`: daemon-side identity surface with peers/edges/transcript helpers
### Snapshot, restore, and continuity
-`checkpoint-store.ts`: checkpoint persistence with pod/continuity context
-`snapshot-capture.ts`: captures pods, continuity state, and startup replay context
6. Source of truth: daemon-backed SQLite (`chat_messages` table), not tmux scrollback
### Config + preflight flow (Post-North-Star)
### Config and preflight flow
-`rig config` reads/writes `~/.openrig/config.json` with legacy fallback from `~/.rigged/config.json`. 5 locked keys: `daemon.port`, `daemon.host`, `db.path`, `transcripts.enabled`, `transcripts.path`
-`rig config` reads/writes `~/.openrig/config.json` with legacy fallback from `~/.rigged/config.json`. The original runtime env keys retain deprecated `RIGGED_*` aliases for upgrade compatibility: `daemon.port`, `daemon.host`, `db.path`, `transcripts.enabled`, `transcripts.path`. New typed config keys use `OPENRIG_*` only.
- Precedence: CLI flag > env var > config file > default
-`rig preflight` checks: Node.js ≥ 20, tmux available, writable home/db/transcript dirs, port available
- Auto-preflight runs on `rig up` and daemon start
- Every preflight error: what failed + why it matters + what to do (3-part pattern)
### Discovery-to-draft-rig flow (North Star)
### Discovery-to-draft-rig flow
`rig discover --draft` → scan tmux sessions → group by CWD → suggest pod structure → generate candidate RigSpec YAML → output to stdout or file
### Spec review and spec library flow (Final Stretch 1)
### Spec review and spec library flow
1. Raw YAML preview:
- UI/CLI posts YAML to `/api/specs/review/rig` or `/api/specs/review/agent`
@@ -606,12 +815,18 @@ Restore now:
2. Filesystem-backed library:
-`SpecLibraryService` scans builtin + user roots (`packages/daemon/specs`, `~/.openrig/specs`) with legacy fallback from `~/.rigged/specs`
- each YAML file is classified via structured review
- service-backed rigs are marked `hasServices`
-`/api/specs/library` serves list/get/review/sync
3. CLI:
-`rig specs ls/show/preview/add/sync`
-`rig specs add` installs either a single YAML spec or a full spec directory containing `rig.yaml` or `agent.yaml`
-`rig up` / `rig bootstrap` resolve library names before falling back to other source kinds
4. UI:
-`Specs` drawer presents a unified library list with `All / Apps / Rigs / Agents`
1. Rig instantiation persists a `rig_services` record when a pod-aware `RigSpec` has a `services` block.
2.`ServiceOrchestrator.boot(...)` runs before agent launch/restore:
- Compose `up`
- evaluate `waitFor`
- persist `EnvReceipt`
- fail honestly on timeout or unhealthy waits
3.`GET /api/rigs/:rigId/env` returns live status:
-`hasServices: false` for non-service rigs
- otherwise kind, compose identity, receipt, and surfaces
4.`rig env status/logs/down` and the rig drawer `Env` tab are thin client surfaces over those routes.
5. Snapshot/restore preserves the latest env receipt and reboots services before agent restore.
6.**Current runtime truth model:** Runtime env truth is provided through boot-time gating (services must be healthy before agents launch) and on-demand inspection via `rig env` / the `Env` tab, which polls with honest `probeStatus` (fresh/stale/no_orchestrator). Background health monitoring and `rig ps` env health summary are deferred.
### Agent-managed app flow
1. A managed app is a service-backed rig plus a specialist agent.
2. The canonical shipped example is:
- rig: `secrets-manager`
- pod: `vault`
- member: `specialist`
- session: `vault-specialist@secrets-manager`
3. Specs/UI flow:
- browse `secrets-manager` under `Apps`
- open library review
- inspect specialist identity, env stack, health gates, and surfaces
- copy the setup prompt
4. Runtime flow:
- launch the rig
- services boot before `vault.specialist`
- the rig drawer `Env` tab exposes state, health, surfaces, logs, and teardown
### Specialist interaction flow
1. Other humans or agents address a specialist by session name, e.g. `vault-specialist@secrets-manager`.
2. Cross-rig communication uses the existing transport seam:
Two SSE surfaces stream coordination events: `/api/stream/watch` for new stream items, `/api/queue/watch` for queue/inbox events.
The event log remains append-only and SQLite-backed. The global SSE stream (`/api/events`) delivers all events; the chat SSE stream (`/api/rigs/:rigId/chat/watch`) delivers chat messages for one rig.
---
@@ -735,7 +999,7 @@ The event log remains append-only and SQLite-backed. The global SSE stream (`/ap
`createDaemon()` now does the following:
1. Open SQLite and run all 17 migrations.
1. Open SQLite and run all 27 migrations (22 existing plus the 5 PL-004 Phase A coordination tables).
2. Construct core repositories and legacy services.
5. Construct North Star + Post-North-Star services:
5. Construct rig environment services:
-`ComposeServicesAdapter`
-`ServiceOrchestrator`
6. Construct operator, transport, and history services:
-`TranscriptStore` (with config from env for transcript path/enabled)
-`SessionTransport`
-`ChatRepository`
-`AskService` (with `HistoryQuery`)
-`ResumeMetadataRefresher`
-`NodeInventory`
6. Construct Final Stretch 1 services:
-`ResumeMetadataRefresher`
-`ContextUsageStore`
-`ContextMonitor`
-`NodeInventory`
7. Construct authoring, identity, and managed-app services:
-`SpecReviewService`
-`SpecLibraryService` (builtin + user roots)
-`WhoamiService`
7. Construct `BootstrapOrchestrator` with both legacy and rebooted seams.
8. Build `AppDeps`, enforce shared-DB invariants in `createApp()`, and mount the full route tree including specs review/library and `whoami`.
8. Construct `BootstrapOrchestrator` with both legacy and rebooted seams.
9. Construct PL-004 Phase A coordination services from a shared `QueueRepository` instance (so `InboxHandler.absorb()` and `/api/queue` write to the same repo): `StreamStore`, `QueueRepository`, `InboxHandler`, `OutboxHandler`.
10. Build `AppDeps`, enforce shared-DB invariants in `createApp()`, and mount the full route tree including env, specs review/library, `whoami`, plus the PL-004 coordination routes (`/api/stream`, `/api/queue`).
---
@@ -763,25 +1033,18 @@ The event log remains append-only and SQLite-backed. The global SSE stream (`/ap
### Test footprint
- daemon: 112 Vitest files / 1,461 tests
- CLI: 29 Vitest files / 275 tests
- UI: 35 Vitest files / 364 tests
- total: 176 files / 2,100 tests
- daemon: 127 Vitest files / 1,668 tests
- CLI: 37 Vitest files / 366 tests
- UI: 37 Vitest files / 388 tests
- total: 201 files / 2,422 tests
### Verified during this doc refresh
- daemon: `1461/1461` passing
- CLI: `275/275` passing
- UI: `361/364` passing
- daemon: `1668/1668` passing
- CLI: `366/366` passing
- UI: `388/388` passing
Current known UI failures during this refresh:
-`design-compliance.test.tsx`
- non-zero border-radius count still above zero
- stale `StatusBar.tsx` file assumption in the mono/data-display assertion
-`tailwind-foundation.test.tsx`
- missing `.bg-card` assertion in generated CSS
### Post-North-Star test suites
### Communication, identity, and authoring suites
In addition to reboot-era coverage:
@@ -804,9 +1067,23 @@ In addition to reboot-era coverage:
- Adoption: materialize + bind/adopt flows from discovery/specs workflows
- MCP: all 17 tools verified
Rig environment and agent-managed software flows additionally verified:
- Managed-app browse flow in the UI:
-`Specs` → `Apps` → `secrets-manager`
- managed-app review with `vault.specialist`
-`secrets-manager` launch as a real service-backed rig with a specialist agent
- Real Vault operations through `vault-specialist@secrets-manager`
- health check
- write secret
- read secret back
- Cross-rig direct messaging to `vault-specialist@secrets-manager`
- Runtime `Env` tab:
- overall state
- health gates
- surfaces
- logs
- explicit teardown
- Failure honesty when Vault is stopped mid-rig
---
## 11. Remaining Compatibility Notes
@@ -834,6 +1130,9 @@ These are the main intentional limits that still describe the shipped system:
6. Chat is rig-scoped only — no cross-rig channels or DMs.
7.`--verify` on `rig send` checks pane content for message visibility but can produce false positives from pre-existing matching content. Known limitation.
8. Terminal node readiness is shell-ready only — no service health probes.
9.`rig env down --volumes` exists in the CLI surface, but the explicit daemon-side override is not fully plumbed through yet.
10. Managed-app service surfaces are descriptive only — OpenRig does not automatically inject service URLs/tokens into agent prompts beyond authored startup/context files.
11. Specialist delegation is conventional, not automatic — other agents/humans address the specialist by session name or other normal communication surfaces.
---
@@ -842,4 +1141,4 @@ These are the main intentional limits that still describe the shipped system:
This document is the architecture-level source of truth.
For file-by-file structure across daemon, CLI, and UI, use:
-`workspace.root` (and other workspace-rooted paths used by `init-workspace`)
Precedence:
- CLI flag
@@ -110,6 +133,13 @@ Precedence:
- config file
- default
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.
Legacy env compatibility: the original runtime keys still accept deprecated
`RIGGED_*` aliases. New typed config keys use `OPENRIG_*` only.
### `rig preflight`
Usage: `rig preflight [--json]`
@@ -124,7 +154,24 @@ Usage: `rig doctor [--json]`
Notes:
- Verifies install health for packaged/local CLI usage.
- Checks daemon dist, UI dist, Node version, tmux availability, writable state paths, and daemon port availability.
- Checks daemon dist, UI dist, Node version, `tmux`, optional `cmux` control health, writable state paths, and daemon port availability.
- On macOS, also warns when tmux mouse mode appears disabled, gives the current-server fix (`tmux set -g mouse on`), and points to the persistent fix in `~/.tmux.conf`.
-`cmux` issues are warnings, not hard failures. OpenRig still works without `cmux`; only `Open CMUX` workflows are unavailable.
-`--json` is suitable for agent use and only exits non-zero on real failures, not warnings.
- This is the destructive recovery surface for polluted local OpenRig state.
-`--state` stops the daemon, clears the active OpenRig listener on the configured port if needed, rotates or deletes the effective state root, and recreates an empty state root.
-`--all` includes `--state` plus managed tmux session cleanup for sessions that are discoverable from the current OpenRig database.
-`--backup` moves the state root aside to a collision-safe timestamped path such as `~/.openrig.backup-YYYYMMDD-HHMMSS`.
- Managed tmux cleanup is intentionally conservative. It only removes sessions that are present in current DB state; unrelated tmux sessions are left alone.
- Human output prints a compact destroy plan followed by the destroy result.
-`rig requirements` is the spec/app-specific dependency surface.
- Use `rig doctor` for host-level install health, then `rig requirements <spec>` for rig-specific requirements.
### `rig up`
Usage: `rig up <source> [--plan] [--yes] [--target <root>] [--json]`
@@ -187,6 +241,7 @@ Current behavior notes:
-`--target <root>` is only for bundle/package installation. It does not override agent working directories.
-`local:``agent_ref` values resolve relative to the rig spec directory, not the caller shell cwd.
- If you copy a built-in spec to a new directory, keep its `agents/` tree beside it or rewrite those refs to `path:/absolute/path`.
- Managed apps are first-class `up` targets. `rig up secrets-manager` launches the shipped Vault example from the library.
Success modes:
- fresh boot
@@ -205,19 +260,42 @@ Flags:
Notes:
- When `--snapshot` succeeds, human output includes the restore command.
- If the rig name is uniquely reusable, the handoff prefers `rig up <rigName>`.
- v0.2.0 known issue: `rig down <name> --delete` can return HTTP 404; use
`rig ps` to find the rig ID and run `rig down <rigId> --delete`.
### `rig env`
Usage:
-`rig env status <rig> [--json]`
-`rig env logs <rig> [service] [--tail <n>]`
-`rig env down <rig> [--volumes]`
Notes:
- This surface is only meaningful for service-backed rigs and managed apps.
-`status` resolves rig names or IDs and returns the env receipt with an honest freshness probe. The response includes `probeStatus` (fresh/stale/no_orchestrator) so operators can distinguish current truth from cached state.
-`logs` proxies compose-backed service logs; `[service]` is optional.
-`down` tears down the rig environment. `--volumes` overrides the stored down policy to force volume removal via `docker compose down --volumes`.
- Note: `rig ps` does not yet surface env health. Runtime env truth is available through `rig env status` and the rig drawer `Env` tab.
-`rig ps --nodes` expands into a cross-rig node inventory including session name, runtime, startup status, restore outcome, attach command, resume command, and latest error.
-Exit codes are defined in help:
-`rig ps` lists rig summaries. Default human columns: `RIG`, `NODES`, `RUNNING`, `STATUS`, `LIFECYCLE`, `UPTIME`, `SNAPSHOT`. The `LIFECYCLE` column shows the rig-level fold of per-node lifecycle states with codes `run`/`rec`/`stp`/`deg`/`att`.
-`rig ps --nodes` expands into a cross-rig node inventory. Default human columns include `STATUS`, `STARTUP`, `LIFECYCLE`, `ACTIVITY`, `RESTORE`, `ERROR` so startup-time and live runtime state can be compared side-by-side without composing a separate diagnostic command.
-JSON output for both rig and node tiers includes a `rigName` alias (equal to `name`) for forward compatibility; agent code should prefer `rigName`. Default `--json` is a bare array (back-compat); the envelope shape `{entries, totalRigs|totalNodes, truncated, hint?}` is only used when `--limit`, `--fields`, `--summary`, or `--filter` is set.
- Default human output is bounded for context-window safety: rigs truncate at 50 with a footer naming the total + `--full` opt-out, nodes truncate at 100 with the same shape. `--full` disables truncation. `--limit <n>` sets an explicit bound.
-`--summary` emits aggregate counts only (`byStatus`, `byLifecycle` for rigs; `bySessionStatus`, `byLifecycle` for nodes); useful for quick fleet checks without per-entry detail. Cross-facet disagreement (e.g. a `running` rig with `attention_required` nodes) is not directly visible in summary mode — narrow with `--filter lifecycleState=attention_required` instead.
-`--fields <list>` projects JSON output to a comma-separated allow-list of top-level fields. Unknown keys are rejected before any HTTP call with an error naming the unknown key(s) and the sorted supported list. Exit code on rejection is `1`. Accepted (rig-level): `rigId`, `name`, `rigName`, `nodeCount`, `runningCount`, `status`, `lifecycleState`, `uptime`, `latestSnapshot`. Accepted (node-level, with `--nodes`): `rigId`, `rigName`, `logicalId`, `podId`, `podNamespace`, `canonicalSessionName`, `nodeKind`, `runtime`, `sessionStatus`, `startupStatus`, `restoreOutcome`, `lifecycleState`, `tmuxAttachCommand`, `resumeCommand`, `latestError`, `agentActivity`. `name` is rig-level only; for node entries use `rigName` (the rejection error includes a hint). Nested fields (e.g. `agentActivity.state`) are not drilled; pass the whole object name (e.g. `agentActivity`) and read the nested value downstream.
-`--filter <key=value>` accepts `status`, `lifecycleState`, `name-prefix`, `name`, and `agentActivity.state` (PL-019; node-level — use with `--nodes`). Unknown keys are rejected before any HTTP call with a clear error naming the supported list. For `agentActivity.state`, allowed values are `running`, `needs_input`, `idle`, `unknown`; invalid values fail fast with a three-part error (what failed / what's allowed / what to do).
-`--active` (PL-019; node-level) is sugar for `--filter agentActivity.state=running`. Combining `--active` with `--filter` is rejected — pick one explicit form. Output is identical to the explicit-filter form on the same fixture.
-`--host <id>` routes the same command to a remote host declared in `~/.openrig/hosts.yaml` via single-hop ssh (CLI-side shell-out; daemon untouched). Forwards every shaping flag (`--nodes`, `--full`, `--limit`, `--fields`, `--summary`, `--filter`, `--json`) to the remote `rig ps`. The remote rig's output is verbatim passthrough on success; failure is distinguished into `ssh-unreachable` / `permission-gate` / `remote-daemon-unreachable` / `remote-command-failed` per the closed cross-host execution contract.
- Exit codes:
-`0` success
-`1` daemon not running
-`1` daemon not running, or invalid `--filter` / `--limit`
-`2` daemon fetch failure
### `rig snapshot`
@@ -240,6 +318,29 @@ Notes:
- Human output prints each restored node and any failed node error.
- Member YAML may carry `session_source` (see "Session source declaration" below) to start the new seat from a prior native conversation (`mode: fork`) or from operator-declared artifacts (`mode: rebuild`).
### Session source declaration (`session_source`)
Member YAML in a rig spec or `rig expand` payload may declare a launch-time `session_source` to control how the new managed seat derives its starting context. Two modes are supported in v1:
```yaml
# Fork from a prior native runtime conversation. Captures and persists a NEW
# post-fork token; the parent token is NEVER persisted onto the new seat.
members:
- id:reviewer-2
runtime:claude-code # or "codex"; not valid on terminal
session_source:
mode:fork
ref:
kind:native_id # v1 fork mode supports "native_id" only
value:"0b0165d7-cb4d-4650-90de-15c0a1ede9e6"
```
```yaml
# Rebuild from operator-declared artifacts (CULTURE, role doc, handover packet,
# queue files, session logs). Fresh-launches the harness and seeds the running
# TUI with the artifacts in the operator-declared trust-precedence order.
# The seat's continuityOutcome is `rebuilt` (NEVER `fresh`/`resumed`/`forked`)
# and NO `resumeToken` is persisted.
members:
- id:writer-2
runtime:claude-code # or "codex"; not valid on terminal
session_source:
mode:rebuild
ref:
kind:artifact_set # v1 rebuild mode supports "artifact_set" only
value:# ordered list, highest-trust first
- <substrate-shared-docs>/rigs/<rig>/CULTURE.md
- <substrate-shared-docs>/specs/agents/<role>.md
- /path/to/handover-packet.md
- /path/to/state/<pod>/<member>.queue.md
- /path/to/state/<pod>/shared.session.log
- /path/to/state/<pod>/<member>.session.log
```
Notes:
-`terminal` runtime rejects `session_source` (no native fork primitive; no agent context to rebuild).
-`mode: fork` requires `ref.kind: native_id` and a non-empty `ref.value` string. Other ref kinds (`artifact_path`, `name`, `last`) are reserved shapes for follow-up slices and are refused in v1 fork mode.
-`mode: rebuild` requires `ref.kind: artifact_set` and a non-empty `ref.value` array of paths. Missing paths are recorded as gaps and the launch proceeds with what resolved; if NO declared paths resolve, the launch fails with a clear error.
- The two modes are mutually exclusive on a given member; mixing is a schema error.
- If the daemon is unreachable but an identity source can still be resolved, `--json` returns a partial result instead of crashing.
- Human-readable output includes transcript location and context usage when available.
- In Claude Code projects, unattended `rig whoami` on boot may require the local permissions allow list to include `Bash(rig:*)`.
-`--host <id>` routes the same command to a remote host declared in `~/.openrig/hosts.yaml` via single-hop ssh (CLI-side shell-out; daemon untouched). Identity resolution happens on the REMOTE rig (each host has its own daemon + tmux + identity context); local `--node-id`/`--session` flags are forwarded to the remote `rig whoami` invocation. The remote rig's output is verbatim passthrough on success; failure is distinguished into the same `ssh-unreachable` / `permission-gate` / `remote-daemon-unreachable` / `remote-command-failed` enum as `rig ps --host` and `rig send --host`.
-`--host <id>` routes the same command to a remote host declared in `~/.openrig/hosts.yaml` via single-hop ssh; see "Cross-host execution" below. SSH success is NOT verify success — the remote rig's `Verified: yes/no` is what counts and is surfaced verbatim.
-`--host <id>` routes the same command to a remote host declared in `~/.openrig/hosts.yaml` via single-hop ssh; see "Cross-host execution" below.
### Cross-host execution (`--host <id>`)
`rig send` and `rig capture` accept a `--host <id>` flag that routes the
command to a remote host over single-hop SSH. v0 is CLI-side shell-out
only — the local daemon is not involved in routing, and there is no
daemon-to-daemon networking. The remote host is expected to have its own
managed `rig` available on `$PATH`.
Hosts are declared by the operator in `~/.openrig/hosts.yaml`:
```yaml
hosts:
- id:vm-claude-test
transport:ssh
target:vm-claude-test.local
user:wrandom # optional
notes:"Tart VM"# optional
- id:laptop-b
transport:ssh
target:laptop-b.tail-scale-net
user:wrandom
```
Validation rules:
-`hosts` is required and must be a non-null array.
- Each entry: `id` required (non-empty, unique), `transport` required (v0 supports `ssh` only), `target` required (non-empty — DNS name, SSH config alias, or IP).
-`user` and `notes` are optional.
- The file is operator-managed; v0 does NOT include any sub-command to add, remove, or list hosts (operators edit the YAML directly).
- A missing or invalid file returns a clear error pointing at the canonical path.
The CLI distinguishes four structured failure modes (operators get an
actionable error per mode; JSON output preserves the `failedStep` enum):
-`ssh-unreachable` — SSH itself failed (connection refused, host key mismatch, etc.). Verify SSH access and the registry entry.
-`permission-gate` — SSH hit an auth/permission gate (Permission denied, Keychain). The error includes a hint to the keychain-over-SSH field note.
-`remote-daemon-unreachable` — SSH succeeded but the remote `rig` reported the remote daemon was not reachable. Start it with `ssh <target> rig daemon start`.
-`remote-command-failed` — SSH succeeded but the remote `rig` exited non-zero for some other reason; the remote stderr is surfaced.
Out of scope for v0: non-SSH transports; `--host` on `rig ps` / `rig whoami` / other commands; connection pooling; multi-hop SSH; cross-host queue routing; cross-host seat handover.
-`clear` is destructive and rig-scoped. Removes all messages for that rig.
-`watch --tmux` starts a dedicated tmux watcher session.
## Coordination Primitive (PL-004 Phase A)
Two top-level commands back the SQLite-canonical coordination layer. They speak only to the daemon HTTP API; they do NOT touch the POC `rigx-stream-proto` / `rigx-queue-proto` filesystem state. POC and daemon coexist at OPERATOR level only.
Hot-potato strict-rejection (load-bearing API contract):
-`update --state done` REQUIRES `--closure-reason` from one of: `handed_off_to`, `blocked_on`, `denied`, `canceled`, `no-follow-on`, `escalation`. Missing or invalid reason → exit 1 with structured error naming the 6 valid values.
-`closure-reason` of `handed_off_to`, `blocked_on`, or `escalation` additionally requires `--closure-target`.
- All hot-potato enforcement happens at the daemon domain layer; every surface (CLI, future MCP, future UI) inherits the same guarantee.
Closure-reason semantics:
-`handed_off_to` — work continues at a different seat (target = new owner). `handoff` subcommand is the preferred path; `update` accepts it for non-handoff terminal closures.
-`blocked_on` — parked pending another qitem (target = blocker qitem_id).
-`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-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.
-`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.
-`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.
- Views emit a `view.changed` SSE event on every state mutation that affects them. Queue update mutations bridge to `queue.updated` and then to `view.changed` for all six built-in views (Phase B R2).
- Custom view registration writes to the `views_custom` table; the daemon hot-reloads on registration.
## Coordination Watchdog (PL-004 Phase C)
`rig watchdog` registers, lists, inspects, and stops daemon-native scheduler jobs. The scheduler runs inside the daemon supervision tree and persists state in SQLite (`watchdog_jobs`/`watchdog_history`); jobs survive daemon restarts. Three policies are available at v1: `periodic-reminder`, `artifact-pool-ready`, and `edge-artifact-required`. The fourth POC policy `workflow-keepalive` is rejected with `policy_deferred_to_phase_d` and ships in Phase D.
### `rig watchdog`
-`register --spec <path> --policy <name> --target-session <s> --interval-seconds <n> --registered-by <s>` — register a job from a YAML spec; `--active-wake-interval-seconds` and `--scan-interval-seconds` are pool-ready-specific opt-ins.
-`list` — list all jobs (active + stopped + terminal).
-`show <job_id>` — show one job.
-`status <job_id>` — show job + recent evaluation history (last 20 entries).
History records only loud evaluations: `sent` (delivery executed) or `terminal` (policy declared the job done). Quiet skip reasons (`not_due`, `no_actionable_artifacts`, `no_missing_edge_artifacts`, `active_wake_not_due`) are NOT recorded — POC parity so agents are not woken about scheduler polls. Loud `skipped` rows are recorded only if a policy returns a non-quiet reason.
Phase D extends the policy enum with `workflow-keepalive` (the policy deferred from Phase C). It reads `workflow_instances` directly via SQLite, requires `status: active|waiting`, and resolves frontier qitem owners from `queue_items`.
## Workflow Runtime (PL-004 Phase D)
`rig workflow` operates on the daemon-native Workflow Runtime: declarative spec validation, instance creation, step projection (transactional-scribe), trace, and idempotent continue. Workflow specs are markdown/YAML files on disk (workspace-surface); the daemon caches them in SQLite for fast lookup.
-`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.
-`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.
-`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.
## Operational Inspection
Read-only inspection commands for context-window state, compaction planning, workflow heartbeat, and seat handover observability. Default mode is read-only across this section.
- Plans Claude compact-in-place candidates without compacting anything (read-only triage).
-`--threshold-tokens <n>` is the estimated used-token threshold; `--threshold-percent <0-100>` is the used-percent threshold when the context window size is missing.
- Output identifies seats that are candidates for compaction by current heuristics; the operator decides what (if anything) to compact.
- Shows workflow execution proof state from queue files.
- Default mode is read-only. `--nudge` sends informational proof instructions to stalled or unproven owners; it does not modify queue files or reroute work.
-`--include-done` includes done/handed-off queue items in the output (excluded by default).
### `rig seat`
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.
Notes:
-`status` reads the seat-handover observability tables (migration `021`); it does not mutate anything.
-`handover` plans the two-phase sequence; actual execution happens through the existing seat-launch surfaces under operator gating.
## Mission Control / Queue Observability (PL-005 Phase A)
Mission Control is an integrated product UI inside the existing shell, NOT a new `rig` command. Per PL-005 PRD line 301: "no new commands at v0; the `rig ps --nodes --json` surface is consumed read-only."
Mission Control is reached via the product UI at the `/mission-control` route. The HTTP API surface (`/api/mission-control/*`) is documented in `docs/as-built/architecture.md` § Mission Control / Queue Observability. The 7 verbs (`approve`, `deny`, `route`, `annotate`, `hold`, `drop`, `handoff`) execute via `POST /api/mission-control/action`; the 7 views are read via `GET /api/mission-control/views/:view-name`.
Mission Control consumes `rig ps --nodes --json` for fleet roll-up where the canonical CLI source is preferred. Cross-CLI-version drift is handled per the 4 sub-clauses of PRD § Runtime/Source Drift Acceptance: missing fields surface as honest "field unavailable on this rig's daemon version" placeholders; once-per-session-per-rig logging avoids spam; the fleet view shows a top-level "rigs running stale CLI" indicator.
## Agent Images, Context Packs, and Workspace (v0.3.0)
Three top-level commands shipped in v0.3.0 for operator-authored library content (agent images and context packs) and the workspace primitive.
-`create <source-session> [options]` — capture a productive seat's resumable state into a new agent image.
-`delete <name-or-id> [options]` — delete an agent image (subject to evidence-preservation guard).
-`pin <name-or-id> [options]` — pin an image so prune cannot delete it.
-`unpin <name-or-id> [options]` — unpin an image.
-`prune [options]` — delete evictable images (protected by evidence-preservation guard).
-`sync [options]` — re-walk discovery roots and refresh the library index.
Notes:
- Images are an operator-authored library form; deletion is gated by an evidence-preservation guard so productive seat snapshots are not lost accidentally.
-`pin` / `unpin` are the operator levers for explicit retention; `prune` honours them.
-`validate [root]` — 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`.
- UI live identity surfaces: `packages/ui/src/components/LiveIdentityDisplay.tsx`, `packages/ui/src/components/LiveNodeDetails.tsx`
- UI data layer: `packages/ui/src/hooks/*`
@@ -38,14 +41,15 @@ Fast lookup:
### Entrypoints And Wiring
-`packages/daemon/src/index.ts`: CLI entrypoint for starting the daemon HTTP server. Exports: startServer. Related: `packages/daemon/src/startup.js`.
-`packages/daemon/src/seed.ts`: Development/test seed entrypoint for populating sample daemon state. Exports: none. Related: `packages/daemon/src/db/connection.js`, `packages/daemon/src/db/migrate.js`, `packages/daemon/src/db/migrations/001_core_schema.js`, `packages/daemon/src/db/migrations/002_bindings_sessions.js`, +3 more.
-`packages/daemon/src/server.ts`: Defines `AppDeps`, enforces shared-DB invariants, and mounts the full public Hono route tree including dual-format rigspec import/export/materialize, bundles, transport/transcripts/ask/chat, specs review/library, and `whoami`. Exports: AppDeps, createApp. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, +36 more.
-`packages/daemon/src/startup.ts`: Constructs the daemon dependency graph, runs all 17 migrations, wires legacy, reboot, North Star, Post-North-Star, and Final Stretch 1 services together, and returns `{ app, db, deps }`. Exports: createDaemon. Related: `packages/daemon/src/adapters/claude-resume.js`, `packages/daemon/src/adapters/cmux-transport.js`, `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/codex-resume.js`, +58 more.
-`packages/daemon/src/server.ts`: Defines `AppDeps`, enforces shared-DB invariants, and mounts the full public Hono route tree including dual-format rigspec import/export/materialize, bundles, transport/transcripts/ask/chat, env status/logs/down, specs review/library, and `whoami`. Exports: AppDeps, createApp. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, +36 more.
-`packages/daemon/src/startup.ts`: Constructs the daemon dependency graph, runs all 36 migrations, wires legacy, reboot, North Star, Post-North-Star, Phase 5 RigEnv, context-usage, Final Stretch 3 managed-app services, and PL-004 coordination services together, and returns `{ app, db, deps }`. Exports: createDaemon. Related: `packages/daemon/src/adapters/claude-resume.js`, `packages/daemon/src/adapters/cmux-transport.js`, `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/codex-resume.js`, +58 more.
### Adapters
-`packages/daemon/src/adapters/claude-code-adapter.ts`: Adapter implementation for claude code integration. Exports: ClaudeAdapterFsOps, ClaudeCodeAdapter. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/projection-planner.js`, `packages/daemon/src/domain/runtime-adapter.js`.
-`packages/daemon/src/adapters/claude-resume.ts`: Resume adapter for claude sessions. Exports: ClaudeResumeAdapter, ResumeResult. Related: `packages/daemon/src/adapters/shell-quote.js`, `packages/daemon/src/adapters/tmux.js`.
-`packages/daemon/src/adapters/cmux-transport.ts`: Adapter/util module for cmux transport integration. Exports: createCmuxCliTransport. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`.
-`packages/daemon/src/db/migrations/017_pod_namespace.ts`: Database migration that persists authored pod namespace separately from pod DB id for export, adoption, and identity parity. Exports: podNamespaceSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/020_rig_services.ts`: Database migration for rig-scoped services records and persisted env receipts. Exports: rigServicesSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/028_project_classifications.ts`: PL-004 Phase B — L2 project classifications table (idempotent on stream_item_id; FK to stream_items). Exports: projectClassificationsSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/029_classifier_leases.ts`: PL-004 Phase B — single-writer classifier lease table (partial UNIQUE INDEX on state='active'). Exports: classifierLeasesSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/031_watchdog_jobs.ts`: PL-004 Phase C — watchdog jobs (canonical scheduler state; policy enum + intervals + lifecycle). Exports: watchdogJobsSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/032_watchdog_history.ts`: PL-004 Phase C — append-only watchdog evaluation history (sent/skipped/terminal; not_due polls not recorded). Exports: watchdogHistorySchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/033_workflow_specs.ts`: PL-004 Phase D — workflow specs (cached read-through from markdown/YAML; UNIQUE on name+version; source_hash invalidation). Exports: workflowSpecsSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/034_workflow_instances.ts`: PL-004 Phase D — workflow instances (live state; status enum; current_frontier_json; hop_count). Exports: workflowInstancesSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/035_workflow_step_trails.ts`: PL-004 Phase D — append-only workflow step trail history (FK to workflow_instances and queue_items). Exports: workflowStepTrailsSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/036_watchdog_policy_enum_extension.ts`: PL-004 Phase D — documenting no-op migration; Phase C uses application-layer enforcement so the enum extension to include workflow-keepalive is implemented in PHASE_D_POLICIES (watchdog-jobs-repository.ts), not in DDL. Exports: watchdogPolicyEnumExtensionSchema. Related: `packages/daemon/src/db/migrate.js`.
-`packages/daemon/src/db/migrations/037_mission_control_actions.ts`: PL-005 Phase A — Mission Control action audit table (append-only; 7-verb enum at app layer; before/after qitem snapshots; FK to queue_items). Exports: missionControlActionsSchema. Related: `packages/daemon/src/db/migrate.js`.
### Domain: Mission Control / Queue Observability (PL-005 Phase A)
### Product UI: Mission Control / Queue Observability (PL-005 Phase A)
-`packages/ui/src/components/mission-control/MissionControlSurface.tsx`: Top-level Mission Control surface inside the existing product UI shell. Tab navigation across the 7 view names. Mounted at the `/mission-control` route. Exports: MissionControlSurface.
-`packages/ui/src/components/mission-control/views/{MyQueueView,HumanGateView,FleetView,ActiveWorkView,RecentShipsView,RecentlyActiveView,RecentObservationsView}.tsx`: 7 view components. Most wrap a shared GenericListView; FleetView combines CliDriftIndicator + per-row MissingFieldPlaceholder; MyQueueView combines HumanSeatCard + GenericListView with verb actions enabled.
-`packages/ui/src/components/mission-control/components/CompactStatusRow.tsx`: 9-field phone-friendly content-model atom (PRD non-negotiable). Renders compact / expanded density modes; UI may collapse but JSON preserves all 9 fields.
-`packages/ui/src/components/mission-control/components/VerbActions.tsx`: Shared 7-verb action sub-component. Renders verb buttons + per-verb required-field input (destination for route/handoff, annotation for annotate, reason for hold/drop). Submits to `POST /api/mission-control/action`. Invalidates Mission Control view queries on success.
-`packages/daemon/src/domain/watchdog-jobs-repository.ts`: Persistence for watchdog_jobs. Phase D extension: enforces four-policy enum (periodic-reminder, artifact-pool-ready, edge-artifact-required, workflow-keepalive) at register-time. Phase C's policy_deferred_to_phase_d rejection has been replaced with positive registration-accept. Exports: WatchdogJobsRepository, WatchdogJobsError, WatchdogJob, PHASE_D_POLICIES, PHASE_C_POLICIES (alias). Related: `packages/daemon/src/db/migrations/031_watchdog_jobs.js`.
-`packages/daemon/src/domain/watchdog-history-log.ts`: Append-only writer for watchdog_history. Records meaningful evaluations only (sent/skipped/terminal). API surface intentionally lacks update/delete. Exports: WatchdogHistoryLog, WatchdogHistoryEntry, WatchdogOutcome. Related: `packages/daemon/src/db/migrations/032_watchdog_history.js`.
-`packages/daemon/src/domain/watchdog-policy-engine.ts`: Resolves policy by name, parses spec_yaml context, dispatches policy.evaluate(), records meaningful outcomes via history-log, emits watchdog.* events, calls injected delivery callback for action=send. Exports: WatchdogPolicyEngine, DeliveryFn, DeliveryRequest, DeliveryOutcome. Related: `packages/daemon/src/domain/watchdog-jobs-repository.js`, `packages/daemon/src/domain/watchdog-history-log.js`, `packages/daemon/src/domain/event-bus.js`.
-`packages/daemon/src/domain/watchdog-scheduler.ts`: Tick-driven daemon-native scheduler that joins the supervision tree. Filters listActive() through isDue(), evaluates due jobs sequentially, supports start/stop with graceful in-flight await. Exports: WatchdogScheduler, isDue. Related: `packages/daemon/src/domain/watchdog-policy-engine.js`, `packages/daemon/src/domain/watchdog-jobs-repository.js`.
-`packages/daemon/src/domain/policies/periodic-reminder.ts`: Policy that returns send {target, message} unconditionally; throws policy_spec_invalid if context.target.session or context.message missing. Translated from POC `lib/policies/periodic-reminder.mjs`. Exports: periodicReminderPolicy. Related: `packages/daemon/src/domain/policies/types.js`.
-`packages/daemon/src/domain/policies/artifact-pool-ready.ts`: Policy that scans context.pools and skips with no_actionable_artifacts when empty; otherwise sends a formatted message capped at max_items. Translated from POC `lib/policies/artifact-pool-ready.mjs`. Exports: artifactPoolReadyPolicy. Related: `packages/daemon/src/domain/policies/artifact-pool-helpers.js`, `packages/daemon/src/domain/policies/types.js`.
-`packages/daemon/src/domain/policies/edge-artifact-required.ts`: Policy that scans source_pools and target_pools, computes source-key per artifact, and sends an edge-repair message for sources lacking matching downstream artifacts. Skips with no_missing_edge_artifacts when all sources matched. Translated from POC `lib/policies/edge-artifact-required.mjs`. Exports: edgeArtifactRequiredPolicy. Related: `packages/daemon/src/domain/policies/artifact-pool-helpers.js`, `packages/daemon/src/domain/policies/types.js`.
-`packages/daemon/src/domain/policies/artifact-pool-helpers.ts`: Shared filesystem scanner + formatter for artifact-pool-ready and edge-artifact-required. Translated from POC `lib/policies/artifact-pool.js`. Exports: scanArtifactPools, formatArtifactList, sourceKeyFor, ArtifactPoolSpec, ScannedArtifact. Related: none.
-`packages/daemon/src/routes/watchdog.ts`: Hono route module for `/api/watchdog` (`POST /register`, `GET /list`, `GET /sse`, `GET /watch`, `GET /:job_id`, `GET /:job_id/status`, `POST /:job_id/stop`). SSE/literal paths mounted before /:job_id catchall per Phase A R1 route-order discipline. Exports: watchdogRoutes. Related: `packages/daemon/src/domain/watchdog-jobs-repository.js`, `packages/daemon/src/domain/watchdog-history-log.js`, `packages/daemon/src/domain/event-bus.js`.
-`packages/daemon/src/domain/policies/workflow-keepalive.ts`: PL-004 Phase D — workflow-keepalive policy (carry-forward from Phase C deferral). Reads workflow_instances directly via SQLite (audit row 18 LOAD-BEARING); requires status active|waiting; resolves frontier qitem owners via queue_items. Translated from POC `lib/policies/workflow-keepalive.mjs`; source-of-truth switched from markdown to SQLite. Exports: makeWorkflowKeepalivePolicy. Related: `packages/daemon/src/domain/policies/types.js`, `packages/daemon/src/db/migrations/034_workflow_instances.js`.
-`packages/daemon/src/domain/workflow-spec-cache.ts`: Read-through cache from markdown/YAML spec files into workflow_specs table. Workspace-surface reconciliation: source_hash invalidates the cache on next read; valid operator edits to spec files win. Uses the `yaml` package for parsing. Exports: WorkflowSpecCache, WorkflowSpecError, parseWorkflowSpec. Related: `packages/daemon/src/db/migrations/033_workflow_specs.js`.
-`packages/daemon/src/domain/workflow-validator.ts`: Validates a workflow spec against role resolution, step uniqueness, allowed-exits consistency, and (optional via injected callback) seat liveness. Returns structured ValidationResult with severity-tagged issues. Exports: WorkflowValidator, ValidationIssue, ValidationResult, SeatLivenessCheckFn. Related: `packages/daemon/src/domain/workflow-types.js`.
-`packages/daemon/src/domain/workflow-projector.ts`: LOAD-BEARING. Implements the transactional-scribe contract: closing a workflow_instances frontier qitem AND creating the next-step queue_items row AND appending the workflow_step_trails entry AND updating instance frontier+status all happen in the SAME daemon transaction (db.transaction). If any side throws, all roll back. Lost handoffs are impossible by design. Post-commit, fans out persisted events + nudges next owner. Uses Phase A QueueRepository.createWithinTransaction() (Phase D extension point). Exports: WorkflowProjector, WorkflowProjectorError, ProjectStepInput, ProjectStepResult. Related: `packages/daemon/src/domain/queue-repository.js`, `packages/daemon/src/domain/workflow-instance-store.js`, `packages/daemon/src/domain/workflow-step-trail-log.js`.
-`packages/daemon/src/domain/errors.ts`: Domain-specific error types, currently including `RigNotFoundError`. Exports: RigNotFoundError. Related: none.
-`packages/daemon/src/domain/event-bus.ts`: Persistent event log plus in-process pub/sub for routes, SSE, and domain emitters. Exports: EventBus. Related: `packages/daemon/src/domain/types.js`.
-`packages/daemon/src/domain/graph-projection.ts`: Pure projection from persisted rig/session state to React Flow graph nodes and edges. Exports: ReactFlowGraph, RigGraphInput, projectRigToGraph. Related: `packages/daemon/src/domain/types.js`.
-`packages/daemon/src/domain/context-monitor.ts`: Daemon-owned context-usage recorder that samples live runtime state and updates persistent usage summaries. Exports: ContextMonitor. Related: `packages/daemon/src/domain/context-usage-store.js`.
-`packages/daemon/src/domain/context-usage-store.ts`: SQLite-backed store for per-node context usage snapshots used by whoami/node inventory surfaces. Exports: ContextUsageStore, ContextUsageSnapshot, ContextUsageStoreOpts. Related: none.
-`packages/daemon/src/domain/node-launcher.ts`: Atomic node launch service that creates tmux, session rows, bindings, and launch events together. Exports: LaunchResult, NodeLauncher. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-name.js`, +2 more.
-`packages/daemon/src/domain/ps-projection.ts`: Builds the `/api/ps` summary view over rigs, nodes, sessions, and latest snapshots. Exports: PsEntry, PsProjectionService. Related: none.
-`packages/daemon/src/domain/reconciler.ts`: Marks missing tmux sessions as detached and keeps persisted session state aligned with reality. Exports: ReconcileResult, Reconciler. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/session-registry.js`.
-`packages/daemon/src/domain/rig-repository.ts`: SQLite-backed repository for rig, node, and edge state. Exports: RigRepository. Related: `packages/daemon/src/domain/types.js`.
-`packages/daemon/src/domain/rig-repository.ts`: SQLite-backed repository for rig, node, edge, and rig-services state. Also projects `hasServices` into rig summaries for runtime/UI gating. Exports: RigRepository. Related: `packages/daemon/src/domain/types.js`.
-`packages/daemon/src/domain/rig-teardown.ts`: Graceful rig shutdown/delete orchestrator used by `/api/down`. Exports: RigTeardownOrchestrator, TeardownResult. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/rig-repository.js`, +2 more.
-`packages/daemon/src/domain/up-command-router.ts`: Source router that classifies a path as a rig spec or rig bundle before bootstrap. Exports: RouteResult, SourceKind, UpCommandRouter. Related: `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
### Domain: Specs, Resolution, Startup
@@ -104,8 +171,8 @@ Fast lookup:
-`packages/daemon/src/domain/rigspec-schema.ts`: Validation and normalization for both legacy and rebooted RigSpec schemas. Exports: LegacyRigSpecSchema, RigSpecSchema. Related: `packages/daemon/src/domain/path-safety.js`, `packages/daemon/src/domain/startup-validation.js`, `packages/daemon/src/domain/types.js`.
-`packages/daemon/src/domain/runtime-adapter.ts`: Runtime-adapter contract plus bridge types used by startup orchestration and runtime-specific adapters. Exports: InstalledResource, NodeBinding, ProjectionResult, ReadinessResult, ResolvedStartupFile, RuntimeAdapter, StartupDeliveryResult. Related: `packages/daemon/src/domain/projection-planner.js`, `packages/daemon/src/domain/types.js`.
-`packages/daemon/src/domain/spec-validation-service.ts`: Pure validation helpers for raw AgentSpec and RigSpec YAML strings. Exports: validateAgentSpecFromYaml, validateRigSpecFromYaml. Related: `packages/daemon/src/domain/agent-manifest.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
-`packages/daemon/src/domain/spec-review-service.ts`: Structured review-model builder for RigSpec and AgentSpec YAML. Produces topology preview data, normalized summaries, and provenance/source-state for preview, library, and live display surfaces. Exports: AgentSpecReview, LegacyRigSpecReview, PodAwareRigSpecReview, RigSpecReview, SourceState, SpecGraphData, SpecReviewError, SpecReviewService. Related: `packages/daemon/src/domain/agent-manifest.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
-`packages/daemon/src/domain/spec-library-service.ts`: Filesystem-backed spec library index over builtin and user roots. Classifies YAML through `SpecReviewService`, assigns stable ids, and serves metadata/YAML lookup. Exports: SpecLibraryEntry, SpecLibraryOpts, SpecLibraryService. Related: `packages/daemon/src/domain/spec-review-service.js`.
-`packages/daemon/src/domain/spec-review-service.ts`: Structured review-model builder for RigSpec and AgentSpec YAML. Produces topology preview data, normalized summaries, provenance/source-state, and managed-app services metadata used by preview, library, and live display surfaces. Exports: AgentSpecReview, LegacyRigSpecReview, PodAwareRigSpecReview, RigSpecReview, RigSpecServicesReview, SourceState, SpecGraphData, SpecReviewError, SpecReviewService. Related: `packages/daemon/src/domain/agent-manifest.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
-`packages/daemon/src/domain/spec-library-service.ts`: Filesystem-backed spec library index over builtin and user roots. Classifies YAML through `SpecReviewService`, assigns stable ids, marks service-backed rigs with `hasServices`, and serves metadata/YAML lookup. Exports: SpecLibraryEntry, SpecLibraryOpts, SpecLibraryService. Related: `packages/daemon/src/domain/spec-review-service.js`.
-`packages/daemon/src/domain/whoami-service.ts`: Daemon-side runtime identity projector used by `/api/whoami`. Resolves by node or session, expands peers/edges/transcript helpers, and surfaces ambiguity honestly. Exports: WhoamiResult, WhoamiAmbiguousError, WhoamiService. Related: `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`, `packages/daemon/src/domain/transcript-store.js`.
-`packages/daemon/src/domain/startup-orchestrator.ts`: Startup execution engine for projection, startup-file delivery, action execution, readiness, and persisted replay context. Exports: StartupInput, StartupOrchestrator, StartupResult. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/projection-planner.js`, `packages/daemon/src/domain/runtime-adapter.js`, +2 more.
-`packages/daemon/src/domain/startup-resolver.ts`: Pure additive startup-layer merge function across agent, profile, rig, pod, member, and operator layers. Exports: StartupLayerInputs, resolveStartup. Related: `packages/daemon/src/domain/types.js`.
### Domain: Coordination Primitive (PL-004 Phase A)
-`packages/daemon/src/domain/stream-store.ts`: L1 coordination stream — append-only intake/audit root. Idempotent emit on stream_item_id, chronological list with cursor pagination, soft archive. Exports: StreamStore, StreamItem, StreamEmitInput, StreamListOptions. Related: `packages/daemon/src/domain/event-bus.js`.
-`packages/daemon/src/domain/queue-repository.ts`: L3 coordination queue — create, claim/unclaim, update with hot-potato strict-rejection, transactional handoff (close source + create new in one transaction), pod-fallback rerouting, overdue lookup. POC qitem_id format preserved. Exports: QueueRepository, QueueRepositoryError, QueueItem, QueueState, QueuePriority, QueueCreateInput, QueueUpdateInput, QueueHandoffInput. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/queue-transition-log.js`, `packages/daemon/src/domain/hot-potato-enforcer.js`.
-`packages/daemon/src/domain/queue-transition-log.ts`: Append-only state transition log for queue_items. No UPDATE / DELETE from domain code. Exports: QueueTransitionLog, QueueTransition, QueueTransitionInput. Related: `packages/daemon/src/domain/hot-potato-enforcer.js`.
-`packages/daemon/src/domain/hot-potato-enforcer.ts`: Pure validator for the load-bearing hot-potato API contract — state=done requires closure_reason from the 6-value enum. Tier→SLA mapping for closure_required_at. Exports: CLOSURE_REASONS, ClosureReason, validateClosure, isClosureReason, computeClosureRequiredAt. Related: none.
-`packages/daemon/src/domain/inbox-handler.ts`: Mailbox handler — authenticated drop (idempotent on inbox_id), absorb (promote pending entry to a queue_item), deny (record reason). Authentication is a pluggable constructor hook. Exports: InboxHandler, InboxHandlerError, InboxEntry, InboxState, InboxDropInput. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/queue-repository.js`.
-`packages/daemon/src/domain/outbox-handler.ts`: Sender-side outbox — symmetric to inbox; idempotent record, mark delivered/failed, list. No event-bus events emitted. Exports: OutboxHandler, OutboxHandlerError, OutboxEntry, OutboxDeliveryState, OutboxRecordInput. Related: none (SQLite-only).
### Routes
-`packages/daemon/src/routes/adapters.ts`: Hono route module for adapters API endpoints. Exports: adaptersRoutes. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`.
-`packages/daemon/src/routes/agents.ts`: Hono route module for AgentSpec validation. POST `/api/agents/validate` using `validateAgentSpecFromYaml`. Exports: agentsRoutes. Related: `packages/daemon/src/domain/spec-validation-service.js`.
@@ -176,6 +251,7 @@ Fast lookup:
-`packages/daemon/src/routes/bundles.ts`: Hono route module for dual-format bundle create/inspect/install. Supports legacy v1 package bundles and schema-version-2 pod bundles, including v2 inspect responses with `agents[]` and integrity data. Exports: bundleRoutes. Related: `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, `packages/daemon/src/domain/bundle-archive.js`, `packages/daemon/src/domain/bundle-assembler.js`, +7 more.
-`packages/daemon/src/routes/discovery.ts`: Hono route module for discovery scan/list/detail plus Final Stretch flows: `draft-rig`, `bind`, and composite `adopt`. Exports: discoveryRoutes. Related: `packages/daemon/src/domain/claim-service.js`, `packages/daemon/src/domain/discovery-coordinator.js`, `packages/daemon/src/domain/discovery-repository.js`, `packages/daemon/src/domain/draft-rig-generator.js`.
-`packages/daemon/src/routes/down.ts`: Hono route module for down API endpoints. Exports: downRoutes. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/rig-teardown.js`.
-`packages/daemon/src/routes/env.ts`: Hono route module for rig-environment status, logs, and teardown. Refreshes env receipts best-effort and returns additive managed-app/runtime metadata (`hasServices`, `surfaces`, `receipt`). Exports: envRoutes. Related: `packages/daemon/src/adapters/compose-services-adapter.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/service-orchestrator.js`.
-`packages/daemon/src/routes/events.ts`: Hono route module for events API endpoints. Exports: eventsRoute. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/types.js`.
-`packages/daemon/src/routes/packages.ts`: Hono route module for the legacy package-install surface (`/validate`, `/plan`, `/install`, history, rollback). AgentSpec validation no longer lives here. Exports: packagesRoutes. Related: `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/install-engine.js`, `packages/daemon/src/domain/install-planner.js`, +6 more.
-`packages/daemon/src/routes/ps.ts`: Hono route module for ps API endpoints. Exports: psRoutes. Related: `packages/daemon/src/domain/ps-projection.js`.
@@ -187,8 +263,10 @@ Fast lookup:
-`packages/daemon/src/routes/transcripts.ts`: Hono route module for transcript access (`GET /:session/tail`, `/:session/grep`). Resolves session to rig/node, delegates to TranscriptStore. Exports: transcriptRoutes. Related: `packages/daemon/src/domain/transcript-store.js`, `packages/daemon/src/domain/rig-repository.js`.
-`packages/daemon/src/routes/ask.ts`: Hono route module for context evidence packs (`POST /`). Delegates to AskService. Exports: askRoutes. Related: `packages/daemon/src/domain/ask-service.js`.
-`packages/daemon/src/routes/chat.ts`: Hono route module for durable rig chat (`GET /watch` SSE stream, `POST /send`, `GET /history`, `POST /topic`). Delegates to ChatRepository, streams via SSE. Exports: chatRoutes. Related: `packages/daemon/src/domain/chat-repository.js`, `packages/daemon/src/domain/event-bus.js`.
-`packages/daemon/src/routes/stream.ts`: Hono route module for the PL-004 Phase A coordination L1 stream (`POST /emit`, `GET /list`, `GET /:id`, `POST /:id/archive`, `GET /watch` SSE). Host-scoped (no rigId). Exports: streamRoutes. Related: `packages/daemon/src/domain/stream-store.js`, `packages/daemon/src/domain/event-bus.js`.
-`packages/daemon/src/routes/queue.ts`: Hono route module for the PL-004 Phase A coordination L3 queue plus inbox/outbox (`POST /create`, `POST /:id/{claim,unclaim,update,handoff,fallback}`, `GET /list`, `GET /overdue`, `GET /:id`, `GET /:id/transitions`, inbox/outbox subroutes, `GET /watch` SSE). Surfaces hot-potato strict-rejection structured errors with validReasons enum. Exports: queueRoutes. Related: `packages/daemon/src/domain/queue-repository.js`, `packages/daemon/src/domain/inbox-handler.js`, `packages/daemon/src/domain/outbox-handler.js`, `packages/daemon/src/domain/event-bus.js`.
-`packages/daemon/src/routes/spec-review.ts`: Hono route module for structured review over raw RigSpec / AgentSpec YAML. Accepts `sourceState` and returns review models or validation errors. Exports: specReviewRoutes. Related: `packages/daemon/src/domain/spec-review-service.js`.
-`packages/daemon/src/routes/spec-library.ts`: Hono route module for the filesystem-backed spec library (`list`, `get`, `review`, `sync`). Exports: specLibraryRoutes. Related: `packages/daemon/src/domain/spec-library-service.js`, `packages/daemon/src/domain/spec-review-service.js`.
-`packages/daemon/src/routes/spec-library.ts`: Hono route module for the filesystem-backed spec library (`list`, `get`, `review`, `sync`). Enriches service-backed reviews with best-effort compose preview data for managed-app UI surfaces. Exports: specLibraryRoutes. Related: `packages/daemon/src/domain/spec-library-service.js`, `packages/daemon/src/domain/spec-review-service.js`.
-`packages/daemon/src/routes/whoami.ts`: Hono route module for daemon-backed runtime identity lookup by `nodeId` or `sessionName`. Exports: whoamiRoutes. Related: `packages/daemon/src/domain/whoami-service.js`.
-`packages/daemon/src/routes/up.ts`: Hono route module for `/api/up`, routing direct rig specs, bundles, and rig-name restore into the staged bootstrap flow. Exports: upRoutes. Related: `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/up-command-router.js`.
@@ -197,7 +275,7 @@ Fast lookup:
### Entrypoints And Infrastructure
-`packages/cli/src/client.ts`: Shared HTTP client for talking to the daemon API, including raw JSON posts and text-body posts with optional extra headers such as `X-Rig-Root`. Exports: DaemonClient, DaemonConnectionError, DaemonResponse. Related: none.
-`packages/cli/src/daemon-lifecycle.ts`: Starts, stops, inspects, and tails the local daemon process. Spawns the daemon with `process.execPath` and bypasses local `daemon.json` gating when `OPENRIG_URL` is set by probing remote `/healthz` directly. Exports: DaemonState, DaemonStatus, LOG_FILE, LifecycleDeps, OPENRIG_DIR, STATE_FILE, StartOptions, getDaemonPath, getDaemonStatus, readLogs, startDaemon, stopDaemon, tailLogs. Related: none.
-`packages/cli/src/index.ts`: Commander entrypoint that assembles the full `rig` CLI program (31 mounted command groups, including Final Stretch 1 `specs`, `whoami`, `bind`, and `adopt`). Exports: ProgramDeps, createProgram. Related: `packages/cli/src/commands/bootstrap.js`, `packages/cli/src/commands/bundle.js`, `packages/cli/src/commands/claim.js`, `packages/cli/src/commands/daemon.js`, +18 more.
-`packages/cli/src/index.ts`: Commander entrypoint that assembles the full `rig` CLI program (40 mounted command groups, including env status/logs/down, `destroy`, and Final Stretch 1 `specs`, `whoami`, `bind`, and `adopt`). Exports: ProgramDeps, createProgram. Related: `packages/cli/src/commands/bootstrap.js`, `packages/cli/src/commands/bundle.js`, `packages/cli/src/commands/claim.js`, `packages/cli/src/commands/daemon.js`, +19 more.
-`packages/cli/src/mcp-server.ts`: Implements the CLI-hosted MCP server over stdio, wrapping 17 daemon-backed tools including node inventory, send/capture, bundle/spec validation, and chatroom. Exports: createMcpServer. Related: `packages/cli/src/client.js`.
-`packages/cli/src/commands/specs.ts`: Commander command module for the agent-facing spec library workflow. Subcommands: `ls`, `show`, `preview`, `add`, `sync`, plus shared name resolution used by `up` and `bootstrap`. Exports: resolveLibrarySpec, specsCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/specs.ts`: Commander command module for the agent-facing spec library workflow. Subcommands: `ls`, `show`, `preview`, `add`, `sync`, plus shared name resolution used by `up` and `bootstrap`.`add` accepts single YAML files and full spec directories containing `rig.yaml` or `agent.yaml`. Help/examples and not-found guidance explicitly call out managed apps. Exports: resolveLibrarySpec, specsCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/up.ts`: Commander command module for `up`. Supports spec paths, bundle paths, rig-name restore, and library-name resolution; fails loudly on rig-name vs library-name ambiguity. Auto-preflight, auto-daemon-start, post-command handoff with attach commands. Exports: upCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/specs.js`, `packages/cli/src/daemon-lifecycle.js`, `packages/cli/src/system-preflight.js`.
-`packages/cli/src/commands/up.ts`: Commander command module for `up`. Supports spec paths, bundle paths, rig-name restore, and library-name resolution; fails loudly on rig-name vs library-name ambiguity. Help/examples now treat managed apps like `secrets-manager` as first-class launch targets. Auto-preflight, auto-daemon-start, post-command handoff with attach commands. Exports: upCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/specs.js`, `packages/cli/src/daemon-lifecycle.js`, `packages/cli/src/system-preflight.js`.
-`packages/cli/src/commands/whoami.ts`: Commander command module for `whoami`. Resolves identity from explicit args, env vars, tmux metadata, and session-name fallback, then delegates to daemon `/api/whoami`. Exports: resolveIdentitySource, whoamiCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/adopt.ts`: Commander command module for `adopt` — materialize topology from a pod-aware RigSpec, then bind discovered live sessions according to `--bind` mappings. Exports: AdoptDeps, adoptCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/import.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/broadcast.ts`: Commander command module for `broadcast` — send to all agents in a rig or pod. Reports per-session delivery status. Exports: broadcastCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/transcript.ts`: Commander command module for `transcript` — composable transcript access via `--tail N` and `--grep "pattern"`. Exports: transcriptCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/stream.ts`: Commander command module for `rig stream` (PL-004 Phase A) — coordination L1 intake stream. Subcommands: `emit`, `list`, `show`, `archive`. Speaks only to the daemon HTTP API; does not touch POC filesystem state. Exports: streamCommand, StreamDeps. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/ui/src/components/SharedDetailDrawer.tsx`: Unified drawer surface keyed by `DrawerSelection`. Routes to rig, node, system, discovery, or specs panels without leaving the current workspace. Exports: DrawerSelection, SharedDetailDrawer. Related: `packages/ui/src/components/DiscoveryPanel.js`, `packages/ui/src/components/NodeDetailPanel.js`, `packages/ui/src/components/RigDetailPanel.js`, `packages/ui/src/components/SpecsPanel.js`, `packages/ui/src/components/SystemPanel.js`.
-`packages/ui/src/components/NodeDetailPlaceholder.tsx`: Empty state for node detail when no node is selected. Exports: NodeDetailPlaceholder. Related: none.
-`packages/ui/src/components/SpecsPanel.tsx`: Drawer-native spec library and authoring launcher. Lists library entries, opens review routes, and bridges into specs workspace tasks. Exports: SpecsPanel. Related: `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useSpecLibrary.js`.
-`packages/ui/src/components/SpecsPanel.tsx`: Drawer-native spec library and authoring launcher. Presents a unified library list with `All / Apps / Rigs / Agents`, type/stability badges, richer managed-app rows, and review-route handoff. Exports: SpecsPanel. Related: `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useSpecLibrary.js`.
-`packages/ui/src/components/RigChatPanel.tsx`: Chat room tab in rig drawer — send messages, watch live stream, view history. Exports: RigChatPanel. Related: `packages/ui/src/hooks/useRigChat.js`.
-`packages/ui/src/components/LiveIdentityDisplay.tsx`: Reusable runtime-identity subsection for peers, edges, transcript helpers, and compact spec summary. Used by the node drawer and full-details route. Exports: LiveIdentityDisplay. Related: none.
-`packages/ui/src/components/LiveNodeDetails.tsx`: Explicit center-workspace route for deep inspection of one live node. Composes runtime identity plus reusable spec display components. Exports: LiveNodeDetails. Related: `packages/ui/src/components/AgentSpecDisplay.js`, `packages/ui/src/components/LiveIdentityDisplay.js`, `packages/ui/src/hooks/useNodeDetail.js`.
-`packages/ui/src/components/RigSpecDisplay.tsx`: Canonical RigSpec display primitive used by draft review, library review, and live/workspace inspection flows. Exports: RigSpecDisplay. Related: `packages/ui/src/components/SpecTopologyPreview.js`.
-`packages/ui/src/components/RigSpecDisplay.tsx`: Canonical RigSpec display primitive used by draft review, library review, and live/workspace inspection flows. Supports a library-only managed-app Environment tab with compose preview, health gates, and surfaces. Exports: RigSpecDisplay. Related: `packages/ui/src/components/SpecTopologyPreview.js`.
-`packages/ui/src/components/AgentSpecDisplay.tsx`: Canonical AgentSpec display primitive used by preview, library, and live-details flows. Exports: AgentSpecDisplay. Related: none.
-`packages/ui/src/components/RigSpecReview.tsx`: Route-level rig review page that consumes `RigSpecDisplay` and specs workspace state. Exports: RigSpecReview. Related: `packages/ui/src/components/RigSpecDisplay.js`, `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useSpecReview.js`.
-`packages/ui/src/components/AgentSpecReview.tsx`: Route-level agent review page that consumes `AgentSpecDisplay` and specs workspace state. Exports: AgentSpecReview. Related: `packages/ui/src/components/AgentSpecDisplay.js`, `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useSpecReview.js`.
-`packages/ui/src/components/LibraryReview.tsx`: Library-entry review page that renders reusable display components plus provenance/source-state. Exports: LibraryReview. Related: `packages/ui/src/components/AgentSpecDisplay.js`, `packages/ui/src/components/RigSpecDisplay.js`, `packages/ui/src/hooks/useSpecLibrary.js`.
-`packages/ui/src/components/LibraryReview.tsx`: Library-entry review page that renders reusable display components plus provenance/source-state. Service-backed rigs surface managed-app copy, specialist identity, setup-prompt copy, and env-aware review details. Exports: LibraryReview. Related: `packages/ui/src/components/AgentSpecDisplay.js`, `packages/ui/src/components/RigSpecDisplay.js`, `packages/ui/src/hooks/useSpecLibrary.js`.
-`packages/ui/src/components/SystemPanel.tsx`: Drawer surface for system-wide event log/status tabs. Replaces the old status-bar role with a richer right-drawer system surface. Exports: SystemPanel. Related: `packages/ui/src/hooks/useActivityFeed.js`.
-`packages/ui/src/components/WorkspaceHome.tsx`: Landing page when no rig is selected. Entry point for explorer-first workflow. Exports: WorkspaceHome. Related: `packages/ui/src/components/AppShell.js`.
-`packages/ui/src/hooks/useRigSummary.ts`: Rig summary list hook used across dashboard and shell surfaces. Exports: RigSummary, useRigSummary. Related: none.
-`packages/ui/src/hooks/useRigSummary.ts`: Rig summary list hook used across dashboard and shell surfaces. Includes `hasServices` so runtime/UI can gate managed-app env surfaces. Exports: RigSummary, useRigSummary. Related: none.
-`packages/ui/src/hooks/useSnapshots.ts`: Snapshot list hook for one rig. Exports: Snapshot, useSnapshots. Related: none.
### Hooks: North Star + Post-North-Star Additions
-`packages/ui/src/hooks/useNodeInventory.ts`: Fetches node inventory for a rig from the canonical `GET /api/rigs/:rigId/nodes` endpoint. Exports: useNodeInventory. Related: none.
-`packages/ui/src/hooks/useNodeDetail.ts`: Fetches extended node detail from `GET /api/rigs/:rigId/nodes/:logicalId`, including peers, edges, transcript helpers, and compact spec summary for the adopted-parity/live-identity drawer. Exports: NodeDetailData, useNodeDetail. Related: none.
-`packages/ui/src/hooks/useRigEnv.ts`: Fetches `GET /api/rigs/:rigId/env` for service-backed rig status, surfaces, and receipt data, with runtime-friendly refetch behavior. Exports: RigEnvData, useRigEnv. Related: none.
-`packages/ui/src/hooks/useRigChat.ts`: Chat send/watch/history hooks for the rig chat panel. Exports: useRigChat. Related: none.
-`packages/ui/src/hooks/useGlobalEvents.ts`: Global SSE subscription for explorer status updates. Exports: useGlobalEvents. Related: none.
-`packages/ui/src/hooks/useSpecLibrary.ts`: Spec library list and library-review fetch hooks. Typed for `sourceState: "library_item"` review results. Exports: SpecLibraryEntry, LibraryReview, LibraryRigReview, LibraryAgentReview, useSpecLibrary, useLibraryReview. Related: none.
-`packages/ui/src/hooks/useSpecReview.ts`: Structured spec-review hooks for raw YAML preview (`rig` and `agent`) over `/api/specs/review/*`. Exports: SpecGraphData, RigSpecReview, AgentSpecReview, useRigSpecReview, useAgentSpecReview. Related: none.
-`packages/ui/src/hooks/useSpecLibrary.ts`: Spec library list and library-review fetch hooks. Typed for `sourceState: "library_item"` review results and `hasServices`-aware managed-app classification. Exports: SpecLibraryEntry, LibraryReview, LibraryRigReview, LibraryAgentReview, useSpecLibrary, useLibraryReview. Related: none.
-`packages/ui/src/hooks/useSpecReview.ts`: Structured spec-review hooks for raw YAML preview (`rig` and `agent`) over `/api/specs/review/*`. The rig review type now carries managed-app services metadata and compose preview. Exports: SpecGraphData, RigSpecReview, AgentSpecReview, useRigSpecReview, useAgentSpecReview. Related: none.
### Lib Utilities: North Star + Post-North-Star Additions
-`packages/ui/src/lib/display-id.ts`: Short ULID display utility — uses ULID tail (not head) for better uniqueness in same-view contexts. UI-only, no backend changes. Exports: shortId. Related: none.
-`packages/ui/src/lib/display-name.ts`: Pod name extraction from qualified logical IDs (e.g., "dev1" from "dev1.impl"). Exports: displayPodName, inferPodName. Related: none.
-`packages/ui/src/lib/build-setup-prompt.ts`: Managed-app setup-prompt generator used by library review copy surfaces. Exports: buildSetupPrompt. Related: none.
-`packages/ui/src/lib/copy-text.ts`: Clipboard copy utility for the "Copy tmux attach" and "Copy resume command" buttons. Exports: copyText. Related: none.
Source of truth: `packages/daemon/src/domain/agent-manifest.ts`, `packages/daemon/src/domain/types.ts`
This is the canonical reference for the AgentSpec YAML format (`agent.yaml`). Every field, validation rule, and default documented here was traced from the actual parser and validator code.
---
## Minimal Valid Example
```yaml
name:my-agent
version:"1.0"
profiles:
default:
uses:
skills:[]
guidance:[]
subagents:[]
hooks:[]
runtime_resources:[]
resources:{}
startup:
files:[]
actions:[]
```
## Practical Example (implementer agent)
```yaml
name:implementer
version:"1.0"
description:Implementation agent — writes code following TDD discipline
| `name` | string | yes | — | Agent name. Used in spec library identification and validation messages. |
| `version` | string | yes | — | Spec version. Informational — not used for compatibility gating. |
| `description` | string | no | — | Human-readable description. Shown in spec library and review surfaces. |
| `defaults` | Defaults | no | — | Default runtime, model, and lifecycle settings. Applied when not overridden by the rig spec or profile. |
| `imports` | Import[] | no | `[]` | Other AgentSpecs to import. Resources from imported specs become available for profile `uses` references. |
| `profiles` | map<string, Profile> | no | `{}` | Named profiles. Each profile selects resources and can override startup/lifecycle. The rig spec member's `profile` field selects which profile to use. |
| `resources` | Resources | no | all empty | Declared resources (skills, guidance, subagents, hooks, runtime resources). These are the available pool that profiles select from via `uses`. |
| `startup` | StartupBlock | no | `{ files: [], actions: [] }` | Agent-level startup files and actions. Applied to all profiles via the startup layering model. |
---
## Defaults
```yaml
defaults:
runtime:claude-code
model:claude-opus-4-6
lifecycle:
execution_mode:interactive_resident
compaction_strategy:harness_native
restore_policy:resume_if_possible
```
| Field | Type | Required | Default | Description |
| `ref` | string | yes | Reference to another AgentSpec directory. Must start with `local:` (relative) or `path:` (absolute). The referenced directory must contain an `agent.yaml`. |
| `version` | string | no | Optional version constraint. Must be an exact version — no ranges (`~`, `^`, `>=`, etc.). |
### Import Resolution
-`local:` paths resolve relative to the importing spec's directory
-`path:` paths are absolute filesystem paths
- Imported resources become available for `uses` references in profiles
- When referencing imported resources in `uses`, use qualified `namespace:id` format (e.g., `shared:openrig-user`)
- Unqualified references (just `id`) resolve against the spec's own local resources first
### The Shared Import Pattern
Most built-in agents import the shared builtin spec:
```yaml
imports:
- ref:local:../../shared
```
This gives access to the full pool of shared skills (openrig-user, systematic-debugging, development-team, etc.) and agents select the ones they need via profile `uses`.
- **Unqualified** (`vault-user`) — resolves against the spec's own `resources` first, then imported specs
- **Qualified** (`shared:openrig-user`) — resolves against a specific imported spec's resources
The `uses` categories are: `skills`, `guidance`, `subagents`, `hooks`, `runtime_resources`.
---
## Resources
```yaml
resources:
skills:
- id:vault-user
path:skills/vault-user
guidance:
- id:role
path:guidance/role.md
subagents:
- id:helper
path:subagents/helper
hooks:
- id:pre-commit
path:hooks/pre-commit.sh
runtimes:[claude-code]
runtime_resources:
- id:claude-settings
path:runtime/claude-settings.fragment.json
runtime:claude-code
type:claude_settings_fragment
```
Resources are the available pool. They are NOT automatically delivered to agents — profiles select them via `uses`. The only resources delivered are those that the active profile's `uses` block references.
### Resource Categories
| Category | Fields | Description |
|----------|--------|-------------|
| `skills` | `id`, `path` | Skill directories containing a SKILL.md. Delivered via `skill_install`. |
| `guidance` | `id`, `path`, `target`*, `merge`* | Guidance files. Delivered via `guidance_merge` into CLAUDE.md/AGENTS.md. |
| `path` | string | yes | — | Relative path to the guidance file. |
| `target` | string | no | — | Target file for merging (e.g., `CLAUDE.md`). |
| `merge` | string | no | `managed_block` | Merge strategy. One of: `managed_block`, `append`. |
---
## Startup Block
The startup block follows the same format as in the RigSpec (see `docs/reference/rig-spec.md` for full details on files and actions).
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.
### Delivery Hint Quick Reference
| Hint | When Delivered | Mechanism |
|------|---------------|-----------|
| `auto` | Before harness boot | System chooses based on file type |
| `guidance_merge` | Before harness boot | Merged into CLAUDE.md/AGENTS.md as managed block |
| `skill_install` | Before harness boot | Installed to runtime skill directory |
| `send_text` | After harness is ready | Sent as text to agent terminal via tmux |
---
## Validation Rules Summary
1. `name` and `version` are required non-empty strings.
2. `imports` must be an array of objects with `ref` field.
3. Import `ref` must start with `local:` (relative) or `path:` (absolute).
4. Import `version` must be an exact version (no ranges).
5. `profiles` must be a map (object), not an array.
6. Profile `uses` references must resolve to declared resources (local or imported).
7. Unqualified `uses` references that don't resolve to local resources require imports to be present.
8. Qualified `uses` references must be in `namespace:id` format.
9. All resource paths must be safe relative paths.
10. Resource IDs must be unique within their category.
This guide teaches you how to think about what goes into an agent's startup experience — what files to write, where to put them, and how the layering model delivers them. It is an authoring guide, not a schema reference. For field-level details, see `rig-spec.md` and `agent-spec.md`.
---
## Two Categories of Startup
Everything an agent receives at boot time falls into one of two categories:
### Category 1: Context Loading
The agent reads markdown files into its context window. This is the **primary mechanism** in OpenRig today and the one you should invest most of your authoring effort in.
Context loading shapes what the agent knows, believes, and is capable of:
- Who it is (role, identity, pod membership)
- How the team works (culture, communication norms, coordination protocols)
- What the project is (codebase context, architecture, domain knowledge)
- What it can do (skills, SOPs, operational procedures)
- What its environment looks like (services, access credentials, tools)
### Category 2: Deterministic Configuration
The rig spec declaratively installs things into the agent's runtime environment:
See the **Current Support Matrix** section at the end of this guide for what is reliable today vs experimental.
**The v0.2.0 recommendation:** Put as much of your setup logic as possible into Category 1 (context loading via markdown files). Describe the desired end state in startup files and let the agent handle the configuration. The deterministic path exists in the spec and will become more reliable over time, but for now the context-loading path is the one that works consistently across runtimes.
---
## Context Loading: What Goes Where
### Skills vs Startup Files — The Key Distinction
**Skills** are reusable SOPs. They teach an agent HOW to do something — how to use OpenRig, how to do TDD, how to run a code review, how to operate Vault. Skills transfer across rigs. A skill you write once can be used by any agent in any rig.
**Startup/guidance files** are rig-specific and role-specific. They tell an agent WHO it is, WHAT it's working on, and HOW this particular team operates. They are authored per-rig and often per-pod or per-member.
| Put in a skill when... | Put in a startup/guidance file when... |
Skills are delivered to agents through two parallel paths:
1.**Spec declaration:** The agent spec's `resources.skills` + profile `uses.skills` ensures the skill files are projected to the agent's workspace (installed before harness boot)
2.**Startup instruction:** The guidance/startup files tell the agent to actually read and load those skills
Both paths are important. The spec projection ensures the files are physically present. The startup instruction ensures the agent knows to read them. This redundancy is intentional — it handles cases where one path fails.
Example: An implementer agent's startup guidance says "Load the following skills: openrig-user, test-driven-development, systematic-debugging" — AND the agent spec's profile uses those same skill IDs. The agent gets the files from projection and the instruction to read them from guidance.
### The File Types
**Role guidance (`guidance/role.md`)**
Tells the agent who it is and what its responsibilities are:
- Title and primary function
- Specific responsibilities (bulleted list)
- Working rhythm (how the agent operates day-to-day)
- Principles (behavioral guidelines)
- Relationship to other team members
This is the agent's identity document. Every agent should have one.
**Rig culture (`CULTURE.md`)**
The rig-wide constitution. Applied to ALL agents in the rig:
- Communication norms (use `rig send`/chatroom, not raw tmux)
- Coordination protocol (how work flows between pods)
- Quality standards (what "done" means)
- Commit/merge policy
- Escalation rules
Think of this as the team operating manual that every team member reads on day one.
**Startup context (`startup/context.md`)**
Boot-time grounding information specific to this agent's operating environment:
- System check instructions (what should be running, how to verify)
- Role-specific delegation information (who to ask, who delegates to you)
This is especially important for managed-app specialists (e.g., a Vault specialist needs to know the Vault address and dev token).
**Project documentation**
Rig-level startup files that teach the agent about the project:
- Architecture overview
- Key conventions
- Domain vocabulary
- Recent context (what's been happening, what's in progress)
These go in rig-level or pod-level startup blocks and are delivered to relevant agents.
---
## The Layering Model
Startup content is merged additively through layers. Each layer adds to what the previous layers provided. Later layers do NOT replace earlier layers — they append.
### The Layers (in delivery order)
```
1. Agent layer — from the AgentSpec's top-level startup block
2. Profile layer — from the active profile's startup block
3. Rig layer — from the RigSpec's top-level startup block
4. Culture layer — from the RigSpec's culture_file
5. Pod layer — from the pod's startup block
6. Member layer — from the member's startup block in the RigSpec
**Most rigs only need three layers:** agent (role.md), rig (culture), and operator (openrig-start). Start simple. Add pod and member layers only when agents in the same pod need different startup content.
**The culture layer is high-value and often skipped.** A rig without a `CULTURE.md` relies on agents to guess how the team communicates and coordinates. Write one. Even a short culture file dramatically improves team coherence.
**Member-level startup is for exceptions, not the rule.** If every member has its own startup block, the layering model is being used as a configuration dump. Refactor shared content up to the pod or rig level.
---
## Delivery Mechanisms
### How Files Reach the Agent
| Delivery Hint | When | How | Use For |
|---------------|------|-----|---------|
| `auto` | Before harness boot | System chooses | Default — let OpenRig decide |
| `guidance_merge` | Before harness boot | Merged into `CLAUDE.md` / `AGENTS.md` as a managed block | Role guidance, culture, project context |
| `skill_install` | Before harness boot | Copied to runtime's skill directory | Skills |
| `send_text` | After harness is ready | Sent as text to agent's terminal via tmux | Boot-time grounding, identity hints, instructions to read skills |
### Delivery Timing Matters
Files delivered via `guidance_merge` and `skill_install` happen BEFORE the agent's harness boots. The agent sees them immediately when it starts — they're part of the initial context.
Files delivered via `send_text` happen AFTER the harness is ready. The agent receives them as messages in its terminal. Use this for:
- Identity grounding (agent reads and processes the instructions)
- Instructions to load skills (the files are already projected, the message tells the agent to read them)
- Context that should feel like an operator briefing, not pre-loaded content
### The `applies_on` Field
Each startup file and action specifies when it applies:
-`fresh_start` — delivered on first launch only
-`restore` — delivered on restore from snapshot
- Default: `[fresh_start, restore]` (both)
Use this to avoid re-sending context that the agent already has from its resumed conversation. For example, a one-time project briefing might only apply on `fresh_start`, while identity grounding should apply on both.
---
## Deterministic Configuration
The AgentSpec and RigSpec allow declaring deterministic environment configuration:
### What the Spec Supports
**Hooks** (in `resources.hooks`):
```yaml
resources:
hooks:
- id:pre-commit
path:hooks/pre-commit.sh
runtimes:[claude-code]
```
Hooks are scripts copied to the agent's workspace. They can be git hooks, automation scripts, or environment setup.
**Runtime resources** (in `resources.runtime_resources`):
```yaml
resources:
runtime_resources:
- id:claude-settings
path:runtime/claude-settings.fragment.json
runtime:claude-code
type:claude_settings_fragment
```
Runtime-specific configuration files projected into the agent's runtime environment.
**Startup actions** (in `startup.actions`):
```yaml
startup:
actions:
- type:send_text
value:"/install-mcp my-server"
phase:after_ready
idempotent:true
```
Commands sent to the agent's terminal after it's ready. Can install MCPs, run setup commands, etc.
| Skill projection (`skill_install`) | **Supported** | Reliable. Skills are copied to workspace. |
| `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. |
| 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. |
### The v0.2.0 Approach: Describe, Then Let the Agent Handle It
For anything beyond `guidance_merge`, `skill_install`, and `send_text`, the recommended approach is:
1.**Describe the desired end state** in a startup file (e.g., "You need these MCP servers configured, these permissions set, these hooks installed")
2.**Include a system check** in the startup instructions ("Verify your environment: check that X is installed, Y is configured, Z is accessible")
3.**Empower the agent to self-configure** ("If any of these are missing, install/configure them")
4.**Optionally also declare it in the spec** for when deterministic support improves — the spec serves as the blueprint, and the startup file serves as the fallback instruction
This way, when deterministic support becomes fully reliable, the agent will boot up, see that everything is already set up (by the deterministic path), run its system check, confirm everything looks good, and proceed. Until then, the agent reads the instructions and handles the setup itself.
### Runtime Config Disclosure
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.
- 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`
Purpose: apply context collector/activity hooks and selected `claude_settings_fragment` resources inside the project without committing them to git.
- Claude project-local MCP config: `.mcp.json`
Purpose: apply selected `claude_mcp_fragment` resources for Claude in that project.
- Codex global config: `~/.codex/config.toml`
Purpose: pre-trust managed workspaces and apply selected `codex_config_fragment` resources. Codex currently has no equivalent project-local MCP config path for global profile settings.
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
### Runtime Differences
**Claude Code:**
- Reads from `CLAUDE.md` and `.claude/` directory
- Recurring tasks via the `/loop` command (e.g., `/loop 5m "check rig health"`) — this is NOT hooks; hooks are event-driven
- MCP server management via `/mcp` interactive command or `claude mcp add` from CLI
- Event-driven hooks system in `.claude/settings.json` — reacts to events like `PreToolUse`, `PostToolUse`, `SessionStart`, etc. (not time-based)
- Permission allowlisting via `.claude/settings.json` (`permissions.allow`, `permissions.deny`, `permissions.defaultMode`)
- Can self-configure MCP servers, permissions, and hooks from startup instructions
**Codex:**
- 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`)
- 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."
---
## Patterns and Anti-Patterns
### Good Patterns
**Start with role + culture + one skill**
```
agent.yaml → guidance/role.md
rig.yaml → culture_file: CULTURE.md
profile → uses.skills: [openrig-user]
```
This is the minimum effective startup. The agent knows who it is, how the team works, and how to use the rig.
**Separate project context from role**
Don't put project documentation inside the role guidance. The role is about the agent's function; project context is about what the agent is working on. Use rig-level or pod-level startup files for project context.
**Tell the agent about its skills explicitly**
In a startup file or guidance, include a line like:
```
You have the following skills loaded: openrig-user, test-driven-development, systematic-debugging. Use them.
```
This prompts the agent to actually invoke the skills, not just have them as passive context.
4. Required tools are available (node, npm, git, etc.)
If anything is missing, fix it before starting work.
```
### Anti-Patterns
**Dumping everything into one giant CLAUDE.md**
Don't. Use the layering model. Role goes in the agent spec. Culture goes in the rig spec. Project context goes in rig/pod startup files. If everything is in one file, you can't reuse any of it.
**Duplicating skill content in guidance files**
If you find yourself copying text from a skill into a guidance file, stop. Reference the skill instead. Skills are projected; guidance should point to them, not duplicate them.
**Using send_text for content that should be pre-loaded**
If the agent needs to know something BEFORE it starts reasoning, use `guidance_merge` (delivered before boot), not `send_text` (delivered after boot). `send_text` is for instructions the agent should process as a first task, not for foundational context.
**Over-specifying member-level startup**
If every member has a large startup block, the rig spec becomes a configuration dump. Refactor shared content up to the pod level. Member-level startup should be small overrides, not complete agent briefings.
**Relying on deterministic hooks for critical setup**
If your rig REQUIRES a hook to function and the hook installation fails silently, the agent won't know something is wrong. Always pair deterministic config with a startup instruction or system check that verifies the result.
---
## Authoring Checklist
When creating a new agent's startup experience:
- [ ] Write a `guidance/role.md` — who is this agent?
- [ ] Reference it in the agent spec's `resources.guidance` AND `startup.files`
- [ ] Write a rig `CULTURE.md` if the rig doesn't have one
- [ ] Choose skills from the shared pool via profile `uses`
- [ ] Write a `startup/context.md` if the agent needs environment grounding
- [ ] Include a system check in the startup context
| `delegates_to` | yes | **yes — affects launch order** | Source delegates work to target |
| `spawned_by` | yes | **yes — affects launch order** | Target was spawned by source |
| `can_observe` | yes | no | Source can observe target's output |
| `collaborates_with` | yes | no | Peer collaboration |
| `escalates_to` | yes | no | Source escalates to target |
## What Edges Actually Do Today (OpenRig 0.1.x)
### Launch ordering
Only `delegates_to` and `spawned_by` affect runtime behavior. They constrain the order in which nodes are launched:
- **`delegates_to`**: source launches BEFORE target. The delegator must be up before the delegate.
- **`spawned_by`**: target (parent) launches BEFORE source (child). The parent must be up before the child it spawned.
This ordering is enforced in both `PodRigInstantiator` (initial launch) and `RestoreOrchestrator` (restore from snapshot). The code uses topological sort over the dependency graph — if there's a cycle, instantiation fails.
All other edge kinds (`can_observe`, `collaborates_with`, `escalates_to`) do NOT constrain launch order.
### Graph visualization
All edges are rendered in the UI topology graph. They create visual connections between nodes, helping the operator understand the team structure. Pod-local edges appear within the pod group; cross-pod edges connect across groups.
### Identity projection
All edges appear in `rig whoami --json` output under `edges.outgoing` and `edges.incoming`, with their kind and the connected peer's identity. This lets an agent know its relationship to other members — but the agent interprets this information, it's not enforced by the runtime.
### Attach hint heuristic
The post-command handoff (the "Attach:" line after `rig up` and `rig restore`) uses edges to prefer the orchestrator as the default attach target. The heuristic looks for the first node with `delegates_to` outgoing edges.
## What Edges Do NOT Do Today
Edges do NOT currently:
- **Route messages** — `rig send` can target any session, regardless of edges
- **Enforce delegation** — an agent can communicate with any peer, not just its edge targets
- **Control permissions** — there's no edge-based access control
- **Affect transport** — `rig capture`, `rig broadcast`, etc. work based on session identity, not edge topology
These are aspirational capabilities. The edge vocabulary is intentionally richer than the current runtime behavior so that the topology captures design intent even before the runtime enforces it.
## Design Intent (Why We Have Five Kinds)
The five kinds represent a taxonomy of how agents relate to each other in a working team:
**`delegates_to`** — the most common edge. An orchestrator delegates to an implementer. A lead delegates to a worker. This is the primary workflow direction. When an orchestrator sends a task, it uses `rig send` to the session it `delegates_to`.
**`spawned_by`** — for hierarchical launch relationships. A subagent spawned by a parent process. Less common in current topologies.
**`can_observe`** — the review/oversight relationship. A reviewer observes an implementer's work. The reviewer can `rig capture` and `rig transcript` the implementation session. This edge communicates intent: "I'm watching your output."
**`collaborates_with`** — peer relationships. Two agents working side-by-side at the same level of the hierarchy. Neither delegates to the other.
**`escalates_to`** — the reverse of delegation. When a worker hits a decision it can't make, it escalates to a lead. This is less common in current topologies but represents a real coordination pattern.
## Choosing Edge Kinds
### Common Patterns
**Orchestrator → workers:**
```yaml
edges:
- kind:delegates_to
from:orch.lead
to:dev.impl
```
**Reviewer → implementer:**
```yaml
edges:
- kind:can_observe
from:rev.r1
to:dev.impl
```
**Implementation pair:**
```yaml
edges:
- kind:delegates_to
from:impl
to:qa
```
### When You're Not Sure
If you're unsure which edge kind to use:
1. Does one agent give work to the other? → `delegates_to`
2. Does one agent watch the other's output? → `can_observe`
3. Are they equals working together? → `collaborates_with`
4. Does one report problems up? → `escalates_to`
5. Did one create the other? → `spawned_by`
If none fit, `can_observe` is the safest default — it documents the relationship without implying a workflow direction and doesn't affect launch ordering.
## Validation Rules
1. Edge kinds must be one of: `delegates_to`, `spawned_by`, `can_observe`, `collaborates_with`, `escalates_to`
2. Pod-local edges use unqualified member IDs — both `from` and `to` must exist in the same pod
3. Cross-pod edges use `pod.member` format — both endpoints must resolve to actual members
4. Cross-pod edges must reference different pods (same-pod edges should use the pod-local syntax)
5. Self-edges are not validated at the schema level but are not meaningful
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.
---
## Archive Format
A `.rigbundle` file is a gzip-compressed tar archive (`.tar.gz`) with a fixed structure.
### File extension
The archive MUST have the `.rigbundle` extension. The packer rejects output paths that don't end with `.rigbundle`.
### Sibling digest file
Every `.rigbundle` has a sibling `.rigbundle.sha256` file containing the SHA-256 hex digest of the archive. This detects corruption during transfer. The unpacker verifies this digest before extraction.
| `path` | string | yes | Relative path to the vendored import within the archive. |
| `original_ref` | string | yes | The original import ref before rewriting. |
| `hash` | string | yes | SHA-256 hash of the imported agent.yaml. |
### Integrity Section
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `algorithm` | string | yes | Must be `sha256`. |
| `files` | map<string, string> | yes | Map of archive-relative file path → SHA-256 hex hash. Every file in the archive (except `bundle.yaml` itself) should be listed. |
---
## Security Model
Bundle integrity provides **self-consistency verification, not authenticity**.
- The sibling `.sha256` file detects corruption during transfer
- The per-file integrity hashes detect tampering of individual files within the archive
- Neither mechanism authenticates the bundle author
An attacker who can rewrite the full bundle + digest can bypass verification. Users must trust the source they obtained the bundle from. This is the same trust model as unsigned npm packages and Docker images.
Future enhancement: cryptographic signing (Ed25519) for author authentication.
---
## Safety Guarantees
The unpacker enforces these safety rules before extraction:
1.**No symlinks or hardlinks** — `SymbolicLink` and `Link` entries are rejected
2.**No absolute paths** — entries starting with `/` are rejected
3.**No path traversal** — entries containing `..` segments are rejected
4.**Digest verification** — archive SHA-256 must match the sibling `.sha256` file
5.**Content integrity** — after extraction, per-file hashes are verified against the manifest
If any check fails, extraction is aborted and an error is thrown.
| `<spec-path>` | yes | — | Path to the rig spec YAML file. |
| `-o, --output` | yes | — | Output path. Must end with `.rigbundle`. |
| `--rig-root` | no | spec directory | Root directory for resolving `agent_ref` and other relative paths. |
| `--name` | no | `my-bundle` | Bundle name in the manifest. |
| `--bundle-version` | no | `0.1.0` | Bundle version in the manifest. |
The create command:
1. Validates the rig spec
2. Resolves all `agent_ref` paths and their imports
3. Vendors all agent specs, resources, and startup files into a staging directory
4. Rewrites `agent_ref` paths to bundle-relative `local:` refs
5. Collects culture file, docs files, and rig-level startup files
6. Computes per-file integrity hashes
7. Writes the manifest (`bundle.yaml`)
8. Packs into a deterministic `.tar.gz`
9. Writes the sibling `.sha256` digest
### Inspect a bundle
```bash
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.
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`.
### Launch directly
```bash
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
-`--cwd <dir>` does **not** change the install target; it only overrides the launched members' working directory for that run
---
## Assembly Process
When `rig bundle create` runs, the `PodBundleAssembler` performs these steps:
- Copy the agent spec and all its resources (skills, guidance, hooks, startup, runtime resources)
- Recursively resolve and copy imports
- Record the agent entry in the manifest with its hash
- Rewrite the ref to a bundle-relative `local:` path
4.**Write the rewritten rig spec** to the staging directory
5.**Compute integrity** — SHA-256 of every file in the staging directory
6.**Write the manifest** (`bundle.yaml`) with all entries and integrity
7.**Pack** the staging directory into a deterministic tar.gz
### Terminal nodes
Members with `agent_ref: "builtin:terminal"` are bundle-native sentinels. They are not vendored — the runtime handles them directly.
### Deduplication
If multiple members reference the same agent spec (same resolved path), the spec is vendored once and all members' refs are rewritten to the same bundle-relative path.
### Import resolution
When an agent spec has `imports`, each import is resolved, vendored into the bundle, and the import refs in the vendored agent.yaml are rewritten to bundle-relative `local:` paths. Import entries are recorded in the manifest's agent entry.
---
## Validation Rules Summary
### Manifest validation (schema version 2)
1.`schema_version` must be `2`
2.`name` is required non-empty string
3.`version` is required non-empty string
4.`created_at` is required non-empty string
5.`rig_spec` is required and must be a safe relative path
6.`agents` must be an array
7. Each agent must have `name`, `path` (safe relative), and `hash`
8. Integrity `algorithm` must be `sha256`
9. Integrity `files` must be a non-empty map of safe-relative-path → 64-char hex hash
### Archive safety (enforced on unpack)
1. No symlinks or hardlinks
2. No absolute paths
3. No `..` path traversal
4. Archive digest must match sibling `.sha256`
5. Per-file content hashes must match integrity section
### Assembly validation
1. Rig spec must validate
2. All `agent_ref` paths must resolve to valid agent specs
3. All declared `docs` files must exist on disk (missing docs fail assembly)
4. Culture file and startup files are collected best-effort (missing = skipped)
---
## Legacy Bundles (Schema Version 1)
Schema version 1 bundles are the pre-reboot format using flat-node rig specs and package-based bundling. They are still supported for backward compatibility but should not be created for new rigs.
Key differences from v2:
-`schema_version: 1` in the manifest
-`packages` array instead of `agents` array
- Package entries have `original_source` instead of `original_ref`
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.
Source of truth: `packages/daemon/src/domain/rigspec-schema.ts`, `packages/daemon/src/domain/types.ts`
This is the canonical reference for the pod-aware RigSpec YAML format. Every field, validation rule, and default documented here was traced from the actual parser and validator code, not from prior documentation.
---
## Minimal Valid Example
```yaml
version:"0.2"
name:my-rig
pods:
- id:dev
label:Development
members:
- id:impl
agent_ref:"local:agents/impl"
profile:default
runtime:claude-code
cwd:"."
edges:[]
edges:[]
```
## Complete Example (all features)
```yaml
version:"0.2"
name:my-product-team
summary:A full product squad with orchestration, development, and review pods.
| `version` | string | yes | — | Must be `"0.2"` for pod-aware specs. |
| `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). |
| `docs` | Doc[] | no | — | Documentation files that should travel with the rig. Included in rig bundles. Each entry has a `path` field (safe relative path). The engine does not consume these — they are for humans and agents setting up the environment before launch. |
| `startup` | StartupBlock | no | — | Rig-level startup files and actions. Applied to all members via the startup layering model. |
| `services` | ServicesBlock | no | — | Optional managed services (Docker Compose). When present, services boot before any agent launches. |
| `pods` | Pod[] | yes | — | At least one pod required. Each pod is a bounded context containing members and pod-local edges. |
| `edges` | CrossPodEdge[] | no | `[]` | Cross-pod edges connecting members in different pods. Must use fully-qualified `pod.member` IDs. |
---
## Pod
| Field | Type | Required | Default | Description |
| `id` | string | yes | — | Pod identifier. Must not contain dots. Must be unique within the rig. Used as the first segment of session names and logical IDs. |
| `label` | string | yes | — | Human-readable pod name. Shown in UI explorer, graph groupings, and detail surfaces. |
| `summary` | string | no | — | Pod description. |
| `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. |
| `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. |
| `model` | string | no | — | Model override. Runtime-specific (e.g., `claude-opus-4-6` for Claude Code). |
| `restore_policy` | string | no | `resume_if_possible` | Restore behavior. One of: `resume_if_possible`, `relaunch_fresh`, `checkpoint_only`. |
| `startup` | StartupBlock | no | — | Member-level startup files and actions. Applied only to this member. |
### Terminal Nodes
Terminal nodes are infrastructure processes (servers, log tails, build watchers) that are not agent runtimes. They require an exact triple:
```yaml
runtime:terminal
agent_ref:"builtin:terminal"
profile:none
```
All three must be present together. Any partial combination is a validation error.
### agent_ref Rules
- Must start with `local:` or `path:`
-`local:` paths are relative to the rig spec file's directory (the rig root)
-`path:` paths are absolute filesystem paths
- The referenced path must contain an `agent.yaml` file
- Exception: `builtin:terminal` for terminal nodes
### Session Naming
The canonical session name is derived from the pod ID, member ID, and rig name:
```
{podId}-{memberId}@{rigName}
```
Example: pod `dev`, member `impl`, rig `my-team` → session `dev-impl@my-team`
This is human-authored (you choose the pod/member IDs) and system-validated (the system enforces the format).
---
## Edges
### Edge Kinds
| Kind | Meaning | Use When |
|------|---------|----------|
| `delegates_to` | Source delegates work to target. Constrains launch order. | Orchestrator → implementer, lead → worker |
| `spawned_by` | Target was spawned by source. Constrains launch order. | Parent → child in hierarchical topologies |
| `can_observe` | Source can observe target's output. Does NOT constrain launch order. | Reviewer → implementer, monitor → worker |
| `collaborates_with` | Peer collaboration relationship. Does NOT constrain launch order. | Co-equal peers working together |
| `escalates_to` | Source escalates to target for decisions. Does NOT constrain launch order. | Worker → lead for escalation |
### Pod-Local Edges
Edges within a pod use **unqualified member IDs** (just the member `id`, not `pod.member`):
```yaml
pods:
- id:dev
members:
- id:impl
# ...
- id:qa
# ...
edges:
- kind:delegates_to
from:impl # NOT dev.impl
to:qa # NOT dev.qa
```
Both `from` and `to` must reference members that exist in the same pod.
### Cross-Pod Edges
Edges between pods use **fully-qualified `pod.member` IDs**:
```yaml
edges:
- kind:delegates_to
from:orch.lead # pod.member format
to:dev.impl # pod.member format
```
Cross-pod edges must reference different pods. An edge where both `from` and `to` are in the same pod is a validation error — use pod-local edges instead.
---
## Startup Block
Startup blocks can appear at three levels: rig, pod, and member. They are merged additively via the startup layering model (see `docs/reference/startup-layering.md`).
### Files
| 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). |
| `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`. |
---
## Services Block
The services block is optional. When present, services boot before any agent node launches. If service health checks fail, agent launch is blocked.
| Field | Type | Required | Default | Description |
| `kind` | string | yes | — | Service backend. Only `compose` is supported in v1. |
| `compose_file` | string | yes | — | Relative path to the Docker Compose file. Must be a safe relative path. Resolved relative to rig root. |
| `project_name` | string | no | derived from rig name | Docker Compose project name. Must match `[a-z0-9][a-z0-9_-]*`. If omitted, derived by sanitizing the rig name. |
| `profiles` | string[] | no | — | Compose profiles to activate. |
| `down_policy` | string | no | `down` | What happens on `rig down`. One of: `leave_running`, `down`, `down_and_volumes`. |
| `wait_for` | WaitTarget[] | no | — | Health targets that must pass before agent launch. |
| `surfaces` | Surfaces | no | — | Metadata about accessible URLs and commands. Not executed — informational only. |
| `checkpoints` | CheckpointHook[] | no | — | Shell commands for checkpoint export/import during snapshot/restore. |
### Wait Targets
Each target must define exactly one of `service`, `url`, or `tcp`:
```yaml
wait_for:
# HTTP probe — hits the URL, expects 2xx
- url:http://127.0.0.1:8200/v1/sys/health
# TCP probe — connects to host:port
- tcp:"127.0.0.1:5432"
# Compose health check — requires Docker health to report "healthy"
- service:postgres
condition:healthy
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | one of three | HTTP URL to probe. |
| `tcp` | string | one of three | `host:port` for TCP probe. |
| `service` | string | one of three | Compose service name. Requires `condition: healthy`. |
| `condition` | string | only with `service` | Must be `healthy`. Only valid with `service` targets. |
### Surfaces
```yaml
surfaces:
urls:
- name:Vault UI
url:http://127.0.0.1:8200/ui
commands:
- name:Vault status
command:"vault status -address=http://127.0.0.1:8200"
```
Surfaces are metadata only. They are displayed in the UI and in `rig env status` output but are NOT executed by OpenRig.
| `id` | string | yes | Unique identifier for this checkpoint. |
| `export` | string | yes | Shell command to export state. `{{artifacts_dir}}` is replaced with a daemon-managed path. |
| `import` | string | no | Shell command to import state on restore. |
Checkpoint hooks are shell commands run by the daemon. They are best-effort — a failed export does not block snapshot, but continuity is classified as `receipt_only` instead of `checkpointed`.
---
## Continuity Policy
Optional pod-level configuration for compaction recovery behavior.
This release hardens startup-state truth, improves transcript cleanup, and makes restore/recovery behavior more reliable across Claude and Codex managed sessions.
## Included In This Release
- Normalize user-clearable startup blockers so they do not masquerade as hard failures.
- Clear stale Claude startup/error residue more aggressively.
- Improve Codex restore continuity detection and resume metadata capture.
- Fix transcript redraw and status-noise cleanup.
- Harden Claude managed-session startup around MCP-related blockers.
- Improve operational reliability around tmux health and instantiate timing.
## User-Facing Changes
- Nodes blocked on interactive startup prompts are now normalized as user-clearable startup blockers instead of being left in misleading stale-failure states.
- Claude startup state is cleaned up more aggressively when current runtime evidence proves the old failure residue is stale.
- Codex restore behavior is more robust when resuming wrapped processes and when the TUI has scrolled or rendered extra footer/header noise.
- Transcript cleanup is less noisy and better at removing redraw/status artifacts without leaving the terminal output in a confusing state.
## Operator / Setup Notes
- After upgrading, restart the daemon so new startup and restore behavior is active:
```bash
rig daemon stop
rig daemon start
```
- This release improves classification and recovery behavior, but it does not turn every interrupted runtime into guaranteed native continuity. Operators should still treat restore results honestly and use manual recovery paths when the product says continuity could not be proved.
## Known Limitations
- Lifecycle/state handling is more truthful than before, but OpenRig still does not have a fully explicit infrastructure-grade state machine.
- Restore remains runtime-specific and evidence-driven; some recovery paths still rely on heuristics rather than perfect provider-native primitives.
## Verification Performed
- Targeted daemon and CLI test coverage during the release workstream.
- Focused review and validation of the transcript-cleanup change set.
- Fresh-VM npm-install verification of the published `@openrig/cli` package, including daemon start, health route, doctor checks, `rig ps --nodes`, and UI load.
## Commits Included
- `8b07461` `cli: detect unhealthy tmux control sockets`
- `8ca8e43` `daemon: ignore stale codex update banner once tui is live`
- `a8adfe2` `daemon: capture codex resume ids from wrapped processes`
- `e4619cf` `daemon: recognize resumed codex tui after header scrolls`
- `30cf604` `daemon: accept codex footer with trailing blank lines`
- `c44507f` `harden restore continuity and recovery guidance`
- `105ed7e` `fix Claude MCP startup blockers for managed sessions`
?`Compact-plan marshal check for ${node.logicalId}`
:`Compact-plan evidence needed for ${node.logicalId}`;
constsession=recipient??node.logicalId;
consttext=status==="candidate_with_caveats"
?[
`Read-only compact-plan flagged ${session} for one-seat-at-a-time Claude continuity triage.`,
`Reasons: ${reasons.join(", ")}.`,
`Before any compaction: verify checkpoint/restore evidence, get explicit operator authorization, compact only this seat, and audit restore after compact-in-place.`,
`No automatic compaction has been run.`,
].join(" ")
:[
`Read-only compact-plan cannot safely plan ${session} yet.`,
nextAction:"Verify checkpoint/restore evidence, get explicit authorization, compact one Claude seat, then audit restore using claude-compact-in-place.",
console.log("Next action: verify checkpoint/restore evidence, get explicit authorization, compact one Claude seat only, then audit restore with claude-compact-in-place.");
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-id>/README.md\nmissions/<mission-id>/PROGRESS.md\nmissions/<mission-id>/slices/<slice-id>/README.md\n```\n";
case"artifacts":
return"# artifacts\n\nWork products live here: plans, drafts, generated outputs, and other files that a slice may reference before closure.\n";
case"evidence":
return"# evidence\n\nHuman-readable verification notes live here. Use this for compact proof summaries that are not tied to a screenshot or video packet.\n";
case"progress":
return"# progress\n\nPROGRESS.md tree. OpenRig's Progress browse view scans this directory recursively for PROGRESS.md files and renders them as a hierarchical tree.\n";
case"field-notes":
return"# field-notes\n\nOperator field notes. Free-form markdown notes from your daily work. OpenRig surfaces these alongside missions and slices for context.\n";
return"# dogfood-evidence\n\nProof packets live here. Each proof packet folder is matched to a Project slice by folder-name tokens and may contain markdown, screenshots, videos, traces, and other verification artifacts.\n";
default:
return`# ${subdir}\n`;
}
}
constWORKSPACE_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 ids for mission and slice folders. Keep slice ids unique inside the workspace.
`;
constSTEERING_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.)
thrownewError(`manifest at ${manifestPath} files[${i}].path '${relPath}' has an unsupported suffix; allowed: ${Array.from(ALLOWED_CONTEXT_PACK_SUFFIXES).join(", ")}`);
thrownewError(`A context pack named '${installName}' already exists at ${targetDir}. Remove it first or use --name to install under a different name.`);
}
cpSync(sourceDir,targetDir,{recursive: true});
// Sync the daemon library so the new pack appears immediately.
reason:"On macOS, scrolling and text selection inside tmux panes is much smoother with mouse mode enabled.",
fix:"Run `tmux set -g mouse on` for the current tmux server. To keep it enabled, add `set -g mouse on` to `~/.tmux.conf` and reload with `tmux source-file ~/.tmux.conf`.",
});
}
}
}else{
constfailure=buildTmuxControlFailure(tmuxProbe.detail??"unknown tmux control failure");
checks.push({
name:"tmux",
status:"fail",
message: failure.message,
reason: failure.reason,
fix: failure.fix,
});
}
// 5. Writable state paths (shared with preflight)
// 5. cmux shell check (optional but recommended for Open CMUX workflows)
?` Likely cause on macOS: automation.socketControlMode is '${socketMode.mode}' in ${CMUX_SETTINGS_DISCLOSURE_PATH}.`
:"";
checks.push({
name:"cmux_shell",
status:"warn",
message:"cmux installed, but control unavailable right now.",
reason:"OpenRig can run without cmux, but Open CMUX actions and cmux-aware node control will be unavailable until cmux control works.",
fix:`Open the cmux app, verify control access/socket sharing is enabled for OpenRig, then rerun 'rig doctor'. If you are running this for someone else, tell the user that cmux is optional but required for Open CMUX. If you do not need cmux features, you can ignore this warning.${modeHint}`,
});
}catch{
checks.push({
name:"cmux_shell",
status:"warn",
message:"cmux not found.",
reason:"OpenRig can run without cmux, but Open CMUX actions and surface control will be unavailable.",
fix:"Install and launch cmux if you want Open CMUX support. If you are running this for someone else, tell the user that cmux is optional and only needed for Open CMUX workflows.",
});
}
voiderr;
}
// 6. Writable state paths (shared with preflight)
?`cmux is still using its default automation.socketControlMode '${socketMode.mode}', so the daemon cannot attach as an external cmux client yet.`
:`automation.socketControlMode is '${socketMode.mode}' in ${CMUX_SETTINGS_DISCLOSURE_PATH}, so the daemon cannot attach as an external cmux client yet.`;
return{
name:"cmux_daemon",
status:"warn",
message:"Shell cmux works, but the daemon cannot control cmux.",
reason,
fix:`Run \`rig setup\` to set automation.socketControlMode to "automation" in ${CMUX_SETTINGS_DISCLOSURE_PATH}, then restart the daemon with \`rig daemon start\`.`,
};
}
return{
name:"cmux_daemon",
status:"warn",
message:"Shell cmux works, but the daemon cannot control cmux.",
reason:"The daemon inherited a terminal/session environment that broke cmux adapter initialization.",
fix:"Restart the daemon with `rig daemon start` after setup. If it still fails, inspect the daemon log with `rig daemon logs`.",
"no session_meta record in source JSONL; supply --source-session-id-override <id> AND --source-rig-override <rig> to set provenance explicitly. Silent fallback to file-path-as-session-id is not allowed (M2c-CLI R2 honest-provenance policy).",
);
}
sourceSessionId=parsedSessionId;
}else{
// hasSession path: operator-named session is authoritative.
`could not derive source_rig from session id '${sourceSessionId}' (does not match <seat>@<rig> shape); supply --source-rig-override <rig> explicitly. Silent fallback to source_rig:"unknown" is not allowed (M2c-CLI R2 honest-provenance policy).`,
"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.",
)
.option("--kind <kind>","Workspace kind to validate against: user | project | knowledge | lab | delivery")
.option("--no-recursive","Do not descend into subdirectories")
.option("--require-frontmatter","Report a gap for every .md file without a frontmatter delimiter")
.option("--max-files <n>","Hard cap on .md files walked","10000")
return`ssh to host=${hostId} (target=${target}) failed: ${oneLine(result.sshStderr)}. Verify SSH access and host registry config.`;
case"permission-gate":
return`ssh to host=${hostId} (target=${target}) hit a permission/auth gate: ${oneLine(result.sshStderr)}. ${result.hint??""}`.trim();
case"remote-daemon-unreachable":
return`remote rig command on host=${hostId} could not reach the remote daemon (exit=${result.remoteExitCode}): ${oneLine(result.stderr||result.stdout)}. Start the daemon on the remote with 'ssh ${target} rig daemon start'.`;
case"remote-command-failed":
return`remote rig command on host=${hostId} failed (exit=${result.remoteExitCode}): ${oneLine(result.stderr||result.stdout)}`;
error:`host registry not found at ${path}. Create it with a 'hosts:' array of { id, transport, target, [user] } entries; v0 supports transport: ssh only.`,
};
}
letraw: string;
try{
raw=readFileSync(path,"utf-8");
}catch(err){
return{ok: false,error:`failed to read host registry at ${path}: ${(errasError).message}`};
}
letparsed: unknown;
try{
parsed=parseYaml(raw);
}catch(err){
return{ok: false,error:`failed to parse host registry YAML at ${path}: ${(errasError).message}`};
`This bounded transcript contains the latest ${latest.length} extracted messages for normal restore. Use transcript.md only when deeper forensic recovery is needed.`,
);
}
functionbuildTranscriptFull(
structured: StructuredTranscript,
sourceSessionId: string,
):string{
returntranscriptMarkdownFromMessages(
"Full Transcript",
sourceSessionId,
structured.messages,
"This transcript contains developer/user/assistant messages extracted from the source. Reasoning records, tool calls, and tool outputs are omitted by design (see restore-summary.json `omitted_classes`).",
:"- No paths detected in extracted messages or tool arguments.";
return`# Touched Files And Path Inventory\n\nBest-effort path inventory from messages and tool arguments. This is a triage aid, not a complete proof of all files read or written.\n\n${rows}\n`;
3.Read\`transcript-latest.md\` for bounded current context.
4.Use\`transcript.md\` only if the bounded transcript is insufficient.
5.State:"restored from packet generated by ${opts.generatorVersion}; current rig is ${opts.targetRig}; default target repo is ${opts.defaultTargetRepo ?? "none"}."
6.Nameanycaveatsbeforeresumingwork.
7.Handofftonextowner:\`${opts.nextOwner}\` (or "self" to continue).
"Path or URI pointing to the role guidance file the restored seat should consume. REQUIRED per v0 standard field #9; resolves at restore time.",
},
bounded_latest_transcript:{
type:"object",
required:["path","message_count","bound"],
properties:{
path:{type:"string"},
message_count:{
type:"integer",
minimum: 0,
description:"Actual count of extracted messages in transcript-latest.md.",
},
bound:{
type:"integer",
const:120,
description:
"v0 bound is fixed at 120 messages to match Velocity prior art (per contract § 7). v1+ may make configurable; v0 schema requires const 120 so packets with the wrong bound cannot pass.",
},
},
},
full_transcript:{
type:"object",
properties:{
path:{type:"string"},
line_count:{type:"integer",minimum: 0},
},
description:
"Optional per v0 standard field #11. Generator emits when full transcript material is available (e.g., from --source-jsonl or full-read --source-session); omitted otherwise.",
},
touched_files:{
type:"object",
required:["path","top_paths"],
properties:{
path:{type:"string"},
top_paths:{
type:"array",
items:{
type:"object",
required:["path","count"],
properties:{
path:{type:"string"},
count:{type:"integer",minimum: 0},
},
},
},
},
},
durable_pointers:{
type:"object",
required:[
"queue_pointers",
"progress_pointers",
"field_note_pointers",
"artifact_pointers",
],
properties:{
queue_pointers:{
type:"array",
items:{type:"string"},
description:"References to durable queue items (qitem ids or queue-file paths).",
},
progress_pointers:{
type:"array",
items:{type:"string"},
description:"References to PROGRESS.md cursors the source seat was operating against.",
},
field_note_pointers:{
type:"array",
items:{type:"string"},
description:"References to field-notes folders or files.",
"description":"Path or URI pointing to the role guidance file the restored seat should consume. REQUIRED per v0 standard field #9; resolves at restore time."
},
"bounded_latest_transcript":{
"type":"object",
"required":["path","message_count","bound"],
"properties":{
"path":{"type":"string"},
"message_count":{"type":"integer","minimum":0,"description":"Actual count of extracted messages in transcript-latest.md."},
"bound":{"type":"integer","const":120,"description":"v0 bound is fixed at 120 messages to match Velocity prior art (per contract § 7). v1+ may make configurable; v0 schema requires const 120 so packets with the wrong bound cannot pass."}
}
},
"full_transcript":{
"type":"object",
"properties":{
"path":{"type":"string"},
"line_count":{"type":"integer","minimum":0}
},
"description":"Optional per v0 standard field #11. Generator emits when full transcript material is available (e.g., from --source-jsonl or full-read --source-session); omitted otherwise."
"queue_pointers":{"type":"array","items":{"type":"string"},"description":"References to durable queue items (qitem ids or queue-file paths)."},
"progress_pointers":{"type":"array","items":{"type":"string"},"description":"References to PROGRESS.md cursors the source seat was operating against."},
"field_note_pointers":{"type":"array","items":{"type":"string"},"description":"References to field-notes folders or files."},
"description":"REQUIRED per v0 standard field #13. Pointers to durable work the source seat was operating against; restored seat reads to recover work-context."
message:"tmux installed, but the default control socket is unhealthy.",
reason:`OpenRig uses tmux control commands to launch, inspect, and manage agent sessions. The default tmux socket is returning: ${message}`,
fix:"Capture any needed state from visible tmux panes, then restart the default tmux server before retrying OpenRig. If this happened after a machine restore, treat it as attention required rather than a healthy running state.",
// Should have the same structure as non-refresh output
expect(json.summary.total).toBeDefined();
expect(process.exitCode).toBeUndefined();
});
});
Loaded 100 of 882 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.