Bridge seam: harness-agnostic FO-event egress (DRC-3798)#445
Conversation
…rn (DRC-3798)
The DRC-3799 audit of the Bridge seam found it is two flows in opposite
directions: ingress (captain intent -> FO, the bridge-inbox drain) was
already harness-agnostic on the portable mod-hook loop, while egress (FO
liveness/activity -> Bridge: events.jsonl, the session->entity marker, the
heartbeat session id) was Claude-Code-coupled with no adapter seam.
This routes the egress through Spacedock's existing per-host adapter pattern
(the same PRESENT/ABSENT idiom fo-dispatch-core.md uses), keeping the schema
Spacedock-owned and the producer per-host:
- docs/dev/bridge-egress-contract.md (new): the harness-neutral schema for all
four egress surfaces (events.jsonl, fo.$SLUG.json heartbeat, fo-feed.jsonl,
the session->entity marker) + the per-host producer bindings. Claude PRESENT;
Codex/Pi ABSENT/TODO with the exact open work named. Records the decision
that the deterministic RUNNING badge is Claude-only for now, with graceful
degradation on other hosts (heartbeat still attaches; git + fo-feed still
drive fleet-history; only the live FO-vs-ensign badge is withheld).
- A "## Bridge egress" binding section in each of the claude/codex/pi
first-officer runtime adapters; the claude ensign badge paragraph and
shared-core step 7b now point at it.
- The bridge-inbox heartbeat session id moves off the hardcoded
${CLAUDE_CODE_SESSION_ID:-} onto a neutral-first token with a built-in
per-host fallback: ${SD_SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-${CODEX_THREAD_ID:-}}}.
The launcher cannot export SD_SESSION_ID (the harness mints the session id
inside the session) and a per-tick FO export is fragile, so the fallback
keeps Claude/Codex populated with no regression while SD_SESSION_ID stays the
neutral override the contract documents.
- bridge_session_link_test.go reframed as TestClaudeAdapterConformsToEgressContract:
the harness-neutral contract is the unit under test (parse-based assertions on
the events.jsonl line shape + nesting + the session-marker shape), so a future
Codex/Pi producer reuses the same assertions with its own input builder.
Doc/contract + test only; no producer behavior change. contractlint, build,
go vet, and the reframed contract test pass. (Pre-existing, unrelated:
TestSurveyCodexPresenceThroughSync.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@infuseai.io>
…o the DRC-3798 egress branch Keeps #445 stacked on the refreshed base so its diff stays just the egress-abstraction changes. Clean merge; contractlint + the egress contract test pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Jared Scott <jared.scott@infuseai.io>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Signed-off-by: Jared Scott <jared.scott@variable.team>
There was a problem hiding this comment.
Pull request overview
This PR extends the Bridge seam to be harness-agnostic on egress by routing FO/ensign lifecycle activity through a shared, host-neutral internal/bridgeegress emitter and binding host-specific wrappers (Claude hooks, Codex hooks, Pi extension) to a single hidden CLI surface (spacedock bridge egress emit --host <host>). It also tightens the Bridge ↔ FO conversation-loop contract (intent ids, frozen target_set, and best-effort _bridge/fo-replies.jsonl acknowledgements) with doc updates and contractlint tests.
Changes:
- Add a host-neutral egress normalizer/writer for
_bridge/events.jsonland first-write-wins_bridge/sessions/*markers, including Pi lifecycle-name normalization. - Add a hidden, silent CLI command (
bridge egress emit) and update Claude/Codex/Pi host bindings to call it. - Update Bridge inbox/egress contract docs and add tests locking the reply-loop semantics and host packaging expectations.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/integration/testdata/codex/bridge-egress-minimal-session-start.json | Adds a minimal Codex hook payload fixture for egress tests. |
| skills/integration/codex_bridge_egress_hook_test.go | Verifies Codex plugin manifest/hooks wiring and wrapper silence/argv contract. |
| skills/integration/bridge_session_link_test.go | Reworks Claude adapter conformance test to assert harness-neutral egress output shapes. |
| skills/first-officer/references/pi-first-officer-runtime.md | Documents Pi Bridge-egress support boundaries (events packaged; markers unclaimed). |
| skills/first-officer/references/first-officer-shared-core.md | Updates fleet-mode + startup guidance to reflect target_set and egress capability split. |
| skills/first-officer/references/codex-first-officer-runtime.md | Documents Codex Bridge-egress bindings and session-id fallback semantics. |
| skills/first-officer/references/claude-first-officer-runtime.md | Documents Claude Bridge-egress bindings (events + deterministic marker path). |
| skills/ensign/references/claude-ensign-runtime.md | Clarifies ensign-side running-badge behavior via Claude egress hooks/markers. |
| scripts/spacedock-bridge-events.sh | Simplifies Claude wrapper to delegate to shared CLI emitter. |
| scripts/codex-bridge-events.sh | Adds Codex wrapper delegating to the shared CLI emitter (silent, observe-only). |
| internal/contractlint/fo_feed_and_eager_drain_test.go | Adds contract locks for inbox id/target_set and _bridge/fo-replies.jsonl semantics. |
| internal/cli/pi.go | Tightens Pi package gating to require the Spacedock Pi extension in addition to skills. |
| internal/cli/pi_frontdoor_test.go | Updates Pi tests for new extension gate and dev-override behavior. |
| internal/cli/pi_egress_test.go | Adds tests for Pi extension wiring + package manifest advertising + runtime extension gate. |
| internal/cli/cli.go | Adds hidden spacedock bridge egress emit --host <host> command wiring to bridgeegress. |
| internal/cli/bridge_egress_test.go | Tests the hidden bridge egress CLI for silence, output, and malformed-payload no-op. |
| internal/bridgeegress/egress.go | Introduces host-neutral egress normalizer/writer (events + markers + truncation). |
| internal/bridgeegress/egress_test.go | Adds unit tests for event schema, Claude marker behavior, truncation, and Pi normalization. |
| hooks/codex-hooks.json | Adds Codex non-async command hooks invoking the Codex wrapper via PLUGIN_ROOT. |
| docs/dev/bridge-egress-contract.md | Adds/updates the harness-neutral egress contract (events, heartbeat, feed, replies, markers). |
| docs/dev/_mods/bridge-inbox.md | Updates inbox schema/routing (id, target_set) and defines _bridge/fo-replies.jsonl rules. |
| .pi/extensions/spacedock.ts | Extends Pi extension to forward lifecycle events into the shared CLI emitter. |
| .codex-plugin/plugin.json | Points Codex plugin manifest at the Codex-specific hooks file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Jared Scott <jared.scott@variable.team>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Review follow-ups on the harness-agnostic FO-event work: - wake: reclaim a stale .wake-lock.codex left by a crashed/killed wake (O_EXCL alone permanently wedged durable delivery on any crash). - wake: validate session ids read from _bridge/ state before they become a codex argv positional (argument-injection hardening). - wake/egress: raise the bufio scan limit above 64KB so a large record cannot hard-fail the inbox scan or silently disable the event-log trim. - codex: drop the orphaned scripts/codex-bridge-events.sh wrapper and its test; codex-hooks.json inlines the emitter call and a test forbids ever wiring the wrapper in, so it was superseded dead code. - docs: fix the Claude events.jsonl schema (was missing timestamp/host/ actor_id), the "Last-write" mislabel on the first-write-wins marker, and the codex/pi marker path (<actor_id>.json, not <session_id>.json). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review: PR #445Reviewed Full antagonistic pass (correctness, security data-flow, cross-reference, error-handling, tests, diff, docs-vs-code) over the egress/ingress/alert Go code, the host wiring (Claude/Codex/Pi hooks + scripts), and the contract/skill docs. Reviewed against base Issues (fixed in
|
gcko
left a comment
There was a problem hiding this comment.
Claude Code Review complete: 5 issues found and fixed in eb1fba3 (durable-wake stale-lock recovery, egress/inbox scan limit, session-id argv hardening, orphaned codex wrapper removed, egress-doc schema/label corrections). Residual notes (contract-pinned doc wording, Pi lifecycle fan-out, test gaps) are non-blocking — see the review comment. No failing Actions exist on this base.
What & why
Makes the Bridge ↔ Spacedock seam harness-agnostic and closes the full captain-intent conversation loop. Implements the DRC-3799 harness-agnosticism audit of the Bridge seam (PR #435) plus the follow-up contract, wake, and alert work needed for a live Bridge conversation across Claude, Codex, and Pi.
The seam is two flows over the shared
_bridge/directory:_bridge/inbox.jsonl; each FO drains it on its own portable mod-hook loop, with an opt-in durable wake for parked Codex sessions.The event schema stays Spacedock-owned and harness-neutral; concrete producers are bound per host. Claude, Codex, and Pi ship packaged event producers via the shared
spacedock bridge egress emit --host <host>command; deterministic session→entity marker parity remains Claude-proven only.What's in this PR
Harness-neutral egress (
internal/bridgeegress/) — normalizes host payloads into a stable_bridge/events.jsonlschema and first-write-wins_bridge/sessions/<actor_id>.jsonmarkers; normalizes Pi-native lifecycle names to Bridge canonical events. Observe-only: malformed input or write failures degrade to no-op.Shared egress command + per-host producers — hidden
spacedock bridge egress emit --host <host>(internal/cli/cli.go). Claude's hook (scripts/spacedock-bridge-events.sh) becomes a thin wrapper; Codex gets its own non-async hooks (scripts/codex-bridge-events.sh,hooks/codex-hooks.json,.codex-plugin/plugin.json) that call the emitter viaSPACEDOCK_BIN/PATHwithout reusing Claude's async file, env vars, or plugin-cache wrapper; Pi forwards lifecycle events via.pi/extensions/spacedock.ts.Durable Codex wake (
internal/bridgeingress/) —spacedock bridge ingress wake --host codexresumes parked Codex FO sessions for inbox records not yet delivered (by cursor or FO reply/ack). A wake is only an attempt; delivery is confirmed by the FO-owned drain + ack, so it stays safe against the gate-vs-inbox race.FO permission alerts (
internal/bridgealert/) —spacedock bridge alert permissionappends non-blocking captain interrupts to_bridge/fo-alerts.jsonl.Front-door
--plugin-dirsplit (internal/cli/frontdoor.go) — before-----plugin-dirdirs are kept separate from host passthrough: Claude receives them at launch, Codex installs them via a local marketplace symlink (Codex has no launch-time--plugin-dir).--no-install+--plugin-diris rejected for Codex.Pi extension gating (
internal/cli/pi.go) — Pi package/runtime checks now require the Spacedock Pi extension and the ensign skill, so a skills-only package is not treated as extension-capable.Contract + skill docs —
docs/dev/bridge-egress-contract.mdanddocs/dev/_mods/bridge-inbox.mdpin the harness-neutral producer contract,target_setrouting, per-workflow cursors,_bridge/fo-replies.jsonlreply/ack semantics, and at-least-once (not exactly-once) delivery; first-officer runtime references document per-host producer support vs. Claude-only marker parity.Validation
go build ./...,go vet ./...,go test ./...(all green except a pre-existing, environment-dependentTestSurveyCodexPresenceThroughSyncthat fails identically on the base branch and is unrelated to this PR).