* dream(swarm): #3242 fix AgentPool health checks that could never detect a dead agent
performHealthChecks() stamped lastHeartbeat = now on every "still healthy"
tick, and updateAgentHeartbeat() (the only other legitimate way to advance
that field) has zero callers anywhere in v3/ — so the check's own tick was
the sole thing keeping every pooled agent's heartbeat fresh, meaning
timeSinceLastActivity could never exceed one health-check interval and
replaceUnhealthyAgent() could never fire for any agent, in any pool, ever.
Fixes the self-stamp; matches the correct pattern already implemented in
UnifiedCoordinator.checkHeartbeats() in the same package. New deterministic
test file (fake timers): baseline fails 2/3 tests, candidate passes 3/3,
223/223 full package suite, tsc clean.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01TAzfKzdv9uRVjKt5TeBgkv
* dream(swarm): fold in adversarial-critique caveats, re-verify, ACCEPT-scoped
Independent critic (separate context) confirmed the fix and re-derived
everything itself (zero-callers grep, full suite, tsc, stash-isolated
baseline/candidate). Two disclosed, non-blocking caveats folded into the
gist: (1) @claude-flow/swarm is not currently a dependency of the shipped
@claude-flow/cli, so tonight's fix is real within the swarm package's own
UnifiedSwarmCoordinator/tests but has no reachable path through the actual
CLI today; (2) health still optimistically recovers +0.1/tick inside the
grace window even with zero real heartbeats (pre-existing, out-of-scope,
mirrors UnifiedSwarmCoordinator's own no-tick-recovery design). Also fixes
a cosmetic class-name typo in the source comment (UnifiedCoordinator ->
UnifiedSwarmCoordinator). Re-ran full suite (223/223) and tsc (clean) after
the comment edit; recomputed the witness stamp over final content.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01TAzfKzdv9uRVjKt5TeBgkv
* dream(swarm): update LEDGER.md for #3242/#3243
Records tonight's row plus verified GitHub state for the trailing nights
(MCP tools, not inferred) and the 2026-09-08 gap note (same
ledger-rows-only-reach-main-via-merge structural issue tracked since
2026-08-19).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01TAzfKzdv9uRVjKt5TeBgkv
---------
Co-authored-by: Claude <noreply@anthropic.com>
* dream(swarm): recover 5 missing live ledger rows (2026-08-14..18)
The v2 live-entries section of docs/dream-cycle/LEDGER.md was never
appended to for 5 consecutive nights despite each night running to
completion (branch + draft PR + issue verified to exist for all 5 via
git ls-remote and GitHub search before concluding this, per the ledger's
own anti-inference rule). Backfilled directly from the PRs/issues.
Root cause of the append gap not diagnosed tonight; flagged in the
gist/issue as a candidate finding for a future automation/meta scan.
* dream(swarm): bound MessageBus retry attempts, fix unreachable message.failed
Given a subscriber whose callback always throws, MessageBus.handleDeliveryError()
incremented the queue entry's attempts counter but addToQueue() always
constructed a fresh entry with attempts:0 on re-queue, discarding it. Retries
were unbounded (96-98 invocations / 0 failures observed in 500ms, git-stash-
verified pre-fix) instead of capped at config.retryAttempts, and the
message.failed branch was unreachable.
Fix: addToQueue() takes an optional existingAttempts parameter; the retry
path in handleDeliveryError() now threads entry.attempts through instead of
resetting it. Fresh-message call sites are unaffected (default 0).
Candidate evaluated: exactly 3 invocations / 2 retries / 1 failure, stable
thereafter. 220/220 tests passing (2 new, 0 regressions). Independently
re-verified by an adversarial critic (fresh session, no authoring context):
CONFIRMED-WITH-CAVEATS — fix and scope match the claim; flagged (and now
disclosed in-code) a separate pre-existing, unfixed broadcast-retry queue-key
bug this diff does not touch.
Full research/evaluation writeup: docs/dream-cycle/dream-gist-2026-08-19.md
* dream(swarm): append 2026-08-19 ledger row
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(swarm): ADR-095 G2 step 1 — pluggable ConsensusTransport + Ed25519 message signing
First concrete piece of ADR-095 G2 (hive-mind multi-process). The
consensus protocols (raft/byzantine/gossip in @claude-flow/swarm) used a
local EventEmitter for *everything* — both observability events
("leader.elected", "consensus.achieved") AND inter-node messages
(append-entries, vote requests, pre-prepare/prepare/commit). The latter
never crossed a process boundary: a node "sent" a message by emitting it
locally and synthesizing the peer's reply inline. That's the single-
process limitation #G2 names.
## What lands
`v3/@claude-flow/swarm/src/consensus/transport.ts`:
- `ConsensusTransport` interface — `send` (request-response), `broadcast`,
`onMessage`, `peers`, `close`. Separates the inter-node-message
dimension from the observability-event dimension (events stay on each
protocol's own EventEmitter).
- `LocalTransport` + `LocalTransportRegistry` — in-process registry,
the default. Matches current single-process behavior so nothing breaks.
Supports optional Ed25519 signing + per-sender monotonic-seq replay
defense when keypairs are configured.
- Real Ed25519 helpers via Node's built-in `crypto` (no new deps):
`generateNodeKeyPair`, `signMessage`, `verifyMessage`,
`canonicalizeForSigning` (deep-sorted-key JSON — deterministic across
hosts), `messageDigest`.
- Exported from `consensus/index.ts`.
## CI guard (the user's ask: "include ci guards for new capabilities")
`plugins/ruflo-core/scripts/test-consensus-transport.mjs`, wired as a step
in the `mcp-roundtrip-smoke` job:
- Static: required exports present in swarm dist; **no `return true`
signature-stub regression** (the original #G2 bug — `verifySignature()`
returned true unconditionally).
- Behavioral: `LocalTransport` round-trip (send → peer handler → reply);
Ed25519 verify is real (true for right key; false for wrong key /
tampered payload / missing sig). 30s watchdog so CI never hangs.
## Tests
14 new tests in `__tests__/consensus-transport.test.ts` (sign/verify
round-trip, tampered-payload rejection, wrong-key rejection, fail-closed
on missing sig, canonicalization stability, LocalTransport send/broadcast/
peers/timeout/closed-peer, signed-transport round-trip, unsigned-rejected,
seq monotonicity). 186/186 swarm suite passes. Build clean.
## Remaining for G2 (subsequent commits)
- FederationTransport adapter (ADR-104 WS wire)
- Wire raft/byzantine/gossip to accept an injected ConsensusTransport
- Replace byzantine-coordinator verifySignature()→true with real verifyMessage
- Per-strategy correctness: BFT f<n/3 vote counting; Raft term/log replication
- Failure-injection tests
ADR-095 G2 status section updated.
Tracks #1872
Co-Authored-By: RuFlo <ruv@ruv.net>
* feat(swarm): ADR-095 G2 step 2 — wire ByzantineConsensus to ConsensusTransport + sha256 digests
Second piece of ADR-095 G2. ByzantineConsensus now accepts an optional
`transport: ConsensusTransport` in its config:
- When set, PBFT messages (pre-prepare / prepare / commit) actually go
over the transport (signed if the transport has a keypair) instead of
being `emit`ted into the void. Inbound transport messages are routed
back into `handlePrePrepare` / `handlePrepare` / `handleCommit`.
- When unset, behavior is unchanged: messages emit as `message.broadcast`
/ `message.sent` events for an external relay layer (the legacy
single-process path). The `emit`s also stay when a transport IS wired,
so observers don't break.
- Transport broadcast failures are non-fatal at the protocol layer — the
proposal simply times out, which is the correct failure mode (lose
liveness, never commit incorrectly).
Also: `computeDigest` was a 32-bit string-hash "for demonstration" —
replaced with sha256. PBFT relies on the digest uniquely identifying the
request being agreed on; a 32-bit hash has trivial collisions. Removed
the toy hash entirely (no test depended on it).
Tests: 3 new in `__tests__/byzantine-transport.test.ts` — emit-only when
no transport (legacy unchanged); broadcasts go over transport AND emit
when wired (and digests are 64-hex sha256); inbound transport messages
route into the PBFT handlers (a pre-prepare in → a prepare out over the
transport). 189/189 swarm suite passes. consensus-transport CI guard
green. Build clean.
Remaining for G2: same wiring for raft.ts + gossip.ts; FederationTransport
adapter; per-strategy correctness (BFT f<n/3 vote counting — the current
`2f+1` thresholds are right but a single node fabricates the quorum
locally; Raft term/log replication); failure-injection tests.
Tracks #1872 · branch feat/adr-095-g2-hive-mind-ws-transport
Co-Authored-By: RuFlo <ruv@ruv.net>
* feat(swarm): ADR-095 G2 step 3 — wire RaftConsensus to ConsensusTransport with real receiver rules
Third piece of ADR-095 G2. RaftConsensus now accepts an optional
`transport: ConsensusTransport`:
- Sender side: `requestVote(peerId)` / `appendEntries(peerId, entries)`
go over the transport as real request-response RPCs (RequestVote /
AppendEntries) instead of mutating the local fake-peer map. On a
higher-term reply, the node steps down (§5.1).
- Receiver side (`handleInboundRaftMessage`): proper Raft rules —
• RequestVote: grant iff `term >= currentTerm` AND (haven't voted this
term OR voted for this candidate) AND candidate's log is at least as
up-to-date (lastLogTerm/lastLogIndex comparison). Adopts a higher
term, resets the election timer on grant.
• AppendEntries: reject if `term < currentTerm`; else become follower,
reset the election timer, append entries by index, advance
`commitIndex` to `min(leaderCommit, lastLogIndex)`.
- Legacy no-transport path unchanged (single-process in-memory peer map).
This is the "Raft term/log replication correctness" piece from the ADR.
(Full prevLogIndex/prevLogTerm conflict resolution is the next refinement;
the current append-by-index is correct for the heartbeat + sequential-
append cases the consensus engine actually exercises.)
Tests: 5 new in `__tests__/raft-transport.test.ts` — vote granted for an
up-to-date candidate; second different candidate denied (vote-once-per-
term); stale-term candidate denied; behind-log candidate denied; valid
AppendEntries accepted + commitIndex updated; stale-term AppendEntries
rejected with the follower's term reported. 194/194 swarm suite passes.
Build clean. consensus-transport CI guard green.
Remaining for G2: same wiring for gossip.ts; FederationTransport adapter
(ADR-104 WS wire); BFT f<n/3 vote-counting correctness; failure-injection
tests; cross-host validation; build/publish.
Tracks #1872 · branch feat/adr-095-g2-hive-mind-ws-transport
Co-Authored-By: RuFlo <ruv@ruv.net>
* feat(swarm): ADR-095 G2 step 4 — FederationTransport (ConsensusTransport over the ADR-104 WS wire)
Fourth piece of ADR-095 G2 — the multi-host transport option.
`v3/@claude-flow/swarm/src/consensus/federation-transport.ts`:
- `FederationTransport implements ConsensusTransport` — backed by the
federation plugin's ADR-104 WS wire (agentic-flow/transport/loader).
- Structural dependency, not a package dep: takes an
`AgenticFlowTransportLike` instance by injection (the `loadQuicTransport`
result satisfies it) plus consensus-level metadata (this node's id, how
to address peers, who the peers are). @claude-flow/swarm stays zero-dep;
the wiring layer (hive-mind + federation) supplies the transport.
- Layers request-response on top of fire-and-forget WS delivery via
per-message correlation ids + a pending-replies map + per-call timeout.
Uses ADR-104 stream-mux (`streamId='ruflo-consensus'`) so consensus
traffic doesn't interleave with application traffic.
- Optional Ed25519 signing — outbound signed, inbound verified against the
sender's published pubkey (resolvePeerPublicKey), per-sender monotonic-
seq replay defense. Fail-closed: unverifiable inbound is dropped.
- Exported from `consensus/index.ts` (+ `AgenticFlowTransportLike` /
`FederationTransportOptions` types).
Tests: 7 new in `__tests__/federation-transport.test.ts` using a mock
in-memory mesh — send→handler→reply correlation; broadcast to all peers;
no-address rejection; send timeout; peers() excludes self; signed
round-trip (seq increments, signature present); unsigned inbound dropped
at a signing-enabled node (sender's send times out, handler never runs).
201/201 swarm suite passes. Build clean. consensus-transport CI guard
extended to assert FederationTransport is exported — green.
Remaining for G2: wire gossip.ts to the transport; BFT f<n/3 vote-
counting correctness (real vote aggregation across distinct senders, not
a locally-fabricated quorum); failure-injection tests; cross-host
validation (mac↔ruvultra over tailscale, root); build/publish; then merge.
Tracks #1872 · branch feat/adr-095-g2-hive-mind-ws-transport
Co-Authored-By: RuFlo <ruv@ruv.net>
* feat(swarm): ADR-095 G2 step 5 — wire GossipConsensus to transport + BFT f-from-cluster-size
Fifth piece of ADR-095 G2 — completes the protocol-wiring trio and adds
the BFT fault-tolerance correctness fix.
## GossipConsensus transport wiring
- `GossipConfig.transport?` — when set, `sendToNeighbor` actually sends
the gossip message over the transport (signed if signing is on) instead
of mutating the local fake-neighbor map. Inbound gossip routes into the
merge logic via `handleInboundGossipMessage`, deduped by message id.
- Legacy no-transport path preserved.
## BFT fault tolerance correctness
- `f` (`maxFaultyNodes`) was hardcoded to 1 in the config default, so a
7- or 10-node cluster ran with the same `2f+1=3` quorum as a 4-node one
— wrong. Now `byzantineF()` derives `f = floor((n-1)/3)` from the actual
cluster size (n = self + known peers), clamped to ≥1. `config.maxFaultyNodes`,
when set, acts as an *upper cap* (never exceed what the operator declared
the cluster can tolerate). The three `2f+1` quorum checks (vote /
handlePrepare / handleCommit) now use the cluster-derived `f`.
- Dropped the `?? 1` config default so an operator who doesn't pass
`maxFaultyNodes` gets the cluster-derived behavior, not a silent cap of 1.
## Tests
- 3 new gossip-transport tests (messages go over transport with hops/path
incremented; inbound deduped by id; non-gossip ignored).
- 5 new byzantine-f tests (f scales 4→1, 7→2, 10→3; explicit cap honored;
no-cap follows cluster size; 3-node clamps to 1).
- 210/210 swarm suite passes. Build clean. consensus-transport CI guard green.
ADR-095 G2 status section updated — all three protocols now wired,
FederationTransport landed, Ed25519 sig verify real, BFT correctness done,
Raft receiver rules done. Remaining: failure-injection tests, cross-host
validation, merge.
Tracks #1872 · branch feat/adr-095-g2-hive-mind-ws-transport
Co-Authored-By: RuFlo <ruv@ruv.net>
* test(swarm): ADR-095 G2 step 6 — failure-injection tests (f<n/3 BFT, f<n/2 Raft)
Sixth piece of ADR-095 G2 — the failure-injection coverage the ADR calls for.
## BFT — prepare/commit quorum under f-bounded faults
`injectQuorum()` builds one ByzantineConsensus with a configured cluster
size (so byzantineF() derives f = floor((n-1)/3)), feeds it a pre-prepare
(creates the proposal), then N distinct external prepares + M distinct
external commits, and reports whether `consensus.achieved` fired. Because
the node contributes 1 prepare + 1 commit of its own, 2f external messages
of each kind hit the 2f+1 quorum.
- 4-node (f=1): 2 ext prepares + 2 ext commits → reached; 1 ext commit → NOT
- 7-node (f=2): 4 + 4 → reached (quorum 5); 3 ext commits → NOT
- 10-node + maxFaultyNodes:1 cap → f stays 1, 2 ext commits suffice
## Raft — election under vote majority/minority
`tallyVotes()` drives a RequestVote round from a candidate to all voters
over LocalTransport; silenced voters never reply (their handler is a no-op)
so the candidate's `send` to them times out → no vote. Tallies grants
including the self-vote.
- 5-node, all reply → 5 votes (≥ majority 3) → wins
- 5-node, 2 silent → 3 votes (= majority) → wins
- 5-node, 3 silent → 2 votes (< majority) → loses
(The full multi-node PBFT cascade over the synchronous LocalTransport is
order-sensitive and flaky as an integration test — the direct
message-injection form above tests the threshold property robustly. A
real-WS cross-host cascade test belongs in the federation cross-host
validation step, not the unit suite.)
218/218 swarm suite passes. Build clean.
This completes the per-strategy correctness + failure-injection items.
Remaining for G2: cross-host validation (mac↔ruvultra over tailscale,
root); merge PR #1905.
Tracks #1872 · branch feat/adr-095-g2-hive-mind-ws-transport
Co-Authored-By: RuFlo <ruv@ruv.net>
---------
Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
- Updated standard-checkpoint-hooks.sh to accept both JSON and plain paths
- Fixed pre-edit and post-edit functions to detect input format
- Fixed v3 CLI hook flags (-f -> --file, -c -> --command, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>