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]>
description:Something OpenRig does is wrong, or a command fails
labels:["bug"]
body:
- type:markdown
attributes:
value:|
Thanks. Include a small reproduction and the relevant versions.
This report will be public. Remove API keys, tokens, private prompts, personal details and private paths from commands, logs and configuration.
For a suspected vulnerability, follow our [security policy](https://github.com/mvschwarz/openrig/security/policy) instead of posting details here.
- type:input
id:version
attributes:
label:OpenRig version
description:Output of `rig --version`
placeholder:0.5.14(2edc1b7a)
validations:
required:true
- type:input
id:platform
attributes:
label:OS, Node and tmux versions
placeholder:macOS 15.6, Node 22.22.1, tmux 3.5a
validations:
required:true
- type:dropdown
id:harness
attributes:
label:Harnesses involved
multiple:true
options:
- None / not sure
- Claude Code
- Codex
- Pi / Oh My Pi
- Other (say which below)
validations:
required:true
- type:textarea
id:repro
attributes:
label:What you ran and what happened
description:The command(s), relevant redacted output, and what you expected instead. Paste text where you can; screenshots are useful for visual issues.
render:shell
validations:
required:true
- type:textarea
id:state
attributes:
label:Instance state that might matter
description:Fresh install or upgrade? Include only relevant redacted lines from status output or your rig spec; do not upload full transcripts, credentials or your instance directory.
- type:checkboxes
id:checks
attributes:
label:Checks
options:
- label:I searched existing issues and Discussions for this.
required:true
- label:I am on the latest published version, or I said which version above.
description:Something you want OpenRig to do that it does not
labels:["enhancement"]
body:
- type:markdown
attributes:
value:|
For open-ended ideas or "how should this work" questions, [Discussions › Ideas](https://github.com/mvschwarz/openrig/discussions/categories/ideas) is the better place. Use this form when you can name the outcome you want.
This request will be public; use examples without credentials, private prompts or personal details.
- type:textarea
id:outcome
attributes:
label:What are you trying to do, and what stops you today?
description:Describe the situation, not the solution. One real example beats a general case.
validations:
required:true
- type:textarea
id:proposal
attributes:
label:What would you like OpenRig to do?
description:A command, a setting, a behaviour. Rough is fine.
<!-- Thanks for the PR. Three short answers are all a reviewer needs. -->
## What a user gets
<!-- One or two sentences: the behaviour before, the behaviour after. Link the issue if there is one. -->
## How you verified it
<!-- What you actually ran and saw, and which revision/local changes you tested. The documented checks are `npm run build`, `npm test`, and `npm run lint`; say what you could not run. Redact credentials and private information from logs or screenshots. -->
## Anything you were unsure about
<!-- Design choices, edge cases you did not cover, places a reviewer should look hardest. Empty is a fine answer. -->
## If this is security-related
<!-- Exploitable? Report it privately first (SECURITY.md). Otherwise: how does this go wrong for someone using OpenRig as designed (your own machine or a trusted private network), with evidence? Who causes it, and how do they reach the install? What does the change cost everyone else? See CONTRIBUTING.md. -->
- [ ] One concern per PR; no version bump; no `CHANGELOG.md` edit
- [ ] Tests added or updated where the change is testable
- [ ] I listed the checks I ran, their results, and any checks I could not run
A harness wraps a model. A rig wraps your harnesses. Define your agent team in YAML, boot it with one command. Claude Code and Codex in the same rig, managed as one system.
OpenRig turns AI coding agents from a pile of terminal sessions into a persistent, organized team. Talk to a lead agent about the outcome you want; it can coordinate specialists across teams and bring you results and decisions that need your attention. Start with a repository and one useful change, then keep the team's work and context at the same addresses.
**Guide:** [Getting started](docs/reference/getting-started.md) · **Stuck?** [Help](docs/reference/help.md) · **Questions:** [Q&A](https://github.com/mvschwarz/openrig/discussions/92) · **Updates and demos:** [@_feralmachine on X](https://x.com/_feralmachine)
## See it running

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

*Captured from the interactive TUI demo using fictional project data.*
With herdr installed and connected, open the starter's terminals together:
```bash
rig terminal open first-project --provider herdr
```
For cmux, use `--provider cmux`. In the TUI, a rig's detail view has a `term ▸ rig <name>` link that opens every running seat of that rig in the default terminal provider; with herdr that is up to 16 seats per tab, in a workspace named after the rig. The underlying sessions remain accessible through tmux. See the [terminal workspace guide](docs/reference/getting-started.md#share-the-dashboard-and-return-to-it) for setup and returning to an existing view.
- **CLI**: 40+ commands designed for both humans and agents. Every mutating command ends with what happened, current state, and next action.
- **UI**: Explorer sidebar, topology graph with pod grouping, node detail panel, system log, chatroom.
- **MCP**: 17 tools so agents can manage their own topology (`rig_up`, `rig_ps`, `rig_send`, `rig_chatroom_send`, etc.)
- **Runtimes**: Claude Code, Codex, and terminal nodes. Adapters for Pi and OpenCode in development.
- **AgentSpec**: Reusable agent blueprint with skills, guidance, hooks, profiles, and startup contracts.
- **Seat**: A stable role and address in a rig, such as `dev-owner@first-project`. The conversation occupying it can change while its identity and authored context remain.
- **Pod**: A group of related seats with shared guidance and context. Each agent still has its own context window.
- **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.
@@ -262,83 +126,13 @@ rig send vault-specialist@secrets-manager "Check Vault health and report status.
Requires Docker for service-backed rigs.
## Upgrading an existing instance
For an existing installation, follow the [upgrade procedure](skills/_canonical/core/openrig-upgrade/SKILL.md) and the [0.5.14 release notes](docs/releases/v0.5.14.md). Preserve live seats during the upgrade; `rig down` is not an upgrade step. Upgrading to 0.6.0 also requires Node.js 22 or 24: see [Moving off Node 20](#moving-off-node-20) and the [0.6.0 release notes](docs/releases/v0.6.0.md).
### Moving off Node 20
OpenRig 0.6.0 supports Node.js 22 and 24 only. Its SQLite binding
(better-sqlite3 13) requires Node 22 or newer. Node 20 is no longer supported;
the install check refuses it with an explanation.
If you run OpenRig on Node 20, switch Node first, then reinstall the CLI under
the new Node (a version manager keeps a separate global package set for each
Node):
```bash
nvm install 22# or 24; fnm or your package manager work the same way
npm install -g @openrig/cli
rig --version
```
Your existing OpenRig data stays where it is. The daemon reopens the same
database under the new binding and applies any pending migrations in place.
Restart the daemon under the new Node by following the upgrade procedure above.
### Crossing the 0.5.9 layout boundary
The migration below still applies when upgrading from a pre-0.5.9 instance.
0.5.9 makes `$OPENRIG_HOME/context` the addressable context library, writes
Claude telemetry to `state/context-usage` (and provider telemetry to
`state/provider-usage`), and installs the default System World at
`context/system/system-world.yaml`. Existing instances cross this boundary by
an **Agent-Operated Migration** from the shipped `openrig-upgrade` skill. The
target runtime reads canonical-first with legacy-fallback while new writes use
the canonical roots; a custom context-library root stays stable during
activation. This is not a directory rename to do while an old collector writes.
```bash
# SKILL_DIR is the installed openrig-upgrade skill directory.
`--help` prints the phase grammar without inventorying the instance. No phase
flag intentionally runs the read-only plan; unknown options fail nonzero before
plan or mutation.
Every phase emits JSON. Stop on any issue or incomplete receipt and follow its
`next` action; do not continue from copied legacy telemetry or retry a partial
mutation blindly. Preparation leaves legacy state and collector settings in
place. Verification accepts exact tail bytes only when that same seat has newer
paired context and provider samples under `state/`; finalization revalidates the
accepted tails, copies the library without overwrite, and switches config last.
The helper never removes the legacy telemetry or library. Retirement follows
separate stable runtime, writer, reader, and recovery proof. Daemon, database,
seat, plugin, and release lifecycle actions remain agent-owned.
## Requirements
- Node.js 22 or 24 (the supported versions in this release). Node 20 is no
longer supported. Node 26 and other versions are untested. On a Mac with Apple
silicon, use Node.js 22: see the [compatibility history](docs/releases/v0.5.15.md#known-compatibility-limitation).
- Node.js 20, 22, or 24 (even-numbered LTS releases; odd releases like 25 lack native addon prebuilds)
- tmux
- macOS or Linux. Native Windows is not supported yet, and WSL2 has not been tested
Optional:
-herdr or cmux for terminal workspaces showing the agents together
-cmux for `Open CMUX` node surface controls
- Docker for service-backed rigs and managed apps
## Setup and Troubleshooting
@@ -349,31 +143,30 @@ Optional:
Both commands support `--json` for agent-driven workflows.
Before setup or managed launch, review [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine), including provider trust, hooks and selected runtime resources.
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:**First ask which available provider(s) the user wants, reusing an explicit choice. Recommend the working account they already have and follow the selected-provider guide above. Offer core setup (`rig setup`) or the fuller workstation path (`rig setup --full`) only when they want those additional tools. Inspect the result with `--json` and use `rig doctor` to finish any remaining machine-specific issues. When OpenRig itself misbehaves, read [docs/reference/help.md](docs/reference/help.md) (installed agents can run `rig context get help`; the same text is at [openrig.dev/help/agents](https://www.openrig.dev/help/agents)): it covers the next step, known problems, and how to reach the team if you're still stuck.
**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
OpenRig is open source and self-hosted, with Claude Code and Codex in the same team. You operate it on your own infrastructure; the selected providers' model usage costs still apply.
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.
"description":"OpenRig is an open-source multi-agent harness: define a team of Claude Code and Codex agents in YAML, boot it with the rig CLI, and coordinate the agents through tmux sessions, a durable queue and a local daemon.",
"folders":[],
"excludeFolders":[
"docs/releases",
"packages/daemon/specs",
"packages/daemon/test",
"packages/test-system",
"skills/_canonical/process",
"skills/_canonical/pm",
"spike",
"docker",
"**/node_modules",
"**/fixtures"
],
"excludeFiles":[],
"rules":[
"Install with npm install -g @openrig/cli. OpenRig requires Node.js 22 or 24 and tmux; the first-project starter also requires an authenticated Codex CLI.",
"Preview before launching: rig specs preview <spec>, then rig up <spec> --cwd . --plan, then rig up <spec> --cwd .",
"Check seat readiness with rig ps --nodes --rig <rig>, not only daemon health from rig status.",
"Inside a rig, an agent runs rig whoami --json first and treats its output as its identity.",
"rig send <session> \"message\" types into a peer's terminal and presses Enter; rig capture <session> reads that peer's screen.",
"Work another agent must act on goes in the queue (rig queue create or rig queue handoff); a message informs but does not assign work.",
"Use rig start to bring rigs back after a reboot or daemon restart.",
"Run rig --help for the current command surface; the CLI is authoritative over older examples."
ca-certificates curl xz-utils git tmux tini procps \
python3 make g++;\
rm -rf /var/lib/apt/lists/*
# --- layer 2: pinned node (an in-range engines version — see scripts/check-engines) ---
ARGNODE_VERSION=22.22.1
ARG TARGETARCH
RUNset -eux;\
case"${TARGETARCH}" in \
amd64)NODE_ARCH=x64 ;;\
arm64)NODE_ARCH=arm64 ;;\
"")echo"TARGETARCH is empty — the legacy (non-BuildKit) builder does not populate it. Pass --build-arg TARGETARCH explicitly (build-testbed-image.sh does) or enable BuildKit. REFUSING to default to amd64, which would install wrong-arch Node." >&2;exit1;;\
| L3 | daemon-in-container: boots vs container-local sqlite, healthz on the published port, `rig up` settles a zero-token stub topology | `L3-daemon-in-container.md` |
| L4 | the 51-02 hermetic contract: the env-helper still REFUSES a foreign `OPENRIG_URL`**inside** the container (fail-closed not weakened by "we're in a container anyway") | `L4-hermetic-fail-closed.md` |
| L5 | multi-host: N containers as N distinct named self-hosts on one docker network, composed via the shipped host registry over HTTP (plan §3) — **and** the 51-09 live-leg rider | `L5-multi-host-and-51-09.md` |
| L6 | the 51-02 runner's **container-mode** (step-3) drives a REAL scenario through a container by manifest identity + host-mode byte-intact parity + the L4 fail-closed step-3 form; the ledger carries the image manifest id | `L6-container-runner-e2e.md` |
Sequencing: **L0 → build → L1 → L2 → L3 → L4 → L5**, with **L6 after L3** (it needs only the built image
+ an in-container daemon, plus the built HOST CLI bin for the read side). L3 is where the stub payload is
exercised (and where `docker/testbed/stub-assets.list` is finalized). L4 depends on the 51-02 hermetic
env-helper being present in the packaged image (it ships in the openrig package the Dockerfile installs);
if it is NOT packaged, its fail-closed leg is exercised in the **step-3 form** — L6.3.
**51-09 live-leg rider (orch, merge desk).** The testbed image's first multi-host run doubles as the
51-09 live-leg executor: L5 boots the containers as the two named hosts `H_A`/`H_B` and then runs the
shipped 51-09 collision/alignment runbook **in the same host-side session** —
`packages/daemon/test/fixtures/self-host-live-legs/RUNBOOK.md` (LEG A cross-host stamped-triple
round-trip, LEG B founder-collision). Execute both families together; return both evidence sets.
## Shared conventions
**Image identity.** All legs run against the image the build verb tagged, by manifest identity:
OpenRig is a local control plane for multi-agent coding topologies — a
multi-agent harness that manages your Claude Code and Codex sessions as a
single system, with a daemon (`@openrig/daemon`), a CLI (`@openrig/cli`), a UI
(`@openrig/ui`), and an MCP server all sitting on one SQLite-backed core. This
tree is the **source-verified** description of that system as it actually
ships — every load-bearing claim is grounded to `packages/*/src` at a named
commit, not to memory, chat, or older docs.
> Verified against source at HEAD `7eaf524c` (`git describe` →
> `v0.3.1-6-g7eaf524c`). Package version is **0.3.1** across all three
> packages; HEAD carries 6 commits of unreleased 0.3.2 work, no `v0.3.2` tag.
## How this tree is organized
The as-built corpus was modularized (slice 08, `context-architecture-v1`) from
two monolithic files into a folder of thematic modules, each independently
loadable, each frontmatter-tagged for retrieval, each ≤300 lines (author-mode
modules with no prior prose may run to ≤400 — see the slice-08 ACK).
```
docs/as-built/
├── README.md ← you are here: map-of-territory + index
├── codemap.md ← navigation index (use-case lookup, source-root pointers)
├── frontmatter-schema.md← the frontmatter convention these docs follow
├── cli-reference.md ← full rig CLI surface (kept whole)
├── architecture/ ← 14 backend/runtime modules
└── ui/ ← 4 operator-surface modules
```
`docs/DESIGN.md` (the canonical visual / brand / design-system spec) **stays
at the repo `docs/` root** by design — it is referenced by many existing
`docs/DESIGN.md` paths and carries no source drift. This tree points at it
(see `ui/library-specs-and-design-system.md`); it is not copied here.
## Module index
### `architecture/` — backend, daemon, runtime
| Module | What it covers |
|---|---|
| [daemon-core.md](architecture/daemon-core.md) | How the daemon boots, `createDaemon` wiring, the SQLite schema/40-migration set, the route-mount surface. |
| [adapters-and-runtimes.md](architecture/adapters-and-runtimes.md) | The five-method RuntimeAdapter contract, the Claude/Codex/Terminal adapters (launch/resume/fork in tmux), and the resume-honesty layer (honest resumed-vs-fresh assessment). |
| [coordination-primitive.md](architecture/coordination-primitive.md) | PL-004 Phase A stream/queue/inbox/outbox; the hot-potato closure contract; where queue closure is enforced. |
| [content-surfaces.md](architecture/content-surfaces.md) | The operator-allowlisted file browser, atomic conflict-checked writes + JSONL edit audit, PROGRESS.md tree indexer, the one-screen Steering composer. |
| [living-notes-review.md](architecture/living-notes-review.md) | **v0.4.4** — the Living Notes review surface: the ONE `ComposedSliceReview` (intent→plan→delivered) projection, pure composer + gatherer, staged-approval locks, C1-bound `verified`, `/api/review/*` routes, freeze export, ranged media serving, and the SDLC on-disk convention it projects. |
| [plugin-agent-image-context-pack.md](architecture/plugin-agent-image-context-pack.md) | The filesystem-canonical content layer — plugin discovery, agent images, context packs, the Claude auto-compaction policy enforcer. |
| [architecture-rules-and-event-system.md](architecture/architecture-rules-and-event-system.md) | The cross-cutting invariants — the 25 architecture rules, the RigEvent union + SSE delivery, intentional compatibility limits. |
### `ui/` — operator surfaces
| Module | What it covers |
|---|---|
| [shell-and-routing.md](ui/shell-and-routing.md) | The UI shell (rail / Explorer / center workspace / drawer / preview stack), the actual route tree the shipped UI mounts, the shared detail drawer + event consumption. |
| [project-and-for-you.md](ui/project-and-for-you.md) | The operator destination surfaces — the For-You attention feed (5-card classifier + verb actions), the Project workspace/mission/slice scope pages, the Dashboard landing on the vellum brand system. |
| [library-specs-and-design-system.md](ui/library-specs-and-design-system.md) | The Library (`/specs`) UI — specs/skills/plugins/agent-images surfaces, the spec-review + spec-library + live-identity flows, the design-system pointer. |
### Root docs
| Doc | What it covers |
|---|---|
| [codemap.md](codemap.md) | Navigation index — module map, structural relationship diagram, fast-lookup-by-use-case, source-root pointer table. Start here when you know *what* you need but not *which module*. |
| [cli-reference.md](cli-reference.md) | The full `rig` CLI surface — command groups, subcommands, flags, JSON output, cross-host, coordination primitives. Kept as one doc (slice-08 Q4). |
| [frontmatter-schema.md](frontmatter-schema.md) | Which frontmatter convention governs every doc in this tree, plus the one as-built-unique field (`last-verified-against-source`). |
| `../DESIGN.md` | The canonical visual / brand / design-system spec (lives at `docs/` root by design; pointer only — not duplicated here). |
## Source-grounding contract
Every module declares `last-verified-against-source: <commit-sha>` — the
commit its claims were checked against. Load-bearing corrections are recorded
inline with an auditable annotation (`> Drift-fix Dx — said X; corrected to Y;
slice-00 §z; re-confirmed <file:line> @HEAD`). OPEN items (counts that are
definitional or runtime-only) are carried verbatim, never smoothed. The
schema is defined in [frontmatter-schema.md](frontmatter-schema.md) and the
governing convention is `openrig-work/conventions/frontmatter-for-context/`.
MID_WORK_PATTERNS)`); legacy mid-work check at `:666`.
3. Two-step tmux send: a unique file/buffer pasted with `paste-buffer -d -r -p` at every payload size → ~200ms delay → separate `C-m`. Bracketed paste preserves multiline input in supporting TUIs; the payload never enters a shell argument. A successful paste proves transport execution, not runtime consumption.
| `/mission-control` | **`<Navigate to="/for-you" />`** redirect-stub (DELETED per SC-18; the Mission Control *system* lives at daemon/PL-005, not a UI destination) | `:440-444` |
- UI live identity surfaces: `packages/ui/src/components/LiveIdentityDisplay.tsx`, `packages/ui/src/components/LiveNodeDetails.tsx`
- UI data layer: `packages/ui/src/hooks/*`
OpenRig is a local control plane for multi-agent coding topologies — a
multi-agent harness that manages your Claude Code and Codex sessions as a
single system. The daemon (`@openrig/daemon`) is the framework-free
SQLite-backed core; the CLI (`@openrig/cli`), UI (`@openrig/ui`), and MCP
server all sit on top of it.
## Daemon
> Source-verified at HEAD `7eaf524c` (`v0.3.1-6-g7eaf524c`); package version
> **0.3.1**, 6 unreleased 0.3.2 commits on HEAD, no `v0.3.2` tag. Frame
> sentence triangulated against `architecture/daemon-core.md` §1 +
>`openrig-internal/product/positioning.md` ("a multi-agent harness, a local
> control plane that manages your Claude Code and Codex sessions as a single
> system").
### 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, 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.
## (b) Module map
### 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/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/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`.
| Module | One-sentence summary | Reach for this when… |
|---|---|---|
|`daemon-core.md` | The framework-free SQLite-backed core the CLI/UI/MCP all sit on. | You need to know how the daemon boots, how `createDaemon` wires the dependency graph, the SQLite schema/migration set, or the route-mount surface. |
|`adapters-and-runtimes.md` | The five-method runtime-adapter contract + the resume-honesty layer. | You need the runtime-adapter contract, how OpenRig launches/resumes a Claude Code, Codex, or terminal harness inside tmux, or how the daemon honestly assesses whether a harness actually resumed vs fresh-launched. |
|`coordination-primitive.md` | PL-004 Phase A: the SQLite-canonical durable-work layer (`/api/stream`, `/api/queue`). | You need the stream/queue/inbox/outbox tables, the hot-potato closure contract, the transactional handoff guarantee, or where queue closure is enforced. |
|`workflow-runtime.md` | PL-004 Phase D: turns an intended sequence of work into durable SQLite state. | You need workflow specs cache, instance state, step trails, the transactional-scribe projection contract, or the watchdog policy set including workflow-keepalive. |
| `mission-control.md` | PL-005: the daemon-backed queue-observability surface inside the existing shell. | You need the seven views, the seven write verbs, the action audit table, the bearer-token middleware, or how queue observability maps to PL-004 sources. |
| `agent-spec-and-startup.md` | How authored YAML specs become a resolved, launched, identity-addressable topology. | You need the AgentSpec/RigSpec/pod-aware reboot types, profile resolution + additive startup layering, the StartupOrchestrator delivery split, or how whoami/materialize/bind/adopt resolve identity. |
| `lifecycle-snapshot-restore.md` | The durable-state half of `down → up → handoff`. | You need how OpenRig captures a snapshot, restores a rig (resume vs rebuild vs fresh), enforces restore honesty, consults live continuity, or how restore-check / restore-packet readiness probes work. |
| `transport-and-transcripts.md` | The communication-and-history layer; tmux is transport, not truth. | You need how rig send/capture/broadcast works, pipe-pane transcript capture + search, durable SQLite chat, what `rig ask` gathers, or the MCP-tool-name vs tmux-metadata-key distinction. |
| `workspace-primitive.md` | PL-007: the typed declaration of where a rig's work lives. | You need how a rig declares a typed workspace (root/repos/defaultRepo/knowledgeRoot), how it persists/resolves into whoami / node-inventory, how per-item `target_repo` scope is validated, or how the file-backed missions/slices tree is indexed and projected into Project. |
| `content-surfaces.md` | The operator-allowlisted, filesystem-canonical read/write layer Project/Steering sits on. | You need how the file browser enforces path safety, how atomic conflict-checked writes + the JSONL edit audit work, how the PROGRESS.md tree is indexed, or how the one-screen Steering surface is composed. |
| `plugin-agent-image-context-pack.md` | Four filesystem-canonical content primitives the daemon discovers and serves. | You need how OpenRig discovers plugins, captures/forks agent images, assembles/sends context packs, or how the Claude auto-compaction enforcer decides to send `/compact`. |
| `packaging-bootstrap-bundles.md` | How a topology is packaged into a shareable bundle and reconstituted elsewhere. | You need how rig/pod bundles are assembled (schema-v2 vs legacy v1), how bundle create/inspect/install + `/api/up` route across source kinds, the staged BootstrapOrchestrator flow, or which legacy install-engine seams still ship. |
| `architecture-rules-and-event-system.md` | The cross-cutting invariants that belong to no single subsystem. | You need the 25 architecture rules + startup/import constraints, the shape of the RigEvent union and its SSE delivery, or the intentional compatibility limits that still describe the shipped system. |
| `living-notes-review.md` | **v0.4.4** — the one intent→plan→delivered review projection over on-disk SDLC markdown. | You need the `ComposedSliceReview` contract, the `/api/review/*` routes, staged-approval locks (`--scope spec|delivery`), the `## Proof contract` ↔ proof-artifact join and C1-bound `verified`, the freeze export, or ranged media serving for review evidence. |
### 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/ui/src/components/mission-control/components/CliDriftIndicator.tsx`: Cross-CLI-version drift indicator (PRD sub-clause 4). Shows fleet-level "rigs running stale CLI" badge + per-row "field unavailable on this rig's daemon version" placeholders via the MissingFieldPlaceholder export.
- `packages/ui/src/components/mission-control/hooks/{useMissionControlView,useMissionControlAction,useMissionControlCliCapabilities}.ts`: TanStack Query hooks fetching from `/api/mission-control/{views/:name,action,cli-capabilities}`.
| Module | One-sentence summary | Reach for this when… |
|---|---|---|
|`shell-and-routing.md` | The shell-first, route-first, primitive-driven operator surface. | You need how the UI shell (rail / Explorer / center workspace / drawer / preview stack) is assembled, the actual route tree the shipped UI mounts, or how the shared detail drawer and event consumption work. |
|`topology.md` | The operator's live picture of host → rig → pod → seat. | You need how the topology surface is built — the host hybrid graph, the table/terminal views, the activity-ring / hot-potato visual language, terminal-preview popovers, and the navigation/overlay contracts. |
|`project-and-for-you.md` | The three operator-facing destination surfaces (For You / Project / Dashboard). | You need how the For-You attention feed (5-card classifier + verb actions), the Project workspace/mission/slice scope pages, and the Dashboard landing on the vellum brand system are built. |
|`library-specs-and-design-system.md` | The Library (`/specs`) destination + the design-system pointer. | You need how the Library UI is assembled — the spec/skills/plugins surfaces, the spec-review + spec-library + live-identity flows that feed it — or where the canonical visual/design-system spec lives. |
- `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`.
| Doc | One-sentence summary | Reach for this when… |
|---|---|---|
|`cli-reference.md` | The full `rig` CLI surface, kept as one doc. | You need the exact rig CLI surface — command groups, subcommands, flags, JSON output, cross-host, coordination primitives. |
|`frontmatter-schema.md` | The frontmatter convention these docs follow + the as-built-unique field. | You are authoring or updating a doc under `docs/as-built/`. |
|`../DESIGN.md` | The canonical visual / brand / design-system spec (repo `docs/` root). | You need the visual system, brand identity, or design-system tokens. (Stays at root by design; pointer only.) |
### Domain: Core Runtime
- `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, 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`.
## (c) Structural relationship diagram
### Domain: Specs, Resolution, Startup
- `packages/daemon/src/domain/agent-manifest.ts`: Canonical `agent.yaml` parser, validator, and normalizer for AgentSpec files. Exports: normalizeAgentSpec, parseAgentSpec, validateAgentSpec. 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/agent-preflight.ts`: Lightweight preflight for a single `agent_ref`, used before full rig instantiation. Exports: agentPreflight. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/types.js`.
- `packages/daemon/src/domain/pod-repository.ts`: SQLite-backed repository for pods and live continuity-state rows. Exports: PodRepository. Related: `packages/daemon/src/domain/types.js`.
- `packages/daemon/src/domain/profile-resolver.ts`: Applies AgentSpec defaults, profile uses, startup layering, and restore-policy narrowing to produce a `ResolvedNodeConfig`. Exports: QualifiedResource, ResolutionContext, ResolutionResult, ResolvedNodeConfig, ResolvedResources, resolveNodeConfig. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/startup-resolver.js`, `packages/daemon/src/domain/types.js`.
- `packages/daemon/src/domain/projection-planner.ts`: Converts resolved node config plus collision data into runtime projection entries, diagnostics, conflicts, and no-ops. Exports: PlanResult, ProjectionClassification, ProjectionEntry, ProjectionFsOps, ProjectionInput, ProjectionPlan, planProjection. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/profile-resolver.js`, `packages/daemon/src/domain/types.js`.
- `packages/daemon/src/domain/rigspec-codec.ts`: YAML codec layer for both legacy flat-node RigSpec and rebooted pod-aware RigSpec shapes. Exports: LegacyRigSpecCodec, RigSpecCodec. Related: `packages/daemon/src/domain/types.js`.
- `packages/daemon/src/domain/rigspec-exporter.ts`: Exports live rigs back into RigSpec YAML/JSON. Dual-format: detects pod-aware rigs (podId non-null) and exports canonical v0.2 RigSpec with pods/members/edges; legacy rigs export as flat-node v1. Exports: RigSpecExporter. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/pod-repository.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`, `packages/daemon/src/domain/types.js`.
- `packages/daemon/src/domain/rigspec-instantiator.ts`: Dual-stack instantiation layer containing both legacy `RigInstantiator` and rebooted `PodRigInstantiator`. Exports: PodRigInstantiator, RigInstantiator. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/node-launcher.js`, `packages/daemon/src/domain/pod-repository.js`, +10 more.
- `packages/daemon/src/domain/rigspec-preflight.ts`: Dual-stack preflight layer: legacy `RigSpecPreflight` plus rebooted pure `rigPreflight`. Exports: RigPreflightInput, RigSpecPreflight, rigPreflight. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/profile-resolver.js`, `packages/daemon/src/domain/rig-repository.js`, +4 more.
- `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, 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`.
- `packages/daemon/src/domain/startup-validation.ts`: Shared validation and normalization for startup files and startup actions. Exports: normalizeStartupBlock, validateStartupAction, validateStartupBlock, validateStartupFile. Related: `packages/daemon/src/domain/path-safety.js`, `packages/daemon/src/domain/types.js`.
Built from each module's `siblings` / `prerequisite-reads` frontmatter.
`daemon-core.md` is the spine (prerequisite for almost every architecture
- `packages/daemon/src/domain/install-repository.ts`: SQLite-backed repository for package install records and journal entries. Exports: Install, InstallRepository, InstallSummary, JournalEntry. Related: none.
- `packages/daemon/src/domain/install-verifier.ts`: Post-apply verification for install records and filesystem targets. Exports: Check, EntryVerification, InstallVerifier, VerificationResult, VerifierFsOps. Related: `packages/daemon/src/domain/install-repository.js`, `packages/daemon/src/domain/package-repository.js`.
- `packages/daemon/src/domain/package-install-service.ts`: Top-level package validate/plan/install/rollback service used by routes and bootstrap. Exports: PackageInstallOutcome, PackageInstallService. Related: `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/install-engine.js`, `packages/daemon/src/domain/install-planner.js`, `packages/daemon/src/domain/install-policy.js`, +4 more.
- `packages/daemon/src/domain/package-repository.ts`: SQLite-backed repository for package metadata and install summaries. Exports: Package, PackageRepository, PackageSummary. Related: none.
- `packages/daemon/src/domain/package-resolve-helper.ts`: Shared helper that wraps package resolution for routes and orchestrators. Exports: ResolveResult, resolvePackage. Related: `packages/daemon/src/domain/package-manifest.js`, `packages/daemon/src/domain/package-resolver.js`.
- `packages/daemon/src/domain/package-resolver.ts`: Resolves local package sources and computes manifest hashes. Exports: FsOps, PackageResolver, ResolvedPackage. Related: `packages/daemon/src/domain/package-manifest.js`.
- `packages/daemon/src/domain/path-safety.ts`: Shared safe-path validator used by manifests and startup config. Exports: validateSafePath. Related: none.
- `packages/daemon/src/domain/role-resolver.ts`: Filters package exports according to selected roles/runtime scope. Exports: DeferredExport, ResolvedExports, resolveExports. Related: `packages/daemon/src/domain/package-manifest.js`.
workspace-primitive.md ──▶ content-surfaces.md
(PL-007 declaration) (filesystem read/write layer on top)
│ │
└──────────┬───────────────┘
▼ (UI counterpart)
ui/project-and-for-you.md
### Domain: Bootstrap, Discovery, Bundles
- `packages/daemon/src/domain/bootstrap-orchestrator.ts`: Staged bootstrap workflow that composes runtime verification, requirement probes, external installs, package installs, legacy rig instantiation, direct pod-aware rig handling, and schema-version-2 pod-bundle delegation. Exports: BootstrapMode, BootstrapOptions, BootstrapOrchestrator, BootstrapResult, BootstrapStageResult. Related: `packages/daemon/src/domain/bootstrap-repository.js`, `packages/daemon/src/domain/bootstrap-types.js`, `packages/daemon/src/domain/bundle-source-resolver.js`, `packages/daemon/src/domain/external-install-executor.js`, +10 more.
- `packages/daemon/src/domain/bootstrap-repository.ts`: SQLite-backed repository for bootstrap runs and action journals. Exports: BootstrapRepository. Related: `packages/daemon/src/domain/bootstrap-types.js`.
- `packages/daemon/src/domain/bootstrap-types.ts`: Shared type definitions for bootstrap data. Exports: ActionKind, ActionStatus, BootstrapAction, BootstrapRun, BootstrapStatus, RuntimeStatus, RuntimeVerification. Related: none.
- `packages/daemon/src/domain/bundle-archive.ts`: Archive pack/unpack and digest verification for `.rigbundle` files. Exports: pack, unpack, verifyArchiveDigest. Related: `packages/daemon/src/domain/bundle-integrity.js`, `packages/daemon/src/domain/bundle-types.js`.
- `packages/daemon/src/domain/external-install-planner.ts`: Turns requirement-probe results into auto-approvable or manual external install actions. Exports: ApprovalClassification, ExternalInstallAction, ExternalInstallPlan, ExternalInstallPlanner. Related: `packages/daemon/src/adapters/shell-quote.js`, `packages/daemon/src/domain/requirements-probe.js`.
- `packages/daemon/src/domain/pod-bundle-assembler.ts`: Rebooted bundle assembler that vendors AgentSpecs, imports, rig startup files, and rewritten `agent_ref`s. Exports: PodAssembleOptions, PodAssembleResult, PodAssemblerFsOps, PodBundleAssembler. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/bundle-types.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`, +1 more.
- `packages/daemon/src/domain/requirements-probe.ts`: Requirement-probe registry and probe result types used during bootstrap. Exports: ProbeResult, ProbeStatus, RequirementSpec, RequirementsProbeRegistry. Related: `packages/daemon/src/adapters/shell-quote.js`, `packages/daemon/src/adapters/tmux.js`.
- `packages/daemon/src/domain/demo-rig-selector.ts`: Helper for existing-rig power-on — finds the right rig by name from ps summary entries. Exports: selectCurrentRigSummary. Related: none.
- `packages/daemon/src/domain/draft-rig-generator.ts`: Synthesizes a candidate RigSpec YAML from discovered sessions — groups by CWD, suggests pods, sanitizes IDs. Exports: generateDraftRig. Related: `packages/daemon/src/domain/discovery-types.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/types.js`.
- `packages/daemon/src/domain/native-resume-probe.ts`: Honest assessment of whether a harness actually resumed vs fresh-launched — probes pane command and content for runtime-specific indicators. Exports: NativeResumeProbeResult, assessNativeResumeProbe, buildNativeResumeCommand, isProbeShellReady. Related: `packages/daemon/src/adapters/shell-quote.js`.
- `packages/daemon/src/domain/resume-metadata-refresher.ts`: Post-launch resume token capture — reads Claude conversation IDs from state files and Codex thread IDs from SQLite. Exports: ResumeMetadataRefresher. Related: `packages/daemon/src/domain/codex-thread-id.js`, `packages/daemon/src/domain/native-resume-probe.js`, `packages/daemon/src/domain/session-registry.js`, `packages/daemon/src/adapters/tmux.js`.
- `packages/daemon/src/domain/codex-thread-id.ts`: Codex-specific thread ID extraction from the Codex SQLite state database. Exports: defaultResolveHomeDirByPid, readCodexThreadIdFromCandidateHomes, ResolveHomeDirByPid. Related: none.
Reading-order rule: open a module's `prerequisite-reads` first
(`README.md`, then `daemon-core.md` for most architecture modules;
`README.md` then `shell-and-routing.md` for most ui modules).
### 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).
| I need to know… | → see |
|---|---|
| How the daemon boots / `createDaemon` wiring / migration set / route mounts | `architecture/daemon-core.md` |
| The runtime-adapter contract / how a harness is launched or resumed in tmux | `architecture/adapters-and-runtimes.md` |
| Where Claude resume honesty lives / resumed-vs-fresh assessment | `architecture/adapters-and-runtimes.md` |
| Where queue closure is enforced / hot-potato contract / durable handoff | `architecture/coordination-primitive.md` |
| How workflow specs project on closure / transactional-scribe / watchdog policies | `architecture/workflow-runtime.md` |
| The Mission Control views/verbs / queue-observability / action audit | `architecture/mission-control.md` |
| Which frontmatter these docs use + the as-built-unique field | `frontmatter-schema.md` |
### 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`.
- `packages/daemon/src/routes/bootstrap.ts`: Hono route module for bootstrap API endpoints. Exports: bootstrapRoutes. 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/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`.
- `packages/daemon/src/routes/rigs.ts`: Hono route module for rig CRUD, graph projection, summary, delete, and existing-rig power-on by rig id. Exports: rigsRoutes. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/graph-projection.js`, `packages/daemon/src/domain/node-inventory.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`.
- `packages/daemon/src/routes/rigspec.ts`: Hono route module for dual-format RigSpec import, validate, preflight, materialize, and export. Pod-aware import/preflight/materialize require `X-Rig-Root`; export emits v0.2 pod-aware YAML/JSON when the live rig has pods. Exports: handleExportJson, handleExportYaml, rigspecImportRoutes. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-exporter.js`, `packages/daemon/src/domain/rigspec-instantiator.js`, +2 more.
- `packages/daemon/src/routes/sessions.ts`: Hono route module for rig sessions and node inventory/detail/launch/focus. `GET /api/rigs/:rigId/nodes/:logicalId` is the backend contract for the live node drawer and full-details workspace. Exports: nodesRoutes, sessionsRoutes. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/domain/node-inventory.js`, `packages/daemon/src/domain/node-launcher.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`.
- `packages/daemon/src/routes/snapshots.ts`: Hono route module for snapshots API endpoints. Exports: restoreRoutes, snapshotsRoutes. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/restore-orchestrator.js`, `packages/daemon/src/domain/snapshot-capture.js`, `packages/daemon/src/domain/snapshot-repository.js`.
- `packages/daemon/src/routes/transport.ts`: Hono route module for communication primitives (`POST /send`,`/capture`, `/broadcast`). Resolves session targets, delegates to SessionTransport. Exports: transportRoutes. Related: `packages/daemon/src/domain/session-transport.js`.
- `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`). 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`.
## (e) Source-root pointer table
## CLI
Module → primary `packages/*/src/...` roots. This replaces the old per-file
`Exports:`/`Related:` dump: it points at the code, it does not transcribe it.
Source-grounding for each claim lives inside the module with file:line cites.
### 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 (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/bootstrap.ts`: Commander command module for `bootstrap`. Supports library-name resolution before falling back to raw source refs. Exports: bootstrapCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/specs.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/bind.ts`: Commander command module for `bind` — bind a discovered session to an existing logical node. Exports: bindCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
-`packages/cli/src/commands/bundle.ts`: Commander command module for `bundle`. Supports dual-format bundle create/inspect/install, `--json`, and pod-aware `--rig-root` on create. Exports: bundleCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.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. 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`.
> No per-file flat dump. File-level detail belongs in the code; this codemap
> points at the source root and the module that explains it. The per-file
> index anti-pattern (old codemap) is retired, not relocated.
### Commands: Post-North-Star Additions
- `packages/cli/src/commands/send.ts`: Commander command module for `send` — send message to agent terminal via reliable tmux two-step. Supports `--verify` and `--force`. Exports: sendCommand. Related: `packages/cli/src/client.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/cli/src/commands/chatroom.ts`: Commander command module for `chatroom` — durable group chat with subcommands `send`, `watch`, `history`, and `topic`. Exports: chatroomCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
### CLI Infrastructure: Post-North-Star Additions
- `packages/cli/src/config-store.ts`: Config file management at `~/.openrig/config.json` with legacy fallback from `~/.rigged/config.json`. 5 locked keys with env var overrides. Precedence: CLI flag > env var > config file > default. Exports: ConfigStore and config helpers. Related: none (filesystem-only).
- `packages/cli/src/system-preflight.ts`: System readiness checks: Node.js version, tmux availability, writable directories, port availability. Every error follows the 3-part pattern (what + why + fix). Exports: SystemPreflight, PreflightCheck, PreflightResult. Related: `packages/cli/src/config-store.js`, `packages/cli/src/daemon-lifecycle.js`.
## UI
### App Entry And Routing
- `packages/ui/src/App.tsx`: Thin app wrapper that renders the TanStack Router provider. Exports: App. Related: `packages/ui/src/routes.js`.
- `packages/ui/src/globals.css`: Tailwind/theme tokens and global UI styling. Exports: none. Related: none.
- `packages/ui/src/main.tsx`: Bootstraps the React app, fonts, and global styles. Exports: none. Related: `packages/ui/src/App.js`, `packages/ui/src/globals.css`.
- `packages/ui/src/routes.tsx`: Declares the explorer-first route tree and root shell composition, including specs review/library routes, discovery/specs drawer bridges, and live-node full-details route `/rigs/$rigId/nodes/$logicalId`. Exports: router. Related: `packages/ui/src/components/AppShell.js`, `packages/ui/src/components/BootstrapWizard.js`, `packages/ui/src/components/BundleInspector.js`, `packages/ui/src/components/BundleInstallFlow.js`, +13 more.
### Components: Application Surfaces
- `packages/ui/src/components/ActivityFeed.tsx`: Rolling global activity/event overlay driven by SSE. Exports: ActivityFeed. Related: `packages/ui/src/hooks/useActivityFeed.js`.
- `packages/ui/src/components/BootstrapWizard.tsx`: Multi-step bootstrap planning/apply flow UI for both rig specs and `.rigbundle` archives. Includes the spec-or-bundle entry field and bundle-inspector handoff link. Exports: BootstrapWizard. Related: `packages/ui/src/components/RequirementsPanel.js`, `packages/ui/src/hooks/useBootstrap.js`.
- `packages/ui/src/components/BundleInspector.tsx`: Bundle inspection screen for manifest and integrity results. Renders `agents[]` plus schema-version badge for v2 pod bundles and legacy package content for v1 bundles. Exports: BundleInspector. Related: `packages/ui/src/hooks/useBundles.js`.
- `packages/ui/src/components/BundleInstallFlow.tsx`: Bundle install plan/apply screen. Labels the install as pod-aware or legacy from bootstrap `resolve_spec.detail.source`. Exports: BundleInstallFlow. Related: `packages/ui/src/hooks/useBundles.js`.
- `packages/ui/src/components/Dashboard.tsx`: Home dashboard that renders rig cards and high-level rig actions. Exports: Dashboard. Related: `packages/ui/src/components/RigCard.js`, `packages/ui/src/hooks/mutations.js`, `packages/ui/src/hooks/usePsEntries.js`, `packages/ui/src/hooks/useRigSummary.js`.
- `packages/ui/src/components/WorkspacePage.tsx`: Shared center-workspace page shell used by specs/library/live-details surfaces. Exports: WorkspacePage. Related: none.
- `packages/ui/src/components/SpecsWorkspace.tsx`: Local persisted workspace state for spec authoring/review tasks, drafts, and active task list. Exports: SpecsDraft, SpecsTask, SPECS_WORKSPACE_STORAGE_KEYS, SpecsWorkspaceProvider, useSpecsWorkspace. Related: none.
- `packages/ui/src/components/PackageDetail.tsx`: Package detail screen with install history, journal, and rollback actions. Exports: PackageDetail. Related: `packages/ui/src/hooks/usePackageDetail.js`.
- `packages/ui/src/components/PackageList.tsx`: Legacy package summary list/grid screen presented under the rebooted `SPECS` navigation, with an empty-state handoff into RigSpec import. Exports: PackageList. Related: `packages/ui/src/hooks/usePackages.js`.
- `packages/ui/src/components/RigCard.tsx`: Dashboard card for one rig with counts, status, and primary actions. Exports: RigCard, RigSummary. Related: `packages/ui/src/hooks/useCountUp.js`, `packages/ui/src/hooks/usePsEntries.js`.
- `packages/ui/src/components/RigGraph.tsx`: Rig graph view that renders topology, discovery overlays, and focus interactions. Exports: RigGraph. Related: `packages/ui/src/components/RigNode.js`, `packages/ui/src/hooks/useDiscovery.js`, `packages/ui/src/hooks/useRigEvents.js`, `packages/ui/src/hooks/useRigGraph.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. 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. 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/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. 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 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.
- `packages/ui/src/lib/restore-status-colors.ts`: Color-class lookup for restore states. Exports: getRestoreStatusColorClass. Related: none.
- `packages/ui/src/lib/status-colors.ts`: Generic rig/session status color mapping. Exports: getStatusColorClass. Related: none.
- `packages/ui/src/lib/utils.ts`: Shared UI utility helpers, currently `cn` class merging. Exports: cn. 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.
| [`ui/project-and-for-you.md`](ui/project-and-for-you.md) | The operator destination surfaces — Project observability (workspace/mission/slice scope tabs), the For-You attention feed (5-card classifier + queue actions), the Dashboard landing on the vellum brand system. |
| [`ui/library-specs-and-design-system.md`](ui/library-specs-and-design-system.md) | The Library (`/specs`) UI — specs/applications/skills surfaces, the graphics layer, the current design constraints, and the design-system pointer to `../DESIGN.md`. |
`AppShell` wraps every route with:
The brand/design rules still live at `docs/DESIGN.md` (repo `docs/` root, by
design — see `ui/library-specs-and-design-system.md`).
- Desktop rail
- Route-aware Explorer sidebar
- Center workspace
- Shared detail drawer
- Preview stack
- Topology overlay state provider
- Shared drawer selection context
- Shared discovery placement context
The rail destinations are:
- Dashboard: `/`
- Topology: `/topology`
- For You: `/for-you`
- Project: `/project`
- Library: `/specs`
- Settings: `/settings`
- Advisor and Operator placeholders/settings links
The Explorer is contextual. It changes its tree based on the current destination
rather than behaving as a generic file browser.
## Route Model
Main routes:
- `/` - Dashboard
- `/topology` - host topology scope
- `/topology/rig/$rigId` - rig scope
- `/topology/pod/$rigId/$podName` - pod scope
- `/topology/seat/$rigId/$logicalId` - seat scope with live node details
- `/for-you` - attention feed
- `/project` - workspace project scope
- `/project/mission/$missionId` - mission scope
- `/project/slice/$sliceId` - slice scope
- `/specs` - Library
- `/specs/applications` - application section
- `/specs/skills/$skillToken` - skill viewer defaulting to `skill.md`
- `/specs/skills/$skillToken/file/$fileToken` - specific skill file viewer
| `/markdown` | **NOT a route** — `MarkdownViewer` is a component used inside `MissionScopePage` | (no route) | grep `routes.tsx`@HEAD: zero `path: "/markdown"` |
Unknown runtime resource types are still copied to the runtime extension directory for agent-visible context.
#### Writing a `codex_config_fragment`
**Begin the fragment with a table header.** A fragment must be a valid TOML
document on its own, and every key it sets must sit under a table it declares.
A fragment that puts keys ahead of its first table header is refused at
projection time, and nothing is written.
The reason is TOML's grammar rather than a policy choice. The managed block is
appended to the end of the user's `config.toml`, and TOML has no syntax for
returning to document root once a table has been opened. So if the user's file
ends inside any table, an appended root-level key does not land at root — it
silently becomes a member of *their* table. Refusal is deterministic and never
inspects the user's file: a fragment author cannot see user state, and a rule
that passed or failed depending on it would be impossible to reproduce.
```toml
# refused — `model` would bind into whatever table the user's file ends inside
model = "gpt-5"
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
```
```toml
# accepted — every key sits under a table this fragment declares
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
```
Where a fragment's table is one the user already declares, the user's table
wins: the managed table is dropped, their values are never merged, rewritten or
overwritten, and the rest of the fragment still applies.
### Resource Path Rules
- All resource paths must be safe relative paths (no `..` traversal, no absolute paths)
@@ -383,12 +350,6 @@ The startup block follows the same format as in the RigSpec (see `docs/reference
Agent-level startup is applied to all profiles. Profile-level startup is applied only when that profile is active. Both merge additively via the startup layering model.
An additional orientation challenge requires an explicit `startup_proof` action
with `value: authenticated` and `idempotent: true`. Omission adds no exercise;
a later applicable `value: none` overrides an earlier selection. See
[startup proof selection](rig-spec.md#startup-proof-selection) for precedence,
restore behavior, and the distinction between readiness and verified proof.
### Delivery Hint Quick Reference
| Hint | When Delivered | Mechanism |
@@ -414,7 +375,7 @@ restore behavior, and the distinction between readiness and verified proof.
10. Resource IDs must be unique within their category.
12. Lifecycle `execution_mode` must be `interactive_resident`.
13. Lifecycle `compaction_strategy` must be one of `default-compaction`, `managed-compaction`, `handover`, `apprentice-handover` — or a deprecated alias (`harness_native`,`pod_continuity`), which validates with a deprecation advisory and normalizes to its canonical value (OPR.0.5.6.20).
13. Lifecycle `compaction_strategy` must be `harness_native` or`pod_continuity`.
14. Lifecycle `restore_policy` must be `resume_if_possible`, `relaunch_fresh`, or `checkpoint_only`.
15. Startup files and actions follow the same validation rules as in RigSpec.
@@ -452,6 +413,6 @@ The only required file is `agent.yaml`. Everything else is referenced by paths i
@@ -225,7 +225,7 @@ Commands sent to the agent's terminal after it's ready. Can install MCPs, run se
| `send_text` delivery after ready | **Supported** | Reliable. Requires harness to be ready. |
| Hook projection | **Experimental** | Files are copied but execution/integration varies by runtime. |
| Runtime resource projection | **Supported for recognized fragments** | `claude_settings_fragment`, `claude_mcp_fragment`, and `codex_config_fragment` are applied to provider config. Unknown types are copied to runtime extension directories. |
| Permission configuration | **Native settings plus managed launch flags** | OpenRig launches Claude with `acceptEdits` and Codex with `workspace-write` unless an explicit supported selection changes them. It does not add a global Claude `Bash(rig:*)` allowance. Use [the first-user permission guide](getting-started.md#opt-in-permissive-operation) for opt-in and custom choices. |
| 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. |
@@ -246,8 +246,7 @@ This way, when deterministic support becomes fully reliable, the agent will boot
OpenRig performs best-effort deterministic runtime configuration for managed sessions. Core bootstrap stays minimal; user/custom policy belongs in spec-selected runtime resources. These writes are intentionally invasive and should be disclosed plainly:
- Claude global config: `~/.claude/settings.json`
OpenRig no longer writes a core `Bash(rig:*)` permission allowance here. Older
installations may retain one; existing user settings are not removed.
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`
@@ -261,11 +260,6 @@ Two important caveats:
- these writes are best-effort and should still be paired with startup guidance so the local agent can verify and repair them if needed
- already-running adopted sessions may need restart before they pick up newly written config
Permission mode and runtime resource projection are separate. A selected fragment
can affect native configuration, but OpenRig's launch flags can override those
values. Recording a config-surface `permission_policy` is not proof that its
rules were translated or applied. See [permission precedence and limits](getting-started.md#custom-settings-and-precedence).
### Runtime Differences
**Claude Code:**
@@ -280,9 +274,7 @@ rules were translated or applied. See [permission precedence and limits](getting
- 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 policy and sandbox access are separate controls. OpenRig's default
`-s workspace-write` selects the sandbox; it does not force `-a`. A member's
`codex_config_profile` selects native `-p`, distinct from the AgentSpec `profile`.
- 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."
@@ -319,7 +311,7 @@ This prompts the agent to actually invoke the skills, not just have them as pass
## System Check
After identity recovery, verify:
1. `rig ps --nodes` shows your rig running (scoped to your session's rig by default; outside a managed session name it explicitly: `rig ps --nodes --rig <name>`)
your agents to run OpenRig commands without repeated permission prompts?”
**Yes — recommended** / **No — keep prompts**. An existing explicit choice is
reused; No or no answer leaves settings unchanged. Broader access is separate.
> Everything below reports **what is currently true**, never a guarantee that
> downstream work will succeed. "Daemon up" does not mean every agent is healthy;
> "kernel ready" does not mean every kernel agent is healthy; a workspace root
> being *live* does not mean it is the *right* one for your project.
## Prepare and launch
Type `rig` in an ordinary terminal to open the startup and work TUI. It shows
the daemon address; **d** expands the selected instance path and diagnostics.
If the daemon is stopped, press Enter
to start that daemon, then choose the rigs and seats you want. Kernel is
recommended first; selecting its operator does not start every kernel seat.
The same view is available with **S** from ordinary TUI work.
**?** opens Help even while connection checks are pending. **w** skips startup;
**Esc** goes back, or leaves startup from its first page. These choices do not
start a daemon or a seat. **L** opens local reading before or after connecting:
choose configured Specs, project intent, projects, or missions and slices, then
select a directory or file. **r** reads the selected source again; **Esc** returns.
Local reading uses this machine's configured workspace paths and file allowlist,
including when the selected daemon address is remote. It shows disk provenance,
missing or denied sources, binary files and the 1 MiB text truncation boundary.
These disk snapshots may change after reading and do not supply live queue,
execution or topology state. Live views load after a confirmed connection and
deliberate entry; a stalled live read does not prevent Help or local reading.
When terminal transport is unavailable, **t** starts the empty terminal service
so recovery choices can be inspected. It launches no seats.
For a previously occupied seat, Enter attempts its previous conversation.
If history is unavailable, read the reason. **f** opens a separate fresh-start
decision for that named seat; **Esc** declines without launching it. A confirmed
fresh conversation receives the configured context and retains the old history,
but does not resume that history. Authentication or runtime failures require
repair of that prerequisite. **o** opens the existing native terminal here; detach
to return (tmux defaults to Ctrl-b, then d). Decide native trust/auth prompts
there. If a fresh start paused before context delivery, **c** finishes that
delivery to the same occupant. **r** reads actual state again; **d** expands details.
### Choose your providers
Ask: **“Which working account do you want this team to use: Claude Code, Codex,
or both?”** Reuse an explicit choice already made. Recommend the account the
user already has working; a second subscription is not a prerequisite.
| Choice | Starter name | Owner / checker runtime and model |
| --- | --- | --- |
| Two Codex agents (existing route) | `first-project` | Both `codex`, pinned `gpt-6-astra` |
| Two Claude agents | `first-project-claude` | Both `claude-code`, configured native default model |
| One of each | `first-project-mixed` | Claude owner (native default); Codex checker (`gpt-6-astra`) |
Claude uses the same unpinned model convention as the existing Claude kernel and
implementation-pair recipes: OpenRig does not pass a model override. Read the
selected harness's configured model and show it alongside the recipe and launch
command before proceeding. Confirm account access to any pin; if unavailable,
ask for a supported model choice rather than silently substituting a model or
provider. After launch, confirm the actual native model before assigning work.
Install OpenRig and check `tmux -V`. Check **only the selected providers**:
- Claude Code: `claude --version` and `claude auth status`. If sign-in is missing,
ask once: “Please run `claude auth login` in your launch environment.”
- Codex: `codex --version` and `codex login status`. If sign-in is missing,
ask once: “Please run `codex login` in your launch environment.”
Install a missing selected CLI using its provider's installation instructions.
The other provider's CLI/login and Herdr/cmux are optional. Do not copy credentials
or start repeated sign-in attempts. Recheck the selected login after the user
completes it. `rig setup --dry-run` previews the broader setup; applying
`rig setup` installs/checks **both** harnesses and cmux, so it is optional for
this selected-provider path, not a requirement to fix an unused provider.
### Kernel startup stays automatic
On a fresh instance, ordinary daemon startup chooses the kernel variant from
successful native auth probes: Claude alone, Codex alone, or both. Thus an absent
unused provider is fine. The project recipe does **not** constrain that probe:
if both accounts are authenticated, automatic kernel startup uses both, even
when the two project agents use just one provider. No manual kernel setup is
needed for this journey. The starter choice is not an instance-wide provider
restriction; a request to use only one provider for everything is separate.
Preserve any existing managed kernel and working rigs.
### Launch the two project seats
Set `starter` to the chosen name from the table; the existing Codex route remains:
```sh
cd <your-repository>
starter=first-project # or first-project-claude or first-project-mixed
rig specs preview "$starter" --kind rig
rig up "$starter" --cwd . --plan
rig up "$starter" --cwd .
rig status
rig ps --nodes --rig "$starter"
```
Preview the selected seats, models and resources. Plan checks resolution and
preflight for the working directory. Launch starts the daemon if needed, with
the kernel behavior above. Read readiness for the project seats, not only daemon
health. Resolve a named authentication, trust or permission prompt before giving
that seat work. No new team is needed when returning to an existing project.
When a seat pauses, open its existing terminal with **o** in the startup view.
Read the proposed command, working directory and target instance. For an intended
local `rig` call, choose the native prompt's one-time approval if that is the scope
you want; a saved command-prefix allowance also affects future matching calls.
Decline an unexpected operation and tell the same agent what to do instead.
Approval controls whether an action may run; the sandbox controls its filesystem
and network access. Turning approvals off does not grant network access.
After answering, watch for the command's result and the agent continuing. Read
the corresponding queue row and transition from your ordinary terminal. If an
operation timed out, read its result before asking for another attempt: it may
already have taken effect. A delivered message or disappearing prompt alone is
not progress. If startup is still waiting for context delivery, use **c** for the
same occupant, then **r** to refresh. Do not start another seat to clear a prompt.
These starters are deliberately small starting points, not universal teams.
For a different installed runtime or team shape, inspect `rig specs ls --kind
rig` and `rig specs preview <name>` before selecting it. A seven-seat showcase
is optional and consumes more concurrent capacity.
## Give the owner an outcome
For example, in a project that imports CSV files:
```sh
rig send "dev-owner@$starter" 'Improve the CSV import error when a required column is missing: name the column and leave the existing data unchanged. Add a regression check, ask dev-check for an independent check of the exact candidate, and record the result and how I can try it. Keep the change local; do not publish.'
```
Replace the example with a real problem in your repository. Include what the
user should observe, a boundary and how success can be checked. The owner
creates and claims a durable task, implements it, and routes the selected
independent check. You should not have to relay the review between terminals.
`rig send` is the initial conversation; the queue and repository artifacts
retain the work. An unbound shell does not need to impersonate a queue owner.
Use the chosen rig name in its seat addresses (for example,
`dev-owner@first-project-claude`). From an actual project seat, follow the work with
`rig queue list --limit 1000`: its default scope is the caller's current rig.
`queue list` has no `--rig` option. From an observer shell or another rig, use
`rig queue list --destination "dev-owner@$starter" --limit 1000` and the same
command for `"dev-check@$starter"`, after verifying those live addresses.
These show each destination's obligations, not a whole-rig view. An unbound shell
must not pretend to be a seat to change scope; use `--all-rigs` only when that
broader view is intended. Then read `rig queue show <id> --full` and
`rig queue transitions <id>`. A delivered message is not a reviewed result.
Read the artifact, exercise its behavior, and check the candidate reviewed.
## Share the dashboard and return to it
```sh
rig tui --shared
```
A fresh kernel runs the ordinary TUI in its existing `operator-human` terminal.
This command attaches another client to that terminal. **Ctrl-b, then d**
detaches without quitting the TUI; return with the same command and the view
stays where it was. Another authorized agent can capture or operate that same
pane. It should tell you before changing your view. The terminal is not a
human inbox and does not prove anyone is watching it.
Plain `rig tui` remains an independent local view. If an older kernel or a TUI
you quit shows a shell, run `rig tui` in that shell once. `--shared` does not
start or replace a terminal, so a missing binding is reported with recovery
guidance rather than creating a second kernel.
Herdr users follow the same launch and task path. To place the managed team in
Herdr, use `rig terminal open "$starter" --provider herdr`; for the shared
dashboard, use `rig terminal open kernel --provider herdr`. The equivalent
cmux provider is also available. Read the opened/absent/degraded result: a
partial terminal view is not a healthy team. Repeated terminal-open calls can
create another provider workspace; return to the one already open when you
want to preserve it. This is terminal integration, not native plugin enrollment.
## Continue real project work
Return to the same owner with the next outcome, citing the earlier result.
The seat address and durable queue survive closing your viewing terminal.
Keep intent, acceptance and evidence in the repository's existing project,
mission and slice artifacts; the starter reads those before inventing a path.
If the project has no work tree, start with `rig workspace doctor` and
`rig scope mission create --help`, then `rig scope slice create --help`. Set
the actual intended outcome before creating work. `rig scope` retains what is
being built. When repeated coordination warrants a workflow, discover with
`rig workflow specs`, inspect its owners and inputs, and instantiate the
selected name with `rig workflow instantiate --help`. A workflow is not needed
merely to make the first local change.
For a continuing team, [OpenRig Software Factory](../../packages/daemon/specs/agents/shared/skills/core/openrig-software-factory/SKILL.md)
offers manual/team work, queue-supported orchestration without Workflow, and an
optional explicit Workflow path, with wake defaults, token costs and permission
choices visible. Give its short request to your existing agent. After
installation, discover the compatible bundled recipe with `rig context show
skills/core/openrig-software-factory --json`; retain a missing/version-mismatch
result rather than silently using newer instructions. Its growth section keeps
three choices clear: stay with the pair, add one or two seats to the running rig
with `rig grow` and no YAML, or optionally author a custom rig. It covers
new-seat context/work ownership, concurrency costs and saving the expanded spec.
This guide remains the short first-use path.
## Incomplete setup and restart
| Observation | Next action |
| --- | --- |
| Tool missing or login fails | Use the specific setup/auth hint; recheck that executable in the launch shell. Do not send work to an unready seat. |
| Daemon is healthy, kernel is still starting | Read `rig status` and `rig ps --nodes --rig kernel`; kernel readiness is separate. |
| Shared terminal is absent | Inspect the existing kernel binding and recovery state; use standalone `rig tui` while resolving it. |
| Viewing terminal was closed | Reattach with `rig tui --shared`; do not relaunch the team. |
| Daemon restarted but tmux survived | Re-read `rig status` and the existing queue; a daemon restart is not a fresh project. |
| Host reboot lost tmux sessions | Open `rig`, start the daemon if needed, and select the existing rig and seats. Resume is the default; a fresh conversation needs a separate decision. |
| Launch reports no usable snapshot | Inspect the existing rig and retained project files, then follow the same-seat recovery below. |
| Work is waiting on a prompt or decision | Read the row, transition and named prompt; preserve the obligation until the missing decision arrives. |
If a snapshot is unavailable, the startup view checks the selected seat's
retained startup source and authoritative occupant relation. It reports a
missing or ambiguous source instead of selecting an arbitrary historical row.
Repair the named source, retry, or leave the seat stopped. Check the retained
queue, project notes and observed result before continuing work.
`rig setup` prints the short form of this path; `rig status` points back here.
## Kernel framing (what `rig setup` does and does not do)
Explicit CLI daemon startup retains its automatic kernel behavior. The TUI
starts the daemon with kernel auto-boot disabled so the user can select seats:
- `rig setup` installs/verifies the runtime; it does not start the daemon or the
kernel.
- Starting the daemon (`rig daemon start`, or implicitly via `rig up`) is what
boots the kernel rig in the background.
- Starting it from bare `rig` prepares no agents automatically. The TUI offers
kernel setup and individual seat selection after connecting.
- **Kernel readiness is a distinct signal from daemon health.** The daemon's HTTP
health binds early; the kernel can still be booting or a kernel agent can be
unhealthy while the daemon is up. `rig status` surfaces kernel readiness
and missing selections' files remain unavailable. No links are followed
recursively and no successor mission is guessed. The reader accepts at most 32
selected addresses, each source file at most 64 KiB, with 128 KiB of selected
content in total; exceeded limits are visible as unavailable references. A hash
for a section covers exactly the returned section bytes. Bare library refs and
remote URLs are not fetched by this local reader.
Authority and current usefulness are separate questions. An incomplete global
outcome census cannot justify retaining a particular restriction whose premise
was disproved. Read current corrections before interpreting historical
dispositions, preserve unrelated valid boundaries such as publication authority,
and distinguish automatic wake/receipt bookkeeping from useful owner action.
Normal interactive planning is not itself pathology. State the relevance and
cost of the interruption in the assessment; do not turn the signal into a
recurring self-audit or require a universal outside reviewer.
Checkpoints are audited under `health/checkpoints/history/`; replaying identical
bytes writes nothing. Later censuses advance observation time. High-to-high
observations retain episode identity; a clearing checkpoint and later recurrence
produce a cleared episode and a new ID. Reads never update checkpoint state.
Stale, unavailable, contradictory, clipped, or missing evidence cannot admit a
diagnosis. No findings is not a healthy assertion. The current source bounds are
200 lineages, 10,000 transitions and 1,000 product outcomes per checkpoint, and
1 MiB per input. Unsupported or invalid sources fail visibly.
## Investigate and record a disposition
Read the exact evidence and the current authority. Trace where the pattern
began, test whether your own actions amplified it, and distinguish another seat
or stale guidance. A second opinion is optional. The packet is a starting point,
not a closed evidence set.
```json
{
"verdict": "insufficient evidence",
"causalStart": null,
"steering": "Inspect the missing outcome evidence; apply separately established corrections within current authority.",
"uncertainty": "The cited artifact does not yet establish the denominator.",
"evidenceRefs": ["<inspected-evidence-path>"]
}
```
```sh
rig health diagnosis record <qitem-id> --file disposition.json
rig health diagnosis show <qitem-id>
```
Verdicts are `false positive`, `early real condition`, `established pathology`,
`insufficient evidence`, or `resolved`. The disposition is retained on the
diagnostic qitem's transitions and visible in CLI output and the queue. Recording
it does not close or alter the underlying product work. Changed dispositions
retain earlier testimony; exact replay is a no-op. A detector clearing is
recorded separately from an agent declaring the problem resolved.
An optional `correction` keeps the causal judgment, proposed or taken action,
and later behavioral effect distinct. It does not require a complete `progress`
census. For example, a retained-case assessment can record:
```json
{
"applicability": "The retired emergency restriction no longer applies; publication still needs its separate decision.",
"causalJudgment": "The retained trace attributes persistence of the restriction to a disproved premise.",
"action": {"state": "taken", "summary": "The authorized restriction was retired in the retained case.", "evidenceRefs": ["evidence/correction-action.md"]},
"effect": {"state": "unobserved", "summary": "No later natural opportunity has been observed.", "evidenceRefs": []}
}
```
Put this object beside `verdict`, `causalStart`, `steering`, `uncertainty` and
`evidenceRefs` in the existing disposition. Action states are `proposed` or
`taken`; effect states are `unobserved` or `observed`. Every referenced artifact
must be available locally; `taken` and `observed` each require evidence. These
checks establish availability, not causal truth. The receipt retains evidence
hashes and `assessment` identifies the actual queue actor, time and transition.
`behavioralEffect` is the latest owner's reported effect, defaulting to
`unobserved` for legacy dispositions. It is not an independent certification.
A later owner submission preserves previous testimony. Record an observed
effect only from a real later opportunity, with its next decision, useful work,
recurrence and interruption burden; a scripted replay proves mechanics only.
Closing a row, changing a prompt or clearing a numerical signal never supplies
that evidence automatically. No opportunity means unobserved, with the release
claim left to its decision owner. This guidance neither re-enables diagnosis nor
assigns corrective work.
## Human delivery
An assigned agent may explicitly request human escalation:
```sh
rig health diagnosis notify <qitem-id>
```
It requires a registered `human.address` and an admitted `human.conditions`
entry (`critical`, `established pathology`, or `confirmed ceremony`). The connector must be enabled
and pass live readiness checks. The current connector implementation verifies
Slack scopes and channel membership; the diagnosis service itself uses a
transport-neutral readiness port. The existing gateway owns delivery policy
and posting. One human request is retained per episode, and its actual delivery
outcome comes from queue receipts. `pending` is never presented as `posted`.
Inspect the returned qitem's transitions for the connector receipt. No periodic
health check performs remediation. With `human.conditions=["confirmed ceremony"]`,
the enabled diagnosis loop automatically requests one notification for an active,
confirmed passive ceremony episode. Provisional, indeterminate, cleared, and stale
episodes never notify. The existing readiness/gateway path owns the effect; an
unready connector leaves a visible, deduplicated readiness receipt. Later checks
may retry readiness but never create a second request for an existing episode.
Read-only list/explain/preview commands never send.
## Read-only consumers and calibration
A consumer such as a later Herder plugin reads `GET /api/health` and
`GET /api/health/:findingId`, or the identical `rig health --json` and
`rig health explain <finding-id> --json` records. The record schema is
`openrig.health/v0alpha1`; list metadata is `openrig.health-list/v0alpha1`.
Preserve ID, status, policy version, time window, freshness, evidence and
threshold together. There is no health score or implicit remediation authority.
The default list excludes cleared records; explicit cleared queries and exact
ID reads retain them while the source can still project them. A recurrence after
clear receives a new episode ID. This is an on-demand projection, not a history
store; a detail 404 does not prove resolution.
Lists default to 100 and cap at 200, with `total` and `truncated` explicit.
Ceremony findings take precedence before the cap so context pressure cannot
hide the primary signal. Narrow a truncated query; do not certify the unseen
remainder. Empty is not healthy, unavailable is not empty, and an indeterminate
finding cannot authorize a diagnostic occurrence except the explicit, fresh
`needs-diagnosis` passive candidate described above. An existing occurrence may
receive one status-observation receipt when its source becomes indeterminate;
that does not create another obligation or wake.
Live sources supply context pressure, passive ceremony candidates and attributed
assessments, plus optional ceremony checkpoints.
Behavioral and epistemic categories intentionally have no detector. Review
carousel, redundant wakes, stale directives and scope admission have typed
evaluators and replay controls, but the live source does not infer their missing
candidate-change, rescue, directive-conflict or admission-authority facts.
Untagged work and undeclared relationships remain outside passive ceremony
coverage; normal tagged work no longer requires a special outcome census. A small lineage below the configured threshold may still deserve an
agent's inspection; the threshold is a conservative admission rule, not a
definition of good process.
`packages/daemon/scripts/probe-health-calibration.mjs` accepts a sealed replay
export and output directory after daemon/CLI/TUI builds. It drives the compiled
checkpoint/list/explain/diagnosis commands, renders the TUI, retains exact
records and screens, compares database and filesystem effects around reads,
and measures query cost against declared budgets. Every write uses a disposable
home and database. Historical expected states must be grounded independently
of the detector formula; incomplete outcome evidence is an indeterminate case,
not a denominator of one. A calibration report describes its selected corpus
and remaining blind spots, not a fleet-wide false-positive rate.
Resolve `topology.root` from configuration; never hardcode the root. The owning
seat is the sole direct author. It writes as the durable position, not as a
particular occupant or predecessor.
The filename describes the situation or trigger and never contains a stage,
date, occupant generation, or version. Its address is therefore stable:
```text
seat:lore/<stable-slug>.md
seat:lore/<stable-slug>.md#situation
```
A stage change edits frontmatter and `date`; it never renames the file. Existing
citations must continue to resolve after the change.
## Required metadata
Every lore item carries these fields from birth:
| field | meaning |
|---|---|
| `taxonomy` | Exactly `lore`. This is the machine-readable privacy class. |
| `stage` | Current epistemic maturity. Legal values come only from [`knowledge-maturity.md`, “Two encodings, one ladder”](knowledge-maturity.md#two-encodings-one-ladder); do not copy or extend that vocabulary here. |
| `method` | How the owning position could know the claim: the observation, comparison, incident, or source method. |
| `date` | UTC date-time of the current stage decision or substantive revision. Update it when `stage` changes. |
| `position` | Canonical owning seat address, `<seat>@<rig>`. It names the position, never an occupant. |
`taxonomy: lore` is required both in each item's frontmatter and on any
manifest-bearing lore pack that serves it. A misspelling is not equivalent and
must fail any machine pin that extracts the class.
## Item template
Start a situation-shaped item with this template. Replace every placeholder;
`wip` is the starting label, while the authoritative vocabulary remains the
citation above.
```markdown
---
taxonomy: lore
stage: wip
method: "<how this position could know>"
date: "YYYY-MM-DDTHH:MM:SSZ"
position: "<seat>@<rig>"
---
# <Short situation or trigger>
## Situation
**Position:** `<seat>@<rig>`
**Moment.** <What is happening when this knowledge becomes useful?>
**Reflex.** <What tempting response is likely to be wrong?>
**Instead.** <What should the reader do?>
**Because.** <What evidence or failure made the difference?>
```
Facts may use `## Fact` instead when a fact genuinely is the transferable unit.
Any H2 intended as a section-level attachment repeats the canonical `Position:`
line so attribution remains visible when the resolver returns only that section.
## Addressing and composition
Lore uses the existing `seat:` grammar and an explicit rig-and-seat read grant.
The address says which file or section to read; the grant says whose seat tree
the address is relative to. No grant means no read.
For an install, compose the whole item address (`seat:lore/<slug>.md`) so its
frontmatter, taxonomy, stage, method, date, and position travel with the content.
A composed piece must visibly retain:
- the `seat` source label supplied by the resolver;
- the owning `position` from the item's bytes; and
- the `taxonomy: lore` class from both item and lore-pack metadata.
Serving one seat's lore into another seat's install is allowed only when the
caller deliberately grants the owning seat tree. The resulting piece remains
attributed to that owner; the receiving seat does not inherit its identity.
An attachment point is only a pointer paired with the owning position:
```yaml
lore:
address: "seat:lore/<stable-slug>.md#situation"
position: "<seat>@<rig>"
```
An attachment never embeds or copies the lore bytes. The reader resolves it
with the named position's explicit grant when the deeper context is relevant.
## Stage changes
To change maturity:
1. Edit only `stage` and `date` unless the claim itself also changed.
2. Re-resolve the same `seat:lore/<stable-slug>.md` address.
3. Confirm the address is unchanged and the new stage and date are visible.
If a stage change would break the address, stop: that storage shape violates
this convention. `superseded` and `retired` follow the semantics at the cited
maturity home; no lore-specific alternatives are invented here.
## The outward gate
A lore artifact never crosses from rig-local position knowledge into a shared
or shippable audience by copy, export, serving, or packaging. Grant-gated
read-side composition inside the rig is not graduation and keeps the source
address and position attached. Its content may inform a new outward artifact
only through authored re-homing:
1. The owning seat authors the destination artifact, or records an explicit
delegation to its author.
2. The destination is written for its new audience in its own words; the lore
file itself remains byte-unchanged.
3. The destination records the graduation event:
```yaml
graduation:
source_address: "seat:lore/<stable-slug>.md"
source_stage: "<stage at graduation>"
date: "YYYY-MM-DDTHH:MM:SSZ"
owner: "<seat>@<rig>"
delegation: "none | <who delegated to whom>"
warrant: "<why the content earned the new audience>"
```
4. If the destination is shippable, it must pass the public-substance admission
gate. Renaming, rewrapping, or declaring copied lore as another pack class is
laundering, not graduation.
The source address, source stage, owner/delegation, date, and warrant make the
move auditable from the destination. The source lore item is never edited merely
to announce that another artifact graduated from it.
## Privacy boundary
Lore is rig-local position knowledge. Shippable projection and bundle paths must
refuse `taxonomy: lore` structurally, independently of token deny-lists or content
scanners. Existing containment protections for seat files remain in force.
This convention deliberately does not create:
- automatic transcript or memory mining;
- a lore librarian, central curation queue, or review ceremony;
- cross-rig lore sharing;
- a new address grammar, resolver kind, or public export form.
Growth is owner-authored and organic. A seat writes an item when lived work makes
the situation worth preserving; consumers attach or compose it by address when
Last validated against code: 2026-09-29 (install paths and `--target` only; other sections last checked 2026-04-11)
Last validated against code: 2026-04-11
Source of truth: `packages/daemon/src/domain/bundle-types.ts`, `packages/daemon/src/domain/bundle-archive.ts`, `packages/daemon/src/domain/pod-bundle-assembler.ts`
A `.rigbundle` is a self-contained distributable archive that packages a rig spec, all referenced agent specs, their resources (skills, guidance, startup files), culture file, documentation, and an integrity manifest into a single file. The recipient can install and launch the rig without needing the original source tree.
@@ -180,13 +180,6 @@ If any check fails, extraction is aborted and an error is thrown.
## CLI Surface
### Paths
`rig bundle create`, `inspect` and `install` resolve every path you give them (`<spec-path>`, `-o`, `--rig-root`,
`<bundle-path>`, `--target`) to an absolute path against **your** current directory before sending the request. The
daemon then reads and writes those paths on **its own host**: nothing is uploaded, so the files must already exist
where the daemon runs.
### Create a bundle
```bash
@@ -218,7 +211,7 @@ The create command:
rig bundle inspect <bundle-path> [--json]
```
Shows the manifest, digest validity, and integrity verification result. Inspect extracts the archive into a temporary directory for safe validation, then cleans that directory up. It does not install or launch anything.
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.
| `<bundle-path>` | yes | — | Path to the `.rigbundle` file. |
| `--plan` | no | `false` | Preview without installing or launching. Not side-effect free: it runs the preflight, which executes the members' runtime `--version` probes, and it records a bootstrap run. It writes nothing to the target. |
| `--plan` | no | `false` | Preview the bootstrap plan without installing or launching. |
| `--yes` | no | `false` | Auto-approve trusted actions during apply mode. |
| `--target <root>` | yes in apply mode | — | Directory the bundle is installed into and launched from. Required unless `--plan` is used. |
| `--target <root>` | yes in apply mode | — | Target root directory for package installation. Required unless `--plan` is used. |
Install **launches the rig**; it does not just unpack it. It extracts the bundle to a temporary directory, validates integrity, and bootstraps the rig. In apply mode, the daemon requires `targetRoot`, so `rig bundle install` must be given `--target <root>` unless you are running with `--plan`.
For a pod-aware (schema version 2) bundle, apply copies the extracted contents (`bundle.yaml`, `rig.yaml`, `agents/`, culture and docs files) into the target and launches from there, then removes the temporary extraction. So:
- the target becomes the rig root: `agent_ref` paths resolve inside it, and a member with `cwd: "."` (or no `cwd`) starts in the target;
- an absolute member `cwd` stays as authored, and `rig up --cwd <dir>` still overrides every member's cwd;
- if the target already has a file with **different** content at any bundle path (for example its own `rig.yaml`), install refuses with `target_conflict` and writes nothing. Identical files are accepted, so reinstalling the same bundle into the same target works. Use an empty or dedicated directory as the target.
Legacy (schema version 1) bundles keep their old behavior: `--target` is only where packages are installed.
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`.
`rig up` auto-detects `.rigbundle` files and routes them through the bundle bootstrap path.
- `--target <root>`is the install target described above (for a schema-version-2 bundle, the directory the bundle is copied into and launched from)
- if `--target` is omitted for a `.rigbundle`, the CLI defaults the install target to the current working directory, so the bundle's files are written there
- `rig up` resolves a relative `--target` against your current directory before sending it, like `rig bundle install`; with `--host`, `--target` is sent as given and must be a path that exists on that host
- `--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
---
@@ -380,4 +364,4 @@ Equivalent explicit form:
rig up /path/to/my-team.rigbundle --target ~/projects/my-project
```
The bundle is extracted to a temporary directory and its integrity is verified. A schema-version-2 bundle is then copied into the target root (refused if the target holds different files at the same paths) and the rig is launched from there, with all agents and resources from the bundle. If you also want agents to launch with a different working directory for that run, pass `--cwd <dir>` separately.
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.
| `name` | string | yes | — | Rig name. Used in session naming (`{pod}-{member}@{name}`), snapshot identification, and spec library lookup. |
| `summary` | string | no | — | Human-readable description. Shown in spec library, review surfaces, and `rig specs show`. |
| `culture_file` | string | no | — | Relative path to a rig-wide culture/constitution file. Must be a safe relative path (no `..`, no absolute). |
| `permission_policy` | string | no | — | Permission policy attached to the rig. Either a built-in (`builtin:locked`, `builtin:standard`, `builtin:open`, `builtin:yolo`) or a safe relative path to a custom policy file (resolved from this spec's directory; no `..`, no absolute). Absent leaves the default floor. A member may set its own `permission_policy`, which takes precedence over the rig-level one. See "Attaching a permission policy" below. |
| `managed_blocks` | map | no | `CLAUDE.md` | File that receives OpenRig's managed instruction blocks for Claude Code members. Only the `claude-code` key is accepted, with `CLAUDE.md` or `CLAUDE.local.md`. Codex members always use `AGENTS.md`. See "Choosing the Claude instruction file" below. |
| `docs` | Doc[] | no | — | Documentation files that should travel with the rig. Included in rig bundles. Each entry has a `path` field (safe relative path). The engine does not consume these — they are for humans and agents setting up the environment before launch. |
| `startup` | StartupBlock | no | — | Rig-level startup files and actions. Applied to all members via the startup layering model. |
| `services` | ServicesBlock | no | — | Optional managed services (Docker Compose). When present, services boot before any agent launches. |
| `pods` | Pod[] | yes | — | At least one pod required. Each pod is a bounded context containing members and pod-local edges. |
| `edges` | CrossPodEdge[] | no | `[]` | Cross-pod edges connecting members in different pods. Must use fully-qualified `pod.member` IDs. |
### Attaching a permission policy
Attach a policy to a rig with `permission_policy`, either at the rig level or on a member:
```yaml
# a built-in, by name:
permission_policy: builtin:standard
# or a custom policy file, by relative path (resolved from this spec's directory):
OpenRig writes its instructions for Claude Code members into managed blocks in
the member's working directory. By default the file is `CLAUDE.md`. If your
repository tracks `CLAUDE.md`, write the blocks to `CLAUDE.local.md` instead:
```yaml
managed_blocks:
claude-code: CLAUDE.local.md
```
Claude Code loads `CLAUDE.local.md` from the working directory as well. By
convention the file is kept out of git, for example with a `.gitignore` entry.
- Accepted values are `CLAUDE.md` and `CLAUDE.local.md`. Any other value or
runtime key is rejected before a member launches.
- The setting applies to launch, restore, relaunch, handover, adding members,
and export. `rig down` removes OpenRig's blocks from the selected file only.
- OpenRig never edits, moves or deletes blocks in the other file.
A rig that already wrote blocks into `CLAUDE.md` keeps them there after you
switch. Until you remove them, `CLAUDE.md` stays modified and Claude Code loads
both copies. Delete each `<!-- BEGIN OpenRig MANAGED BLOCK: … -->` …
`<!-- END OpenRig MANAGED BLOCK: … -->` section by hand and keep the rest of the
file. If `CLAUDE.md` has no other uncommitted edits you need to keep, you can
instead run `git restore CLAUDE.md`; that command discards every unstaged change
to the file, not only OpenRig's blocks. Running `rig down` on a rig that still uses
the default is not a substitute: it strips every OpenRig block from that
directory's `CLAUDE.md`, including blocks written by other rigs.
---
## Pod
@@ -271,7 +210,6 @@ directory's `CLAUDE.md`, including blocks written by other rigs.
| `id` | string | yes | — | Member identifier. Must not contain dots. Must be unique within the pod. |
| `agent_ref` | string | yes | — | Reference to an AgentSpec. Must start with `local:` (relative) or `path:` (absolute). Exception: `builtin:terminal` for infrastructure nodes. |
| `profile` | string | yes | — | Profile name from the referenced AgentSpec. Use `default` for the default profile. Exception: `none` for terminal nodes. |
| `codex_config_profile` | string | no | — | Codex-only native profile passed as `-p <name>`; letters, numbers, `_`, `.`, `-`. Separate from the AgentSpec `profile`. With the normal launch mode, this replaces OpenRig's explicit workspace-write sandbox flag. A full-bypass policy instead emits danger-full-access and omits this profile argument. |
| `cwd` | string | yes | — | Working directory for the agent. Resolved relative to the rig root (the directory containing the rig spec). Use `"."` for the rig root itself. Can be overridden at launch time with `rig up --cwd`. |
| `label` | string | no | — | Human-readable member name. Shown in UI when present. |
@@ -386,51 +324,12 @@ Startup blocks can appear at three levels: rig, pod, and member. They are merged
| Field | Type | Required | Default | Description |
| `type` | string | yes | — | Action type. One of: `slash_command`, `send_text`, `startup_proof`. Note: `shell` is explicitly NOT supported in v1. |
| `value` | string | yes | — | Command/text to send, or `authenticated` / `none` for `startup_proof`. |
| `phase` | string | no | `after_files` | When to execute text/commands. One of: `after_files` (after startup files are delivered), `after_ready` (after harness readiness check passes). Proof selection is resolved before projection regardless of phase. |
| `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`. |
### Startup proof selection
Startup adds no orientation exercise by default. To select the authenticated
startup challenge, declare an action in an agent, profile, rig, pod, member, or
operator startup block:
```yaml
startup:
actions:
- type: startup_proof
value: authenticated
idempotent: true
```
Use `value: none` in a later layer to select lean startup explicitly. The last
applicable declaration wins in agent → profile → rig → pod → member → operator
order. Culture contributes files, not a proof selection. With no applicable
declaration, the result is `none`; the number of startup files never selects
proof. Invalid values and non-idempotent proof declarations fail validation,
including declarations overridden later. These actions declare policy and are
never typed into a terminal.
An authenticated selection challenges only a fresh or fresh-fallback managed
agent launch. Resumed, forked, rebuilt, and adopted sessions receive no new
challenge; terminal nodes never receive one. `applies_on` follows the requested
startup context, so a fresh fallback during restore uses `restore` selections.
Keep the default `[fresh_start, restore]` to cover both fresh launch paths.
Identity delivery, projection, readiness, and ordinary startup actions still
run. `startup_status: ready` means startup completed, while `oriented: missing`
means a selected proof awaits authenticated submission. Omission/`none` yields
`oriented: n-a` on a new fresh launch and retires an older challenge without
deleting its audit history. Retirement follows successful harness launch,
before readiness checks, so attention, timeout, or a readiness exception cannot
retain the preceding proof. A replacement that fails to launch does not retire
the current proof; resume/adoption also preserves existing proof history.
The effective selection is recorded on `node.startup_pending`; actions are
persisted in startup context for restore and fresh relaunch.
---
## Services Block
@@ -559,7 +458,7 @@ These rules are enforced by the validator. A spec that violates any of these wil
19. Each wait target must define exactly one of: `service`, `url`, `tcp`.
20. `condition` is only valid on `service` targets and must be `healthy`.
21. Startup file `delivery_hint` must be one of: `auto`, `guidance_merge`, `skill_install`, `send_text`.
22. Startup action `type` must be one of: `slash_command`, `send_text`, `startup_proof`. (`shell` is explicitly rejected.) Proof selection requires `value: authenticated` or `none` and `idempotent: true`.
22. Startup action `type` must be one of: `slash_command`, `send_text`. (`shell` is explicitly rejected.)
23. Startup action `phase` must be one of: `after_files`, `after_ready`.
24. Startup action `idempotent` is a required boolean.
25. Non-idempotent actions must not include `restore` in `applies_on`.
@@ -573,8 +472,8 @@ These are the built-in specs shipped with OpenRig. Read them as worked examples.
rig mode set delegated --scope mission --qualifier my-project/release-1 --evidence "Operator delegated this outcome"
# The proposal above exits 2 without writing. Apply the deliberate choice:
rig mode set delegated --scope mission --qualifier my-project/release-1 --evidence "Operator delegated this outcome" --confirm
rig mode set human-led --scope mission --qualifier my-project/release-1 --evidence "Return to interactive planning" --confirm
rig mode unset mission my-project/release-1
```
Unsetting reveals the next applicable explicit choice, or the visible human-led
product default. A new, resolved rig with no binding reports `human-led`,
`source: product-default`, and `binding: null`. Missing scope identity, conflicting
linkage, unavailable storage or malformed authored context reports `unknown` with
a reason. Calling `effective` without a scope does not select a rig implicitly.
The existing operator bearer requirement still applies. `set` requires `--confirm`;
`unset` is an explicit deletion command and applies immediately.
## One scope and phase contract
`GET /api/rig-mode/effective` accepts `rig`, `project`, `mission`, `workstream`, and
`qitem`. Its additive `operatingPosture` object is also attached to health records
and diagnosis findings. CLI explanation and TUI health detail display the same data.
Consumers of posture, phase, and health should use this object rather than invent
another preference store or infer delegation from workflow existence or activity.
| Field | Meaning |
| --- | --- |
| `posture` | `human-led`, `delegated`, or `unknown` |
| `source` | `product-default`, `binding`, or `unknown` |
| `context` | Resolved rig/project/mission/workstream/qitem IDs, source addresses, canonical authored paths and phase; null if scope resolution failed |
| `context.phase` | `{value, source}` from an explicit workflow packet step, otherwise slice stage/status, otherwise mission release phase/status; null fields when unavailable |
| `binding` | Winning binding ID, scope, timestamp and evidence citation; null for default/unknown |
| `reason` | Explanation of the result or the missing/conflicting fact |
| `grantsAuthority` | Always `false` |
| `members` | For queue-backed findings, each member's qitem, posture, source and binding ID; differing or unknown member postures yield unknown aggregate posture |
The existing SQLite mode-binding table remains the only preference store. Matching
posture bindings resolve in this order: qitem, workstream, mission, project, rig,
global host. The qualifiers are respectively a qitem ID, `project/mission/slice-id`,
`project/mission`, project ID, canonical rig ID, and null. Rig names resolve to IDs.
The workstream is an existing authored slice, found by its directory or SPEC ID;
the returned identity uses its SPEC ID and project/mission qualification.
> **Source of truth:** this repo document is the living SSOT for the SDLC
> conventions. Scaffold templates, the advisory scope audit, the
> `mission-slice-sop` skill, and the shipped bootstrap overlay all point HERE —
> they must not restate this document in full (restatement is how drift is born).
OpenRig is a software factory the human steers from a high altitude. The human
records intent; agents turn intent into a plan, the plan into a build, the
build into proof. The TUI projects authored work on disk and, where an owning
proof policy is selected, derives readiness from recorded judgments. These
authoring conventions are **advisory / fail-open for agents**: the audit
records and advises; the selected proof policy separately governs who may judge.
## HOW TO READ THIS DOCUMENT — two parts, and only one of them is the default
| | applies | contents |
|---|---|---|
| **PART A — THE SIMPLE SDLC** | **ALWAYS. This is the default.** | artifact conventions, proportionality, the advisory audit, the honesty rails |
| **PART B — THE RIGOROUS OVERLAY** | **ONLY when the human operator, or an orchestrator relaying that decision, ASSIGNS the heavy path to a NAMED piece of work** | the proof-contract format, the two locks, C1 proof drops, the locked role contracts |
**You may not select Part B for yourself.** If you believe something earns it, say so in ONE
sentence and continue on Part A until told otherwise. Part A corresponds to the `mission-slice-sop`
skill; Part B is defined below and applies only when assigned.
## THE COMPONENT MENU — this document is a menu, not a pipeline
The SDLC is assembled from **components chosen per mission**, not a single mandated flow.
This document describes the components and carries recommendations; a mission (or the team
running it) picks per phase. Multiple valid options can exist for the same phase — choosing
one does not deprecate the others. Nothing below deletes or overrides an existing component.
| phase | components available today |
|---|---|
| **PRODUCT / MISSION PREPARATION** | **The product-management pass** — the layer above planning, run by the advisor/PM seat before planners touch a mission. The build is a lossy compression pipeline (PM intent → planner spec → builder code); nothing leaks downward by proximity, so this pass grounds in the raw sources, synthesizes ONE scope of record, presents owner decisions decision-ready, mints slices at plan time, deposits the full design contract INLINE in each design-bearing spec (owner words verbatim, settled decisions, evidence, shape of done, anti-goals), routes the complete reading list and internet-research assignments from the spec itself, frames each slice's planning-dial rung with its reason, and rereads every spec as the narrow downstream reader. The measuring stick: SPEC.md alone, plus what it explicitly routes to, reproduces the intent in a reader with none of the author's context. Full reference: `product-management-pass.md`. |
| **PLAN** | **The planning dial** — planning rigor is a spectrum chosen per piece: P0 mini-requirements and pointers (simple, reversible work) · P1 an authored spec with a proof contract (the default) · P2 plus a research round before the spec freezes, run BEFORE build dispatch · P3 plus an adversarial pass by a non-author, judged against product goals, landing amendments with proof-contract teeth · P4 plus a blind from-scratch design diffed against priors before proceeding. Dial up by what the piece is, exactly as build care is priced below. Full reference: `planning-dial.md`. |
| **BUILD** | **Part A, the simple flow** (below — the default) · **product-journey components** — a mission-selectable menu for greenfield work, drift recovery, public journey probes, one evidence-gated look at the layer below, minimal repair, independent QA/review, integration, and installed smoke; compose only what the work needs in mission/slice YAML, never as a new mode or mandatory pipeline. Full reference: `product-journey-sdlc.md` · **the wave model** — slices build in parallel in disjoint file territories, an integrator merges serially, and independent review fires once per wave (two reviewers with different vantages, never the writers; fix rounds re-earn verdicts at one final revision; per-slice discipline stays failing-test-first and verify-by-effect). The wave IS the care dial: what shares a wave decides how much review each piece gets · **Part B, the rigorous overlay** (below — only when assigned). These combine: a wave can carry one Part-B piece. Full wave reference: `wave-sdlc.md`. |
| **DISPATCH DATA** | **The execution-data conventions (S27)** — three tiny data disciplines that make execution state derivable instead of hand-tabulated, consumed by `rig view show execution`. **EC-1, edges in frontmatter:** every open slice carries its hard edges in `depends_on:` (an inline JSON array; an explicit `[]` is a statement, absence is INDETERMINATE); soft/serialization edges are one machine-readable Territory line, exactly `SOFT-AFTER: [ids] — reason`. **EC-2, wave-as-data:** wave composition + review model live on a durable queue row tagged `wave-map` + `format:wave-map-v1` whose body carries one fenced ```json block (`{format:"wave-map-v1", mission, waves:[{id, slices, serialized_order?, review_model?}]}`) — composition only, never status labels (status derives at read time); the planning dial recorded at lock surfaces as the frontmatter field `approved-spec-dial:` beside the approve stamp. **EC-3, dispatch names the worktree:** build batons carry the exact body line `worktree_path=<path>` from dispatch onward — it is Q1's join key; batons without it join by naming only and the view marks them `fragile_join`. Existing batons are never rewritten. |
| **RELEASE** | The release ceremony conventions (publication identity, honest release notes) and the CAPABILITY-DELTA LIFECYCLE LAW: each release's delta is authored at the fence and bound to the exact candidate, then reconciled to the published cut. Expiry requires both the canon header naming the absorbed delta and a distinct successor delta existing; a draft header alone is insufficient. Verify the actual boundary event and stop citing an expired delta. Full statement: `release-boundary.md` §6. |
| **BOUNDARY** | **The release boundary** — housekeeping run once per fence so the next release starts clean: seats reset or re-primed per their continuity needs, memory distilled (keep what changed a decision recently), every queue swept by destination and dispositioned, substrate torn down, boards frozen as records, a clean-box baseline captured. A checklist of judgment guides, not gates. Full reference: `release-boundary.md`. |
> Full reference documents for each component (graduated 2026-08-28): the
> product-management pass — `product-management-pass.md` · the
`--full` returns the complete record (raw byte-equivalent). **Slice 34 — breadth default flipped to CURRENT-RIG** (derived from `OPENRIG_SESSION_NAME`'s `@<rig>` suffix) to match the docker/kubectl-aligned `rig queue list` grammar: `-A`/`--all-rigs` for fleet breadth; `--rig <name>` overrides the current-rig default; `--session <sess>` narrows further. All-states stays the default (per orch-lead's ruling: ps surfaces topology/readiness, where stopped/recoverable/attention is the actionable signal — unlike queue-list where the actionable set is active items). `--active` / `--running` remains the opt-in active-state filter. Closes a ~77,000-token status-glance incident at root + a fleet-scale unbounded-default-output bomb. **Resume-token security**: `--full` JSON now emits `resumeTokenPresent` (boolean) instead of the resume token value itself, so an orch glance never accidentally leaks token material.
Loaded 100 of 2717 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.