diff --git a/11-Features.md b/11-Features.md index e3e624d..4fc5a24 100644 --- a/11-Features.md +++ b/11-Features.md @@ -5239,3 +5239,32 @@ revocation that did not revoke, with nothing in the logs to say so. privilege. fleetd #424. + +## A worker's `fleet_list` no longer carries the lead's coordination state + +**What.** `fleet_list`'s reply used to include a `coordinator` block for every caller: this +daemon's own coord-id, its mailbox state, a preview of the peer mail held for it, and each +configured peer's live reachability. That is lead-to-lead state. Now a worker or an architect +calling `fleet_list` gets **no `coordinator` key at all**. The key is absent, not present and +empty. + +**The knob.** None. It follows the caller's role, which the daemon resolves from the connection, +not from anything the caller sends. + +**Why it exists.** A worker has no use for peer names, held-mail previews, or this daemon's +coord-id, and it should not learn them from a roster call it makes for other reasons. Absent beats +empty on purpose: an empty object still tells the caller the feature is configured, and it makes a +client that tests `if (coordinator)` behave differently from one that tests +`coordinator.peers.length`. So the gate runs *before* the row is built. + +**Gotchas.** + +- **A worker cannot tell "no coordination configured" from "not for you".** Both look like a + missing key. That is the intended trade: the alternative leaks the fact that peers exist. +- **The lead sees no change.** Same key, same fields. +- **The compat overloads still default to showing the row.** `listFleet` has six overloads that do + not take the caller's role, and they inherit `true` from one line. Nothing is exposed today — + the one production call site passes the real answer — but a new call site that forgets the + argument would disclose the row silently. Tracked in fleetd #463. + +fleetd #439. diff --git a/CB-548-Lead-Quorum-Design.md b/CB-548-Lead-Quorum-Design.md new file mode 100644 index 0000000..982c33c --- /dev/null +++ b/CB-548-Lead-Quorum-Design.md @@ -0,0 +1,429 @@ +# CB-548 follow-up: lead quorum — a deterministic decision procedure + +**Status: design proposal, not accepted, not built.** This page answers one question from the +operator: *"a solid/deterministic way to have leaders as main orchestrator/decision maker — I want +full autonomous team with leader tries to convey architect and make decision base on quorum."* + +This page is deliberately **not** a numbered chapter. Chapters 1–16 describe things that are built. +This page proposes something that is not built yet. If the proposal is accepted and shipped, its +as-built form goes to chapter 9 (Implementation) and chapter 11 (Features), and this page becomes a +design record. + +--- + +## 1. What I checked and what I did not + +I read this in the code (all paths under `fleetd/src/main/java/dev/ltms/fleet/`): + +- `auth/MemberRegistry.java` — the architect slot registry. `reserve` / `bind(SlotReservation, terminal)` / + `release` / `released` exist and enforce one terminal per slot, one slot per terminal. +- `session/SessionManager.java` — `acquire(...)` calls `memberLifecycle.reserve(...)` before spawn + and `bind(reservation, terminal)` after spawn (the private `acquired(...)` helper). So the older + memory note "MemberRegistry.bind is never called" is out of date: reserve-then-bind is wired. +- `auth/MemberLifecycle.java` — the `SlotReservation` record and the lifecycle contract. +- `auth/Authz.java` — the role table. `SPAWN`, `STOP`, `DRAIN` are primary-only. `SEND` is primary + **or** architect. `REPLY` and `ASK` need `ownsSession(targetSession)`. `READ` is open to all roles. +- `mcp/ConnectionIdentity.java` — identity comes from the loopback peer PID and herdr's PID→pane + map. A caller cannot claim another identity. Off-host callers resolve to "not a member". +- `mcp/FleetMcp.java` — the tool surface (`fleet_spawn/send/reply/ask/poll/ack/list/status/stop/whoami`), + the `fleet_ask` ~55s cap comment, and the "worker finished without a structured fleet_reply — + transcript tail follows" scrape fallback. +- `inject/Injector.java` — delivery is status-gated: a message lands only when the target is + injectable (`idle` / `blocked`), at most one message per turn, FIFO per target. +- `member/HerdrPeerLauncher.java` (`spawnInternal`) — the role charter comes from the **live** + config (`fleet.charters()` read at each spawn), composed with `REPLY_CHARTER`, fingerprinted by + `CharterReceipt`. +- `fleetd/fleetd.yaml` — two live architect slots (`fleet.architects.opus`, `fleet.architects.sol`) + and a live `fleet.charters.architect` text that encodes the 2026-08-14 decision (own position + first, at most two exchange rounds, then the lead decides). +- Issue #16 (CB-548) — the full body, including the deferred list: *"Daemon-enforced quorum, + voting, or adjudication."* +- `wiki/` numbering and `_Sidebar.md`, to place this page. + +I did **not** check these myself: + +- I did not spawn a live architect. So I did not watch a real two-architect exchange end to end. +- I did not check whether `fleet_ask` works from an architect pane. `Authz` would permit it + (`ownsSession`), but the `FleetMcp` handler's error text says "for workers only" and I did not + trace whether an architect connection passes that resolve step. +- I did not measure the exact ticket TTL constant in code; the ~10-minute figure and the ~55s + `fleet_ask` window come from the task constraints and from a comment in `FleetMcp.java` line 73. +- I did not check the CB-637 lead-to-lead path against this design; the design below stays inside + one gateway, as #16 requires. + +**Correction, re-measured 2026-09-10.** An earlier draft of this page reported a concrete defect +here: that the live `fleet.charters.architect` text told the architect to call `bridge_send`, a tool +CB-634 renamed away. **That does not reproduce.** Measured today in `fleetd/fleetd.yaml`: +`bridge_send` appears 0 times, any `bridge_*` tool name appears 0 times, and `fleet_send` appears +twice. Either it was fixed after the draft, or the draft measured wrongly. Re-measure with +`grep -c 'bridge_[a-z]' fleetd/fleetd.yaml` — a 0 means this correction still holds and the example +below is the live version of the argument; anything above 0 means a stale name is back. + +**What is still true, and it is the argument of this page.** Nothing checks charter *text* against +the tool surface the server actually registers. `FleetConfig.validateCharters()` checks two things +only: that the key is a role wire name, and that the text is not blank. It never reads the text. +The repo already has this exact check for a document — `McpContractDocTest` fails if +`docs/MCP-Contract.md` names a `fleet_*` tool the server does not register — and there is no +equivalent for `fleet.charters`. So a renamed tool can still go stale inside a charter with no test +going red. That is the point in one line: **the current quorum design lives in prose, and nothing +tests prose.** The fix is cheap and is not part of this proposal: point the `McpContractDocTest` +idiom at charter text as well. + +--- + +## 2. The requirement, and the recorded decision that points the other way + +The operator wants three things: + +1. The **lead** is the single orchestrator and decision maker. +2. The lead **convenes architects** before a decision. +3. The lead decides **based on quorum**, and the whole thing must be **deterministic**. + +The recorded decision of 2026-08-14 chose "charter rule + lead gate": the agreement requirement +lives in the reloadable architect charter, and the lead refuses a ticket that does not carry both +positions. It rejected a daemon-enforced K-of-N quorum because that "puts workflow policy in the +bus". Issue #16 defers "daemon-enforced quorum, voting, or adjudication" explicitly. + +That decision was right about **who judges** — and it is wrong if it is read as an answer to +**determinism**, because it never claimed to be one. A charter is text in a system prompt. The lead +gate is a model following an instruction. Both can fail silently, and nothing tests either — see +the measured note at the end of §1: charter text is never checked against the registered tool +surface. So the honest position is: + +- The 2026-08-14 decision correctly keeps **judgment** out of the daemon. +- It gives **zero** deterministic guarantees about the procedure around that judgment. +- The operator's new requirement is about the procedure. So the decision does not need to be + reversed — it needs to be **split**. + +--- + +## 3. What "deterministic" can and cannot mean here + +Be precise about the word, or the design lies. There are four layers, and only some can be +deterministic: + +| Layer | Can it be deterministic? | Why | +|---|---|---| +| The **content** of an architect's position | No | It is model output. | +| The **lead's verdict** | No | Also model output. The operator wants the lead to judge, so this is by design. | +| The **procedure**: who was convened, who answered, in how many rounds, by when, attributed to whom | **Yes** | These are countable events the daemon already sees. | +| The **record**: a durable, attributable, tamper-evident log of the above | **Yes** | The daemon can write it and refuse an incomplete one. | + +So the achievable goal is: **a deterministic procedure wrapped around non-deterministic +judgments.** Anything that promises more — for example "the lead provably always consults before +acting" — is only possible by taking the decision away from the lead and giving it to the daemon. +That contradicts requirement 1, and it contradicts the operator's own "message bus, not a workflow +engine" principle. This page does not propose it. + +One consequence must be said plainly: **the lead can still ignore the whole procedure.** Nothing in +this design physically stops a lead from merging without convening anyone. The design makes that +visible and auditable (a decision with no ledger entry is loud in review), not impossible. The only +way to make it impossible is to gate an action the daemon controls — and the daemon controls no +merge. Gating `fleet_spawn` on a quorate ticket was considered and rejected: it couples spawn to +ticket semantics, which really is workflow policy in the bus. That is the honest limit of +determinism here. + +--- + +## 4. What "quorum" means — three options, one pick + +The word covers three different mechanisms. They must not be mixed up: + +**Option A — agreement quorum (the current charter shape).** Two architects try to agree within two +rounds. Agreement produces a joint position; disagreement hands both positions to the lead. +"Quorum" here means "the ticket carries both positions". Enforced by prose only. + +**Option B — vote counting (K-of-N).** Three or more architects vote; the daemon counts; 2-of-3 +wins. This *looks* deterministic, and the counting genuinely is. But it is fake determinism where +it matters: the votes themselves are model outputs, their independence is unproven (two models on +one vendor share blind spots), and a third strong-model slot costs real money on every decision. +Worse, it removes the lead as decision maker — the count decides. That contradicts requirement 1. +**Rejected.** + +**Option C — presence quorum (the pick).** Quorum is an **evidence threshold, not a vote**: the +lead's verdict is valid only when every convened slot has a recorded terminal outcome — a +*position*, an explicit *abstain*, or a daemon-verified *timeout*. The lead still judges alone; the +quorum rule says what must be on the table before it may judge. This matches the operator's own +words — the leader decides, based on quorum — and it is the only option where the deterministic +part (presence, attribution, bounds) is separable from the non-deterministic part (judgment). + +Under Option C, the edge cases have exact answers: + +- **Tie:** cannot exist. With a lead adjudicator there is no vote to tie. Two disagreeing + architects is the normal "dissent" outcome: the lead's verdict is recorded together with the + dissent, and the record shows which position lost. +- **Abstention:** a first-class terminal outcome. The architect records `abstain` with a reason + ("outside my evidence", "both options equal"). It satisfies presence. A decision where **all** + convened slots abstain still closes — as `degraded`, see §7 — because a procedure that cannot + terminate on universal abstention is not deterministic. +- **Death:** if an architect's backend dies before it records a position (the 2026-09-01 outage + killed a reply exactly this way), the slot's outcome becomes `timeout` once the deadline declared + at convene time passes. The daemon verifies the deadline against its own clock, so the lead + cannot record a premature timeout. The decision then closes as `degraded` with the surviving + positions. A quorum that hangs when a participant dies is not deterministic; this one degrades + and terminates. + +--- + +## 5. The smallest daemon change: a decision ledger, not a decision engine + +The proposal is one narrow primitive: **`fleet_decision`** — a daemon-held, append-only decision +record. Three verbs, most simply as one tool with an `action` argument: + +- `fleet_decision{action:"open", subject, slots:[...], deadline, maxRounds}` — **primary only.** + Opens a record, names the convened slots (they must be configured slots in `MemberRegistry`), + fixes the deadline and the round cap. Returns a `decisionId`. +- `fleet_decision{action:"position", decisionId, round, kind:"position"|"abstain", content}` — + **architect only, attributed by connection.** The daemon resolves the caller's slot through + `MemberRegistry.slotForTerminal` — exactly the same unforgeable path `fleet_whoami` uses — and + stamps slot, round, and time itself. The caller cannot write another slot's entry, cannot write + for a decision that did not convene its slot, and a `round` above `maxRounds` is refused. +- `fleet_decision{action:"close", decisionId, verdict, rationale}` — **primary only.** The daemon + refuses the close unless every convened slot has a terminal outcome: a position, an abstain, or — + only after the daemon's own clock passes the declared deadline — an implied `timeout`. A close + where at least one real position is present records `quorate`; a close carried only by abstains + and timeouts records `degraded`. The daemon never reads `verdict` for meaning. It checks + completeness, nothing else. + +Each accepted entry is appended to a JSONL file under the daemon's data directory, and each entry +carries a hash of the previous entry. That makes the log tamper-evident: editing a past entry +breaks every hash after it. It is not tamper-**proof** — on this host the operator's own user can +rewrite the file and recompute the chain — and the page says so rather than pretending otherwise. +Against the actual threat (a model participant forging or bending the record mid-flight), the +connection-resolved attribution plus append-only writes are sufficient: **no participant, lead +included, can write an entry in another identity's name, and nobody can silently remove one.** + +The lead should additionally mirror the closed verdict into the Gitea ticket for human readers. +The mirror is convenience; the ledger is the record. + +### Why this is not "workflow policy in the bus" + +The 2026-08-14 rejection targeted daemon-enforced **adjudication** — the daemon deciding who wins. +This primitive never adjudicates, never routes, never schedules, and never blocks any message. It +does exactly what the bus already does elsewhere: `Authz` refuses a forged reply, `ConnectionIdentity` +refuses a claimed identity, the `Injector` refuses an unsafe delivery. Refusing an **incomplete or +mis-attributed record** is the same category — a trust and bookkeeping rule, not a workflow rule. +The line the operator drew stays where it was: judgment stays out of the daemon. What moves into +the daemon is only the part prose was never able to hold: attribution, completeness, bounds, and +durability. And #16's deferred bullet — "daemon-enforced quorum, voting, or adjudication" — bundled +those two categories into one sentence; this page proposes un-bundling it, not overriding it. + +The ledger also directly fixes two measured failure modes that no prose can fix: + +- **Ticket TTL (~10 min from send):** a long engagement's `fleet_reply` can outlive its ticket. + With the ledger, the architect records its position **before** replying, in a separate short + call. The reply becomes a courtesy summary; losing it loses nothing. +- **Backend death mid-turn (2026-09-01):** same mechanism. The position survives in daemon state + even when the reply never arrives, exactly like the PR body was the only survivor that day. + +--- + +## 6. The decision flow, with explicit turn boundaries + +The flow must respect the two hard delivery facts I read in `Injector.java`: a member receives a +message only while injectable (`idle` / `blocked`), and an inbound message **starts** a turn. So no +participant is ever briefed to "wait for a message" — waiting keeps it busy, and busy is +undeliverable. The working shape is always: *work → record → reply → end the turn*; the next input +arrives as a new turn. The sequence below shows one full decision with the optional second round. + +```mermaid +sequenceDiagram + participant L as Lead + participant D as "fleetd (ledger)" + participant A as "Architect opus" + participant B as "Architect sol" + + L->>D: fleet_decision open (slots, deadline, maxRounds=2) + D-->>L: decisionId + L->>D: fleet_spawn (role=architect, profile=opus) + L->>D: fleet_spawn (role=architect, profile=sol) + Note over L: spawn BOTH first, then send both (parallel, not serial) + L->>A: fleet_send wait:false — brief + decisionId + defaults + L->>B: fleet_send wait:false — same brief, independently + Note over A: turn 1 — works alone, sees nothing of B + A->>D: fleet_decision position (round 1) + A->>L: fleet_reply — summary + Note over A: turn ends — pane goes idle, deliverable again + Note over B: turn 1 — works alone + B->>D: fleet_decision position (round 1) + B->>L: fleet_reply — summary + Note over B: turn ends + Note over L: reads both round-1 positions. Agreement? close now.
Disagreement? one exchange round: + L->>A: fleet_send wait:false — B's position verbatim + L->>B: fleet_send wait:false — A's position verbatim + Note over A,B: turn 2 each — keep, change, or reject points on evidence + A->>D: fleet_decision position (round 2) + A->>L: fleet_reply + B->>D: fleet_decision position (round 2) + B->>L: fleet_reply + L->>D: fleet_decision close (verdict + rationale) + D-->>L: recorded quorate — every slot has a terminal outcome + L->>D: fleet_stop (both architect panes) +``` + +*One decision, two rounds, every turn boundary explicit. The daemon appears only as ledger and +transport; it never carries an opinion.* + +Notes on the flow: + +- The lead relays round-2 positions **verbatim**. The 2026-08-14 note warned that lead-relayed + agreement "becomes the lead's read"; verbatim relay avoids that, and it avoids relying on direct + architect-to-architect `fleet_send`. That direct path is `Authz`-permitted today (`SEND` allows + architects) and the live charter even instructs it — while #16 defers architect-to-architect + communication. That drift between `Authz`, the charter, and #16 should be resolved either way; + this design does not depend on which way it goes. +- The brief must carry **defaults for every foreseeable question**, because `fleet_ask` holds only + ~55 seconds and an autonomous lead may not be watching. "If X is ambiguous, assume Y and say so + in your position" belongs in every architect brief. +- The lead uses `wait:false` everywhere. A blocking `fleet_send` dies at the caller's ~60s MCP + timeout, far below a real engagement. + +### Slot lifecycle under a decision + +The engagement rides on the existing CB-548 slot machinery unchanged. I read this lifecycle in +`MemberRegistry` and `SessionManager.acquire`; the diagram is the as-built shape plus the queue +behaviour #16 specifies. + +```mermaid +stateDiagram-v2 + [*] --> Idle : configured in fleet.architects + Idle --> Reserved : reserve(role, profile) before launch + Reserved --> Live : bind(reservation, terminal) after spawn + Reserved --> Idle : release(reservation) on failed launch + Live --> Idle : released(terminal) on stop or teardown + Idle --> Idle : second engagement queues FIFO,
promoted when the slot frees +``` + +*Slot states: `reserve` holds the slot before the process exists, `bind` makes the connection +resolve as ARCHITECT, `release`/`released` free it. A decision convenes slots; it never changes +this machine.* + +### Decision record lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Open : fleet_decision open (primary only) + Open --> Open : position or abstain recorded
(architect, round ≤ maxRounds) + Open --> ClosedQuorate : close — all slots answered,
at least one real position + Open --> ClosedDegraded : close after deadline —
only abstains and timeouts + Open --> Abandoned : lead never closes
(visible in status, never auto-closed) + ClosedQuorate --> [*] + ClosedDegraded --> [*] +``` + +*The record has exactly three ends. `Abandoned` is deliberately a visible state, not a transition +the daemon performs — see §7 on the dead-lead case.* + +--- + +## 7. Enforcement map — every rule, its layer, and what a bypass costs + +This is the core table the operator asked for. "Deterministic" means the enforcing layer is code +with no model in the loop. + +| Rule | Enforcing layer | Deterministic? | If bypassed or ignored | +|---|---|---|---| +| A position is attributed to the slot that wrote it | Daemon: `ConnectionIdentity` + `MemberRegistry.slotForTerminal` in `fleet_decision position` | Yes | Cannot be bypassed by a participant; identity is the connection | +| Only the primary opens and closes a decision | Daemon: `Authz` row for the new action | Yes | Refused, like any other role violation | +| A close needs a terminal outcome from every convened slot | Daemon: the `close` completeness check | Yes | The close is refused; the lead must wait, record the deadline pass, or abandon visibly | +| A timeout outcome is only valid after the declared deadline | Daemon clock vs the deadline stamped at `open` | Yes | A premature timeout is refused | +| At most `maxRounds` rounds | Daemon: refuses a higher `round` | Yes | Refused | +| The record cannot be silently edited | Daemon: append-only JSONL + hash chain | Yes, tamper-evident (not tamper-proof against the OS user) | Any edit breaks the chain and is loud | +| The lead convenes architects at all before deciding | **Nothing** — lead prose + operator audit | **No** | The decision happens with no ledger entry; only audit catches it | +| Architects work independently in round 1 | Charter prose + the lead sending separate identical briefs | No | Anchoring; the record still shows two entries, but not their independence | +| Positions cite checked evidence, mark unchecked claims | Charter prose | No | A weak position; the lead's judgment is the only filter | +| The lead's verdict is reasonable | Nothing — that is the lead's job | No | This is requirement 1; making it deterministic would delete the role | +| Architects reply before the pane is reaped | `lifecycle.idleTtlSeconds` (1800) is a hard clock | It is deterministic *against* you — the ledger removes the harm | Position already in the ledger; only the courtesy reply is lost | + +Read the two "No" rows in the middle honestly: prose keeps a real job in this design — shaping the +*quality* of positions. The daemon change does not replace the charter; it takes over only the four +things the charter could never actually hold (attribution, completeness, bounds, durability). + +--- + +## 8. Termination — every bound and its failure mode + +A deterministic procedure must end. Each bound, and what breaks if it is the one that fires: + +- **Round cap (`maxRounds`, default 2).** Daemon-refused beyond the cap. Failure mode: a genuine + near-agreement gets cut at round 2 and lands on the lead as dissent. Acceptable — that is exactly + the 2026-08-14 deadlock rule, now enforced instead of requested. +- **Deadline (declared at `open`).** After it, missing slots become `timeout` and the close may + proceed as `degraded`. Failure mode: a deadline set too short converts a slow-but-alive architect + into a timeout, and its late position is refused. The lead should set deadlines from the + engagement size, and a refused-late position is still visible in the daemon log. +- **Architect death (backend outage, reaped pane).** Covered by the deadline; the round-1 position, + if already recorded, survives. Failure mode: death before *any* position leaves the decision + resting on one architect — recorded as `degraded`, so the thinner evidence base is visible + forever, not hidden. +- **Lead death.** The record stays `Open`. The daemon does **not** auto-close it — an auto-verdict + would be the daemon adjudicating. Failure mode: an abandoned decision needs an operator (or the + next lead session) to notice; open decisions must therefore be listed in `fleet_list` or + `fleet_status` output so they are impossible to miss. This is the one place the procedure + terminates only by *visibility*, not by clock. +- **Both slots busy with another lead's engagement.** #16 makes slot admission FIFO with bounded + queue waits. The decision's deadline still runs. Failure mode: contention can eat the whole + deadline; the lead sees a `degraded` close and should reopen with a fresh deadline rather than + decide on nothing. + +--- + +## 9. What "fully autonomous" costs + +Per decision, with the default two rounds and two architects (`opus` + `sol`, both strong, both +costly): + +- 2 `fleet_spawn` (fresh clean-room sessions — #16 forbids resume, so each pays a cold start), +- 2 architect turns in round 1, 2 in round 2 (round 2 is skippable on agreement), +- 4–5 short lead turns (open, 2 briefs, read + relay, close + stop), +- 2 `fleet_decision position` calls per architect (cheap; the turns are the cost). + +So the marginal cost of the *quorum* over a single-advisor consult is one extra strong-model +engagement plus at most one exchange round. That is the price of requirement 2, and no design +choice on this page changes it — only skipping round 2 on agreement does. + +Where a human still stands, and why: + +1. **Auditing that decisions go through the ledger at all.** §7's honest "No" row: lead compliance + is not enforceable without dethroning the lead. A periodic human read of the ledger (or a cheap + script that diffs merged tickets against decision subjects) is the backstop. +2. **Abandoned decisions** after a lead death (§8) — visible, not self-healing. +3. **Spend control.** Every convening burns two strong-model engagements; nothing in the daemon + knows the operator's budget. The autonomy is bounded by money before it is bounded by design. + +None of these puts a human inside the decision loop. All three put a human on a review loop around +it. That is the honest reading of "fully autonomous": autonomous per decision, audited per week. + +--- + +## 10. Verdict on the recorded decision and on #16 + +- **The 2026-08-14 decision stands for judgment and falls for procedure.** Keeping adjudication + out of the daemon was and is right. But "charter rule + lead gate" was recorded as if it also + answered enforcement, and it does not: it is prose enforced by prose. Nothing checks a charter's + text against the tool surface, so this layer can rot without any test going red (§1). +- **#16's deferred bullet conflates three things.** "Daemon-enforced quorum, voting, or + adjudication" treats presence-checking, vote-counting, and verdict-making as one item. Voting and + adjudication should stay deferred forever. Presence, attribution, bounds, and the durable record + are a different category — the same category as `Authz` — and un-deferring only them is the + proposal of this page. +- **One #16 claim is now inconsistent with the shipped code:** architect-to-architect communication + is listed as deferred, but `Authz.SEND` permits architects and the live charter instructs peer + exchange. Pick one: either tighten `Authz` and keep lead-relayed rounds (this page's flow works + either way), or strike the deferred bullet. Today the three sources disagree. + +## 11. The cheapest experiment before building anything + +Run one real convening **with no daemon change**: two architect spawns, the current charter, the +flow of §6, and the lead writing the "ledger" by hand into a Gitea ticket comment. Measure four +things: whether both architects survive their engagement (TTL, backend), whether the stale +every tool the charter names still exists, whether round 2 changes any position, and whether the +hand-written record is complete. That one run costs two strong-model engagements and settles the riskiest +assumption of this page — that the *flow* works and only the *record* is fragile. If the +hand-written record comes back complete and attributable, the ledger can wait; if it comes back +partial (the 2026-09-01 pattern), the ledger is justified by measurement, not by argument. + +Independent of the outcome, add the missing guard: a test that reads `fleet.charters` text and +fails on any `fleet_*` or `bridge_*` tool name the server does not register, in the shape +`McpContractDocTest` already uses for `docs/MCP-Contract.md`. That is the check whose absence let +this page's own example go stale between drafts. diff --git a/_Sidebar.md b/_Sidebar.md index fcd651d..e95e131 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -21,5 +21,9 @@ 15. [REST API Reference](15-REST-API-Reference) — all 14 routes, roles, and bodies 16. [Security & Trust Boundary](16-Security-and-Trust-Boundary) — the guard · authz · what a member inherits +**Design proposals** (not built) + +- [CB-548 Lead Quorum](CB-548-Lead-Quorum-Design) — a deterministic decision procedure around a lead's judgment + --- 🟢 herdr-centric `fleetd` · AgentAPI = research, never built