Features: fleet block, role pools, role-aware tab labels, lead auto-launch (CB-557, CB-558)
+125
-11
@@ -21,14 +21,18 @@ six weeks, and the table alone will not carry it.
|
||||
|---|---|---|---|
|
||||
| [Ask the bridge who you are](#ask-the-bridge-who-you-are) | `bridge_whoami` | CB-517 | `mcp/BridgeMcp` |
|
||||
| [Primary inside a herdr pane](#primary-inside-a-herdr-pane) | `primary.terminal:` | CB-522 | `auth/CallerResolver` |
|
||||
| [More than one lead](#more-than-one-lead) | `leaders:` | CB-530 | `auth/CallerResolver` |
|
||||
| [More than one lead](#more-than-one-lead) | `fleet.leaders:` | CB-530 | `auth/CallerResolver` |
|
||||
| [Unknown config keys are named](#unknown-config-keys-are-named) | (always on) | CB-530 | `config/BridgedConfig` |
|
||||
| [Find leads by tab name](#find-leads-by-tab-name) | `leadScan:` | CB-531 | `herdr/LeadTabScanner` |
|
||||
| [Find leads by tab name](#find-leads-by-tab-name) | `fleet.leaders.<n>.tabPrefix` | CB-531 | `herdr/LeadTabScanner` |
|
||||
| [One fleet block, role as the key](#one-fleet-block-role-as-the-key) | `fleet:` | CB-557 | `config/BridgedConfig` |
|
||||
| [Role pools decide the backend](#role-pools-decide-the-backend) | `fleet.developers:` etc. | CB-557 | `member/CompositePeerLauncher` |
|
||||
| [Tab labels name the role](#tab-labels-name-the-role) | `fleet.tabLabel:` | CB-557 | `member/HerdrPeerLauncher` |
|
||||
| [Launch a lead when none is live](#launch-a-lead-when-none-is-live) | `fleet.leaders.<n>.profile` + `instances` | CB-558 | `lead/LeadLauncher` |
|
||||
| [Leads talk to each other](#leads-talk-to-each-other) | (always on, two leads) | CB-532 | `auth/Principal` |
|
||||
| [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` |
|
||||
| [Advisory architect slots](#advisory-architect-slots) | `fleet.architects:` | CB-548 | `auth/MemberRegistry` |
|
||||
| [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` |
|
||||
@@ -48,18 +52,27 @@ Nearly every knob above lives in one file, on one profile:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Y["bridged.yaml"] --> G["bind / auth / leaders / architects"]
|
||||
Y["bridged.yaml"] --> G["bind / auth / guard"]
|
||||
Y --> B["broker"]
|
||||
Y --> W["workers:"]
|
||||
W --> P1["profile: gx10"]
|
||||
W --> P2["profile: ollama"]
|
||||
P1 --> K["placement · weight · maxLoad<br/>env · gitTokenEnv · parityOverlay<br/>configDir · cwd · argv"]
|
||||
P2 --> K
|
||||
Y --> L["lifecycle"]
|
||||
Y --> W["profiles:"]
|
||||
W --> P1["profile: gx10"]
|
||||
W --> P2["profile: opus"]
|
||||
P1 --> K["weight · maxLoad · model<br/>env · gitTokenEnv · parityOverlay<br/>configDir · cwd · argv"]
|
||||
P2 --> K
|
||||
Y --> F["fleet:"]
|
||||
F --> FL["leaders"]
|
||||
F --> FA["architects"]
|
||||
F --> FD["developers"]
|
||||
F --> FR["reviewers"]
|
||||
FA --> K
|
||||
FD --> K
|
||||
FR --> K
|
||||
```
|
||||
|
||||
*The configuration surface: daemon-wide settings, then one block per worker profile. A profile is
|
||||
a backend, not a host.*
|
||||
*The configuration surface has two halves. `profiles:` answers "which backend" — model, adapter,
|
||||
cost. `fleet:` answers "who runs, and on which of those backends". A role pool holds profile names,
|
||||
so the arrows meet: the same profile may serve several roles.*
|
||||
|
||||
---
|
||||
|
||||
@@ -566,6 +579,107 @@ something this build has not learned yet" into a daemon that will not boot, dest
|
||||
forward-compatibility the annotation exists for. Nested unknown keys are still silent; only the top
|
||||
level is checked.
|
||||
|
||||
## One fleet block, role as the key
|
||||
|
||||
**What.** `fleet:` replaces four top-level keys — `leaders:`, `members:`, `leadScan:` and
|
||||
`defaultProfile:`. A member's role is now the map key that contains it, not a `role:` field inside
|
||||
it.
|
||||
|
||||
**On.** Write a `fleet:` block. The four old keys are hard errors that name what to use instead, so
|
||||
an old config does not start silently changed.
|
||||
|
||||
```yaml
|
||||
fleet:
|
||||
leaders:
|
||||
opus: {profile: opus, instances: 1, tabPrefix: "lead:"}
|
||||
architects:
|
||||
opus: {profile: opus}
|
||||
developers:
|
||||
local: {profile: local}
|
||||
sonnet: {profile: sonnet}
|
||||
reviewers:
|
||||
sonnet: {profile: sonnet}
|
||||
```
|
||||
|
||||
**Why.** A misspelled `role: architct` used to parse into a member with a profile, a name, and no
|
||||
contract at all — nothing rejected it, because `role:` was just a string. A misspelled pool name
|
||||
declares nothing, which is a shape the loader can see. The four keys also had no relationship to
|
||||
each other on the page, while all four describe one thing: who is in the fleet.
|
||||
|
||||
**Gotcha.** `defaultProfile:` has no single successor key, so its error message explains the new
|
||||
model rather than pointing at a key that does not exist. Role pools took over its job — see below.
|
||||
|
||||
---
|
||||
|
||||
## Role pools decide the backend
|
||||
|
||||
**What.** `fleet.architects` / `developers` / `reviewers` list the profiles that role **may** run
|
||||
on. A spawn that names no profile is placed inside the pool of the role it asked for, instead of
|
||||
across every configured profile.
|
||||
|
||||
**On.** List profile names under the role. Order matters under `placement: fixed` — the first entry
|
||||
wins.
|
||||
|
||||
**Why.** Role and profile are separate axes, and collapsing them loses real cases: a reviewer may
|
||||
run on the very same profile as the dev whose diff it reads. Before this, an unqualified spawn
|
||||
ranged over all profiles, so a reviewer could land on the architect-only backend and quietly spend
|
||||
the subscription. Pools also replaced the single global `defaultProfile:`, which could only ever
|
||||
have one answer for a fleet that has three kinds of member.
|
||||
|
||||
**Gotcha.** A role with **no** pool is unconstrained, not blocked — it falls back to every profile,
|
||||
so a config that pools some roles and not others keeps working. And an **explicit** profile is not
|
||||
confined to the pool: `bridge_spawn{profile:"opus"}` carries no role, so it defaults to `dev`, and
|
||||
judging it against the dev pool would refuse a spawn the operator asked for by name. `maxLoad` still
|
||||
applies to it.
|
||||
|
||||
---
|
||||
|
||||
## Tab labels name the role
|
||||
|
||||
**What.** A member's tab reads `dev: sonnet #4` — role first, then backend, then a counter.
|
||||
|
||||
**On.** `fleet.tabLabel:` (default `"{role}: {profile} #{n}"`). `{role}`, `{profile}`, `{model}` and
|
||||
`{n}` are substituted. A profile may override it with its own `tabLabel:`.
|
||||
|
||||
**Why.** The label lives on `fleet:` because a profile cannot know the role of the member launched
|
||||
on it, and the role is the thing an operator scanning a tab bar actually wants. `{n}` counts per
|
||||
role **and** profile, so a dev and a reviewer on one profile each start at `#1` — a single fleet-wide
|
||||
counter would make the number meaningless. Putting `{role}` first also turns the lead/member
|
||||
namespace check into a structural guarantee: roles are a closed enum, so only a hand-written
|
||||
template can still collide with a lead's `tabPrefix`.
|
||||
|
||||
**Gotcha.** The knob was inert on first release — `HerdrPeerLauncher` accepted the template and
|
||||
nothing passed it, so the label only looked right because the fallback happened to match the default.
|
||||
Fixed in CB-557; tests now pin the wiring rather than the coincidence.
|
||||
|
||||
---
|
||||
|
||||
## Launch a lead when none is live
|
||||
|
||||
**What.** The daemon starts a lead declared under `fleet.leaders:` when fewer than `instances` are
|
||||
running. It labels the tab by the same convention the scanner reads.
|
||||
|
||||
**On.** Give the lead a `profile:`. Omit it and the lead stays recognise-only, exactly as before.
|
||||
`instances: 0` is an off switch. `workspace:` (default `leads`) and `cwd:` control where it lands.
|
||||
|
||||
**Why.** A lead was the one pane a human had to open by hand before anything else worked, so a
|
||||
daemon restart after a reboot left a fleet with no orchestrator and no sign of why.
|
||||
|
||||
**Gotcha — three, and they are the whole design.** An auto-launched lead is **not** a member: it
|
||||
gets no worker reply charter (that text tells its reader it is an off-subscription worker who must
|
||||
end every turn with `bridge_reply` — the opposite of an orchestrator), it is never registered with
|
||||
`SessionManager` (the idle reaper would kill it for being idle, which is a lead's normal state), and
|
||||
`ANTHROPIC_BASE_URL`/`AUTH_TOKEN` are stripped from its env whatever the profile says.
|
||||
|
||||
A lead counts as live only when herdr reports a **running agent** — either in a tab labelled
|
||||
`lead: <name>`, or on a pinned `terminal:`. Both are needed: label-only would relaunch a hand-opened
|
||||
pinned lead on every boot, and pin-only would miss one the daemon started itself. A labelled tab with
|
||||
nothing running in it is **not** a lead, so one crash does not disable auto-launch forever. If herdr
|
||||
cannot be reached the daemon starts **nothing** — a second orchestrator is worse than none.
|
||||
|
||||
`workspace:` must not name a member workspace: those are excluded from the lead scan, so a lead
|
||||
placed in one would never be found again and would be relaunched on every boot.
|
||||
|
||||
---
|
||||
|
||||
## Backfill status
|
||||
|
||||
Reference in New Issue
Block a user