CB-401: Peer Launcher SPI — pluggable peers (Stage 4) #3

Closed
opened 2026-07-17 17:49:40 +02:00 by ltms · 2 comments
Owner

Goal

Make claude-bridge scalable to heterogeneous peers (Claude Code, Codex, human, another Claude)
without the core learning any one peer's environment. Introduce a PeerLauncher SPI so the
communication bus delegates peer materialization to a config-selected adapter, while the core keeps
only transport, session/turn lifecycle, and routing.

This also gives the CB-301-ext (worktree/parity) and CB-302 (checkpoint token) environment features a
principled home — inside an adapter — instead of sitting in the core spawn path.

Design

See docs/CB-401-Peer-Launcher-SPI.md on branch feature/peer-launcher-spi.

Key points:

  • WorkerService is already the de-facto Claude-Code launcher (~90% of coupling funnels through it).
  • PeerLauncher interface + opaque PeerHandle (routing id, today == paneId) + Capability set.
  • SessionManager depends on the interface; routes on PeerHandle.id().
  • Capability model (MID_TURN_ASK, SELF_PR, WORKTREE, ORPHAN_REAP) so the protocol degrades
    gracefully per peer and the core never assumes "every peer is a Claude in a worktree".

Staging

  • Stage A (this ticket): extract the SPI in-tree, one impl (ClaudeCodeLauncher = adapted
    WorkerService), config-selected. Behaviour-preserving, no config change. Full gate green.
  • Stage B (CB-402+): a second in-tree adapter (Codex/human) proves the SPI held.
  • Stage C (later, gated): dynamic external plugin loading — only behind a trust/capability model,
    because a launcher injects env/tokens into peers at daemon privilege.

The primary remains the merge/verify gate regardless of stage.

Acceptance (Stage A)

  • New dev.ltms.bridged.peer package: PeerLauncher, PeerHandle, SpawnRequest, Capability.
  • ClaudeCodeLauncher implements PeerLauncher; SessionManager depends on the interface.
  • No behaviour/config change; all existing tests pass; mvn clean install green with MVN_EXIT
    captured (no masking pipe); IDE diagnostics clean (0 errors / 0 warnings).
## Goal Make `claude-bridge` scalable to heterogeneous peers (Claude Code, Codex, human, another Claude) without the core learning any one peer's environment. Introduce a **`PeerLauncher` SPI** so the communication bus *delegates* peer materialization to a config-selected adapter, while the core keeps only transport, session/turn lifecycle, and routing. This also gives the CB-301-ext (worktree/parity) and CB-302 (checkpoint token) environment features a principled home — inside an adapter — instead of sitting in the core spawn path. ## Design See `docs/CB-401-Peer-Launcher-SPI.md` on branch `feature/peer-launcher-spi`. Key points: - `WorkerService` is already the de-facto Claude-Code launcher (~90% of coupling funnels through it). - `PeerLauncher` interface + opaque `PeerHandle` (routing id, today == paneId) + `Capability` set. - `SessionManager` depends on the interface; routes on `PeerHandle.id()`. - Capability model (`MID_TURN_ASK`, `SELF_PR`, `WORKTREE`, `ORPHAN_REAP`) so the protocol degrades gracefully per peer and the core never assumes "every peer is a Claude in a worktree". ## Staging - **Stage A (this ticket):** extract the SPI in-tree, one impl (`ClaudeCodeLauncher` = adapted `WorkerService`), config-selected. Behaviour-preserving, no config change. Full gate green. - **Stage B (CB-402+):** a second in-tree adapter (Codex/human) proves the SPI held. - **Stage C (later, gated):** dynamic external plugin loading — only behind a trust/capability model, because a launcher injects env/tokens into peers at daemon privilege. The primary remains the merge/verify gate regardless of stage. ## Acceptance (Stage A) - New `dev.ltms.bridged.peer` package: `PeerLauncher`, `PeerHandle`, `SpawnRequest`, `Capability`. - `ClaudeCodeLauncher implements PeerLauncher`; `SessionManager` depends on the interface. - No behaviour/config change; all existing tests pass; `mvn clean install` green with `MVN_EXIT` captured (no masking pipe); IDE diagnostics clean (0 errors / 0 warnings).
Author
Owner

Stage A — delivered, primary-verified, integrated ✅

Delegated to an off-subscription gx10 worker (isolated pre-trusted worktree); implemented, then verified and integrated by the primary gate.

Landed: feature/peer-launcher-spi @ e056c7e (fast-forward, pushed to origin).

Delivered

  • New dev.ltms.bridged.peer package: PeerLauncher, PeerHandle, SpawnRequest, Capability.
  • WorkerService implements PeerLauncher; spawn(SpawnRequest) → PeerHandle (id == paneId today) delegates to the existing spawn(profile, cwd, callerCwd) → Agent — no existing signature changed, so the un-migrated MCP/REST callers keep compiling.
  • SessionManager now depends on PeerLauncher (all ctors), keys its registry on PeerHandle.id(), builds a SpawnRequest, and delegates stop/effectiveCwd/parityOverlay/defaultProfile.
  • capabilities() = MID_TURN_ASK, WORKTREE, ORPHAN_REAP always; SELF_PR added when any profile carries a git-forge token.
  • Bridged.main holds the PeerLauncher type, casting back to WorkerService only at the two not-yet-migrated callsites (BridgeMcp, BridgedApp), honestly commented.

