Features: the coordinator row is lead-only; add the CB-548 quorum design page

Features entry for fleetd #439, merged as 92a96fc. A worker or architect
calling fleet_list now gets no coordinator key at all, and the gate runs
before the row is assembled so the key is absent rather than empty. Notes
the compat overloads that still default to showing it (fleetd #463).

Also commits CB-548-Lead-Quorum-Design.md, which was sitting untracked in
this working tree. 418 lines answering the operator's question about a
deterministic lead-plus-quorum decision procedure. It self-labels as a
proposal, not as built, and it is linked from the sidebar under a new
"Design proposals" heading rather than being numbered as a chapter.

One correction to that page before committing it. It reported a live defect:
that fleet.charters.architect told an architect to call bridge_send, a tool
CB-634 renamed away. That does not reproduce. Measured today in
fleetd/fleetd.yaml: bridge_send 0 times, any bridge_[a-z] name 0 times,
fleet_send twice, with "architects" twice as the control that the grep read
the right file. The page now carries that measurement, its re-measure
command, and the part of the argument that is still true: nothing checks
charter text against the registered tool surface, because
FleetConfig.validateCharters() reads only the key and the blankness. Filed
as fleetd #464.

The three mermaid diagrams were checked against the authoring rules by hand
(no hardcoded fills, quoted labels with parentheses, self-closing <br/>).
No mermaid renderer is installed on this host, so they were not rendered.
Dai Ha
2026-09-10 18:44:02 +07:00
parent 9b0a77b0ba
commit 840aa28def
3 changed files with 462 additions and 0 deletions
+29
@@ -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.
+429
@@ -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.<br/>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,<br/>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<br/>(architect, round ≤ maxRounds)
Open --> ClosedQuorate : close — all slots answered,<br/>at least one real position
Open --> ClosedDegraded : close after deadline —<br/>only abstains and timeouts
Open --> Abandoned : lead never closes<br/>(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.
+4
@@ -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