Skip to content

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

Closed
gcko wants to merge 19 commits into
mainfrom
bridge-seam-inbox-events
Closed

Bridge seam: bridge-inbox drain mod + normalized FO event hooks + opt-in fleet-mode FO#435
gcko wants to merge 19 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 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>
…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>
gcko and others added 2 commits June 30, 2026 19:05
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>
Bring PR 435 up to date with main (65 commits), reconciling the bridge-seam
work with two paradigm shifts main introduced:

- Contract gate: main replaced the integer contract with minor-version
  coupling (#468). plugin.json now carries the frozen D4 tombstone
  requires-contract ">=3,<4" alongside the branch's "hooks" registration.

- FO Startup collapse: main collapsed the FO Startup recipe to <=4 steps
  ("boot identifies, engage converges") and caps first-officer-shared-core.md
  at 26755 bytes. Rewove PR 435's boot additions onto that spine:
    * launch-branch pin -> a sub-bullet of the boot-identify step
    * before-greet Bridge liveness heartbeat -> step 2b (non-counting),
      an explicit carve-out from main's "boot writes nothing" rule
    * gate drain-before-present kept in «gate.assemble-verdict»
  To fit the byte cap while preserving the opt-in fleet feature, relocated
  the Fleet Mode section to a deferred references/fo-fleet.md (idiomatic to
  main's defer-non-common-prose philosophy), rewritten against the new
  boot-identify + engage paradigm, and repointed fo-dispatch-core + SKILL.md.

.gitignore: kept the _bridge/ block, dropped main's duplicate /spacedock.

All contractlint + bridge/fleet integration tests pass; full suite green
except the pre-existing agentsview TestSurveyCodexPresenceThroughSync
(fails identically on clean main — local binary quirk).

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

gcko commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #509 — the consolidated Bridge seam.

#509 replaces this PR and #445 (which was stacked on this one) with a single, smaller seam authored fresh off current main (both branches were 40 commits behind, with 9 conflicting prose files). It keeps everything Bridge actually consumes while shrinking the Spacedock surface: Bridge reads files, not verbs, so the packaged inbox drain|ack|commit verbs and the fleet-mode/drain mod prose collapse into one contract-conformant bridge-seam mod that writes the _bridge/ files directly, per Bridge's published docs/seam-contract.md. The before-greet heartbeat, the eager top-of-loop drain, and the per-member fleet routing from this PR are all carried forward (re-authored on current main).

Not lost: the shared-working-tree collision hardening (RC1 no-bare-root-commit, RC2b branch-pin halt) and CONTRIBUTING.md that rode along here are orthogonal to the seam and were deliberately excluded from #509 — tracked for re-land in #508.

Closing in favor of #509. History preserved on the bridge-seam-inbox-events branch.

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