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 19 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>
…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>
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>
|
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 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 |
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