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.
+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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user