CB-552: sync architect and session docs
@@ -25,13 +25,14 @@ Every cross-host interaction is one of these. Each is a message flow between two
|
||||
| U3 | **Stranded / late reply** — B replies after A's blocking send timed out (or was never open); held durably until A pulls | B → A (held) | `REPLY` (deferred) |
|
||||
| U4 | **Cross-host spawn** — A requests a new agent *on host B*; gateway B launches it locally and announces it | A → gateway B (control) | `SPAWN` |
|
||||
| U5 | **Roster / presence** — every gateway announces its local agents so all build one union "who/where/status" view | each gateway → all | `PRESENCE` |
|
||||
| U6 | **Main-pair** — the two mains (Opus + cloud, CB-500 B) message each other | A ↔ A' | `SEND`/`REPLY` |
|
||||
| U7 | **Orchestrator ↔ mains** — the orchestrator tier (CB-500 C) drives/monitors its main sessions | O ↔ main | `SEND`/`REPLY` |
|
||||
| U8 | **Broadcast / group announce** — a main publishes **once** to all workers (or a named group); the broker copies the message into every bound inbox | A → all | `BROADCAST` |
|
||||
| U6 | **Lead ↔ architect** — the human-driven lead engages either independent advisory architect | L ↔ A | `SEND`/`REPLY` |
|
||||
| U7 | **Parallel architect advice** — the lead sends the same brief to Claude Sonnet 5 and GPT-5.6/opencode, then compares independent results | L → A1, A2 | `SEND`/`REPLY` |
|
||||
| U8 | **Broadcast / group announce** — a lead publishes **once** to all workers (or a named group); the broker copies the message into every bound inbox | L → all | `BROADCAST` |
|
||||
|
||||
U1–U3 are the [CB-307 rendezvous](9-Implementation) semantics, now spanning hosts. U4–U5 are the
|
||||
net-new federation control plane. U6–U7 reuse the *same* per-entity inbox as U1 — a main and an
|
||||
orchestrator are just message-addressable entities with their own inbox. U8 is for **identical
|
||||
net-new federation control plane. U6–U7 reuse the *same* per-entity inbox as U1 — a lead and an
|
||||
architect are just message-addressable entities with their own inbox. The two architect model families
|
||||
are deliberate: independent agreement is evidence rather than correlated echo. U8 is for **identical
|
||||
announcements** ("everyone: stop", "everyone: report status") — tailored task briefs keep the
|
||||
per-worker fan-out pattern ([Team](6-Team#parallel-fan-out-map--reduce)), which works unchanged
|
||||
across hosts because each send routes to its recipient's inbox wherever it lives.
|
||||
@@ -41,12 +42,12 @@ across hosts because each send routes to its recipient's inbox wherever it lives
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph e["Message-addressable entities (each owns ONE inbox)"]
|
||||
orch["orchestrator<br/>(MCP client)"]
|
||||
main["primary / main<br/>(MCP client)"]
|
||||
arch["architect<br/>(MCP client)"]
|
||||
main["human-driven lead<br/>(MCP client)"]
|
||||
wrk["worker / sandboxed agent<br/>(herdr pane)"]
|
||||
end
|
||||
gw["gateway = bridged<br/>(one per host)"]
|
||||
orch -->|"delivered by local MCP pull"| gw
|
||||
arch -->|"delivered by local MCP pull"| gw
|
||||
main -->|"delivered by local MCP pull"| gw
|
||||
wrk -->|"delivered by local herdr inject"| gw
|
||||
gw -->|"consumes its local entities' inboxes,<br/>publishes to remote inboxes"| broker["BROKER (AMQP)"]
|
||||
|
||||
+95
-1
@@ -28,14 +28,18 @@ six weeks, and the table alone will not carry it.
|
||||
| [A lead can be delivered to](#a-lead-can-be-delivered-to) | automatic | CB-534 | `Bridged.deliverableTo` |
|
||||
| [Leads are visible in bridge_list](#leads-are-visible-in-bridge_list) | automatic | CB-535 | `mcp/BridgeMcp.listFleet` |
|
||||
| [Reply nudges follow the delegating lead](#reply-nudges-follow-the-delegating-lead) | automatic (retires `primary:`) | CB-532 | `mcp/PrimaryRegistry` |
|
||||
| [Advisory architect slots](#advisory-architect-slots) | `architects:` | CB-548 | `auth/ArchitectRegistry` |
|
||||
| [Weighted worker placement](#weighted-worker-placement) | `placement: weighted` + `weight` / `maxLoad` | CB-518 | `placement/` |
|
||||
| [Give workers a toolchain](#give-workers-a-toolchain) | per-profile `env:` | CB-511 | `worker/HerdrPeerLauncher` |
|
||||
| [Run a worker on the subscription](#run-a-worker-on-the-subscription) | profile `subscription: true` | CB-539 | `worker/ClaudeCodeLauncher` |
|
||||
| [Keep a worker conversation](#keep-a-worker-conversation) | session name + resume id on spawn | CB-547 | `peer/SpawnRequest` |
|
||||
| [Isolated worktree per worker](#isolated-worktree-per-worker) | `bridge_spawn{worktree, ticket}` | CB-301-ext | `session/GitWorktrees` |
|
||||
| [Worker tool-surface isolation](#worker-tool-surface-isolation) | automatic | CB-525 | `session/GitWorktrees` |
|
||||
| [Worktree-hostile config isolation](#worktree-hostile-config-isolation) | automatic | CB-543 | `session/GitWorktrees` |
|
||||
| [Worker opens its own PR](#worker-opens-its-own-pr) | `gitTokenEnv:` / `gitHostEnv:` | CB-302 | `worker/HerdrPeerLauncher` |
|
||||
| [Session lifecycle caps](#session-lifecycle-caps) | `lifecycle:` | CB-303 | `session/SessionManager` |
|
||||
| [Durable reply inbox](#durable-reply-inbox) | `broker:` | CB-307 | `msg/AmqpReplyInbox` |
|
||||
| [Reject overlapping rendezvous](#reject-overlapping-rendezvous) | automatic | CB-548 | `msg/Rendezvous` |
|
||||
| [Pin an opencode endpoint](#pin-an-opencode-endpoint) | profile `baseUrl:` | CB-508 | `worker/OpenCodeLauncher` |
|
||||
| [Onboard a project with the plugin](#onboard-a-project-with-the-plugin) | `/plugin install claude-bridge` → `/claude-bridge:setup` | CB-527 | `plugin/` |
|
||||
| [Port a workspace to OpenCode](#port-a-workspace-to-opencode) | `port-to-opencode` skill + `opencode.json` | CB-529 | `.claude/skills/port-to-opencode` |
|
||||
@@ -44,7 +48,7 @@ Nearly every knob above lives in one file, on one profile:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Y["bridged.yaml"] --> G["bind / auth / primary.terminal"]
|
||||
Y["bridged.yaml"] --> G["bind / auth / leaders / architects"]
|
||||
Y --> B["broker"]
|
||||
Y --> W["workers:"]
|
||||
W --> P1["profile: gx10"]
|
||||
@@ -121,6 +125,15 @@ requirement *for that profile only* — every other profile keeps the hard refus
|
||||
boundary: a claude-code profile with no base_url may not spawn, because doing so would bill the
|
||||
subscription.
|
||||
|
||||
```yaml
|
||||
workers:
|
||||
sonnet:
|
||||
kind: claude-code
|
||||
subscription: true
|
||||
model: claude-sonnet-5
|
||||
argv: ["ccs", "sonnet"]
|
||||
```
|
||||
|
||||
**Why.** Some model families (e.g. `sonnet` on `ccs`) have no off-subscription endpoint to point a
|
||||
worker at. Rather than leave those profiles unspawnable, this is an explicit, visible opt-in — a
|
||||
spawn under it logs a WARN naming the profile, so billing the subscription is never an accident.
|
||||
@@ -130,6 +143,57 @@ refused at spawn if both are set. The same contradiction is refused at config lo
|
||||
map: on the subscription path the guard is skipped and the adapter writes neither Anthropic key, so
|
||||
an `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` in `env:` would reach the worker having passed no
|
||||
guard at all (CB-542). A subscription profile may still carry `env:` — just not those two keys.
|
||||
`weight: 0` cannot reserve this profile from unqualified weighted placement: worker-config
|
||||
normalization changes non-positive weights to `1.0`. Use an explicit `profile: sonnet` spawn until
|
||||
the placement policy gains an exclusion setting.
|
||||
|
||||
## Advisory architect slots
|
||||
|
||||
**What.** Declares named, strong-model advisory slots. A bound architect resolves as `architect`, can
|
||||
send, reply, ask, and read, but cannot spawn, stop, or drain the fleet. The operating shape is one
|
||||
human-driven lead, two short-lived architects, and N workers: the lead consults the architects sideways
|
||||
and discards them, rather than creating a supervisor above the lead.
|
||||
|
||||
**On.** Declare each slot against a configured worker profile:
|
||||
|
||||
```yaml
|
||||
architects:
|
||||
sonnet-adviser:
|
||||
profile: sonnet
|
||||
gpt-adviser:
|
||||
profile: gpt
|
||||
```
|
||||
|
||||
**Why.** The rejected alternative put an orchestrator above the lead and made the lead a managed,
|
||||
resumable session. That breaks the actual control boundary: the human drives a lead that pre-exists,
|
||||
is recognised, and cannot be resumed by bridged. Two advisory model families, Claude Sonnet 5 and
|
||||
GPT-5.6 through opencode, receive the same brief independently so agreement is evidence rather than
|
||||
correlated echo.
|
||||
|
||||
**Gotcha.** `architects:` declares slots, not sessions: no terminal is configured and nothing becomes
|
||||
an architect until the lifecycle binds a live terminal to a slot. The profile name is validated at boot,
|
||||
as are duplicate slot names. An architect has delegation authority but no lifecycle authority, by
|
||||
design.
|
||||
|
||||
## Keep a worker conversation
|
||||
|
||||
**What.** Carries a bridge logical session name and a peer-owned resume id through `SpawnRequest`, then
|
||||
returns them from the peer handle where the adapter supports them. Claude Code mints a UUID for a fresh
|
||||
named session, passes it as `--session-id`, exposes the logical name with `-n`, and resumes with `-r`.
|
||||
OpenCode resumes with `-s` and discovers its id after launch from its on-disk session record by matching
|
||||
the worker's unique worktree cwd.
|
||||
|
||||
**On.** Supply a session name and/or resume id when the spawn lifecycle has one to carry. Adapter
|
||||
capabilities state the asymmetry: Claude Code offers `SESSION_NAME` and `SESSION_RESUME`; OpenCode
|
||||
offers `SESSION_RESUME` only.
|
||||
|
||||
**Why.** A bridge session name is an operator-facing roster label, while a provider session id is the
|
||||
only handle that can resume the actual conversation. Treating them as peer-neutral values prevents the
|
||||
core from assuming Claude Code's flags are a universal protocol.
|
||||
|
||||
**Gotcha.** OpenCode has no name flag, and it writes its session record only after it persists a
|
||||
conversation. Its id is therefore discovered lazily and may be absent immediately after spawn; its slot
|
||||
name remains only in bridged's roster.
|
||||
|
||||
## Give workers a toolchain
|
||||
|
||||
@@ -183,6 +247,22 @@ build passed.
|
||||
is deliberate — the bridge is a message bus, and the primary is the gate. Never accept a worker's
|
||||
claim about a check it had no way to run.
|
||||
|
||||
## Worktree-hostile config isolation
|
||||
|
||||
**What.** Every provisioned worktree neutralizes tracked `.mcp.json`, `opencode.json`, and `.autoenv`
|
||||
with a valid format-specific stub, then marks a tracked replacement `--skip-worktree`.
|
||||
|
||||
**On.** Automatic at worktree provisioning; no configuration.
|
||||
|
||||
**Why.** The tracked `opencode.json` can reference gitignored `.secrets/` files that a worktree never
|
||||
contains, and OpenCode refuses to start with that dangling reference. The same isolation rule also
|
||||
prevents a primary-only MCP configuration or an autoenv authorization prompt from entering a worker's
|
||||
tool surface.
|
||||
|
||||
**Gotcha.** The replacement is deliberately valid, not deleted: `{}` for `opencode.json`, an empty
|
||||
server map for `.mcp.json`, and an empty `.autoenv`. A deletion could be undone by a later checkout; the
|
||||
skip-worktree bit keeps the safe local replacement from looking like work for a worker to commit.
|
||||
|
||||
## Worker opens its own PR
|
||||
|
||||
**What.** Injects a repo-scoped forge token so a worker can commit, push over SSH, and open its own
|
||||
@@ -228,6 +308,20 @@ real task's runtime. Without a durable inbox, a reply arriving after that window
|
||||
writing into someone else's broker. Also see the known hole: an async ticket that times out while
|
||||
the session is still BUSY currently discards the later completion rather than parking it.
|
||||
|
||||
## Reject overlapping rendezvous
|
||||
|
||||
**What.** A second attempt to open a reply waiter for the same worker session fails atomically instead
|
||||
of replacing the first waiter.
|
||||
|
||||
**On.** Always on; no configuration.
|
||||
|
||||
**Why.** One session has one outstanding delegated turn. Replacing its waiter silently would strand the
|
||||
first caller and let a reply resolve the wrong request. `MessageService` serializes normal sends, but
|
||||
the atomic rejection is the tripwire that makes a future violation loud rather than corrupt.
|
||||
|
||||
**Gotcha.** This is not concurrent-turn support. A terminal send closes its own waiter before the next
|
||||
turn can open one; a double-open is an invariant failure that must be investigated.
|
||||
|
||||
## Pin an opencode endpoint
|
||||
|
||||
**What.** An opencode profile can target its own OpenAI-compatible endpoint.
|
||||
|
||||
Reference in New Issue
Block a user