Skip to content

Bridge seam: consolidated egress + file-protocol drain (replaces #435, #445) - #509

Open
gcko wants to merge 12 commits into
mainfrom
feature/consolidated-bridge-seam
Open

Bridge seam: consolidated egress + file-protocol drain (replaces #435, #445)#509
gcko wants to merge 12 commits into
mainfrom
feature/consolidated-bridge-seam

Conversation

@gcko

@gcko gcko commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What this is

One consolidated Bridge seam that replaces open PRs #435 and #445 (#445 is stacked on #435). It gives Bridge everything it consumes from the _bridge/ file seam, shrunk to the minimal Spacedock surface per the analysis in the bridge repo (docs/plans/2026-07-14-spacedock-optionality-findings.md §B/§D) against the Bridge-owned schema contract (bridge/docs/seam-contract.md).

Built fresh off main (the two branches were 40 commits behind, with 9 conflicting prose files), so all prose was re-authored against current main, never rebased.

The consolidation, in one line

Bridge consumes files, not verbs. The five packaged CLI verbs from #445 (inbox drain|ack|commit, alert, initiate) are retired — the FO writes the seam files directly per the bridge-seam mod and the published contract. What survives is only what genuinely needs the harness.

What ships (the irreducible core)

  1. Three spacedock bridge entrypoints, all hook-/daemon-invoked, never FO-called: egress emit (normalize a turn payload → events.jsonl + the session→entity marker), inbox check (the synchronous Claude Stop-hook block-until-drained wake), ingress wake --host codex (external resume). The internal/bridgealert and internal/bridgeinitiate packages and the drain/ack/commit verbs are gone.
  2. One contract-conformant FO mod (mods/bridge-seam.md, + byte-identical dogfood copy) carrying the whole drain/heartbeat/ack/gate/alert file protocol — a bare LLM agent with only the contract can implement it.
  3. Hook registrations for all three hosts (hooks/hooks.json + hooks/codex-hooks.json + the pi extension), and the "hooks" key added to both plugin manifests.
  4. Re-authored FO/ensign prose speaking the file protocol (boot before-greet heartbeat, eager top-of-loop drain, per-host ## Bridge seam sections, present-gate direct fo-initiate emission, fleet mode).

Completeness — every Bridge reader has a producer

A fresh-eyes audit walked every reader in bridge/internal/spacedock/*.go against a producer: inbox/cursor/replies, heartbeat, events, session marker, initiate cards, alerts — all satisfied at the right schema; fo-feed.jsonl is the one deliberate drop (redundant with git narration + the marker-derived feed, contract §5). Verdict: PASS.

Load-bearing detail

The fo.<slug>.json heartbeat must carry the harness session id ($CLAUDE_CODE_SESSION_ID / $CODEX_THREAD_ID); the Claude Stop-hook check resolves which slugs belong to a stopping session by matching the Stop payload's session_id against the heartbeats. A heartbeat without it → the check resolves nothing → a Claude FO is never nudged and queued intent sits forever. The mod requires it, a contractlint gate pins the markers, and an integration test exercises the block chain.

Process

Plan-first with two adversarial gates. The plan review caught two blockers pre-code: the plugin hooks key was undispositioned (the whole Claude seam would have been inert) and the prose-written heartbeat had to carry the harness session id or Claude delivery silently dies. The implementation review returned SHIP + completeness PASS with four MINORs — all folded in: torn-trailing-line count now matches Bridge's wc -l, the ack dedup is id-primary and terminal-status-filtered (matching the mod's real ack shape), inbox check walks to the git root like the egress producer, and the dogfood mod copy is gated byte-identical.

Reviewer attention — contractlint ratchet re-baselines

Spacedock enforces monotonically-shrinking FO-prompt-surface ratchets. The seam is a genuine new capability adding prose across the boot/dispatch/gate/runtime surfaces (as #435/#445 also did), so this bumps two baseline constants (foFunctionReferenceBaselineBytes, preChangeSharedCoreBytes) and the «dispatch.next-action» ordered-marker sequence, each with an attributing comment. Growth was minimized by keeping the full protocol in the uncounted mod + deferred refs; the constants sit ~150 bytes above the actual measured surface. This is the one spot worth a close look.

Fresh-eyes compatibility review (applied)

Three independent fresh-eyes reviewers audited this PR bottom-up from code (not from these docs), hunting false assumptions, hallucinations, and Bridge↔Spacedock mismatches:

  • Schema parity — COMPLETE. Every one of the nine _bridge/ files matches field-for-field between the producer (egress/check code + the FO-authored JSON in the mod) and Bridge's reader struct tags/validation — names, types, required-vs-optional, and the reply/initiate/alert enum vocabularies all line up.
  • Prose fact-check — one real defect, fixed. The docs were faithful to the code except a fabricated "1 MiB per-line cap on inbox check" in docs/dev/bridge-seam.md (the inbox scan is uncapped; the 1 MiB cap is the events.jsonl scan) — removed, and it had contradicted the authoritative docs/seam-contract.md, which itself verified accurate line-by-line.
  • Runtime/behavioral — end-to-end compatible. Clean daemon coexistence (no double heartbeat/cursor writer; managed FOs drain via the daemon's in-process resume, not the Stop hook), no Bridge dependency on a retired verb, and the load-bearing session-id binding empirically confirmed real ($CLAUDE_CODE_SESSION_ID is set and spacedock already reads it in internal/dispatch/build.go).

Also folded in: the durable-wake caveat (the Stop-hook block is the interactive path; headless is daemon-managed), a timestamp-passthrough guard so a non-RFC3339 Codex/Pi hook value can't silently drop an event line, and a cursor-advance change (advance to the highest line actually read, not an independent recount) that makes over-advancing past an unread intent structurally impossible.

Two flagged as gating follow-ups (not this PR): the bridge-seam mod is only wired into the dogfood workflow today — commission/refit scaffolding must land before the daemon manages a non-dogfood fleet (else a live managed FO writes no heartbeat and Bridge shows it detached); and the LLM-driven cursor is now the endorsed files-not-verbs tradeoff (the step-5 change removes the recount race, but no code enforces the count).

Validation

go test ./... and go test ./... -race green (the only failure is the pre-existing, environment-caused TestSurveyCodexPresenceThroughSync, unrelated to this change); gofmt clean; 105 contractlint gates pass including 4 new structural ones (dropped-verb absence, reference closure, mod frontmatter, session-id markers).

Follow-ups (spacedock issues)

Closes the need for #435 and #445.

🤖 Generated with Claude Code

gcko and others added 11 commits July 14, 2026 20:27
Port internal/bridgeegress (events.jsonl + session-marker writer) intact from
the harness-agnostic-fo-events branch: host normalization
(canonicalEventName/canonicalPiEventName), worktree-stop canonicalBridgeRoot,
first-write-wins markers, and the 2000->1000 events trim. Wire the hidden
`spacedock bridge egress emit --host <host>` entrypoint in newBridgeCommand
(observe-only, silent, no-op-safe). Add a contract-shape test asserting the
emitted line carries the seam-contract §2.5 ts/event/session_id keys for a
Claude SessionStart payload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Port internal/bridgeingress/wake.go + wake_test.go intact from the
harness-agnostic-fo-events branch: the codex-only guard, stale-lock TTL
reclaim, pending-line grouping by resumable session, and the detached
`codex exec resume` launch. wake.go carries the shared inbox/reply/heartbeat
helpers (inboxCursor, loadReplies, replyKey, targetsFor,
discoverHeartbeatSlugs, loadHeartbeatAnyAge, safeSessionID, normalizeHost,
lineScanner, safeSlugPattern) the Stop-hook check will reuse.

Rewrite wakePrompt to speak the direct _bridge/ file protocol (seam-contract
§3): read inbox lines past the per-slug cursor, dedup against fo-replies.jsonl,
append terminal acks, whole-file-replace the .inbox-cursor.<slug>, refresh the
heartbeat — naming no dropped `spacedock bridge inbox` verb. Wire the hidden
`spacedock bridge ingress wake --host codex` entrypoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Carve internal/bridgeingress/check.go out of the harness-agnostic-fo-events
branch's drain.go, taking ONLY the read-only Stop-hook decision path: stopPayload
parse (incl. stop_hook_active anti-loop), resolveSessionSlugs (heartbeat +
session-marker match), workflowForSession, readInboxFull (+ its fullInboxRecord
type and routing() projection), addressedTo (target_set authoritative), the
pending computation, HookDecision emission, validSlug, and absRootOr. Drain/Ack/
Commit and their exclusive helpers (appendReply, truncateReplies,
lowestCommittedCursor, adoptCursor, stampHeartbeat, resolveSessionID, ...) are
NOT ported — FO judgment is direct _bridge/ file writes now (scope 2). Check
never called adoptCursor, so its absence is behavior-neutral.

Rewrite drainReason to instruct the file protocol (read inbox lines past the
per-slug cursor, act, append terminal acks to fo-replies.jsonl, whole-file-
replace the cursor, refresh the heartbeat) and name no dropped verb; drop the
now-unused host parameter. Adapt check_test.go to the file-protocol reason, add
a no-dropped-verb assertion, and add the review-B2 case (a heartbeat missing its
session_id resolves nothing -> no block). Wire the hidden
`spacedock bridge inbox check` entrypoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: test <test@test.com>
Wire the harness lifecycle hooks that drive the seam entrypoints, so the
egress producer and the synchronous Stop inbox-check actually fire.

- scripts/spacedock-bridge-events.sh (async, observe-only) and
  scripts/spacedock-bridge-inbox-check.sh (synchronous Stop decision),
  both with $SPACEDOCK_BIN -> command -v spacedock -> silent exit 0
  bin-resolution; 0755.
- hooks/hooks.json: six Claude egress events (SessionStart,
  UserPromptSubmit, PostToolUse, Notification, Stop, SubagentStop) plus
  the synchronous inbox-check on Stop. Notification is kept per contract
  §2.5 (liveness ping, preserve semantics).
- hooks/codex-hooks.json: six non-async sh -c one-liners; no Stop-block
  (codex delivery is external wake via `bridge ingress wake`).
- .claude-plugin/plugin.json + .codex-plugin/plugin.json each gain the
  "hooks" key — without it the loader never reads the hook config and the
  seam is inert (review blocker B1). Versions left at main's current value.
- .pi/extensions/spacedock.ts: re-apply the pi egress registration onto
  main's current extension (skill discovery + lifecycle egress emit).
- Port the hook->file production smoke tests: codex_bridge_egress_hook_test.go
  (codex manifest/hooks shape + minimal fixture) and bridge_session_link_test.go
  (Claude adapter -> events.jsonl + session marker contract conformance).

Signed-off-by: Jared Scott <jared.scott@variable.team>
Author the single seam mod as a direct-file producer per Bridge's
docs/seam-contract.md, replacing the retired verb-based bridge-inbox mod.

- mods/bridge-seam.md (canonical) + docs/dev/_mods/bridge-seam.md
  (identical dogfood copy). Non-standing frontmatter (name/description/
  version, no `standing:`); `## Hook: startup` + `## Hook: idle` prose
  conventions before `## Agent Prompt` (parser requires Agent Prompt last).
- Encodes the B2 load-bearing requirement: the heartbeat MUST carry
  session_id = the harness session id, with a per-host source table
  (claude $CLAUDE_CODE_SESSION_ID, codex $CODEX_THREAD_ID, pi its id).
  A missing/wrong id silently kills Claude Stop-check wake delivery.
- Verbatim JSON shapes quoted from contract §2: heartbeat (§2.4),
  inbox line (§2.1), fo-replies ack with the kind/status validity table
  (§2.3), fo-initiate cards (§2.7), fo-alerts permission-request (§2.8).
- Drain recipe per §3: cursor read -> physical-line route (target_set
  authoritative) -> dedup against terminal acks by in_reply_to_id ->
  act -> wc -l -> monotonic whole-file cursor replace. Cursor-safety
  note: never lower; over-advance silently and permanently skips intent
  (the check only sees pending below the cursor).
- Per-member fleet clause (one heartbeat/cursor/ack set per slug); park
  obligations (open gate-review before parking, state:idle heartbeat);
  fo-feed noted optional (§5); events/marker noted hook-driven.
- Allowlist mods/bridge-seam.md in the contractlint lifecycle-hook
  absence guard (TestNoUnexpectedModHookOrPRMergeIntroduced), alongside
  the canonical pr-merge mod.

Signed-off-by: Jared Scott <jared.scott@variable.team>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Re-authored against current main (the branches drifted 40 commits): the
before-greet «bridge.boot-liveness» step + function in shared-core, the eager
top-of-loop drain step in dispatch-core, the per-host ## Bridge seam sections
(claude/codex/pi runtime), present-gate's direct fo-initiate gate emission +
channel-boundary rule, the fleet operating contract (fo-fleet.md), the
first-officer SKILL fleet clause, and the ensign running-badge note. All speak
the direct file protocol per docs/seam-contract.md — no dropped verbs.

Re-baselines two contractlint shrink ratchets (foFunctionReferenceBaselineBytes,
preChangeSharedCoreBytes) and the «dispatch.next-action» ordered-marker sequence
to their new floors: the seam is a real new capability adding prose across the FO
boot/dispatch/gate/runtime surfaces, mirroring #435/#445's growth. Counted-surface
growth was minimized by keeping the full drain protocol in the uncounted
bridge-seam mod + deferred fo-bridge.md/fo-fleet.md.

Signed-off-by: Jared Scott <jared.scott@variable.team>
…sure, mod frontmatter, session-id marker)

Signed-off-by: Jared Scott <jared.scott@variable.team>
…-seam.md

Signed-off-by: Jared Scott <jared.scott@variable.team>
Signed-off-by: Jared Scott <jared.scott@variable.team>
…gfood gate)

- check.go/readInboxFull counts only newline-terminated lines (bufio.Reader),
  matching Bridge's fointents.go wc-l semantics — a torn trailing inbox write no
  longer over-counts pending and forces a spurious Stop-block.
- replyKey is id-primary and loadReplies accepts id-only acks (what the seam mod
  writes) filtered to TERMINAL statuses — the dedup guard is now operative for
  the real ack shape, not dead for it.
- Check resolves the _bridge/ root by walking to the nearest .git (gitRootOr),
  matching bridgeegress.canonicalBridgeRoot, so a subdir Stop-payload cwd still
  finds the fleet inbox.
- Dogfood mod copy made byte-identical to canonical + a dedup gate
  (TestBridgeSeamDogfoodCopyMatchesCanonical) and both mod paths added to the
  dropped-verb absence gate, so one-path drift cannot slip through.

Signed-off-by: Jared Scott <jared.scott@variable.team>
…+ cursor)

Three parallel fresh-eyes reviews (schema parity, prose fact-check, runtime
behavioral) confirmed producer↔reader schema parity COMPLETE and end-to-end
compatibility. Folded their findings:

- docs/dev/bridge-seam.md: removed a FALSE "1 MiB per-line inbox-check cap"
  claim (check.go reads the inbox uncapped; the 1 MiB cap is the events.jsonl
  scan, and it contradicted the authoritative seam-contract.md). Corrected the
  Stop-hook slug resolution to note heartbeat AND markers (heartbeat effective).
  Added the durable-wake caveat (Stop-hook block is the interactive path;
  headless is daemon-managed via in-process resume, not the hook). Softened the
  fo-feed drop note (complete-verb + free text are absent, not reconstructed).
- claude-first-officer-runtime.md: terse headless-wake caveat pointer.
- egress.go timestampFor: only pass a payload timestamp through when it parses
  as RFC3339, else stamp our own — a non-RFC3339 Codex/Pi value would otherwise
  fail Bridge's time.Time unmarshal and silently drop the event line.
- mods/bridge-seam.md drain step 5: advance the cursor to the highest line
  actually read in step 2 (not an independent wc -l recount), making
  over-advance past an unread intent structurally impossible; dogfood copy synced.

Empirically confirmed the load-bearing B2 binding is sound: $CLAUDE_CODE_SESSION_ID
is a real env var (spacedock already reads it in internal/dispatch/build.go), so
the Claude Stop-hook slug resolution is not broken.

Signed-off-by: Jared Scott <jared.scott@variable.team>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant