Skip to content

Bridge seam: bridge-inbox drain mod + normalized FO event hooks + opt-in fleet-mode FO#435

Open
gcko wants to merge 18 commits into
mainfrom
bridge-seam-inbox-events
Open

Bridge seam: bridge-inbox drain mod + normalized FO event hooks + opt-in fleet-mode FO#435
gcko wants to merge 18 commits into
mainfrom
bridge-seam-inbox-events

Conversation

@gcko

@gcko gcko commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Closes #434
Closes #436
Closes #437

Implements the Spacedock half of the multi-session-control seam — #434 / DRC-3727 (drain mod + event hooks, §1–§2) and #436 / DRC-3732 (target-filtered drain + per-workflow cursor + FO heartbeat, §1) — paired with the merged Bridge half, DRC-3731 (spacedock-dev/bridge#21, already merged, no spacedock issue to close cross-repo). Also lands the opt-in fleet-mode First Officer (#437, §4) — the single-session counterpart to the multi-session seam.

The Spacedock side of the seam to the Bridge command-center UI. Two additive pieces — no core-skill change, no new binary. The contract below was cross-checked field-for-field against the merged Bridge producer (internal/spacedock/foinbox.go, foroster.go).

1. bridge-inbox drain mod (+ heartbeat)

docs/dev/_mods/bridge-inbox.md — an idle + startup hook (mirroring pr-merge) that drains captain intent Bridge queues at _bridge/inbox.jsonl and writes a per-workflow liveness heartbeat:

  • Record schema (Bridge writes): {ts, kind:"tell"|"conn", text, granted?, target}. tell → a talk-to-FO directive; conn → handover grant/revoke within a stated goal. target routes the intent — a workflow slug or "all"; absent/empty ⇒ all (backward-compatible with older Bridge records). The FO acts only on records where target == "$SLUG" or target == "all"; records for other workflows are skipped but still advance this workflow's cursor.
  • Per-workflow cursor (_bridge/.inbox-cursor.$SLUG): Bridge only appends to the one shared inbox.jsonl; each workflow's FO advances only its own cursor, so several FOs draining the same inbox never clobber each other, and re-firing with no new lines is a no-op. One-time migration: on first run a per-slug cursor is seeded from the old shared _bridge/.inbox-cursor when present, so upgrading never re-drains (and re-applies) already-processed intent such as a stale conn grant.
  • FO heartbeat (_bridge/fo.$SLUG.json): present-time UTC RFC3339 ts + state:"idle" + best-effort session_id, written on boot and each idle tick. Bridge keys liveness on ts freshness ([now-30m, now]; future-dated rejected) so an attached FO shows live. state is idle only — the mod runs at startup/idle boundaries; finer working/idle telemetry lives in events.jsonl.
  • Slug safety: $SLUG = basename of the workflow dir, validated to a plain filename component (rejects empty/./../path separators/whitespace) before it ever names a _bridge/*.$SLUG path — an unsafe slug no-ops rather than escaping _bridge/.
  • Honest: delivery is FO-pull — read on the FO's next loop tick (latency = one loop cadence), never synchronous push, and at-least-once (the cursor advances after acting, so a crash mid-drain re-delivers rather than loses; conn/tell handling is idempotent). A Claude Code session has no inbound API; the mod makes the read first-class, it doesn't fake a push.

The mod is workflow-scoped (lives in the dev workflow's _mods/, copied manually into other workflows' _mods/); promoting it to a built-in plugin mod is a deferred follow-up.

2. Normalized FO event hooks

hooks/hooks.json + scripts/spacedock-bridge-events.sh — observe-only, async Claude Code hooks (SessionStart/UserPromptSubmit/PostToolUse/Notification/Stop/SubagentStop) that append a stable, normalized line to «cwd»/_bridge/events.jsonl:

{"ts","event","session_id","agent_id","agent_type","detail":{"tool","source"}}

so Bridge can tail a Spacedock-owned contract instead of coupling to Claude Code's internal transcript JSONL. agent_id/agent_type are empty for the main FO and set for ensign subagents (Bridge can tell FO vs ensign). The script never blocks the session: exits 0 and no-ops on missing jq/cwd/write — a telemetry side-channel must not be able to break the FO. Emits liveness only (event + tool name), never tool inputs/outputs or prompt text. A best-effort, lock-free size cap keeps events.jsonl bounded (it would otherwise grow on every tool call). Wired via the plugin manifest "hooks" key.

_bridge/ is gitignored (transient per-session runtime state).

Path alignment (load-bearing)

The mod reads _bridge/ at the FO's cwd (repo root). Bridge anchors the inbox, heartbeat, and feed on its --repo-root flag, falling back to --fleet only when --repo-root is unset (bridge:internal/server/server.go sessionRoot()). So routing reaches this FO only when Bridge is launched with --repo-root pointing at the FO's cwd; under a multi-workflow --fleet <repo>/docs/spacedock layout with no --repo-root, Bridge writes to docs/spacedock/_bridge/ while the FO reads <repo>/_bridge/ and intent silently never arrives. The dogfood aligns both at the repo root via --repo-root. Making the path independently configurable is a possible follow-up.

3. Contributor docs — local build & run

CONTRIBUTING.md gains Develop, Build from Source, and Run your Branch sections (previously tribal knowledge, scattered across AGENTS.md and docs/runtime-live-ci.md):

  • Develop — Go 1.22+ module layout (cmd/spacedock/, internal/cli/, skills/), the baseline gate (go test ./..., -race, gofmt -w ./cmd ./internal), and pointers to AGENTS.md + the live-tagged E2E suites.
  • Build from Sourcego build -o ./spacedock ./cmd/spacedock; .gitignore gains /spacedock so the artifact never dirties the tree.
  • Run your Branch./spacedock claude --plugin-dir "$PWD" runs the binary + checkout skills with no install and no merge to main, plus an "Avoid colliding with an installed Spacedock" note covering the three surfaces (PATH → explicit ./spacedock, skills → --plugin-dir, host install → the next/main channel stamp) and the one combination that does collide.

4. Fleet-mode First Officer (opt-in, #437)

The single-session counterpart to the multi-session seam above: one spacedock claude session driving multiple commissions at once, alongside — not replacing — the default single-workflow model. Both operating models are user-selectable; neither is hardcoded away.

Established by an antagonistic review of #437, this is a skill-only change — the binary already enumerates multiple workflows (status --discover, locked by TestNativeDiscoverParity) and parameterizes every call by --workflow-dir, so fleet mode changes what the FO does with the discovered set, not the command surface. (The review also dropped the draft's --fleet-dir CLI idea, corrected a wrong "reconcile is per-workflow" claim — dispatch reconcile is session-roster-scoped — and surfaced the omitted hard part: per-workflow state.)

  • skills/first-officer/references/first-officer-shared-core.md — a quotable launch directive ("drive the fleet" / "run all workflows" / "fleet mode", same quotable-grant discipline as the conn) flips fleet mode; the FO adopts the workflows the directive names as the member set — or all discovered when it names none — instead of presenting the list, and the interactive greet lists the resolved set to confirm before dispatch (Startup step 3 + a new ## Fleet Mode section). Boot steps 4–7 (taxonomy read, state.boot/ensure-ready/sweep-merged) run per member, including independent split-root .spacedock-state checkouts whose rebase-conflict halts suspend only that member, never its peers.
  • skills/first-officer/references/fo-dispatch-core.md — the event loop round-robins the per-entity iteration across members, each scoped to that member's {workflow_dir}; one roster reconcile spans the whole fleet. Standing teammates accumulate as the union of members' declarations (idempotent; members sharing a teammate name share one live instance). Kept host-neutral — no Claude-team tokens in the shared dispatch core (enforced by TestDispatchCoreHasNoClaudeTeamImperative).
  • Captain intent for free: §1's per-record target gives fleet mode per-workflow conn/tell granularity (all = fleet-wide, <slug> = scoped) — and one fleet FO owns one cursor, so it's simpler to route to than N sessions.
  • Compatibility-first: default (no directive) is byte-for-byte unchanged — zero discover still reports-and-stops, multiple still presents the list.
  • skills/integration/fleet_discover_smoke_test.go — locks the adopt-all precondition: status --discover lists every commissioned member and each member's status runs independently with no cross-member leak.

Testing

  • jq-valid manifests; shellcheck clean; gofmt clean (incl. the new smoke test).
  • The suites that exercise the changed surfaces pass: contract (plugin manifest), status + dispatch (mod parsing), contractlint (64 — the skill-prose code gate stays green: host-neutral dispatch core, no new hook headings), and the new skills/integration fleet-discover smoke.
  • Three failures on internal/release, internal/cli, skills/integration/TestSurveyCodexPresenceThroughSync are pre-existing on main in this environment (git-tag-message rejection + Codex CLI absence) — verified by re-running them with this change stashed; unrelated to this PR.

Notes / follow-ups

  • A //go:build live multi-workflow fleet-drive behavioral test (mirroring the existing live ensign-cycle) remains a follow-up, like the other live-tagged E2E suites.
  • Per-member context pressure: one session juggling N workflows shares one context window — documented as an operator trade-off in ## Fleet Mode; standing-teammate identity/lifetime across members may want further tightening.
  • Path configurability (see Path alignment above) — a possible follow-up to remove the --repo-root co-location requirement.
  • Event list is the conservative, well-established set; can extend if richer signals are wanted.

🤖 Generated with Claude Code

The Spacedock side of the seam to the Bridge command-center UI (the Bridge half
shipped in spacedock-dev/bridge#12). Two additive pieces; no core-skill or binary
change.

1. docs/dev/_mods/bridge-inbox.md — an idle/startup-hook mod (mirroring pr-merge)
   that drains captain intent Bridge queues at _bridge/inbox.jsonl. Records are
   {ts, kind:tell|conn, text, granted}; tell is a talk-to-FO directive, conn is a
   handover grant/revoke. Consumes via an append-only-safe cursor
   (_bridge/.inbox-cursor) so neither side rewrites the inbox and re-firing is
   idempotent. Honest: delivery is FO-pull (one loop cadence), never synchronous.

2. hooks/hooks.json + scripts/spacedock-bridge-events.sh — register observe-only,
   async Claude Code hooks (SessionStart/UserPromptSubmit/PostToolUse/Notification/
   Stop/SubagentStop) that append a stable, normalized event line
   ({ts,event,session_id,agent_id,agent_type,detail}) to «cwd»/_bridge/events.jsonl,
   so Bridge can tail a Spacedock-owned contract instead of coupling to Claude
   Code's internal transcript format. The script never blocks the session: exits 0
   and no-ops on missing jq/cwd/write. agent_id/agent_type distinguish FO (empty)
   from ensign subagents. Wired via the plugin manifest "hooks" key.

_bridge/ is gitignored (transient per-session runtime state).

Verified: jq-valid manifests, shellcheck clean, gofmt clean; the contract (plugin
manifest), status and dispatch (mod parsing) suites pass. (Three pre-existing
env-only failures on main — git-tag signing + Codex CLI absence — are unrelated.)

Closes #434

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nuow8cNPWYSEZNLnakn6Wt
@gcko gcko requested a review from clkao June 23, 2026 01:12
@gcko gcko self-assigned this Jun 23, 2026
@gcko

gcko commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Code Review: PR #435

SHA d93b2aa3 · Verdict GO

Additive Bridge seam: a bridge-inbox drain mod, a normalized event-emitter
script wired through six Claude Code hooks, a plugin.json hooks key, and a
.gitignore entry. The script is defensively correct (set -u, every failure
path || exit 0, no set -e), emits liveness only (event + tool name, never
inputs/outputs/prompt text), and the cursor-drain logic is sound. Manifests are
jq-valid, shellcheck is clean, the script carries the executable bit, and
the contract / status / dispatch suites pass locally (confirming the new
mod parses without breaking standing-mod or hook-section extraction). No
blocking findings.

Notes

  1. scripts/spacedock-bridge-events.sh:33-34agent_id/agent_type
    attribution is thinner than the PR sells. Per the Claude Code hook schema
    these keys are present mainly on SubagentStop (and events fired inside a
    subagent context); they are absent from the high-frequency main-session
    PostToolUse / UserPromptSubmit / Notification payloads. So "Bridge can
    tell FO vs ensign" holds only where the platform actually populates them —
    per-event attribution will be empty for most emitted lines. The // ""
    defaults mean nothing breaks. I could not verify empirically (no dogfooded
    _bridge/events.jsonl exists yet — the hooks key is new in this PR);
    recommend confirming against one real ensign-context payload before Bridge
    relies on per-event attribution. Pass C.

  2. scripts/spacedock-bridge-events.sh:43events.jsonl has no rotation or
    size bound, and PostToolUse appends one line per tool call. A long-lived
    FO session can grow it large. Gitignored/transient state mitigates the blast
    radius, but a cap (or a documented "Bridge truncates on read") would prevent
    surprise disk growth in extended sessions. Pass G.

  3. docs/dev/_mods/bridge-inbox.md:42 — the cursor advances only after acting
    on each record (step 5). A crash between acting and the
    echo "$NEW" > .inbox-cursor write re-reads and re-acts on already-processed
    tell/conn records on the next boot — at-least-once, not exactly-once. Low
    frequency and the captain-reporting step make this tolerable, but a tell
    that commissions work is not idempotent on replay. Worth a one-line caveat in
    the mod. Pass D.

Verified: manifests jq-valid, shellcheck clean, script executable,
go test ./internal/{contract,status,dispatch}/ all pass. Concurrent appends
from parallel ensigns are safe while lines stay small (single write() under
PIPE_BUF with O_APPEND), which the fixed small-field schema guarantees.

… sections

Document the from-source dev loop that was previously tribal knowledge:
build (`go build -o ./spacedock ./cmd/spacedock`), run the branch against
checkout skills (`--plugin-dir "$PWD"`), and avoid colliding with an
installed (e.g. Homebrew) Spacedock across all three surfaces — PATH,
plugin resolution, and the next/main channel stamp. Gitignore the local
`/spacedock` build artifact so the documented build keeps the tree clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Spacedock half of multi-session control (the Bridge half is merged):
teach the bridge-inbox mod to honor the per-record `target` Bridge writes,
drain via a per-workflow cursor, and emit the per-workflow FO heartbeat
Bridge's roster reads.

- Inbox schema block now documents `target` ("<slug>"|"all"; absent => all).
- Drain acts only on records targeted at this workflow ($SLUG) or "all";
  a record for another workflow is skipped but still advances the cursor.
- Cursor is per-workflow `.inbox-cursor.$SLUG`; concurrent FOs never race.
  One-time migration seeds it from the old shared `.inbox-cursor` so the
  first run does not re-drain (and replay) already-processed intent.
- Heartbeat `fo.$SLUG.json` {session_id, ts (present UTC RFC3339), state:idle}
  written on boot + each idle tick; state is idle (the mod runs between
  dispatches and cannot honestly claim working).
- Slug derived from {workflow_dir} basename and validated (rejects empty/./..
  and any non-[A-Za-z0-9._-] char) so it can never escape _bridge/.

Markdown mod only; no Go change. Closes #436.
@gcko

gcko commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Added the multi-session control half of the Bridge seam (mirrors issue #436 / Linear DRC-3732, pairs with the existing DRC-3727 work on this branch):

  • docs/dev/_mods/bridge-inbox.md now honors the per-record target Bridge writes — the FO drains only records addressed to its workflow ($SLUG) or all, via a per-workflow cursor _bridge/.inbox-cursor.$SLUG, so concurrent FOs sharing one _bridge/ never race. A one-time migration seeds the per-slug cursor from the old shared .inbox-cursor to avoid re-draining (and replaying) already-processed intent.
  • The mod also writes the per-workflow FO heartbeat _bridge/fo.$SLUG.json ({session_id, ts, state:"idle"}, present-time UTC ts) on boot + each idle tick — the liveness signal Bridge's per-workflow roster reads. state is idle (the mod runs between dispatches; finer working/idle telemetry stays in events.jsonl).
  • Slug is derived from {workflow_dir} basename and validated so it can never escape _bridge/.

Markdown mod only — no Go change. Spec was hardened by an antagonistic review (fixed: the unwritable working state, the cursor-migration re-drain hazard, and the schema-doc gap).

Note: the repo's go test ./... has two pre-existing, unrelated failures on this branch (internal/release "no tag message"; the Codex TestSurveyCodexPresenceThroughSync env-dependent check) — both reproduce without this change, which touches only markdown.

@gcko

gcko commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Code Review: PR #435

SHA da70ac2e · Verdict GO

Combined antagonistic review of the two Spacedock-side seam tickets — DRC-3727 (bridge-inbox drain mod + normalized FO event hooks) and DRC-3732 (target-filtered drain + per-workflow cursor + FO heartbeat) — checked against the already-merged Bridge half, DRC-3731 (spacedock-dev/bridge#21).

The review cross-referenced the producer side in the local bridge checkout. The cross-repo contract matches field-for-field, which is the thing that mattered most:

  • Inbox (Bridge Intent → mod drain): ts/kind/text/granted/target, target absent ⇒ all. Bridge writes complete line\n via atomic O_APPEND (bridge:internal/spacedock/foinbox.go:51), so the mod's wc -l + bounded sed line-counting is sound — a half-written line is never read, the cursor catches it next tick.
  • Heartbeat (mod → Bridge LoadFORoster, bridge:internal/spacedock/foroster.go:37): {session_id,ts,state} at _bridge/fo.<slug>.json; freshness window [now-30m, now], future-dated rejected. The mod's date -u +%Y-%m-%dT%H:%M:%SZ is valid RFC3339 and parses cleanly into Go's time.Time.
  • Cursor is purely Spacedock-side; Bridge never reads or writes it — no cross-repo race surface.
  • Both agent_id/agent_type (used to distinguish FO vs ensign) are real Claude Code hook payload fields, populated only under --agent/subagents — the distinction works as described.
  • ${CLAUDE_CODE_SESSION_ID} is not in the public hook-input contract, but it is empirically present in Spacedock's FO runtime and already relied on (docs/roadmap/0221-layered-fo/m4-readiness.md:40 auto-team registry match) — so the heartbeat session_id is populated in practice, not dead.

Quality gates: shellcheck clean, hooks.json/plugin.json valid JSON, plugin hooks key + ${CLAUDE_PLUGIN_ROOT} usage match the documented plugin-hook schema.

No blockers, no issues. The notes below are non-blocking.

Notes

  1. PR description describes the stale DRC-3727 contract. The body still lists the inbox schema as {ts, kind, text, granted} and the cursor as the shared _bridge/.inbox-cursor — but the committed mod (docs/dev/_mods/bridge-inbox.md) implements the DRC-3732 contract: target field, per-slug .inbox-cursor.$SLUG, one-time migration seed, and the fo.$SLUG.json heartbeat. A cross-repo reviewer reading the body gets the wrong contract. The code is correct; the description is not — update it before merge.

  2. docs/dev/_mods/bridge-inbox.md:10 (and PR body) — inbox path alignment is conditional on --repo-root, not coincidental. Bridge resolves the inbox and heartbeat from sessionRoot(), which is --repo-root (the FO cwd) when set and falls back to --fleet only when it isn't (bridge:internal/server/server.go:60-64,896). So routing works iff Bridge is launched with --repo-root <repo> matching the FO's cwd; under the multi-workflow --fleet <repo>/docs/spacedock layout without --repo-root, Bridge writes to docs/spacedock/_bridge/ while the FO reads <repo>/_bridge/ and intent silently never arrives. The PR body's "Bridge writes its inbox at its --fleet root … make the path configurable is a follow-up" undersells this — the --repo-root anchor already exists. Worth stating the launch requirement in the mod, not just the PR body.

  3. scripts/spacedock-bridge-events.sh:46_bridge/events.jsonl grows unbounded. Every PostToolUse (i.e. every tool call) appends a line with no rotation or truncation; a long-running FO session produces a large file that Bridge tails on every poll. It is gitignored and transient per session, so not urgent, but a size cap or rotation is worth a follow-up.

  4. docs/dev/_mods/bridge-inbox.md:73-76drain is at-least-once on crash, not exactly-once. The cursor advances (step 5) only after acting on the batch (step 4), so a crash/kill between acting and the echo "$NEW" > .inbox-cursor.$SLUG write re-delivers those records next tick — a duplicate conn/tell. The ordering chosen is the safer one (no lost intent; the migration note already guards against re-applying old grants on first run), and a repeated conn grant is largely idempotent, so this is acceptable — flagging it as the known semantic.

  5. docs/dev/_mods/bridge-inbox.md:44-76$SLUG does not survive across separate FO Bash invocations. The heartbeat block (48-50) and drain block (61-76) both use $SLUG, derived once in the intro (17-22). Each Bash tool call is a fresh shell; if the FO runs these blocks as separate calls without carrying $SLUG, the heartbeat writes _bridge/fo..json (empty slug), which Bridge never reads → the workflow shows not-attached despite a live FO. The slug-safety case guard is not re-run in the heartbeat block, so it doesn't catch the empty-var case. No corruption (the write stays inside _bridge/), just a silently useless heartbeat. Consider instructing the FO to derive + validate $SLUG at the top of each hook block.

  6. docs/dev/_mods/bridge-inbox.md:17placeholder naming inconsistency. This mod uses {workflow_dir}; the sibling mods/pr-merge.md it mirrors uses {dir}/{slug}. The FO interprets either, and an unsubstituted literal fails safe via the slug guard (no-op, not corruption), so harmless — but align the convention.

@gcko gcko left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review: GO — no blockers. See review comment for 6 non-blocking notes (top: PR body describes the stale DRC-3727 contract; update before merge).

… semantics, bounded event log

Review of #435 (DRC-3727 + DRC-3732) surfaced six non-blocking notes; this fixes them:

- bridge-inbox.md: state the load-bearing --repo-root path-alignment requirement
  (Bridge anchors inbox/heartbeat/feed on --repo-root, falls back to --fleet) so
  routing to this FO can't silently break under a multi-workflow --fleet layout.
- bridge-inbox.md: require each hook to derive+validate $SLUG in the same shell it
  runs the heartbeat/drain in — a fresh-shell split would write a stray fo..json.
- bridge-inbox.md: document at-least-once drain delivery (cursor advances after
  acting) and the idempotent conn/tell handling it implies.
- bridge-inbox.md: align placeholder to {dir} (matches the sibling pr-merge mod).
- spacedock-bridge-events.sh: best-effort, lock-free size cap on events.jsonl so the
  PostToolUse-driven liveness log can't grow without bound; degrades to a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an opt-in fleet mode so a single FO session can drive MULTIPLE
commissioned workflows, alongside (not replacing) the default single-
workflow model. Skill-only: the binary already enumerates multiple
workflows (status --discover) and parameterizes every call by
--workflow-dir, so fleet mode changes what the FO does with the
discovered set, not the command surface.

- shared core: a quotable launch directive ("drive the fleet" / "run
  all workflows" / "fleet mode") flips fleet mode; the FO adopts ALL
  discovered workflows as a member set instead of presenting the list.
  Per-member boot (taxonomy read, state.boot/ensure-ready/sweep-merged),
  including independent split-root checkouts whose halts don't block
  peers. New "## Fleet Mode" section; Startup step 3 + SKILL.md trigger.
- dispatch core: round-robin the per-entity event loop across members,
  each iteration scoped to the member's {workflow_dir}; one roster
  reconcile spans the fleet. Standing teammates accumulate as the union
  of members' declarations (idempotent; shared names share one instance).
  Kept host-neutral — no Claude-team tokens in the shared dispatch core.
- captain intent: the bridge-inbox `target` (this PR) gives fleet mode
  per-workflow conn/tell granularity for free; one fleet FO, one cursor.

Default (no directive) is byte-for-byte unchanged: zero discover still
reports-and-stops; multiple still presents the list.

Tests: skills/integration fleet-discover smoke locks the adopt-all
precondition (status --discover lists all members; per-member status is
independent). contractlint stays green (host-neutral dispatch core, no
new hook headings). A //go:build live multi-workflow drive remains
follow-up, like the existing live ensign-cycle.

Refs #437.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ign prose

Bridge's "running" badge maps a live working session to its ship via
_bridge/sessions/<session_id>.json. That marker was written by an ensign
first-action shell — which the ensign LLM skipped ~3 of every 4 dispatches
(measured: 10 of 13 recent ensign sessions had no marker), so the
actively-working ensigns couldn't be mapped and almost nothing showed as
running.

Move the write into the event hook, which fires deterministically on every
tool call (the same mechanism that keeps events.jsonl accurate). On an
ENSIGN's Read of its entity file (.../docs/spacedock/<workflow>/<slug>.md,
flat or <slug>/index.md) it records {session_id, entity, workflow}:

- Reliable — hook-driven, not LLM-compliance-driven.
- Collision-free — the path carries the WORKFLOW, so a ticket id reused
  across workflows (e.g. drc-3467 in both linear-drc-review and
  linear-drc-ship) is no longer ambiguous (Bridge scopes the join by
  workflow; consumer change is a separate Bridge PR).
- First-write-wins per session → the ensign's OWN entity (read before any
  duplicate-check sibling reads) is recorded; siblings don't overwrite.
- Ensign-only (agent_type), _archive/_mods and unsafe slugs rejected,
  observe-only and best-effort (every step degrades to a no-op).

Removes the now-redundant "Bridge Session Link" first-action from the
Claude ensign runtime (the ensign needs to do nothing — reading its entity
file is already part of its work).

Test: skills/integration drives the hook with synthetic payloads — entity
+ workflow recorded, first-write-wins on a sibling Read, FO Read writes
nothing, _archive Read skipped. contractlint stays green (66).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r hook

The FO passes a repo-relative {entity_file_path} (docs/spacedock/<wf>/<slug>.md),
so an ensign's scoped Read carries that relative path. The marker hook only
matched */docs/spacedock/*/*.md (leading slash required), so it missed EVERY live
ensign — markers were only written by my synthetic absolute-path tests. Match the
relative form too. Verified: relative-path Read now records {entity, workflow}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A review ensign for a single-root, non-worktree, local-only-entity workflow
(linear-drc-review) followed the generic "MUST commit before signaling" and ran
a bare git add/commit at the repo ROOT. A concurrent actor had switched that
shared working tree to feature/more-migration-issues, so the entity landed on an
unrelated branch (and that ancient branch lacked the workflow gitignore rule, so
the meant-to-be-local entity got committed). Root cause: the commit contract only
defined a target for worktree and split-root stages; a single-root non-worktree
stage fell through to a bare-root commit.

Scope the rule: commit ONLY to your isolated target (a worktree, or a split-root
state checkout) — NEVER git add/commit at the bare repo root. A single-root
non-worktree stage has no ensign commit target: write the entity in place (plus
the stage external write, e.g. Linear) and signal; trunk/state-transition commits
are the FO scope. New "### Single-Root, No Commit Target" subsection; step 5 and
the MUST-commit rule both scoped.

Test: contractlint anchor locks the bare-root prohibition + the carve-out and reds
if MUST-commit goes unqualified again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The FO shares one repo-root working tree with every non-worktree agent and any
concurrent human/CI actor. A concurrent git checkout moved HEAD to
feature/more-migration-issues mid-session, deleting the tracked workflow READMEs
and stranding the FO on the wrong branch.

Boot step 2 now records the launch branch (git rev-parse --abbrev-ref HEAD) and
requires the FO to re-check it before each dispatch / state-changing git op, and
HALT with a captain-facing surface if it changed — rather than dispatching or
committing into a switched tree. Pinned on the branch NAME so a same-branch
fast-forward is normal, not a halt. Points at the fleet playbook isolation guidance
(RC2a) as the real prevention; this is the fail-safe.

Defense-in-depth with RC1 (no bare-root commit) and RC2a (isolated checkout).
Test: contractlint anchor locks the launch-branch record + halt + branch-NAME pin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leet-history feed

Two Bridge-seam gaps for a busy/Driving FO:

- Steering + liveness (Issue 2, event loop step 0.6): the captain inbox + heartbeat
  were only serviced at the idle boundary (step 3), which a continuously-dispatching
  FO never reaches — so a queued `pause`/redirect sat unread AND the heartbeat went
  stale (Bridge showed the FO not-attached mid-drive). Now run the bridge-inbox idle
  work — heartbeat refresh AND drain — at the TOP of every iteration. A drained pause
  halts further dispatch this iteration. (The heartbeat half is the antagonistic-review
  fix: draining alone left liveness broken for the exact busy-FO case.)

- Fleet history (Issue 1, bridge-inbox mod): a local-only workflow commits no
  dispatch:/advance: git narration, so Bridge\047s fleet-history was empty while the FO
  drove. The mod now appends a narration line to _bridge/fo-feed.jsonl on
  dispatch/advance/complete (best-effort enrichment; Bridge also derives reliable
  dispatch events from the hook-written session markers, so the history is never empty
  even when this prose is skipped).

Consumer + the reliable marker-derived feed land in Bridge (feat/fo-feed-consumer).
Tests: contractlint anchors for the eager step (incl. heartbeat refresh) and the mod feed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Under split-root state (recce-cloud-infra#1484) an ensign's entity now
lives at docs/spacedock/<wf>/.spacedock-state/<slug>.md, so deriving the
workflow from the entity file's parent dir recorded ".spacedock-state" as
the workflow — breaking Bridge's workflow-scoped running-badge join for
every split-root ensign.

Derive the workflow from the path SEGMENT after docs/spacedock/ instead of
the parent dir, handling absolute, repo-relative, and nested (.spacedock-state)
paths uniformly. _archive entities (flat or in the state checkout) still
write no marker. Slug comes from index.md's folder or the file basename.

Adds session-marker test cases for a split-root entity path (workflow =
the dir above .spacedock-state, not ".spacedock-state") and a split-root
_archive skip.

Signed-off-by: Jared Scott <jared.scott@variable.team>
Brings the seam PR up to latest main (0.23.0): the skill<->binary contract
bump 1->2 (#443), the resident launcher (#442), the signal-forward pump fix
(#444), and the entity end-value gate (#441).

Conflict: .claude-plugin/plugin.json — kept this branch's `hooks` key and
adopted main's `version: 0.23.0` + `requires-contract: >=2,<3` (CONTRACT_VERSION
is now 2). shared-core.md auto-merged (disjoint regions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@infuseai.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant