Bridge seam: bridge-inbox drain mod + normalized FO event hooks + opt-in fleet-mode FO#435
Bridge seam: bridge-inbox drain mod + normalized FO event hooks + opt-in fleet-mode FO#435gcko wants to merge 18 commits into
Conversation
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
Code Review: PR #435SHA Additive Bridge seam: a Notes
Verified: manifests |
… 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.
|
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):
Markdown mod only — no Go change. Spec was hardened by an antagonistic review (fixed: the unwritable Note: the repo's |
Code Review: PR #435SHA 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 ( The review cross-referenced the producer side in the local
Quality gates: No blockers, no issues. The notes below are non-blocking. Notes
|
gcko
left a comment
There was a problem hiding this comment.
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>
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-inboxdrain mod (+ heartbeat)docs/dev/_mods/bridge-inbox.md— an idle + startup hook (mirroringpr-merge) that drains captain intent Bridge queues at_bridge/inbox.jsonland writes a per-workflow liveness heartbeat:{ts, kind:"tell"|"conn", text, granted?, target}.tell→ a talk-to-FO directive;conn→ handover grant/revoke within a stated goal.targetroutes the intent — a workflow slug or"all"; absent/empty ⇒all(backward-compatible with older Bridge records). The FO acts only on records wheretarget == "$SLUG"ortarget == "all"; records for other workflows are skipped but still advance this workflow's cursor._bridge/.inbox-cursor.$SLUG): Bridge only appends to the one sharedinbox.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-cursorwhen present, so upgrading never re-drains (and re-applies) already-processed intent such as a staleconngrant._bridge/fo.$SLUG.json): present-time UTC RFC3339ts+state:"idle"+ best-effortsession_id, written on boot and each idle tick. Bridge keys liveness ontsfreshness ([now-30m, now]; future-dated rejected) so an attached FO shows live.stateisidleonly — the mod runs at startup/idle boundaries; finer working/idle telemetry lives inevents.jsonl.$SLUG = basenameof the workflow dir, validated to a plain filename component (rejects empty/./../path separators/whitespace) before it ever names a_bridge/*.$SLUGpath — an unsafe slug no-ops rather than escaping_bridge/.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,asyncClaude Code hooks (SessionStart/UserPromptSubmit/PostToolUse/Notification/Stop/SubagentStop) that append a stable, normalized line to«cwd»/_bridge/events.jsonl:so Bridge can tail a Spacedock-owned contract instead of coupling to Claude Code's internal transcript JSONL.
agent_id/agent_typeare 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 missingjq/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 keepsevents.jsonlbounded (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-rootflag, falling back to--fleetonly when--repo-rootis unset (bridge:internal/server/server.gosessionRoot()). So routing reaches this FO only when Bridge is launched with--repo-rootpointing at the FO's cwd; under a multi-workflow--fleet <repo>/docs/spacedocklayout with no--repo-root, Bridge writes todocs/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.mdgains Develop, Build from Source, and Run your Branch sections (previously tribal knowledge, scattered acrossAGENTS.mdanddocs/runtime-live-ci.md):cmd/spacedock/,internal/cli/,skills/), the baseline gate (go test ./...,-race,gofmt -w ./cmd ./internal), and pointers toAGENTS.md+ thelive-tagged E2E suites.go build -o ./spacedock ./cmd/spacedock;.gitignoregains/spacedockso the artifact never dirties the tree../spacedock claude --plugin-dir "$PWD"runs the binary + checkout skills with no install and no merge tomain, plus an "Avoid colliding with an installed Spacedock" note covering the three surfaces (PATH → explicit./spacedock, skills →--plugin-dir, host install → thenext/mainchannel 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 claudesession 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 byTestNativeDiscoverParity) 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-dirCLI idea, corrected a wrong "reconcile is per-workflow" claim —dispatch reconcileis 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 Modesection). Boot steps 4–7 (taxonomy read,state.boot/ensure-ready/sweep-merged) run per member, including independent split-root.spacedock-statecheckouts 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 byTestDispatchCoreHasNoClaudeTeamImperative).targetgives fleet mode per-workflowconn/tellgranularity (all= fleet-wide,<slug>= scoped) — and one fleet FO owns one cursor, so it's simpler to route to than N sessions.skills/integration/fleet_discover_smoke_test.go— locks the adopt-all precondition:status --discoverlists every commissioned member and each member'sstatusruns independently with no cross-member leak.Testing
jq-valid manifests;shellcheckclean;gofmtclean (incl. the new smoke test).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 newskills/integrationfleet-discover smoke.internal/release,internal/cli,skills/integration/TestSurveyCodexPresenceThroughSyncare pre-existing onmainin 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
//go:build livemulti-workflow fleet-drive behavioral test (mirroring the existing live ensign-cycle) remains a follow-up, like the otherlive-tagged E2E suites.## Fleet Mode; standing-teammate identity/lifetime across members may want further tightening.--repo-rootco-location requirement.🤖 Generated with Claude Code