Primary gate (the worker could not run these)

  • IDE diagnostics on all 8 changed files: 0 errors / 0 warnings on CB-401 code. (3 warnings in SessionManager are pre-existing — CB-303 8d51066, CB-301 54d907c — outside every diff hunk; confirmed via blame.)
  • mvn clean install: BUILD SUCCESS, MVN_EXIT=0.
  • Tests: 183 run, 0 failures, 0 errors (+10 new PeerHandle indirection tests).

Deviations / follow-ups (deferred, not blocking)

  1. Impl kept as WorkerService, not renamed to ClaudeCodeLauncher. The rename is a mechanical IDE refactor (updates all refs/tests) best done as its own commit so it doesn't muddy the SPI-extraction diff — tracked for a follow-up.
  2. PeerHandle gained terminalId() (default → null) beyond the pure-opaque-id ideal, because SessionManager needs it to build WorkerSession. Non-herdr peers return null; revisit under Stage B when a real second peer exists.
  3. Stage B (CB-402+, second adapter) and Stage C (dynamic loading, behind the trust gate) remain open as separate tickets.

Closing Stage A — the SPI seam is proven and green.

## Stage A — delivered, primary-verified, integrated ✅ Delegated to an off-subscription gx10 worker (isolated pre-trusted worktree); implemented, then verified and integrated by the primary gate. **Landed:** `feature/peer-launcher-spi` @ `e056c7e` (fast-forward, pushed to origin). ### Delivered - New `dev.ltms.bridged.peer` package: `PeerLauncher`, `PeerHandle`, `SpawnRequest`, `Capability`. - `WorkerService implements PeerLauncher`; `spawn(SpawnRequest) → PeerHandle` (id == paneId today) delegates to the existing `spawn(profile, cwd, callerCwd) → Agent` — **no existing signature changed**, so the un-migrated MCP/REST callers keep compiling. - `SessionManager` now depends on `PeerLauncher` (all ctors), keys its registry on `PeerHandle.id()`, builds a `SpawnRequest`, and delegates `stop`/`effectiveCwd`/`parityOverlay`/`defaultProfile`. - `capabilities()` = `MID_TURN_ASK, WORKTREE, ORPHAN_REAP` always; `SELF_PR` added when any profile carries a git-forge token. - `Bridged.main` holds the `PeerLauncher` type, casting back to `WorkerService` only at the two not-yet-migrated callsites (`BridgeMcp`, `BridgedApp`), honestly commented. ### Primary gate (the worker could not run these) - IDE diagnostics on all 8 changed files: **0 errors / 0 warnings** on CB-401 code. (3 warnings in `SessionManager` are pre-existing — CB-303 `8d51066`, CB-301 `54d907c` — outside every diff hunk; confirmed via blame.) - `mvn clean install`: **BUILD SUCCESS**, `MVN_EXIT=0`. - Tests: **183 run, 0 failures, 0 errors** (+10 new `PeerHandle` indirection tests). ### Deviations / follow-ups (deferred, not blocking) 1. **Impl kept as `WorkerService`**, not renamed to `ClaudeCodeLauncher`. The rename is a mechanical IDE refactor (updates all refs/tests) best done as its own commit so it doesn't muddy the SPI-extraction diff — tracked for a follow-up. 2. **`PeerHandle` gained `terminalId()`** (default → `null`) beyond the pure-opaque-id ideal, because `SessionManager` needs it to build `WorkerSession`. Non-herdr peers return null; revisit under Stage B when a real second peer exists. 3. Stage B (CB-402+, second adapter) and Stage C (dynamic loading, behind the trust gate) remain open as separate tickets. Closing Stage A — the SPI seam is proven and green.
ltms closed this issue 2026-07-18 07:56:53 +02:00
Author
Owner

Follow-up deferral #1 cleared — WorkerService → ClaudeCodeLauncher (3aa69a9 on feature/peer-launcher-spi).

Claude Code is the first-class peer, so its adapter now says so by name. Pure IDE refactor_rename (class + file + WorkerServiceTest → ClaudeCodeLauncherTest) plus stale Javadoc/comment mentions swept. No behaviour change. Gate: IDE 0/0 on touched files, mvn clean install BUILD SUCCESS MVN_EXIT=0, 183 tests.

Remaining follow-up: deferral #2 (PeerHandle.terminalId() herdr-ness) — now looks less urgent since the roadmap is more terminal-based coding agents, which also carry a terminalId; will revisit when the second adapter (CB-402) actually lands.

**Follow-up deferral #1 cleared** — `WorkerService` → `ClaudeCodeLauncher` (`3aa69a9` on `feature/peer-launcher-spi`). Claude Code is the first-class peer, so its adapter now says so by name. Pure IDE `refactor_rename` (class + file + `WorkerServiceTest` → `ClaudeCodeLauncherTest`) plus stale Javadoc/comment mentions swept. No behaviour change. Gate: IDE 0/0 on touched files, `mvn clean install` BUILD SUCCESS `MVN_EXIT=0`, 183 tests. Remaining follow-up: deferral #2 (`PeerHandle.terminalId()` herdr-ness) — now looks less urgent since the roadmap is *more terminal-based coding agents*, which also carry a terminalId; will revisit when the second adapter (CB-402) actually lands.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#3