P3 — θ Consensus — Agent Prompts
Copy-paste-ready prompts for agents tackling each of the 13 sub-tasks in Phase 3 θ Consensus. This file is staged during R89.C (2026-05-12) so that when Phase 3 opens, every sub-task is a zero-friction T3 dispatch — but dispatch is gated on the conditions below.
Canonical specs:
task-breakdown.md §Phase 3docs/spec/s06-consensus.md— authoritative BFT specdocs/spec/s08-gossip.md— IHAVE/IWANT, Bloom, fanout, STAdocs/3-world/physics/laws/consensus.md— concept doc, worked examplesdocs/architecture/decisions/ADR-002-vrf-implementation.md— VRF librarydocs/architecture/decisions/ADR-003-bft-library.md— BFT librarydocs/5-time/roadmap.md§Phase 3 — R121+ schedulingdocs/agents/executor-contract.md— T3 5-step chainCLAUDE.md— §3 worktree, §5 gate, §6 5-step chain, §7 writeback
Axis goals
θ is how multiple arbiters agree on a single order of events when they each see a potentially different subset of the network. It bridges η (“one process wrote something”) and the legitimacy axis (“the network agrees it happened”). Phase 3 turns Colibri from single-node into a P2P consensus network: nodes gossip, arbiters sign votes, quorum must agree before events finalize, and Byzantine Fault Tolerance tolerates up to
(n−1)/3faulty nodes. The full activation horizon is R121+ per the roadmap.Dependency map
Phase 3 is the most cross-phase-dependent slice in the system:
- κ Rule Engine (Phase 1, closed R87) —
rule_version_hashis part of every vote tuple(round_id, merkle_root, rule_version_hash). Vote payloads use κ P1.5.4 canonical serialization for hash determinism. Vote version-hashes use κ P1.5.1 SHA-256.- η Proof Store (Phase 0, closed R75 Wave I) — the
merkle_rootvoted upon is η’s output. P3.1.1 consumes η’s Merkle root format.- λ Reputation (Phase 2, in flight at staging time) — equivocation penalty (hard scar, weight −10, arbitration domain) is applied by λ’s penalty surface. STA eligible-publisher selection uses λ’s reputation scores. Two slices have hard λ deps: P3.4.1 (STA) and P3.5.1 (equivocation slashing).
- ζ Decision Trail (Phase 0, closed) — view-change rotations emit
VIEW_CHANGE_ACCEPTEDevents into ζ for audit reconstruction.Downstream consumers:
- ι State Fork (Phase 5) — graceful divergence path when θ cannot reach quorum. P3.9.1 stages the hook surface; the ι implementation is out of Phase 3 scope.
- π Governance (Phase 6) — may cap an equivocating arbiter’s voting weight or suspend them.
- μ Integrity Monitor (Phase 4) — observes consensus traces.
Ordering rationale + wave structure
Wave Sub-tasks Parallelism Gate 1 P3.1.1 solo post-Phase-2-λ-seal 2 P3.1.2, P3.3.1, P3.4.1 3-parallel post-P3.1.1 3 P3.1.3, P3.2.1, P3.5.1, P3.6.1 4-parallel (stretches 3-safe ceiling) post-P3.1.2 4 P3.3.2, P3.3.3, P3.7.1 3-parallel post-P3.3.1 + post-P3.2.1 5 P3.8.1, P3.9.1 2-parallel post-Wave-4 P3.1.1 (Vote Messages) gates everything. After it lands, Wave 2 fans out three independent surfaces (Quorum, Gossip wire, Time Anchors). Wave 3 is the state-machine wave (View Change + Finality + Equivocation + VRF stub). Wave 4 extends gossip and registers MCP tools. Wave 5 closes with the parity harness and the ι handoff stub.
ADR status at R89.C staging (2026-05-12)
- ADR-002 — VRF library — PROPOSED, not Accepted. Decision: HMAC-SHA256 internal (Option A) vs
@noble/curvesECVRF per RFC 9381 (Option B). Prompt file response: ships P3.6.1 as an Option A HMAC stub with a clear swap-to-B interface (mirrors how δ Phase 0 stubs were shipped per ADR-005 §Decision). Wave 3 dispatch is unblocked.- ADR-003 — BFT library — PROPOSED, not Accepted. Decision: build from scratch (Option A) vs build on libp2p (Option B) vs two-phase spike (Option C — the ADR’s own recommendation). Prompt file response: state-machine slices (P3.1.x, P3.2.1, P3.5.1) ship unconditionally as the Option C “Phase 3a spike” content. Gossip-transport slices (P3.3.x) carry an in-entry gate requiring PM to confirm Option C-spike is the active strategy before Wave 4 dispatches.
Phase 2 λ Reputation closed at R89 2026-05-12
Phase 2 λ closed at R89 2026-05-12; P3 dispatch is now authorized per T0 autonomous Phase 2+3 mandate. The two λ-dependent slices are unblocked:
- P3.4.1 (STA) consumes
P2.1.1 reputation schema— shipped via #226- P3.5.1 (Equivocation Slashing) consumes
P2.2.2 penalties— shipped via #229ADR-002 (VRF) and ADR-003 (BFT library) gates remain in effect per the §ADR status block above — those gates are about library acceptance, not Phase 2 closure.
Roadmap budget reference
Per
roadmap.md§Phase 3 (R121–R150, ~15 weeks, ~30 tasks budget). Task-breakdown.md ships 7 canonical entries. This prompt file splits to 13 granular entries — kappa precedent (10 → 20 sub-tasks via granularity refinement). 5 waves over ~5 weeks wallclock at the 3-safe parallel ceiling, modeled on the R85→R87 κ close shape.Round + base + writeback expectations
- Round: R89 Phase A (this staging file lands during the autonomous Phase 2+3 mandate)
- Phase 3 kickoff round: R121+ (per roadmap)
- Base SHA at staging:
fab4bf57- Writeback: every executor MUST follow CLAUDE.md §7. The final
thought_recordMUST precedemerkle_finalizefor proof-grade work. PM (at phase-3 kickoff) will pre-create β tasks per slice and pass the UUID into the dispatch packet.Design invariants preserved in every sub-task
- 64-bit signed integer arithmetic — no floating point anywhere (inherited from κ; required for cross-node parity)
- No wall-clock reads in vote / aggregation logic — only Lamport logical clocks in signed payloads
- Ed25519 for arbiter signatures — public-key cryptography per s06 §Event validation step 1
- Canonical JSON serialization — deterministic key order, no whitespace, single-byte separators (P1.5.4 from κ)
- Aggregate-all-errors validator pattern — multi-error per pass (inherited from P1.2.3)
- Idempotent slashing — proof-hash dedup (P3.5.1 acceptance)
- No external side effects before HARD finality — rollback safety
- Single-arbiter compatibility — every state-machine slice MUST degrade gracefully for
n=1so Phase 0 deployment continues to work- AST cap inherited — κ’s MAX_INTEGER_OPS, MAX_CALL_DEPTH bounds apply when rules are evaluated during vote validation
- Per-task scoped PR — no slice’s worktree may touch another’s files
Scope bound: do not graduate the prompts to dependency-less parallel PRs. The sub-task graph has hard prerequisites; respect the Depends-on field in each section.
Group summary
| Task ID | Title | Depends on | Effort | Wave | Unblocks |
|---|---|---|---|---|---|
| P3.1.1 | Vote Message Types + Canonical Wire | Phase 2 λ seal; κ P1.5.4 (shipped) | M | 1 | P3.1.2, P3.3.1, P3.4.1, P3.6.1 |
| P3.1.2 | Quorum Computation | P3.1.1 | M | 2 | P3.1.3, P3.2.1, P3.5.1, P3.7.1, ι, π |
| P3.1.3 | Round / View State Machine | P3.1.2, P3.6.1 | L | 3 | (operational rollout) |
| P3.2.1 | Finality State Machine (5 levels) | P3.1.2 | L | 3 | P3.7.1 |
| P3.3.1 | Gossip Protocol — IHAVE/IWANT Wire | P3.1.1; ADR-003 gate | M | 2 | P3.3.2, P3.3.3 |
| P3.3.2 | Gossip — Bloom Filter Dedup | P3.3.1; ADR-003 gate | M | 4 | (operational rollout) |
| P3.3.3 | Gossip — Adaptive Fanout | P3.3.1; ADR-003 gate | S | 4 | (operational rollout) |
| P3.4.1 | Signed Time Anchors (STA) | P3.1.1, λ P2.1.1 | M | 2 | (governance time-grounding) |
| P3.5.1 | Equivocation Detection + Idempotent Slashing | P3.1.2, λ P2.2.2 | M | 3 | π suspension flows |
| P3.6.1 | VRF Stub (Leader Election) | P3.1.1; ADR-002 gate | M | 3 | P3.1.3 |
| P3.7.1 | θ MCP Tool Surface | P3.1.2, P3.2.1, P3.4.1 | M | 4 | client-side consumers |
| P3.8.1 | Test Corpus + Parity Harness | P3.1.2, P3.2.1, P3.5.1 | L | 5 | (Phase 3 seal) |
| P3.9.1 | Fork Trigger Hook (ι handoff stub) | P3.1.2 | S | 5 | ι Phase 5 |
§P3.1.1 — Vote Message Types + Canonical Wire — Phase 3 θ Wave 1
Spec source: task-breakdown.md §P3.1.1
Concept reference: consensus.md §What the arbiters vote on + consensus.md §Gossip message envelope + s06 §Event validation
Worktree: feature/p3-1-1-vote-messages
Branch command: git worktree add .worktrees/claude/p3-1-1-vote-messages -b feature/p3-1-1-vote-messages origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: Phase 2 λ seal (gate); κ P1.5.4 canonical serialization (shipped at 799e70a9)
Unblocks: P3.1.2 (Quorum reads vote shapes), P3.3.1 (Gossip envelope), P3.4.1 (STA reuses signature path), P3.6.1 (VRF feeds leader proof into commit), P3.5.1 (Equivocation reads conflicting tuples)
Files to create
src/domains/consensus/messages.ts— typed message shapes + Ed25519 helpers + canonical-serializesrc/__tests__/domains/consensus/messages.test.ts— roundtrip, determinism, signature verification
Acceptance criteria
- Message types exported:
Vote,Commit,Reveal,ViewChange,EquivocationProof - All messages carry the 3-tuple
(round_id: bigint, merkle_root: Buffer, rule_version_hash: Buffer)plussender_id: string(soul ID) +signature: Buffer+timestamp_logical: bigint(Lamport, NOT wall-clock) - Vote types enumerated:
ACCEPT,REJECT,ABSTAIN - All messages signed with Ed25519; sign+verify roundtrip tested
- Canonical JSON serialization: deterministic key order (alphabetical), no whitespace, single-byte separators — bit-identical across two calls
Buffervalues serialize as hex strings (canonical); the canonical form is what gets signedEquivocationProofcarries 2 conflictingsigned_vote_a+signed_vote_bplusattacker_id,evidence_hash,submitter- Hash determinism: SHA-256 over canonical bytes produces same digest across 1000 iterations
- No
Date.now(),Math.random(), orwall-clockin this module - Type-discriminator field
msg_type: "COMMIT" | "REVEAL" | "VIEW_CHANGE" | "EQUIVOCATION_PROOF"for envelope routing - Schema-ready columns documented in test fixture:
(round_id, arbiter_id, merkle_root, rule_version_hash, signed BLOB, threshold_count INT, finality_level ENUM)per consensus.md §Phase 0 posture
Pre-flight reading
CLAUDE.md(§3 worktree, §5 gate, §6 5-step chain, §7 writeback)docs/guides/implementation/task-breakdown.md§P3.1.1docs/spec/s06-consensus.md§Event validation + §Quorumdocs/3-world/physics/laws/consensus.md§What the arbiters vote on + §Gossip message envelope + §Equivocation proof structuredocs/architecture/decisions/ADR-003-bft-library.md(for context — message format is transport-agnostic)src/domains/rules/canonical.ts(κ P1.5.4 — REUSE this, do not duplicate)src/domains/rules/version-hash.ts(κ P1.5.1 — REUSE for rule_version_hash format)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.1.1 — Vote Message Types + Canonical Wire
Define the typed message shapes that EVERY downstream θ slice consumes,
including Ed25519 signing and canonical JSON serialization. This is Wave-1
solo — the entire Phase 3 graph fans out from here.
GATE STATUS: Phase 2 λ Reputation sealed at R89 2026-05-12 (main @
`0c858381` post-#232). This slice is unblocked.
FILES TO READ FIRST:
1. CLAUDE.md (§3 worktree, §5 gate, §6 5-step chain, §7 writeback)
2. docs/guides/implementation/task-breakdown.md §P3.1.1
3. docs/spec/s06-consensus.md §Event validation
4. docs/3-world/physics/laws/consensus.md §What the arbiters vote on + §Gossip message envelope + §Equivocation proof structure
5. src/domains/rules/canonical.ts (κ P1.5.4 — REUSE)
6. src/domains/rules/version-hash.ts (κ P1.5.1 — REUSE)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-1-1-vote-messages -b feature/p3-1-1-vote-messages origin/main
cd .worktrees/claude/p3-1-1-vote-messages
FILES TO CREATE:
- src/domains/consensus/messages.ts
* Types (all fields use bigint for ids/counts, Buffer for hashes/sigs):
- VoteTuple = {round_id: bigint, merkle_root: Buffer, rule_version_hash: Buffer}
- VoteType = "ACCEPT" | "REJECT" | "ABSTAIN"
- Vote = VoteTuple & {sender_id: string, vote_type: VoteType, signature: Buffer, timestamp_logical: bigint}
- Commit = {msg_type: "COMMIT", round_id: bigint, sender_id: string, commit_hash: Buffer, timestamp_logical: bigint, signature: Buffer}
- Reveal = {msg_type: "REVEAL", round_id: bigint, sender_id: string, vote: Vote, salt: Buffer, timestamp_logical: bigint, signature: Buffer}
- ViewChange = {msg_type: "VIEW_CHANGE", round_id: bigint, sender_id: string, current_leader: string, reason: "timeout"|"equivocation_observed"|"malformed_proposal", timestamp_logical: bigint, signature: Buffer}
- EquivocationProof = {msg_type: "EQUIVOCATION_PROOF", attacker_id: string, epoch: bigint, round_id: bigint, signed_vote_a: Vote, signed_vote_b: Vote, submitter: string, evidence_hash: Buffer}
* Functions:
- canonicalSerialize(msg): Buffer — delegates to κ canonical for sub-shapes
- signMessage(msg, privateKey): Buffer — Ed25519 over canonicalSerialize
- verifySignature(msg, publicKey): boolean — Ed25519 over canonicalSerialize
- hashMessage(msg): Buffer — SHA-256 over canonicalSerialize
* Use node:crypto for Ed25519; pin no new deps beyond what κ already uses
* Buffer fields serialize as hex strings via κ canonical (NOT base64)
- src/__tests__/domains/consensus/messages.test.ts
* Roundtrip: build each of 5 message types, canonicalSerialize then parse, structural equality
* Determinism: canonicalSerialize × 1000 iterations same bytes
* Hash determinism: hashMessage × 1000 iterations same digest
* Signature roundtrip: sign with privKey, verify with corresponding pubKey, true; verify with WRONG pubKey, false
* EquivocationProof: build with 2 conflicting Votes on same (round_id, finality_level), verify evidence_hash recomputes
* No Date.now() / Math.random() in module (static scanner, similar to P1.1.2 determinism harness)
* Single-arbiter fixture: n=1 vote serializes + verifies cleanly
ACCEPTANCE CRITERIA (headline):
✓ 5 message types exported with discriminator field
✓ All sigs are Ed25519
✓ canonicalSerialize bit-identical across iterations
✓ Hash + signature roundtrip
✓ EquivocationProof carries conflicting votes
✓ No wall-clock or randomness in module
SUCCESS CHECK:
cd .worktrees/claude/p3-1-1-vote-messages && npm run build && npm run lint && npm test
WRITEBACK (after success, per CLAUDE.md §7):
task_update(id="<PM-supplied UUID for P3.1.1>", status="done", progress=100)
thought_record(
thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-1-1-vote-messages
worktree: .worktrees/claude/p3-1-1-vote-messages
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Phase 3 θ Wave 1 — defined 5 typed message shapes (Vote, Commit, Reveal, ViewChange, EquivocationProof) with Ed25519 sign/verify and κ-canonical serialization. Hash + signature roundtrip tested; no wall-clock / randomness.
blockers: none"
)
FORBIDDENS:
✗ No floating point in any field (bigint for ids/counts)
✗ No Date.now() / wall-clock — Lamport logical clocks only
✗ No new npm deps beyond node:crypto + existing κ deps
✗ Do not re-implement κ canonical — REUSE src/domains/rules/canonical.ts
✗ Do not edit main checkout (CLAUDE.md §3)
NEXT:
P3.1.2 — Quorum Computation (consumes Vote message type)
Verification checklist (for reviewer agent)
- 5 message types exported with
msg_typediscriminator - All
bigintfor ids/counts;Bufferfor hashes/sigs - Ed25519 sign+verify roundtrip tested
- Canonical serialization delegates to κ P1.5.4 (no duplicate impl)
- No wall-clock / randomness in source (static scanner clean)
- Determinism: 1000-iter hash + serialize identical bytes
- EquivocationProof shape matches consensus.md §Equivocation proof structure
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.1.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-1-1-vote-messages
worktree: .worktrees/claude/p3-1-1-vote-messages
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Defined 5 typed θ message shapes (Vote, Commit, Reveal, ViewChange, EquivocationProof). All carry 3-tuple (round_id, merkle_root, rule_version_hash) + Ed25519 sig + Lamport logical clock. Canonical serialization REUSES κ P1.5.4. Hash determinism over 1000 iterations.
blockers: none
Common gotchas
- Buffer vs string in canonical — κ canonical serializes Buffer as hex by default; do not auto-base64. Two different hex normalizations (case, with-without 0x prefix) break determinism. Pin one.
- Ed25519 in node:crypto requires Node 12+ — already met. Use
crypto.sign('ed25519', null, privKey)(Ed25519’s PH mode is unsupported; use the “pure” variant which is what RFC 8032 specifies). - Lamport logical clocks —
timestamp_logicalincrements per local send; it is NOT epoch ms. Initialize to 0n at module load; providenextLogical()helper. - Discriminator unions in TS — use
msg_typeliterally typed; TS narrows correctly. Don’t use class hierarchies — pure data only.
§P3.1.2 — Quorum Computation — Phase 3 θ Wave 2
Spec source: task-breakdown.md §P3.1.2
Concept reference: consensus.md §Quorum formula + worked table + s06 §Quorum
Worktree: feature/p3-1-2-quorum
Branch command: git worktree add .worktrees/claude/p3-1-2-quorum -b feature/p3-1-2-quorum origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: P3.1.1 (consumes Vote message type)
Unblocks: P3.1.3 (View change uses quorum check), P3.2.1 (Finality SM uses has_quorum), P3.5.1 (Equivocation uses honest-majority intersection), P3.7.1 (MCP tools), ι (fork trigger reads quorum-failed flag), π (governance proposals use quorum threshold)
Files to create
src/domains/consensus/quorum.ts— pure functions, no I/Osrc/__tests__/domains/consensus/quorum.test.ts— exhaustive property tests + worked-table fixtures
Acceptance criteria
quorumThreshold(n: bigint): bigint=(n * 2n) / 3n + 1n(BigInt floor division)maxFaulty(n: bigint): bigint=(n - 1n) / 3nhasQuorum(votes: Vote[], n: bigint): boolean=count(votes matching (round, root, version)) >= quorumThreshold(n)intersect(quorumA: Set<string>, quorumB: Set<string>): Set<string>— honest-majority overlap check; returns size of intersection- Worked-table fixture from consensus.md §Quorum math:
- n=4 → quorum=3, maxFaulty=1
- n=7 → quorum=5, maxFaulty=2
- n=10 → quorum=7, maxFaulty=3
- n=13 → quorum=9, maxFaulty=4
- Single-arbiter clause (consensus.md §Phase 0 posture):
- n=1 → quorumThreshold=1, maxFaulty=0; hasQuorum trivially satisfied
- Honest-majority property (consensus.md §Quorum math): for any
n ≥ 4 and any two quorum-sized sets A, B drawn from
[1, n],|A ∩ B| ≥ 1(any two quorums intersect in at least one honest node) - Property test: random n ∈ [1, 100], verify quorum + f satisfy
quorum + f >= n + 1 - (n mod 3 === 0n ? 1n : 0n) - Equivocation hooks:
detectDoubleVote(arbiter_id, round_id, votes)returns array of conflicting Vote pairs (empty if no equivocation) - All inputs are
bigint, notnumber - Zero
Math.*/Date.*/ I/O in source
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P3.1.2docs/3-world/physics/laws/consensus.md§Quorum formula + §Worked example (4-node BFT)docs/spec/s06-consensus.md§Quorumsrc/domains/consensus/messages.ts(P3.1.1)src/__tests__/domains/rules/determinism.test.ts(κ P1.1.2 — pattern for property tests)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.1.2 — Quorum Computation
Pure-function module that computes BFT quorum and faulty-node thresholds.
This unblocks Wave 3 (finality, view change, equivocation, VRF).
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P3.1.2
3. docs/3-world/physics/laws/consensus.md §Quorum formula + §Worked example
4. docs/spec/s06-consensus.md §Quorum
5. src/domains/consensus/messages.ts (P3.1.1)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-1-2-quorum -b feature/p3-1-2-quorum origin/main
cd .worktrees/claude/p3-1-2-quorum
FILES TO CREATE:
- src/domains/consensus/quorum.ts
* export function quorumThreshold(n: bigint): bigint
return (n * 2n) / 3n + 1n; // BigInt floor division
* export function maxFaulty(n: bigint): bigint
return (n - 1n) / 3n;
* export function hasQuorum(votes: Vote[], n: bigint): boolean
// Group votes by canonical (round_id, merkle_root, rule_version_hash).
// Largest group ≥ quorumThreshold(n) ⇒ true.
* export function intersect(setA: Set<string>, setB: Set<string>): Set<string>
// For honest-majority property check.
* export function detectDoubleVote(arbiter_id: string, round_id: bigint, votes: Vote[]): [Vote, Vote][]
// Equivocation detection: same arbiter signing distinct tuples in same round.
// Returns pairs of conflicting votes.
* Zero I/O, zero Math.*, zero Date.*, bigint everywhere
- src/__tests__/domains/consensus/quorum.test.ts
* Worked-table fixture (consensus.md):
[(4n, 3n, 1n), (7n, 5n, 2n), (10n, 7n, 3n), (13n, 9n, 4n)]
For each, assert quorumThreshold and maxFaulty match.
* Single-arbiter clause:
expect(quorumThreshold(1n)).toBe(1n);
expect(maxFaulty(1n)).toBe(0n);
* Property test (1000 iter, n ∈ [1, 100] bigint):
const q = quorumThreshold(n), f = maxFaulty(n);
const honest_invariant = (n % 3n === 0n) ? n : (n + 1n);
expect(q + f >= honest_invariant - 1n).toBe(true);
* hasQuorum tests:
- 4 votes all matching → true (n=4 needs 3)
- 4 votes, 2 matching root_A + 2 matching root_B → false
- 5 votes, 3 matching → true at n=4
* detectDoubleVote:
- Arbiter A signs two distinct (round=42, root) tuples → 1 pair returned
- Arbiter A signs same tuple twice → 0 pairs (not equivocation, just retry)
* intersect property: for two n=7 quorums (size 5 each), |A ∩ B| ≥ 3
ACCEPTANCE CRITERIA (headline):
✓ quorumThreshold + maxFaulty match worked table
✓ Single-arbiter clause (n=1 trivial)
✓ Honest-majority property over [1, 100] random n
✓ hasQuorum groups by canonical tuple
✓ detectDoubleVote returns conflicting pairs only
SUCCESS CHECK:
cd .worktrees/claude/p3-1-2-quorum && npm run build && npm run lint && npm test
WRITEBACK (after success):
task_update(id="<PM-supplied UUID for P3.1.2>", status="done", progress=100)
thought_record(
thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-1-2-quorum
worktree: .worktrees/claude/p3-1-2-quorum
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Pure-function quorum module: quorumThreshold, maxFaulty, hasQuorum, intersect, detectDoubleVote. Worked-table fixture from consensus.md §Quorum math. Single-arbiter clause: n=1 trivial. Honest-majority property over n ∈ [1, 100].
blockers: none"
)
FORBIDDENS:
✗ No `number` type — only bigint for counts
✗ No I/O, no Math.*, no Date.*
✗ Do not group votes by raw object identity — use canonical serialization
✗ Do not edit main checkout
NEXT:
P3.1.3 (View Change SM), P3.2.1 (Finality SM), P3.5.1 (Equivocation), P3.6.1 (VRF stub) — Wave 3 fans out
Verification checklist (for reviewer agent)
- All functions use bigint
- Worked-table fixture passes (4 rows from consensus.md)
- Single-arbiter clause:
quorumThreshold(1n) === 1ntested - Property test runs 1000 iterations with seeded PRNG
- hasQuorum groups by canonical-serialized tuple (not object identity)
- detectDoubleVote distinguishes equivocation from retry
- Zero
Math.*/Date.*/ I/O in source npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.1.2>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-1-2-quorum
worktree: .worktrees/claude/p3-1-2-quorum
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: BFT quorum computation: quorumThreshold(n)=floor(2n/3)+1, maxFaulty(n)=floor((n-1)/3), hasQuorum, intersect, detectDoubleVote. Worked table from consensus.md verified for n∈{4,7,10,13}. Single-arbiter clause: n=1 trivially satisfied. Honest-majority intersection property over n∈[1,100].
blockers: none
Common gotchas
- BigInt division floors toward zero for non-negatives — same as math
floor.
(2n * 2n) / 3n === 1n(NOT 1.33 rounded). - Honest-majority invariant formula — the spec text claims
quorum + f = n + 1 - (n mod 3 == 0 ? 1 : 0). Verify in tests with ASSERTIONS, not just trust the formula — boundary at n=3 specifically. - Group-by-tuple needs canonical hashing — two Vote objects with the
same logical content but different field ordering must hash identically.
REUSE P3.1.1’s
hashMessage(). - detectDoubleVote is the equivocation primitive — P3.5.1 builds on it. Keep the return shape as pairs (NOT a flat array of single votes) so P3.5.1 can directly construct EquivocationProof from each pair.
§P3.1.3 — Round / View State Machine — Phase 3 θ Wave 3
Spec source: task-breakdown.md §P3.1.3
Concept reference: consensus.md §Voting protocol (commit-reveal) + consensus.md §View-change procedure
Worktree: feature/p3-1-3-round-state-machine
Branch command: git worktree add .worktrees/claude/p3-1-3-round-state-machine -b feature/p3-1-3-round-state-machine origin/main
Estimated effort: L (Large — 1–2 days)
Depends on: P3.1.2 (uses quorum check), P3.6.1 (calls VRF stub for next-leader)
Unblocks: Operational Phase 3 rollout
Files to create
src/domains/consensus/round-state.ts— commit-reveal FSM + view-change handlersrc/__tests__/domains/consensus/round-state.test.ts— protocol traces, timeout tests, view-change scenarios
Acceptance criteria
- States exported:
COMMIT_PHASE,REVEAL_PHASE,VERIFY_PHASE,VIEW_CHANGE,COMPLETED - Timer constants from consensus.md (governable via π under rule_version_hash):
T_round = 30000n(ms; bigint for determinism)T_commit_phase = 10000nT_reveal_phase = 10000nT_timeout = 60000n(2 × T_round)
- Commit-reveal protocol:
- Each arbiter computes
salt = random_bytes(32)(seeded for determinism in tests) - Sign vote tuple; compute
c = SHA-256(canonicalSerialize(vote) || salt) - Broadcast
COMMIT(round_id, arbiter_id, c) - Wait until
commit_count >= quorumORelapsed > T_commit_phase - Broadcast
REVEAL(round_id, arbiter_id, vote, salt) - Verify each reveal:
SHA-256(canonicalSerialize(vote_i) || salt_i) === c_i - Aggregate signatures → threshold signature σ
- Emit
QUORUM(round_id, merkle_root, σ)
- Each arbiter computes
- View-change triggers:
timeout: proposer fails to emit proposal withinT_timeoutequivocation_observed: detectDoubleVote returns non-emptymalformed_proposal: proposal fails schema validation
- View-change collects
VIEW_CHANGEmessages; ifcount >= quorum, computesnext_leader = VRF(prev_merkle_root, round_id)via P3.6.1 - Emits
VIEW_CHANGE_ACCEPTEDevent into ζ Decision Trail (uses existingthought_recordinterface; thought_type=”consensus”) - Liveness fault (arbiter commits but doesn’t reveal): softer
penalty than equivocation; deferred to λ reputation decay, NOT immediate
scar — call
λ.markLivenessFault(arbiter_id)(interface lands with λ) - Timer-driven transitions implemented via injected clock (testable); in production, the clock is a Lamport logical timer
- Single-arbiter clause: n=1 → COMMIT_PHASE → REVEAL_PHASE → VERIFY_PHASE → COMPLETED in one tick
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P3.1.3docs/3-world/physics/laws/consensus.md§Voting protocol + §View-change procedure + §Worked example (4-node)docs/spec/s06-consensus.md§Quorum + §Equivocationsrc/domains/consensus/quorum.ts(P3.1.2)src/domains/consensus/vrf-stub.ts(P3.6.1)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.1.3 — Round / View State Machine
Implement the commit-reveal voting protocol with view-change on leader
failure. This is the engine driving every consensus round.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P3.1.3
3. docs/3-world/physics/laws/consensus.md §Voting protocol (commit-reveal pseudocode) + §View-change procedure + §Worked example
4. docs/spec/s06-consensus.md §Quorum
5. src/domains/consensus/quorum.ts (P3.1.2)
6. src/domains/consensus/vrf-stub.ts (P3.6.1)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-1-3-round-state-machine -b feature/p3-1-3-round-state-machine origin/main
cd .worktrees/claude/p3-1-3-round-state-machine
FILES TO CREATE:
- src/domains/consensus/round-state.ts
* State enum: COMMIT_PHASE | REVEAL_PHASE | VERIFY_PHASE | VIEW_CHANGE | COMPLETED
* Constants (bigint ms):
- T_round = 30000n, T_commit_phase = 10000n, T_reveal_phase = 10000n, T_timeout = 60000n
* Phase A (commit):
- input: my vote tuple + round_id + n_arbiters
- generate salt (32 bytes); seeded RNG injection for tests
- compute c = sha256(canonicalSerialize(vote) || salt); broadcast Commit{c}
- collect Commit messages until count >= quorum OR elapsed > T_commit_phase
- if quorum: transition to REVEAL_PHASE
- else: transition to VIEW_CHANGE with reason="timeout"
* Phase B (reveal):
- broadcast Reveal{vote, salt}
- collect Reveal messages until count >= quorum OR elapsed > T_reveal_phase
- for each, verify sha256(canonicalSerialize(vote_i) || salt_i) === commit_i
- mismatched commit → flag arbiter for liveness fault (NOT equivocation)
- quorum reached: transition to VERIFY_PHASE
* Phase C (verify):
- for each verified reveal: signature check via P3.1.1 verifySignature
- group revealed votes by canonical (round, root, version); largest group is winner
- if largest_group.size >= quorum: aggregate sigs into σ; emit QUORUM event
- else: transition to VIEW_CHANGE with reason="malformed_proposal"
* View-change handler:
- broadcast ViewChange{round, leader, reason}
- collect ViewChange messages
- if count >= quorum: next_leader = vrf_eval(prev_merkle_root, round_id) (P3.6.1)
- emit VIEW_CHANGE_ACCEPTED into ζ via thought_record interface
- reset to COMMIT_PHASE with new leader
* Clock injection: accept logical clock as constructor param; default to a counter
* Liveness fault: callback into λ (interface name λ.markLivenessFault(arbiter_id))
* Equivocation observation: callback into λ + into P3.5.1 (Equivocation slasher)
- src/__tests__/domains/consensus/round-state.test.ts
* Happy path n=4, all honest: COMMIT → REVEAL → VERIFY → COMPLETED in 4 ticks
* Single-arbiter n=1: COMPLETED in one tick
* Leader timeout (proposer never sends): VIEW_CHANGE after T_timeout; next leader via mocked VRF
* Equivocator (Byzantine): detected by detectDoubleVote in VERIFY_PHASE → VIEW_CHANGE with reason="equivocation_observed"
* Liveness fault (commit without reveal): arbiter flagged but round still completes if quorum reached without them
* 4-node BFT worked example from consensus.md §Worked example: nodes A,B,C honest; D Byzantine; round 42 reaches QUORUM with merkle_root=0xab12
* Determinism: same seed → same trace
* ζ emission: VIEW_CHANGE_ACCEPTED has correct shape
ACCEPTANCE CRITERIA (headline):
✓ 5-state FSM implemented
✓ Commit-reveal protocol per consensus.md pseudocode
✓ View-change on timeout / equivocation / malformed
✓ VRF used for next-leader selection
✓ ζ emission for VIEW_CHANGE_ACCEPTED
✓ Liveness vs equivocation differentiated
✓ Single-arbiter clause: n=1 completes in one tick
✓ Worked example from consensus.md §Worked example passes
SUCCESS CHECK:
cd .worktrees/claude/p3-1-3-round-state-machine && npm run build && npm run lint && npm test
WRITEBACK (after success):
task_update(id="<PM-supplied UUID for P3.1.3>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-1-3-round-state-machine
worktree: .worktrees/claude/p3-1-3-round-state-machine
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Commit-reveal FSM with view-change on timeout/equivocation/malformed. 5 states. T_round=30s, T_timeout=60s. VRF used for next-leader. VIEW_CHANGE_ACCEPTED emitted into ζ. Worked example (4-node, A/B/C honest, D Byzantine) passes.
blockers: none"
)
FORBIDDENS:
✗ No real wall-clock (Lamport / injected logical clock only)
✗ No real RNG for salt — seeded in tests, controllable in prod via secret-state
✗ Do not skip the ζ emission — VIEW_CHANGE_ACCEPTED is part of the spec audit chain
✗ Do not edit main checkout
NEXT:
Wave 4 — operational rollout
Verification checklist (for reviewer agent)
- 5-state FSM exported
- All 4 timer constants are bigint and match consensus.md values
- Commit-reveal verified bit-by-bit against worked example
- View-change triggers all 3 reasons (timeout / equivocation / malformed)
- VRF called via P3.6.1 (mock in tests)
- ζ emission tested (mock thought_record sink)
- Liveness fault distinguished from equivocation
- Single-arbiter clause: n=1 completes in one tick
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.1.3>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-1-3-round-state-machine
worktree: .worktrees/claude/p3-1-3-round-state-machine
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: 5-state FSM (COMMIT → REVEAL → VERIFY → VIEW_CHANGE → COMPLETED) implementing commit-reveal voting. Timer constants T_round=30s, T_commit/reveal=10s, T_timeout=60s. View-change on timeout/equivocation/malformed_proposal; next leader via VRF stub. VIEW_CHANGE_ACCEPTED emitted into ζ. Liveness fault separated from equivocation. Single-arbiter clause tested.
blockers: none
Common gotchas
- Salt generation in tests must be deterministic — inject a seeded
PRNG via constructor (
new RoundState({rng: seededPrng})). In production usecrypto.randomBytes(32); in tests use the seeded fixture. - The clock for protocol timing is logical, not wall — inject as
param; provide
tick()method that advances by 1n. AvoidsetTimeoutfor tests (non-determinism). - VIEW_CHANGE_ACCEPTED into ζ must use the existing
thought_recordinterface — do not introduce a new domain. Setthought_type="consensus"(new value, document it). - Liveness fault is NOT equivocation — equivocation is signing conflicting tuples; liveness fault is committing without revealing. The λ surface has two different penalty paths (decay vs scar).
§P3.2.1 — Finality State Machine (5 levels) — Phase 3 θ Wave 3
Spec source: task-breakdown.md §P3.2.1
Concept reference: consensus.md §Five finality levels + s06 §Finality levels
Worktree: feature/p3-2-1-finality-sm
Branch command: git worktree add .worktrees/claude/p3-2-1-finality-sm -b feature/p3-2-1-finality-sm origin/main
Estimated effort: L (Large — 1–2 days)
Depends on: P3.1.2 (consumes hasQuorum)
Unblocks: P3.7.1 (consensus_finality tool)
Files to create
src/domains/consensus/finality.ts— monotonic 5-level FSMsrc/__tests__/domains/consensus/finality.test.ts— monotonicity + dispute-window tests
Acceptance criteria
- States:
PENDING,SOFT,QUORUM,HARD,ABSOLUTE(string enum) - Transitions:
PENDING → SOFT: first vote received (any single signature)SOFT → QUORUM:hasQuorum(votes, n) === truefor current roundQUORUM → HARD: two consecutive rounds reached QUORUM without conflicting reveal (per consensus.md §Five finality levels)HARD → ABSOLUTE: epoch sealed — root embedded in next epoch’s genesis (dispute window: 100 epochs per task-breakdown.md §P3.2.1)
- Monotonicity invariant:
level_t >= level_{t-1}always; backward transitions throwFinalityRollbackError - No external side effects before HARD — implement an
externalEffectguard that throwsPrematureExternalEffectErrorif called when level < HARD - Single-arbiter clause: n=1 → first vote → reaches QUORUM trivially within one round; HARD after 2 epochs; ABSOLUTE on epoch seal
- Each transition recorded with
{epoch: bigint, evidence: Buffer} - Evidence for
SOFT→QUORUM: list of signed votes (Vote[]) - Evidence for
QUORUM→HARD: pair of consecutive QUORUM votes - Evidence for
HARD→ABSOLUTE: epoch-seal merkle root - State exposed via getters that DO NOT mutate
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P3.2.1docs/3-world/physics/laws/consensus.md§The five finality levelsdocs/spec/s06-consensus.md§Finality levelssrc/domains/consensus/quorum.ts(P3.1.2)src/domains/consensus/messages.ts(P3.1.1)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.2.1 — Finality State Machine (5 levels)
Build the monotonic 5-state finality FSM that gates external side effects.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P3.2.1
3. docs/3-world/physics/laws/consensus.md §The five finality levels
4. docs/spec/s06-consensus.md §Finality levels
5. src/domains/consensus/quorum.ts (P3.1.2)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-2-1-finality-sm -b feature/p3-2-1-finality-sm origin/main
cd .worktrees/claude/p3-2-1-finality-sm
FILES TO CREATE:
- src/domains/consensus/finality.ts
* type FinalityLevel = "PENDING" | "SOFT" | "QUORUM" | "HARD" | "ABSOLUTE"
* const FINALITY_ORDER: FinalityLevel[] = ["PENDING", "SOFT", "QUORUM", "HARD", "ABSOLUTE"]
* class FinalitySM {
constructor(round_id: bigint, n_arbiters: bigint, dispute_window_epochs: bigint = 100n)
current(): FinalityLevel
receiveVote(vote: Vote, currentEpoch: bigint): void
sealEpoch(epoch: bigint, sealRoot: Buffer): void
requireExternalEffectsAllowed(): void // throws if current < HARD
transitions(): Array<{from: FinalityLevel, to: FinalityLevel, epoch: bigint, evidence: Buffer}>
}
* Transition rules:
- PENDING → SOFT: first valid receiveVote (signature verifies)
- SOFT → QUORUM: hasQuorum(receivedVotes, n_arbiters) === true
- QUORUM → HARD: previous round AND current round both QUORUM, no conflicting reveal between
- HARD → ABSOLUTE: sealEpoch called with seal embedded
* Monotonicity: throw FinalityRollbackError on any attempt to set level lower
* requireExternalEffectsAllowed throws PrematureExternalEffectError if level !== HARD AND level !== ABSOLUTE
- src/__tests__/domains/consensus/finality.test.ts
* Single-arbiter n=1 trace:
- new FinalitySM(round=1, n=1, window=100) starts at PENDING
- receiveVote → SOFT immediately
- receiveVote (same) → QUORUM (hasQuorum true at n=1)
- 2nd round same → HARD
- sealEpoch → ABSOLUTE
* n=4 happy path:
- 4 votes → SOFT after vote 1, QUORUM after vote 3 (quorum=3)
- 2nd round QUORUM → HARD
- sealEpoch → ABSOLUTE
* Monotonicity: attempt to receiveVote that would lower level → FinalityRollbackError
* Premature effect: requireExternalEffectsAllowed() at SOFT or QUORUM → PrematureExternalEffectError
* Premature effect: requireExternalEffectsAllowed() at HARD or ABSOLUTE → no throw
* Transitions array: 4 entries after full PENDING → ABSOLUTE traversal; each carries epoch + evidence
ACCEPTANCE CRITERIA (headline):
✓ 5 levels in strict order
✓ Monotonicity enforced
✓ No external effects before HARD
✓ Single-arbiter clause (n=1 reaches QUORUM trivially)
✓ Dispute window 100 epochs configurable
✓ Each transition records epoch + evidence
SUCCESS CHECK:
cd .worktrees/claude/p3-2-1-finality-sm && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.2.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-2-1-finality-sm
worktree: .worktrees/claude/p3-2-1-finality-sm
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: 5-level finality FSM PENDING→SOFT→QUORUM→HARD→ABSOLUTE. Monotonic; rollback throws. No external effects before HARD. Single-arbiter clause: n=1 reaches QUORUM trivially. Dispute window 100 epochs. Each transition records epoch + evidence.
blockers: none"
)
FORBIDDENS:
✗ No state mutation without transition recording
✗ Do not allow external effects below HARD
✗ Do not use number for epoch — bigint
✗ Do not skip monotonicity check
✗ Do not edit main checkout
NEXT:
Wave 4 — P3.7.1 MCP tool surface (exposes finality query)
Verification checklist (for reviewer agent)
- 5 levels in strict order; FINALITY_ORDER array exported
- Monotonicity enforced; rollback throws typed error
- External effects gated to HARD/ABSOLUTE only
- Single-arbiter clause: n=1 → QUORUM trivially
- Dispute window configurable; default 100 epochs
- Transitions tracked with epoch + evidence
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.2.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-2-1-finality-sm
worktree: .worktrees/claude/p3-2-1-finality-sm
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Monotonic 5-level finality FSM. Transitions PENDING→SOFT→QUORUM→HARD→ABSOLUTE. Rollback throws FinalityRollbackError. requireExternalEffectsAllowed gates side effects to HARD+. Single-arbiter (n=1) reaches QUORUM trivially. Dispute window default 100 epochs.
blockers: none
Common gotchas
- The “two consecutive rounds without conflicting reveal” rule is the trickiest gate. Implement it as: keep last round’s QUORUM evidence; on next round’s QUORUM, compare merkle_roots; if same and no equivocation observed between, transition to HARD. Otherwise stay at QUORUM and reset the counter.
- Dispute window is in epochs, not ms — the FSM doesn’t run a wall timer. The epoch counter is incremented externally; sealEpoch is called by the epoch sealing logic.
- Monotonicity error semantics — DO NOT silently ignore a downward attempt; throw a typed error. Silent ignore is a class of bug that masks state corruption.
- The
transitions()getter must return a copy — not the internal array, so callers can’t mutate it.
§P3.3.1 — Gossip Protocol — IHAVE/IWANT Wire — Phase 3 θ Wave 2
Spec source: task-breakdown.md §P3.3.1
Concept reference: s08 §IHAVE/IWANT + s08 §Triple-Anchor validation + consensus.md §Gossip
Worktree: feature/p3-3-1-gossip-ihave-iwant
Branch command: git worktree add .worktrees/claude/p3-3-1-gossip-ihave-iwant -b feature/p3-3-1-gossip-ihave-iwant origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: P3.1.1 (consumes message envelope shape)
Unblocks: P3.3.2 (Bloom dedup), P3.3.3 (Adaptive fanout)
GATE: This slice ships gossip-transport code. ADR-003 is PROPOSED at R89.C staging. Executor should confirm with PM that ADR-003 has been Accepted (or that Option C-spike is the active strategy) BEFORE picking up this entry. If ADR-003 selects Option B (libp2p /
@chainsafe/libp2p-gossipsub), the file paths and dependency footprint in this entry MAY change; re-issue the prompt with a revised packet.
Files to create
src/domains/consensus/gossip-wire.ts— IHAVE / IWANT message shapes + triple-anchor validatorsrc/__tests__/domains/consensus/gossip-wire.test.ts— triple-anchor pass/fail scenarios + retention horizon
Acceptance criteria
- Wire types exported:
IHAVE,IWANT IHAVEcarries:event_ids: Buffer[],state_root_pre: Buffer,rule_version_hash: Buffer,fork_id: Buffer,sender_id: string,timestamp_logical: bigint,signature: BufferIWANTcarries:event_ids: Buffer[],sender_id: string,timestamp_logical: bigint,signature: Buffer- Triple-anchor validation (returns
{valid: boolean, failed_anchor?: "rule_version" | "state_root" | "fork_id"}):rule_version_hashmust match receiver’s active rule setstate_root_premust be reachable from last known checkpoint (no gap > 1 epoch)fork_idmust match receiver’s current fork
- All three must pass before IWANT is issued; single failure → entire batch rejected (no partial acceptance)
- Retention horizon: messages older than 2 epochs dropped without reply (per consensus.md §Gossip)
- Deduplication interface:
seen?: (event_id: Buffer) => boolean— accepts P3.3.2’s Bloom; default identity-set - All messages Ed25519-signed; signature verified before triple-anchor check
- Lamport logical clocks only — NO wall-clock in signed payload
- Single-arbiter clause: n=1 means there are no peers; IHAVE/IWANT functions return without crashing (no-op publish/subscribe)
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P3.3.1docs/spec/s08-gossip.md§IHAVE/IWANT + §Triple-Anchor validationdocs/3-world/physics/laws/consensus.md§Gossip + §Gossip message envelopedocs/architecture/decisions/ADR-003-bft-library.md(transport-choice context)src/domains/consensus/messages.ts(P3.1.1)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.3.1 — Gossip Protocol — IHAVE/IWANT Wire
Implement the IHAVE / IWANT lazy push/pull message shapes and the
triple-anchor validator that rejects malformed gossip batches before any
events are requested.
ADR GATE: Confirm with PM that ADR-003 has been Accepted (or that the
Option C spike is the active strategy) BEFORE you start. If Option B
(libp2p) wins, this slice rewrites against @chainsafe/libp2p-gossipsub.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P3.3.1
3. docs/spec/s08-gossip.md §IHAVE/IWANT + §Triple-Anchor validation
4. docs/3-world/physics/laws/consensus.md §Gossip
5. docs/architecture/decisions/ADR-003-bft-library.md
6. src/domains/consensus/messages.ts (P3.1.1)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-3-1-gossip-ihave-iwant -b feature/p3-3-1-gossip-ihave-iwant origin/main
cd .worktrees/claude/p3-3-1-gossip-ihave-iwant
FILES TO CREATE:
- src/domains/consensus/gossip-wire.ts
* type IHAVE = {
msg_type: "IHAVE",
event_ids: Buffer[],
state_root_pre: Buffer,
rule_version_hash: Buffer,
fork_id: Buffer,
sender_id: string,
timestamp_logical: bigint,
signature: Buffer,
}
* type IWANT = {
msg_type: "IWANT",
event_ids: Buffer[],
sender_id: string,
timestamp_logical: bigint,
signature: Buffer,
}
* type TripleAnchorResult = {valid: true} | {valid: false, failed_anchor: "rule_version" | "state_root" | "fork_id"}
* function validateTripleAnchor(
msg: IHAVE,
receiver: {active_rule_version: Buffer, last_checkpoint_state_root: Buffer, current_fork_id: Buffer, known_state_roots: Set<string>}
): TripleAnchorResult
- Check rule_version_hash === receiver.active_rule_version
- Check state_root_pre reachable from last_checkpoint via known_state_roots (allow gap up to 1 epoch)
- Check fork_id === receiver.current_fork_id
- Return first-fail result
* function buildIWANT(ihave: IHAVE, locally_have: (id: Buffer) => boolean): IWANT
- For each event_id in ihave.event_ids, if !locally_have(id), include in IWANT
- Inject sender + timestamp_logical + signature externally
* function withinRetentionHorizon(
msg_timestamp_logical: bigint,
current_epoch: bigint,
retention_epochs: bigint = 2n
): boolean
- Drop if msg older than 2 epochs of logical time
- src/__tests__/domains/consensus/gossip-wire.test.ts
* Triple-anchor PASS: all three match → {valid: true}
* Triple-anchor FAIL rule_version: mismatch → {valid: false, failed_anchor: "rule_version"}
* Triple-anchor FAIL state_root: gap > 1 epoch → {valid: false, failed_anchor: "state_root"}
* Triple-anchor FAIL fork_id: divergent → {valid: false, failed_anchor: "fork_id"}
* buildIWANT: 5 event_ids in, 3 already known locally → IWANT carries 2
* Retention horizon: msg from 3 epochs ago → dropped; msg from 1 epoch → kept
* Single-arbiter no-op: peer list empty → buildIWANT returns IWANT with zero events
* Signature verified before any anchor check (reject early)
* No partial acceptance: even if rule_version + state_root pass, fork_id fail → reject entire batch
ACCEPTANCE CRITERIA (headline):
✓ IHAVE + IWANT wire shapes
✓ Triple-anchor validator with first-fail short-circuit
✓ Retention horizon = 2 epochs default
✓ Signature verified before anchor check
✓ buildIWANT filters by locally_have predicate
✓ Single-arbiter no-op compatible
✓ No partial acceptance: any anchor failure rejects whole batch
SUCCESS CHECK:
cd .worktrees/claude/p3-3-1-gossip-ihave-iwant && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.3.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-3-1-gossip-ihave-iwant
worktree: .worktrees/claude/p3-3-1-gossip-ihave-iwant
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: IHAVE/IWANT wire layer with triple-anchor validator (rule_version, state_root continuity, fork_id). First-fail short-circuit; no partial acceptance. Retention horizon 2 epochs. Single-arbiter no-op.
blockers: none"
)
FORBIDDENS:
✗ No real socket I/O — this is a pure wire/validator layer
✗ No new npm deps unless ADR-003 picks Option B
✗ No wall-clock; logical timestamps only
✗ Do not edit main checkout
NEXT:
Wave 4 — P3.3.2 Bloom dedup + P3.3.3 Adaptive fanout
Verification checklist (for reviewer agent)
- IHAVE + IWANT types exported with discriminator
- Triple-anchor validator covers all 3 failure modes
- First-fail short-circuit (does not check anchors past first failure)
- Retention horizon default 2 epochs
- buildIWANT filters via predicate (no real I/O)
- Signature verified before anchor checks
- ADR-003 GATE in the entry header
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.3.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-3-1-gossip-ihave-iwant
worktree: .worktrees/claude/p3-3-1-gossip-ihave-iwant
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: IHAVE/IWANT wire layer + triple-anchor validator. 3 failure modes (rule_version mismatch, state_root gap > 1 epoch, fork_id divergent) — any one rejects entire batch. Retention horizon default 2 epochs. Signature verified before anchor check. ADR-003 gate disclosed in header.
blockers: none
Common gotchas
- “State root continuity, no gaps > 1 epoch” is fuzzy in the spec —
s08 line 27 says “chain of verified state roots, no gaps > 1 epoch”.
Interpret as: if the IHAVE’s
state_root_preis not in receiver’sknown_state_roots, AND the receiver’s last checkpoint is from an epoch more than 1 earlier, the anchor fails. The receiver may request a full checkpoint sync as the fail-over behavior. - Signature ALWAYS checked first — never run the anchor checks on an unsigned or invalid-sig payload (gives attacker free CPU spend on the receiver).
- “No partial acceptance” — even if some events would be safe, the whole batch is rejected on any anchor failure. This is fundamental: the triple-anchor’s job is to prevent inconsistent state from entering the local event log.
- Lamport clock vs epoch —
timestamp_logicalis a Lamport counter, not an epoch index. Retention is computed in epochs, so the helper needs both; passcurrent_epochseparately.
§P3.3.2 — Gossip — Bloom Filter Dedup — Phase 3 θ Wave 4
Spec source: task-breakdown.md §P3.3.1 (dedup criterion)
Concept reference: s08 §Bloom filter algorithm + s08 §Deduplication
Worktree: feature/p3-3-2-bloom-dedup
Branch command: git worktree add .worktrees/claude/p3-3-2-bloom-dedup -b feature/p3-3-2-bloom-dedup origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: P3.3.1 (filter wraps the IHAVE → IWANT path)
Unblocks: Operational rollout
GATE: ADR-003 must be Accepted or in Option-C-spike before dispatch.
Files to create
src/domains/consensus/bloom-dedup.ts— sized Bloom filter, per-round lifecyclesrc/__tests__/domains/consensus/bloom-dedup.test.ts— false-positive rate + sizing fidelity
Acceptance criteria
- Filter sized via s08 formula:
m = -n * ln(p) / (ln(2))²(m bits, n expected events, p target FPR) - Hash count:
k = (m / n) * ln(2)rounded; defaultk ≈ 7forn=1000, p=0.01 - Default config:
n=1000, p=0.01 → m≈9585 bits (~1.2 KB), k=7 insert(event_id: Buffer): voidmightContain(event_id: Buffer): boolean— false-positive rate ≤ p- Fresh filter per round —
reset()zeroes the bit array - Filters are NOT persisted — in-memory only, exist for the duration of one IHAVE exchange
- Empirical FPR test: insert 1000 unique event_ids, query 10000 distinct event_ids, observed FPR < 1.5% (allow some margin over 1% theoretical)
- All math uses Number for sizing computation (this is the ONE exception to the bigint rule — Bloom math uses ln() which doesn’t have a bigint equivalent in stdlib; document this exception and isolate)
- Hash functions: K independent SHA-256 truncations seeded by
i(i ∈ [0, k))
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P3.3.1 (Bloom criterion)docs/spec/s08-gossip.md§Bloom filter algorithm + §Deduplicationsrc/domains/consensus/gossip-wire.ts(P3.3.1)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.3.2 — Gossip — Bloom Filter Dedup
Build a sized Bloom filter for per-round IHAVE deduplication. False
positive rate < 1% at n=1000.
ADR GATE: confirm ADR-003 Accepted or Option-C-spike active.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/spec/s08-gossip.md §Bloom filter algorithm + §Deduplication
3. docs/guides/implementation/task-breakdown.md §P3.3.1 (Bloom criterion)
4. src/domains/consensus/gossip-wire.ts (P3.3.1)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-3-2-bloom-dedup -b feature/p3-3-2-bloom-dedup origin/main
cd .worktrees/claude/p3-3-2-bloom-dedup
FILES TO CREATE:
- src/domains/consensus/bloom-dedup.ts
* Sizing helper:
function sizeFilter(n: number, p: number): {m: number, k: number}
- m = Math.ceil(-n * Math.log(p) / (Math.log(2) ** 2))
- k = Math.round((m / n) * Math.log(2))
- DOCUMENTED EXCEPTION: this is the ONE place we use Number not bigint,
because ln() has no bigint equivalent. Output integers are passed to
the filter constructor.
* class BloomFilter {
constructor({n, p}: {n: number, p: number})
private m: number // bit count
private k: number // hash count
private bits: Uint8Array // ceil(m/8) bytes
insert(event_id: Buffer): void
mightContain(event_id: Buffer): boolean
reset(): void
stats(): {m: number, k: number, set_bits: number, occupancy: number}
}
* Hash function: K = 7 SHA-256 truncations seeded by hash index i:
indexFor(i, event_id) = (sha256(i.toString() || event_id) read as uint32) mod m
- src/__tests__/domains/consensus/bloom-dedup.test.ts
* sizeFilter(1000, 0.01) returns m ≈ 9585, k = 7
* Empirical FPR: insert 1000 random Buffers, query 10000 disjoint Buffers; FPR < 1.5%
* reset() zeroes all bits
* mightContain after insert always returns true (no false negatives)
* Determinism: same insert sequence → same bit pattern (regardless of order? — yes, OR is commutative)
ACCEPTANCE CRITERIA (headline):
✓ sizeFilter matches s08 formula
✓ Empirical FPR < 1.5% at n=1000
✓ k=7 default hash count
✓ No false negatives
✓ reset() works
✓ Number-vs-bigint exception clearly documented
SUCCESS CHECK:
cd .worktrees/claude/p3-3-2-bloom-dedup && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.3.2>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-3-2-bloom-dedup
worktree: .worktrees/claude/p3-3-2-bloom-dedup
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Bloom filter dedup module. Sizing per s08 formula (m=-n·ln(p)/(ln(2))², k=(m/n)·ln(2)). Default n=1000, p=0.01 → m≈9585, k=7. Empirical FPR < 1.5%. Number-not-bigint exception isolated and documented (ln() has no bigint stdlib).
blockers: none"
)
FORBIDDENS:
✗ Do not persist filters to disk — in-memory only
✗ Do not skip the Number-vs-bigint exception comment
✗ Do not use a non-standard hash (must be SHA-256-derived)
✗ Do not edit main checkout
NEXT:
P3.3.3 — Adaptive fanout
Verification checklist (for reviewer agent)
- Sizing formula matches s08
- Empirical FPR < 1.5% over 10k queries
- No false negatives (post-insert mightContain always true)
- Number-not-bigint exception documented and isolated
- K=7 default for n=1000, p=0.01
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.3.2>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-3-2-bloom-dedup
worktree: .worktrees/claude/p3-3-2-bloom-dedup
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Per-round Bloom filter with s08 sizing (m=-n·ln(p)/(ln(2))², k=(m/n)·ln(2)). Default n=1000 p=0.01 → m=9585, k=7. SHA-256-derived hash family. Empirical FPR < 1.5% over 10k disjoint queries. No false negatives. In-memory only.
blockers: none
Common gotchas
- The Number-vs-bigint exception must be a single isolated function
(
sizeFilter). Everywhere else in this slice (and the file) stays bigint or Buffer. The exception is justified by stdlib:Math.loghas no bigint twin without a polyfill, and adding one for one helper is worse than documenting the carve-out. - Bit array vs byte array — store as
Uint8Array; usebyte = Math.floor(bit/8),mask = 1 << (bit % 8). Two-step is cheaper than any “BitVector” library. - Empirical FPR is stochastic — the test must use a seeded PRNG for reproducibility. Run the FPR check 5 times on different seeds to confirm the < 1.5% margin holds (not just one lucky run).
- The filter is per-round and per-direction — receiver may have inserted events into its filter that the sender’s filter does NOT have; do not auto-merge.
§P3.3.3 — Gossip — Adaptive Fanout — Phase 3 θ Wave 4
Spec source: task-breakdown.md §P3.3.1 (fanout criterion)
Concept reference: s08 §Adaptive fanout
Worktree: feature/p3-3-3-adaptive-fanout
Branch command: git worktree add .worktrees/claude/p3-3-3-adaptive-fanout -b feature/p3-3-3-adaptive-fanout origin/main
Estimated effort: S (Small — 2–4 hours)
Depends on: P3.3.1 (consumes peer list shape)
Unblocks: Operational rollout
GATE: ADR-003 must be Accepted or in Option-C-spike before dispatch.
Files to create
src/domains/consensus/adaptive-fanout.ts— fanout helper + connectivity trackingsrc/__tests__/domains/consensus/adaptive-fanout.test.ts— table fixture + epoch-recompute test
Acceptance criteria
computeFanout(connectivity_score: bigint): bigintreturnsmax(3n, min(10n, 15n - connectivity_score))- Worked table fixture (s08 §Adaptive fanout):
- connectivity_score=0 → 10 (clamped at min(10, 15))
- connectivity_score=3 → 10 (clamped at min(10, 12))
- connectivity_score=5 → 10 (15-5=10)
- connectivity_score=7 → 8 (15-7=8)
- connectivity_score=10 → 5 (15-10=5)
- connectivity_score=12 → 3 (15-12=3, clamped at max(3))
- Connectivity score clamped to
[0n, 12n]— values outside this range coerced - Recomputed every 5 epochs based on successful IHAVE/IWANT exchanges
trackExchange(peer_id, success: boolean)records exchange outcomerecomputeIfDue(current_epoch: bigint, last_recompute_epoch: bigint, period_epochs: bigint = 5n)returns updated score iffcurrent - last >= period- Score uses live-peer count: number of peers with at least 1 successful exchange in last
period_epochsepochs
Pre-flight reading
CLAUDE.mddocs/spec/s08-gossip.md§Adaptive fanoutdocs/guides/implementation/task-breakdown.md§P3.3.1 (fanout criterion)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.3.3 — Gossip — Adaptive Fanout
Implement the fanout helper that scales gossip aggressiveness inversely with
connectivity: isolated nodes broadcast wider; well-connected nodes broadcast
narrower. Bounded [3, 10] per s08.
ADR GATE: confirm ADR-003 Accepted or Option-C-spike active.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/spec/s08-gossip.md §Adaptive fanout
3. docs/guides/implementation/task-breakdown.md §P3.3.1
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-3-3-adaptive-fanout -b feature/p3-3-3-adaptive-fanout origin/main
cd .worktrees/claude/p3-3-3-adaptive-fanout
FILES TO CREATE:
- src/domains/consensus/adaptive-fanout.ts
* function computeFanout(connectivity_score: bigint): bigint
const score = clamp(connectivity_score, 0n, 12n);
const raw = 15n - score;
return raw < 3n ? 3n : raw > 10n ? 10n : raw;
* function clamp(x: bigint, lo: bigint, hi: bigint): bigint
* class FanoutTracker {
private exchanges: Map<string, {ok: boolean, epoch: bigint}[]>
private last_recompute_epoch: bigint
private score: bigint
constructor(period_epochs: bigint = 5n)
trackExchange(peer_id: string, success: boolean, current_epoch: bigint): void
recomputeIfDue(current_epoch: bigint): bigint // returns latest score
currentScore(): bigint
currentFanout(): bigint // computeFanout(currentScore())
}
* The recompute reads `live_peers = count of peers with ≥1 success in last period_epochs`
* score = clamp(live_peers, 0n, 12n)
- src/__tests__/domains/consensus/adaptive-fanout.test.ts
* Worked table fixture (s08 §Adaptive fanout):
[
[0n, 10n],
[3n, 10n],
[5n, 10n],
[7n, 8n],
[10n, 5n],
[12n, 3n],
].forEach(([score, expected]) => expect(computeFanout(score)).toBe(expected));
* Clamping: computeFanout(-5n) → 10n (treated as 0); computeFanout(20n) → 3n (treated as 12)
* Tracker:
- track 8 peers all successful in epoch 0..4
- recomputeIfDue(epoch=5) → score=8n
- currentFanout = computeFanout(8n) = 7n
* Tracker: peer that hasn't succeeded in period_epochs is excluded from live count
ACCEPTANCE CRITERIA (headline):
✓ Worked table fixture passes
✓ Clamping at both ends
✓ Recompute period = 5 epochs default
✓ Live-peer counting uses success-in-period
SUCCESS CHECK:
cd .worktrees/claude/p3-3-3-adaptive-fanout && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.3.3>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-3-3-adaptive-fanout
worktree: .worktrees/claude/p3-3-3-adaptive-fanout
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Adaptive fanout helper: computeFanout(score)=max(3, min(10, 15-score)). FanoutTracker recomputes every 5 epochs from live-peer count. Worked table fixture (s08) passes for connectivity_score ∈ {0,3,5,7,10,12}.
blockers: none"
)
FORBIDDENS:
✗ No socket I/O — tracker is pure data
✗ Do not skip the clamp on both ends
✗ Do not use Number — bigint throughout
✗ Do not edit main checkout
NEXT:
P3.7.1 — MCP tool surface
Verification checklist (for reviewer agent)
- computeFanout matches all 6 rows of s08 worked table
- Clamping at both ends tested
- Recompute period default 5n epochs
- Live-peer counting excludes stale peers
- All bigint
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.3.3>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-3-3-adaptive-fanout
worktree: .worktrees/claude/p3-3-3-adaptive-fanout
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Adaptive fanout f = max(3, min(10, 15 - connectivity_score)), clamped [3, 10]. FanoutTracker recomputes every 5 epochs from live-peer count (peers with ≥1 success in period). Worked table fixture (s08) passes.
blockers: none
Common gotchas
- Clamping order matters — clamp the score first (to [0, 12]), THEN compute raw, THEN clamp output (to [3, 10]). If you only clamp at the end, negative scores produce weird raw values.
- “Live peer” definition is “≥ 1 successful exchange in last
period_epochsepochs”. A peer with only failed exchanges is NOT live. - Tracker should drop old exchange records during recompute to keep
memory bounded. After recompute, discard records older than
period_epochs. - bigint Math.min/max aren’t standard — use ternaries explicitly.
§P3.4.1 — Signed Time Anchors (STA) — Phase 3 θ Wave 2
Spec source: task-breakdown.md §P3.4.1
Concept reference: consensus.md §Signed time anchors (intersection) + s06 §Signed time anchors + s08 §Signed Time Anchors (STA)
Worktree: feature/p3-4-1-time-anchors
Branch command: git worktree add .worktrees/claude/p3-4-1-time-anchors -b feature/p3-4-1-time-anchors origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: P3.1.1 (signature path); λ P2.1.1 reputation schema (in flight)
Unblocks: Governance time-grounding
λ dependency in flight at R89.C: STA’s “eligible publishers” criterion reads
λ.reputation.arbitration(per s06 §Signed time anchors: “High- reputation nodes periodically broadcast signed timestamps”). This slice MUST wait until Phase 2 λ P2.1.1 (reputation schema) seals before dispatch.
Files to create
src/domains/consensus/time-anchors.ts— STA shape, median computation, drift detectionsrc/__tests__/domains/consensus/time-anchors.test.ts— fixtures for drift, replay, monotonicity
Acceptance criteria
- STA shape:
{publisher: string, timestamp_ms: bigint, epoch: bigint, signature: Buffer} - Eligible publishers: top N arbiters by
λ.reputation.arbitration(N default 7, configurable; reads λ’s reputation surface) - Anchor signature: Ed25519 over canonical
(publisher, timestamp_ms, epoch) - Median computation: collect anchors from last K epochs (K default 10), take median of
timestamp_msvalues, weight equal per publisher - Drift detection:
|local_clock - STA_median| > 30_000ms→ mark proposals from local node as deprioritized (does NOT reject; lowers priority queue) - Monotonicity per publisher: anchors from same publisher must be non-decreasing (
epoch_{n+1} > epoch_nANDtimestamp_{n+1} >= timestamp_n); violations flagged as soft fault - Replay protection: anchors with
epoch < current_epoch - 10rejected - Median over even-count of anchors: take average of two middle values (rounded down to nearest ms)
- Tests use seeded clock; production never reads wall-clock for signing — uses Lamport timestamp on the wire, comparison is offline against STA median
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P3.4.1docs/spec/s06-consensus.md§Signed time anchorsdocs/spec/s08-gossip.md§Signed Time Anchors (STA)src/domains/consensus/messages.ts(P3.1.1)src/domains/reputation/...(λ P2.1.1 — wait until shipped)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.4.1 — Signed Time Anchors (STA)
Build the STA broadcasting + median + drift detection module. High-rep
arbiters publish signed (timestamp, epoch); nodes compute median; drift > 30s
deprioritizes the offending node's proposals.
λ DEPENDENCY: confirm Phase 2 λ P2.1.1 (reputation schema) is sealed before
starting. Eligible publishers are read from λ.reputation.arbitration.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/spec/s06-consensus.md §Signed time anchors
3. docs/spec/s08-gossip.md §Signed Time Anchors (STA)
4. docs/guides/implementation/task-breakdown.md §P3.4.1
5. src/domains/consensus/messages.ts (P3.1.1)
6. src/domains/reputation/ (λ P2.1.1 — must be shipped)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-4-1-time-anchors -b feature/p3-4-1-time-anchors origin/main
cd .worktrees/claude/p3-4-1-time-anchors
FILES TO CREATE:
- src/domains/consensus/time-anchors.ts
* type STA = {publisher: string, timestamp_ms: bigint, epoch: bigint, signature: Buffer}
* function signSTA(publisher: string, timestamp_ms: bigint, epoch: bigint, privKey: Buffer): STA
* function verifySTA(sta: STA, pubKey: Buffer): boolean
* function isEligiblePublisher(
publisher_id: string,
reputationSnapshot: ReadonlyMap<string, bigint>, // arbiter_id → arbitration score
top_n: bigint = 7n,
): boolean
- sort reputation entries desc; take top_n; check publisher_id is in the set
* function median(anchors: STA[], k_epochs: bigint, current_epoch: bigint): bigint | null
- filter anchors within [current_epoch - k_epochs, current_epoch]
- reject anchors with epoch < current_epoch - 10n (replay protection)
- reject monotonicity violations per publisher
- return median timestamp_ms; null if no eligible anchors
* function detectDrift(local_clock_ms: bigint, sta_median: bigint, threshold_ms: bigint = 30000n): "OK" | "DRIFTED"
* function shouldDeprioritize(local_clock_ms: bigint, sta_median: bigint): boolean
- detectDrift(...) === "DRIFTED"
- src/__tests__/domains/consensus/time-anchors.test.ts
* Sign+verify STA roundtrip
* Eligible publisher: 10-node fixture with rep [9,8,7,6,5,4,3,2,1,0]; top_n=7; publisher with rep=3 → eligible; publisher with rep=2 → not eligible
* Median: 5 anchors (1000, 1010, 1020, 1030, 1040) → 1020
* Median even count: 4 anchors (1000, 1010, 1020, 1030) → 1015 (floor of average)
* Replay protection: anchor at current_epoch - 11 → excluded
* Monotonicity violation: same publisher submits epoch 5 timestamp=1000, then epoch 6 timestamp=900 → flagged + excluded
* Drift detection: local=1000000, median=1029999 → OK (29999 < 30000)
* Drift detection: local=1000000, median=1030001 → DRIFTED
* Single-arbiter: 1 publisher; eligible (trivially top 7); median = its anchor; drift checked against itself = 0 → OK
ACCEPTANCE CRITERIA (headline):
✓ Sign/verify Ed25519 roundtrip
✓ Eligible publishers from top-N by reputation
✓ Median over last K epochs, K=10 default
✓ Replay protection: epoch < current - 10 rejected
✓ Monotonicity per publisher enforced
✓ Drift detection at 30s threshold
✓ Single-arbiter compatibility: 1 publisher trivially eligible
SUCCESS CHECK:
cd .worktrees/claude/p3-4-1-time-anchors && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.4.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-4-1-time-anchors
worktree: .worktrees/claude/p3-4-1-time-anchors
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: STA module. signSTA / verifySTA / isEligiblePublisher (top-N by λ.reputation.arbitration) / median (over last 10 epochs) / detectDrift (30s threshold) / shouldDeprioritize. Replay protection at current-10. Per-publisher monotonicity. Single-arbiter compat.
blockers: none"
)
FORBIDDENS:
✗ No real Date.now() for signing — Lamport logical timestamps on wire, sticker comparison only
✗ No new npm deps
✗ Do not allow drift > 30s to silently affect ordering — deprioritization is explicit
✗ Do not edit main checkout
NEXT:
Wave 3 — state-machine + VRF stub
Verification checklist (for reviewer agent)
- Sign+verify STA roundtrip works
- isEligiblePublisher reads top-N from a reputation snapshot
- Median computed over
[current - k, current]window - Replay rejection at
current - 10 - Monotonicity per publisher enforced (epoch + timestamp non-decreasing)
- Drift threshold exactly 30000ms
- Single-arbiter clause tested
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.4.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-4-1-time-anchors
worktree: .worktrees/claude/p3-4-1-time-anchors
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: STA — signSTA / verifySTA / median / detectDrift / shouldDeprioritize. Top-N publishers from λ.reputation.arbitration. K=10 epoch window. Replay protection at current-10. Per-publisher monotonicity. 30s drift threshold. Single-arbiter compatible.
blockers: none
Common gotchas
- STA
timestamp_msIS a wall-clock unit — this is the one place in θ that names physical milliseconds. But the signing path NEVER readsDate.now()in production; an offline ticker or external time source feeds thetimestamp_msargument. The whole point of STA is for nodes to converge on a shared clock estimate despite drift. - Median over even-count — floor of (a+b)/2 (bigint division), to preserve integer semantics.
- Eligible-publisher pre-filter — reject the STA’s median contribution if the publisher isn’t in top-N; the test fixture has this case.
- The
30000msconstant is governable via π — leave it as a defaulted parameter, not a hardcode, so π can adjust it without a code change.
§P3.5.1 — Equivocation Detection + Idempotent Slashing — Phase 3 θ Wave 3
Spec source: task-breakdown.md §P3.5.1
Concept reference: consensus.md §Equivocation + §Equivocation proof structure + s06 §Quorum (equivocation clause)
Worktree: feature/p3-5-1-equivocation
Branch command: git worktree add .worktrees/claude/p3-5-1-equivocation -b feature/p3-5-1-equivocation origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: P3.1.2 (uses detectDoubleVote); λ P2.2.2 penalties (in flight)
Unblocks: π suspension flows
λ dependency satisfied at R89: Equivocation slashing applies an 8000bps penalty to
domain="arbitration"via λ’s penalty surface (shipped at #229, P2.2.2). This slice is unblocked for dispatch.
Files to create
src/domains/consensus/equivocation.ts— proof construction + idempotent slashersrc/__tests__/domains/consensus/equivocation.test.ts— proof verification + idempotency
Acceptance criteria
buildEquivocationProof(arbiter_id, conflicting_pair: [Vote, Vote]): EquivocationProof- Uses P3.1.1’s EquivocationProof shape
evidence_hash = SHA-256(canonicalSerialize([vote_a, vote_b]))
verifyEquivocationProof(proof: EquivocationProof, arbiter_pubkey: Buffer): {valid: boolean, reason?: string}- Both signatures must be valid under same pubkey
- Tuples must be distinct (different merkle_root OR different rule_version_hash at same round_id, finality_level)
- Same
(round_id, finality_level)in both votes -
Returns reason on failure: “sig_a_invalid” “sig_b_invalid” “same_tuple” “different_round_or_level”
applyEquivocationSlash(proof: EquivocationProof, alreadyApplied: Set<string>): {applied: boolean, reason?: string}- Idempotent: returns
{applied: false, reason: "duplicate"}ifproof.evidence_hashalready inalreadyApplied - Otherwise calls
λ.applyPenalty(attacker_id, DAMAGE_FRAUD=8000n, domain="arbitration", event_id=evidence_hash) - Records slashing in λ.reputation_history with
event_id = evidence_hash(per task-breakdown.md acceptance)
- Idempotent: returns
- Slash amount fixed at 8000bps (DAMAGE_FRAUD constant per κ P1.1.3); maps to “critical” offense
- Integration test: create equivocation → verify slash applied → re-submit same proof → idempotent (no double-slash)
- Equivocation observation cycles back into P3.1.3 (view-change trigger reason
"equivocation_observed") - Single-arbiter clause: n=1 → equivocation by sole arbiter still detected and slashed (the arbiter slashes themselves; trivially correct)
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P3.5.1docs/3-world/physics/laws/consensus.md§Equivocation + §Equivocation proof structuredocs/spec/s06-consensus.md§Quorum (equivocation clause)src/domains/consensus/quorum.ts(P3.1.2 — uses detectDoubleVote)src/domains/rules/bps-constants.ts(κ P1.1.3 — DAMAGE_FRAUD)src/domains/reputation/penalties.ts(λ P2.2.2 — wait until shipped)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.5.1 — Equivocation Detection + Idempotent Slashing
Build the equivocation slasher: take a conflicting-vote pair from P3.1.2's
detectDoubleVote, produce a proof, verify it, apply an 8000bps penalty via
λ's penalty surface, dedup by evidence_hash so re-submission doesn't
double-slash.
λ DEPENDENCY: confirm Phase 2 λ P2.2.2 (penalties) is sealed before starting.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P3.5.1
3. docs/3-world/physics/laws/consensus.md §Equivocation + §Equivocation proof structure
4. docs/spec/s06-consensus.md §Quorum (equivocation clause)
5. src/domains/consensus/quorum.ts (P3.1.2)
6. src/domains/rules/bps-constants.ts (κ P1.1.3 — DAMAGE_FRAUD)
7. src/domains/reputation/penalties.ts (λ P2.2.2)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-5-1-equivocation -b feature/p3-5-1-equivocation origin/main
cd .worktrees/claude/p3-5-1-equivocation
FILES TO CREATE:
- src/domains/consensus/equivocation.ts
* function buildEquivocationProof(
attacker_id: string,
pair: [Vote, Vote],
submitter: string,
): EquivocationProof
- vote_a, vote_b come from detectDoubleVote
- evidence_hash = sha256(canonicalSerialize([vote_a, vote_b]))
- epoch derived from vote_a (both at same round)
* function verifyEquivocationProof(
proof: EquivocationProof,
attacker_pubkey: Buffer,
): {valid: true} | {valid: false, reason: "sig_a_invalid" | "sig_b_invalid" | "same_tuple" | "different_round_or_level"}
- verify sig_a; if false → sig_a_invalid
- verify sig_b; if false → sig_b_invalid
- check tuples distinct (different root OR version); if not → same_tuple
- check same (round_id, finality_level) in both; if not → different_round_or_level
* function applyEquivocationSlash(
proof: EquivocationProof,
attacker_pubkey: Buffer,
alreadyApplied: Set<string>,
lambdaPenaltyApplier: (args: {arbiter_id, bps, domain, event_id}) => void,
): {applied: true} | {applied: false, reason: "invalid_proof" | "duplicate"}
- verify proof first; if invalid → {applied: false, reason: "invalid_proof"}
- if evidence_hash hex in alreadyApplied → {applied: false, reason: "duplicate"}
- else: lambdaPenaltyApplier({arbiter_id: proof.attacker_id, bps: DAMAGE_FRAUD, domain: "arbitration", event_id: evidence_hash})
- add evidence_hash to alreadyApplied set
- return {applied: true}
- src/__tests__/domains/consensus/equivocation.test.ts
* Build proof from conflicting pair → evidence_hash recomputes
* Verify proof: valid signatures, distinct tuples, same round+level → {valid: true}
* Verify fail: sig_a tampered → sig_a_invalid
* Verify fail: same tuple → same_tuple
* Verify fail: different round_id → different_round_or_level
* Apply slash: first call → {applied: true}, lambdaPenaltyApplier called with 8000n bps
* Apply slash idempotency: re-submit same proof → {applied: false, reason: "duplicate"}
* Single-arbiter equivocation: n=1, arbiter signs two conflicting tuples → still detected + slashed
* Integration: pair from detectDoubleVote → buildProof → verify → apply → idempotent
ACCEPTANCE CRITERIA (headline):
✓ buildEquivocationProof matches consensus.md §Equivocation proof structure
✓ verifyEquivocationProof returns typed reason on each failure mode
✓ applyEquivocationSlash is idempotent (proof_hash dedup)
✓ Penalty = 8000bps (DAMAGE_FRAUD) to "arbitration" domain via λ
✓ Single-arbiter equivocation still slashable
SUCCESS CHECK:
cd .worktrees/claude/p3-5-1-equivocation && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.5.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-5-1-equivocation
worktree: .worktrees/claude/p3-5-1-equivocation
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Equivocation slasher. buildEquivocationProof + verifyEquivocationProof (4 failure modes) + idempotent applyEquivocationSlash via λ.applyPenalty(8000bps DAMAGE_FRAUD, domain=arbitration). Proof-hash dedup. Single-arbiter equivocation still slashable.
blockers: none"
)
FORBIDDENS:
✗ Do not slash twice on same proof_hash
✗ Do not skip the 4 failure-mode reason codes
✗ No mutable global state — pass alreadyApplied set explicitly
✗ Do not edit main checkout
NEXT:
Wave 4 — operational rollout
Verification checklist (for reviewer agent)
- EquivocationProof shape matches consensus.md fixture exactly
- verifyEquivocationProof returns 4 distinct reason codes
- applyEquivocationSlash is idempotent on evidence_hash
- DAMAGE_FRAUD = 8000n imported from κ bps-constants
- Single-arbiter equivocation tested
- λ penalty applier called with correct args
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.5.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-5-1-equivocation
worktree: .worktrees/claude/p3-5-1-equivocation
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Equivocation slasher. Proof construction per consensus.md §Equivocation proof structure. Verification with 4 failure-mode codes (sig_a_invalid, sig_b_invalid, same_tuple, different_round_or_level). Slash via λ.applyPenalty(DAMAGE_FRAUD=8000bps, domain="arbitration", event_id=evidence_hash). Idempotent on evidence_hash. Single-arbiter still slashable.
blockers: none
Common gotchas
- Idempotency state shape — pass
alreadyApplied: Set<string>as a parameter, not module-level state. Production uses a persisted set (likely a SQLite table keyed by evidence_hash hex); tests use an in-memory Set. - “Critical” offense maps to 8000bps — per task-breakdown.md line 963:
“Slash amount: maps to
criticaloffense (8000bps loss)”. Use the κDAMAGE_FRAUD = 10000nconstant forfraud, but s06’s equivocation text positions this between “severe” and “fraud” — task-breakdown gives the exact mapping at 8000bps which equalsDAMAGE_CRITICALper κ P1.1.3. UseDAMAGE_CRITICAL, notDAMAGE_FRAUD. (Document this explicitly in the impl.) - Same round_id, same finality_level is the distinguishing condition for equivocation. Two signatures on the SAME tuple is not equivocation (it’s a retry — the spec text in consensus.md §Equivocation makes this clear via “two distinct tuples at the same finality level in the same round”).
§P3.6.1 — VRF Stub (Leader Election) — Phase 3 θ Wave 3
Spec source: New entry (rolled out of P3.1.3’s view-change procedure)
Concept reference: consensus.md §View-change procedure + ADR-002 §Option A
Worktree: feature/p3-6-1-vrf-stub
Branch command: git worktree add .worktrees/claude/p3-6-1-vrf-stub -b feature/p3-6-1-vrf-stub origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: P3.1.1 (signature path for verify); ADR-002 status
Unblocks: P3.1.3 (leader election after view change)
ADR-002 status disclosure (R89.C staging): ADR-002 is PROPOSED, not Accepted. This slice ships Option A (HMAC-SHA256 internal) as a Phase 3 stub. The interface is designed so a future swap to Option B (
@noble/curvesEd25519 ECVRF per RFC 9381) is internal — the public API (vrfEval,vrfVerify) does not change. This mirrors how δ shipped Phase 0 library stubs per ADR-005 §Decision.
Files to create
src/domains/consensus/vrf-stub.ts— HMAC-SHA256 VRF stub + verifysrc/__tests__/domains/consensus/vrf-stub.test.ts— determinism + verify roundtrip
Acceptance criteria
vrfEval(seed: Buffer, input: Buffer, privKey: Buffer): {output: Buffer, proof: Buffer}-
HMAC-SHA256(privKey, seed input) → output (32 bytes) -
proof = HMAC-SHA256(privKey, output seed) (32 bytes) — simplified
-
vrfVerify(seed: Buffer, input: Buffer, output: Buffer, proof: Buffer, pubKey: Buffer): boolean- Recomputes HMAC; checks equality
- NOTE in code (HEAVILY DOCUMENTED): “This is NOT RFC 9381 ECVRF.
External verification is impossible. Swap-in path is
@noble/curvesECVRF-EDWARDS25519-SHA512-TAI per ADR-002 Option B.”
- Determinism: same
(seed, input, privKey)always produces same(output, proof) - No external verifiability: documentation explicitly warns
- Test: 10000 distinct
(seed, input)pairs same privKey → 10000 distinct outputs (collision-free property over the test range) selectLeader(arbiters: string[], seed: Buffer, round_id: bigint): string— selects arbiter indexoutput_uint32 mod n- Interface designed for swap: a
VrfProviderinterface witheval+verifymethods; HmacVrfProvider implements it; futureNobleCurvesVrfProviderwill too
Pre-flight reading
CLAUDE.mddocs/architecture/decisions/ADR-002-vrf-implementation.md§Option Adocs/3-world/physics/laws/consensus.md§View-change proceduredocs/architecture/decisions/ADR-005-multi-model-defer.md§Decision (precedent for shipping stubs)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.6.1 — VRF Stub (Leader Election)
Implement the HMAC-SHA256 VRF stub for arbiter leader election per
ADR-002 Option A. Interface designed for transparent swap to Option B
(@noble/curves ECVRF) without API change.
ADR-002 STATUS: PROPOSED at R89.C. This slice ships the Option A stub
unconditionally; the API design (VrfProvider interface) accommodates
later swap to Option B.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/architecture/decisions/ADR-002-vrf-implementation.md §Option A
3. docs/3-world/physics/laws/consensus.md §View-change procedure
4. docs/architecture/decisions/ADR-005-multi-model-defer.md §Decision
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-6-1-vrf-stub -b feature/p3-6-1-vrf-stub origin/main
cd .worktrees/claude/p3-6-1-vrf-stub
FILES TO CREATE:
- src/domains/consensus/vrf-stub.ts
/**
* HMAC-SHA256 VRF stub.
*
* NOT RFC 9381 ECVRF — outputs are deterministic and verifiable by holders
* of the private key, but NOT externally verifiable. External verifiability
* is gated on ADR-002 acceptance of Option B (@noble/curves ECVRF). When
* that lands, replace HmacVrfProvider with NobleCurvesVrfProvider; the
* public API (vrfEval, vrfVerify, selectLeader) does not change.
*
* Per ADR-005 §Decision precedent: ship stubs as Phase 0 / Phase 3 library
* code with the final wire shape; swap internals later.
*/
* interface VrfProvider {
eval(seed: Buffer, input: Buffer, privKey: Buffer): {output: Buffer, proof: Buffer}
verify(seed: Buffer, input: Buffer, output: Buffer, proof: Buffer, pubKey: Buffer): boolean
}
* class HmacVrfProvider implements VrfProvider { ... }
* export const defaultVrf: VrfProvider = new HmacVrfProvider();
* export function vrfEval(seed, input, privKey) { return defaultVrf.eval(...) }
* export function vrfVerify(seed, input, output, proof, pubKey) { return defaultVrf.verify(...) }
* export function selectLeader(arbiters: string[], seed: Buffer, round_id: bigint): string
- const evalInput = Buffer.concat([seed, Buffer.from(round_id.toString())])
- const {output} = vrfEval(seed, evalInput, /*PUBLIC seed-based key for selection — see ADR-002 Option A*/)
- const idx = output.readUInt32BE(0) % arbiters.length
- return arbiters[idx]
- src/__tests__/domains/consensus/vrf-stub.test.ts
* Determinism: same (seed, input, privKey) → same (output, proof) 10000 iterations
* Verify roundtrip: eval then verify → true
* Verify with wrong key → false
* Collision-free: 10000 distinct (seed, input) → 10000 distinct outputs
* selectLeader: deterministic given seed + round_id
* selectLeader: distribution check — 1000 random round_ids over 4 arbiters yields ~250 per arbiter (chi-squared test loose)
* Single-arbiter: selectLeader(["A"], any_seed, any_round) → "A"
ACCEPTANCE CRITERIA (headline):
✓ HMAC-SHA256 internals (per ADR-002 Option A)
✓ vrfEval + vrfVerify roundtrip
✓ Deterministic
✓ Module DOC COMMENT explicitly says NOT RFC 9381, NOT externally verifiable
✓ VrfProvider interface ready for swap to NobleCurvesVrfProvider
✓ selectLeader distributes uniformly
SUCCESS CHECK:
cd .worktrees/claude/p3-6-1-vrf-stub && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.6.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-6-1-vrf-stub
worktree: .worktrees/claude/p3-6-1-vrf-stub
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: VRF stub per ADR-002 Option A (HMAC-SHA256 internal). VrfProvider interface accommodates swap to Option B (@noble/curves ECVRF) without API change. NOT RFC 9381, NOT externally verifiable — documented in module header. selectLeader deterministic + uniform. Single-arbiter trivially returns sole arbiter.
blockers: ADR-002 still PROPOSED at slice-dispatch time — stub interpretation per Option A; swap path documented"
)
FORBIDDENS:
✗ Do not omit the doc-comment warning that this is NOT RFC 9381
✗ Do not export HmacVrfProvider directly — go through defaultVrf so swap is one-line
✗ Do not use a non-deterministic source (e.g. random nonce inside eval)
✗ Do not edit main checkout
NEXT:
P3.7.1 — MCP tool surface (registers vrf_eval as a tool)
Verification checklist (for reviewer agent)
- Module header explicitly says NOT RFC 9381, NOT externally verifiable
- VrfProvider interface exported for future swap
- Determinism over 10k iterations
- Collision-free over 10k distinct inputs (test range)
- selectLeader distribution check passes
- Single-arbiter clause tested
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.6.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-6-1-vrf-stub
worktree: .worktrees/claude/p3-6-1-vrf-stub
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: VRF stub per ADR-002 Option A (HMAC-SHA256 internal). VrfProvider interface for transparent swap to Option B (@noble/curves ECVRF). NOT RFC 9381, NOT externally verifiable (documented). vrfEval, vrfVerify, selectLeader all deterministic. Distribution check passes.
blockers: ADR-002 still PROPOSED; stub ships per Option A
Common gotchas
- The “proof” field is meaningless for HMAC but must exist in the API for swap compatibility — implement it as a redundant HMAC over the output. Future ECVRF will fill it with the actual π proof.
- Distribution test is stochastic — use a seeded PRNG and a generous chi-squared bound. The point is to catch a stuck/biased implementation, not to prove cryptographic uniformity.
- The interface accepts
privKey: Buffer— for HMAC this is just a secret key; for ECVRF this will be the Ed25519 private scalar. Both shapes areBufferso no API change. - selectLeader’s
output.readUInt32BE(0) % arbiters.lengthhas modulo bias for non-power-of-2 array sizes; for arbiter sets ≤ 1000 the bias is negligible. Document.
§P3.7.1 — θ MCP Tool Surface — Phase 3 θ Wave 4
Spec source: New entry (rolled out of roadmap.md §Phase 3 tool list)
Concept reference: roadmap.md §Phase 3 new tools + consensus.md §Phase 0 posture
Worktree: feature/p3-7-1-mcp-tools
Branch command: git worktree add .worktrees/claude/p3-7-1-mcp-tools -b feature/p3-7-1-mcp-tools origin/main
Estimated effort: M (Medium — 4–8 hours)
Depends on: P3.1.2 (quorum), P3.2.1 (finality), P3.4.1 (STA), P3.6.1 (VRF)
Unblocks: Client-side consumers (Phase 4 μ observer, governance proposals)
Files to create
src/domains/consensus/tools.ts— MCP tool registrations + handlerssrc/__tests__/domains/consensus/tools.test.ts— in-process MCP harness tests
Acceptance criteria
- 5 MCP tools registered, all with Zod schemas (v3.23 per CLAUDE.md):
consensus_propose— propose an event; returns{round_id, status}consensus_vote— sign a vote; returns{vote_signed, sig_b64}consensus_finality— query finality level for a round; returns{round_id, level, evidence?}consensus_gossip— exchange state with peer (no-op in n=1); returns{events_sent, events_received}vrf_eval— evaluate VRF for an input; returns{output_hex, proof_hex}
- Single-arbiter posture (consensus.md §Phase 0 posture):
consensus_proposeaccepts proposal; returns{round_id, status: "QUORUM"}immediately whenn=1consensus_votesigns and stores; returns immediate QUORUMconsensus_finalityreturnsQUORUMwhen only 1 arbiter has voted (trivial finalization)consensus_gossipreturns empty arrays (no peers)vrf_evalreturns deterministic stub output
- Mode gate: tools available only when
COLIBRI_MODE >= "phase3"(or always, per the “all tools available in all modes” Phase 0 advisory) - Each tool routes through Phase 0 middleware (logging, validation, error mapping)
- All inputs validated with Zod; invalid input → typed
INVALID_INPUTerror - Failure modes documented per tool:
consensus_votereturnsALREADY_VOTEDif same arbiter signs same round twice (NOT equivocation — same tuple is retry)consensus_finalityreturnsROUND_NOT_FOUNDfor unknown round_idvrf_evalreturnsINVALID_KEYfor malformed privKey
Pre-flight reading
CLAUDE.mddocs/5-time/roadmap.md§Phase 3 new toolsdocs/3-world/physics/laws/consensus.md§Phase 0 posturesrc/server.ts(registration patterns)src/middleware/(inlined in server.ts — middleware shape)src/domains/consensus/{quorum,finality,time-anchors,vrf-stub,messages}.ts
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.7.1 — θ MCP Tool Surface
Register 5 MCP tools that expose the θ surface to MCP clients. All must
work in single-arbiter mode (return trivially-finalized results when n=1)
to maintain Phase 0 deployment compatibility.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/5-time/roadmap.md §Phase 3 new tools
3. docs/3-world/physics/laws/consensus.md §Phase 0 posture
4. src/server.ts (tool registration patterns)
5. src/domains/consensus/{quorum,finality,time-anchors,vrf-stub,messages}.ts
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-7-1-mcp-tools -b feature/p3-7-1-mcp-tools origin/main
cd .worktrees/claude/p3-7-1-mcp-tools
FILES TO CREATE:
- src/domains/consensus/tools.ts
* import { z } from 'zod'; (v3.23 — match existing pattern)
* 5 tool registrations following server.ts pattern:
- registerTool("consensus_propose", { inputSchema, outputSchema, handler })
- registerTool("consensus_vote", ...)
- registerTool("consensus_finality", ...)
- registerTool("consensus_gossip", ...)
- registerTool("vrf_eval", ...)
* Handlers:
- consensus_propose: build proposal, store in mcp_consensus_proposals table (schema-ready per consensus.md §Phase 0 posture); if n=1, return QUORUM immediately
- consensus_vote: sign vote via P3.1.1 messages; check for retry (same tuple, same arbiter) vs equivocation (distinct tuple); store in mcp_consensus_votes table; if n=1, immediate QUORUM
- consensus_finality: load FinalitySM state from DB; return current level + evidence
- consensus_gossip: peer-list-empty no-op when n=1; return {events_sent: [], events_received: []}
- vrf_eval: call P3.6.1 vrfEval; return hex-encoded output + proof
* Zod input schemas for each
* Zod output schemas (or .nullable() for ROUND_NOT_FOUND)
- src/__tests__/domains/consensus/tools.test.ts
* Use existing test-harness pattern (e.g. src/__tests__/server.smoke.test.ts)
* consensus_propose single-arbiter: returns {round_id: 1n, status: "QUORUM"}
* consensus_vote single-arbiter: signs, immediate QUORUM
* consensus_vote retry: same arbiter same tuple twice → ALREADY_VOTED
* consensus_finality unknown round: ROUND_NOT_FOUND error
* consensus_gossip single-arbiter: empty arrays
* vrf_eval: deterministic output hex
* vrf_eval malformed key: INVALID_KEY error
* Zod rejection: invalid input → INVALID_INPUT
* Mode gate (if added): tools unavailable in phase0-explicit mode? — TBD per Phase 0 advisory
ACCEPTANCE CRITERIA (headline):
✓ 5 tools registered with Zod schemas
✓ Single-arbiter trivial-finalization behavior
✓ Retry vs equivocation distinguished
✓ 3 typed error modes (ALREADY_VOTED, ROUND_NOT_FOUND, INVALID_KEY, INVALID_INPUT)
✓ Routed through Phase 0 middleware
SUCCESS CHECK:
cd .worktrees/claude/p3-7-1-mcp-tools && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.7.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-7-1-mcp-tools
worktree: .worktrees/claude/p3-7-1-mcp-tools
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: 5 θ MCP tools (consensus_propose, consensus_vote, consensus_finality, consensus_gossip, vrf_eval) with Zod schemas. Single-arbiter compatibility per consensus.md §Phase 0 posture: tools return trivially-finalized results when n=1. Typed error modes for retry/unknown-round/malformed-key/invalid-input.
blockers: none"
)
FORBIDDENS:
✗ No new Zod major version — use v3.23 per CLAUDE.md §1
✗ Do not skip the Phase 0 single-arbiter posture — tools MUST work in n=1
✗ Do not log signatures or private keys
✗ Do not edit main checkout
NEXT:
Wave 5 — P3.8.1 parity harness, P3.9.1 fork hook stub
Verification checklist (for reviewer agent)
- 5 tools registered
- Zod schemas for all inputs + outputs
- Single-arbiter behavior tested per tool
- 4 typed error modes
- Routed through Phase 0 middleware
- No private-key logging
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.7.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-7-1-mcp-tools
worktree: .worktrees/claude/p3-7-1-mcp-tools
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: 5 MCP tools — consensus_propose, consensus_vote, consensus_finality, consensus_gossip, vrf_eval — registered with Zod v3.23 schemas. Single-arbiter trivial-finalization per consensus.md §Phase 0 posture. Typed errors ALREADY_VOTED, ROUND_NOT_FOUND, INVALID_KEY, INVALID_INPUT.
blockers: none
Common gotchas
- Tool registration pattern — check how κ surface tools (none in
Phase 0, but follow existing β/ε/ζ/η patterns in
src/tools/*.ts). Use the sameregisterToolhelper. - Single-arbiter shortcut — DO NOT branch on
if (n === 1) return trivial; instead, run the real state machines withn=1, which by construction (P3.1.2 quorumThreshold(1n)===1n) yields trivial results. This keeps the code path one path. - Database schema — Phase 0 already has
mcp_consensus_votesschema-ready per consensus.md §Phase 0 posture. Verify it exists; if not, add the migration as part of this slice (and document). - Output hex encoding — use lowercase hex without
0xprefix for consistency with η Merkle root encoding.
§P3.8.1 — Test Corpus + Parity Harness — Phase 3 θ Wave 5
Spec source: New entry (mirrors κ P1.5.5 parity harness)
Concept reference: consensus.md §Worked example + κ P1.5.5 pattern
Worktree: feature/p3-8-1-parity-harness
Branch command: git worktree add .worktrees/claude/p3-8-1-parity-harness -b feature/p3-8-1-parity-harness origin/main
Estimated effort: L (Large — 1–2 days)
Depends on: P3.1.2, P3.2.1, P3.5.1 (full state-machine surface)
Unblocks: Phase 3 seal
Files to create
src/domains/consensus/parity-harness.ts— multi-arbiter simulation harnesssrc/__tests__/domains/consensus/parity-harness.test.ts— 4 default-corpus scenariossrc/domains/consensus/default-corpus.ts— curated test scenarios
Acceptance criteria
- 4 scenarios in default corpus:
- n=1 (single-arbiter Phase 0 compat — happy path)
- n=4, all honest (quorum reached trivially)
- n=4, 1 Byzantine (D votes divergent root; A/B/C reach QUORUM on majority root)
- n=4, equivocator (D double-signs; slashing fires; idempotent on re-submission)
- The 4-node worked example from consensus.md §Worked example IS scenario 3 (verbatim)
- Harness produces structured report:
type ParityReport = { scenario_id: string; n: bigint; rounds_executed: bigint; finality_reached: FinalityLevel; equivocation_proofs: EquivocationProof[]; slashings_applied: bigint; determinism_check: {seed: bigint, second_run_identical: boolean}; }; - Determinism: two runs with same seed produce byte-identical reports
- Performance: 10000 synthetic events × all 4 scenarios completes in < 5 seconds
- Tests run all 4 scenarios; pass conditions:
- Scenario 1: finality reaches QUORUM in 1 round
- Scenario 2: finality reaches QUORUM in 1 round
- Scenario 3: finality reaches QUORUM (3/4) despite D divergence; A/B/C agree
- Scenario 4: equivocation_proofs has 1 entry; slashings_applied = 1; re-run with same proof → still 1
Pre-flight reading
CLAUDE.mddocs/3-world/physics/laws/consensus.md§Worked examplesrc/__tests__/domains/rules/parity-harness.test.ts(κ P1.5.5 — pattern source)- All P3 modules
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.8.1 — Test Corpus + Parity Harness
Build the multi-arbiter simulation harness with a 4-scenario default
corpus. Mirrors κ P1.5.5's parity harness. Closes the Phase 3 verification
loop.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/3-world/physics/laws/consensus.md §Worked example (4-node BFT vote on event E)
3. src/__tests__/domains/rules/parity-harness.test.ts (κ P1.5.5 — pattern source)
4. All P3.1–P3.7 modules
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-8-1-parity-harness -b feature/p3-8-1-parity-harness origin/main
cd .worktrees/claude/p3-8-1-parity-harness
FILES TO CREATE:
- src/domains/consensus/default-corpus.ts
* Export 4 scenarios as data:
SCENARIO_1 = {id: "single-arbiter", n: 1n, arbiters: ["A"], rounds: [{proposal_root: "0xab12", honest: {A: "0xab12"}}]}
SCENARIO_2 = {id: "n4-all-honest", n: 4n, arbiters: ["A","B","C","D"], rounds: [{proposal_root: "0xab12", honest: {A: "0xab12", B: "0xab12", C: "0xab12", D: "0xab12"}}]}
SCENARIO_3 = {id: "n4-byzantine-D", n: 4n, arbiters: ["A","B","C","D"], rounds: [{proposal_root: "0xab12", honest: {A: "0xab12", B: "0xab12", C: "0xab12"}, byzantine: {D: "0xCAFE"}}]}
SCENARIO_4 = {id: "n4-equivocator-D", n: 4n, arbiters: ["A","B","C","D"], rounds: [{proposal_root: "0xab12", honest: {A: "0xab12", B: "0xab12", C: "0xab12"}, equivocator: {D: ["0xab12", "0xCAFE"]}}]}
DEFAULT_CORPUS = [SCENARIO_1, SCENARIO_2, SCENARIO_3, SCENARIO_4]
- src/domains/consensus/parity-harness.ts
* function runScenario(scenario, seed: bigint): ParityReport
1. Build keypairs deterministically from seed
2. For each arbiter, build Votes per honest/byzantine/equivocator
3. Run RoundState (P3.1.3) for each round (mock VRF)
4. Track FinalitySM (P3.2.1) state
5. For equivocator scenario: detect double-vote (P3.1.2), build proof (P3.5.1), apply slash (P3.5.1)
6. Build ParityReport with all stats
* function runDefaultCorpus(seed: bigint = 42n): ParityReport[]
return DEFAULT_CORPUS.map(s => runScenario(s, seed));
- src/__tests__/domains/consensus/parity-harness.test.ts
* Run all 4 scenarios:
const reports = runDefaultCorpus(42n);
expect(reports[0].finality_reached).toBe("QUORUM"); // n=1
expect(reports[1].finality_reached).toBe("QUORUM"); // n=4 happy
expect(reports[2].finality_reached).toBe("QUORUM"); // n=4 Byzantine still reaches majority
expect(reports[3].slashings_applied).toBe(1n); // 1 equivocation slashed
* Determinism: runDefaultCorpus(42n) === runDefaultCorpus(42n) (deep equal)
* Performance: 10000 synthetic rounds across all scenarios < 5s
* Scenario 3 verbatim worked-example check:
- merkle_root "0xab12" matches majority count = 3
- merkle_root "0xCAFE" matches minority count = 1
- QUORUM reached on "0xab12"
ACCEPTANCE CRITERIA (headline):
✓ 4 scenarios in default corpus
✓ Scenario 3 is consensus.md §Worked example verbatim
✓ Determinism: same seed → byte-identical reports
✓ Performance: 10k events × 4 scenarios < 5s
✓ All 4 pass conditions verified
SUCCESS CHECK:
cd .worktrees/claude/p3-8-1-parity-harness && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.8.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-8-1-parity-harness
worktree: .worktrees/claude/p3-8-1-parity-harness
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: θ parity harness with 4-scenario default corpus (single-arbiter, n=4 honest, n=4 Byzantine, n=4 equivocator). Scenario 3 is consensus.md §Worked example verbatim. Determinism over seed=42. 10k events < 5s.
blockers: none"
)
FORBIDDENS:
✗ Do not run scenarios in parallel (non-deterministic thread scheduling)
✗ Do not use Date.now() for any timing
✗ Do not load corpus from disk inside the harness — tests pass it explicitly
✗ Do not edit main checkout
NEXT:
P3.9.1 — Fork trigger hook (ι handoff stub)
Verification checklist (for reviewer agent)
- 4 scenarios in DEFAULT_CORPUS
- Scenario 3 matches consensus.md §Worked example byte-for-byte
- Determinism: deep-equal across two runs
- Performance: 10k events × 4 scenarios < 5s
- All pass conditions verified (1 quorum, 2 quorum, 3 quorum, 4 slash=1)
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.8.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-8-1-parity-harness
worktree: .worktrees/claude/p3-8-1-parity-harness
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: Parity harness + 4-scenario corpus. Scenario 1 n=1 (Phase 0 compat). Scenario 2 n=4 honest. Scenario 3 n=4 Byzantine (consensus.md §Worked example verbatim). Scenario 4 n=4 equivocator (1 slashing applied; idempotent on re-submission). Determinism over seed=42n. 10k events < 5s.
blockers: none
Common gotchas
- The harness MUST be deterministic — no real RNG, no parallelism, no wall-clock. Inject seed everywhere.
- Worked-example “0xab12…” / “0xCAFE…” are short-prefixes — the test needs full 32-byte roots. Generate from seed deterministically; assert on prefix equality not full root in the readability-of-test sense.
- Performance budget 5s for 10k synthetic — if the harness is naive (per-event Ed25519 sign), 10k sigs at ~50µs each is 0.5s — well inside budget. Watch for accidental N² loops in proof building.
- Default corpus is data, not code — keep it as
default-corpus.tsimportable; later phases may add scenarios.
§P3.9.1 — Fork Trigger Hook (ι handoff stub) — Phase 3 θ Wave 5
Spec source: New entry (ι Phase 5 handoff surface; ι implementation OUT OF SCOPE)
Concept reference: consensus.md §Interaction with ι (fork) + state-fork.md §Fork identity
Worktree: feature/p3-9-1-fork-hook
Branch command: git worktree add .worktrees/claude/p3-9-1-fork-hook -b feature/p3-9-1-fork-hook origin/main
Estimated effort: S (Small — 2–4 hours)
Depends on: P3.1.2 (quorum failure signal)
Unblocks: ι Phase 5
Files to create
src/domains/consensus/fork-hook.ts— handler registration + fire-on-quorum-failuresrc/__tests__/domains/consensus/fork-hook.test.ts— fire/no-fire + default no-op
Acceptance criteria
type ForkTriggerEvent = {round_id: bigint, divergent_roots: Buffer[], reason: "CONSENSUS_SPLIT" | "PARTITION_RECOVERY", rule_version_hash: Buffer, timestamp_logical: bigint}type ForkHookHandler = (event: ForkTriggerEvent) => void | Promise<void>class ForkHookRegistry { register(h: ForkHookHandler): void; fire(event: ForkTriggerEvent): Promise<void>; clear(): void; handlers(): readonly ForkHookHandler[] }- Default handler is no-op (logs to ζ via thought_record at most)
- Fires when: θ cannot reach quorum within
2 * T_timeout(i.e. one full round of view-change attempts exhausted) - Payload populated from RoundState (P3.1.3):
divergent_rootsis the union of distinct merkle_roots from REVEAL phase;reasonisCONSENSUS_SPLITfor vote-genuinely-split,PARTITION_RECOVERYfor re-converging branches (Phase 3 only setsCONSENSUS_SPLIT;PARTITION_RECOVERYis Phase 5 ι) - Handlers run sequentially; one handler’s error does NOT prevent others from firing (try/catch each)
- ι actual implementation is OUT OF SCOPE for Phase 3. Tests verify the hook fires with right payload shape and the no-op handler doesn’t crash; no fork creation logic.
Pre-flight reading
CLAUDE.mddocs/3-world/physics/laws/consensus.md§Interaction with ι (fork)docs/3-world/physics/laws/state-fork.md§Fork identity (background — DO NOT implement)src/domains/consensus/round-state.ts(P3.1.3 — where the hook fires from)
Ready-to-paste agent prompt
You are a Phase 3 builder agent for Colibri (θ Consensus).
TASK: P3.9.1 — Fork Trigger Hook (ι handoff stub)
Build the hook surface that ι (Phase 5 State Fork) will subscribe to. Fires
when θ exhausts view-change attempts without reaching quorum. ι itself is
NOT implemented here — this is a handoff stub only.
FILES TO READ FIRST:
1. CLAUDE.md
2. docs/3-world/physics/laws/consensus.md §Interaction with ι (fork)
3. docs/3-world/physics/laws/state-fork.md §Fork identity (background only)
4. src/domains/consensus/round-state.ts (P3.1.3)
WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p3-9-1-fork-hook -b feature/p3-9-1-fork-hook origin/main
cd .worktrees/claude/p3-9-1-fork-hook
FILES TO CREATE:
- src/domains/consensus/fork-hook.ts
* export type ForkReason = "CONSENSUS_SPLIT" | "PARTITION_RECOVERY"; // Phase 3 only fires CONSENSUS_SPLIT
* export type ForkTriggerEvent = {
round_id: bigint;
divergent_roots: Buffer[];
reason: ForkReason;
rule_version_hash: Buffer;
timestamp_logical: bigint;
}
* export type ForkHookHandler = (event: ForkTriggerEvent) => void | Promise<void>;
* export class ForkHookRegistry {
private hs: ForkHookHandler[] = [];
register(h: ForkHookHandler): void { this.hs.push(h); }
async fire(event: ForkTriggerEvent): Promise<void> {
for (const h of this.hs) {
try { await h(event); } catch (e) { /* swallow; one handler should not stop others */ }
}
}
clear(): void { this.hs = []; }
handlers(): readonly ForkHookHandler[] { return this.hs; }
}
* export const defaultRegistry = new ForkHookRegistry();
* export const noOpHandler: ForkHookHandler = (_event) => { /* intentional no-op */ };
* defaultRegistry.register(noOpHandler); // safe default
- src/__tests__/domains/consensus/fork-hook.test.ts
* Register a handler; fire with sample event; handler called with right payload
* Default no-op handler doesn't crash on fire
* Register 3 handlers; fire; all 3 called
* One handler throws; others still fire
* clear() removes all
* Payload shape: divergent_roots is array of Buffer; reason is "CONSENSUS_SPLIT" or "PARTITION_RECOVERY"
* Phase 3 only emits CONSENSUS_SPLIT (test fixture: only this string in test events)
ACCEPTANCE CRITERIA (headline):
✓ ForkTriggerEvent shape locked
✓ ForkHookRegistry with register / fire / clear / handlers
✓ Default no-op handler registered
✓ Error in one handler doesn't stop others
✓ ι implementation NOT included — this is hook surface only
SUCCESS CHECK:
cd .worktrees/claude/p3-9-1-fork-hook && npm run build && npm run lint && npm test
WRITEBACK:
task_update(id="<PM-supplied UUID for P3.9.1>", status="done", progress=100)
thought_record(thought_type="reflection",
content="task_id: <UUID>
branch: feature/p3-9-1-fork-hook
worktree: .worktrees/claude/p3-9-1-fork-hook
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: ι handoff stub. ForkHookRegistry with register/fire/clear/handlers. ForkTriggerEvent shape: round_id, divergent_roots[], reason, rule_version_hash, timestamp_logical. Default no-op handler. Handler errors swallowed (one fail does not stop others). ι implementation is OUT OF SCOPE.
blockers: none"
)
FORBIDDENS:
✗ Do not implement any ι fork creation logic — this is hook surface ONLY
✗ Do not skip the "errors swallowed" semantics — important for handler isolation
✗ Do not auto-register more than the no-op handler in defaultRegistry
✗ Do not edit main checkout
NEXT:
Phase 3 close. Hand off to ι Phase 5 + π Phase 6 planning.
Verification checklist (for reviewer agent)
- ForkTriggerEvent shape matches spec
- Registry has register / fire / clear / handlers methods
- Default no-op handler registered
- Error isolation: one handler throws, others continue
- Phase 3 only emits CONSENSUS_SPLIT
- NO ι fork-creation logic
npm run build && npm run lint && npm testpass
Writeback template
task_update:
id: "<PM-supplied UUID for P3.9.1>"
status: done
progress: 100
thought_record:
thought_type: reflection
content: |
task_id: <UUID>
branch: feature/p3-9-1-fork-hook
worktree: .worktrees/claude/p3-9-1-fork-hook
commit: <SHA>
tests: npm run build && npm run lint && npm test (<N>/<T> pass)
summary: ι handoff stub — ForkHookRegistry with register/fire/clear. ForkTriggerEvent {round_id, divergent_roots, reason, rule_version_hash, timestamp_logical}. Default no-op handler. Phase 3 only fires CONSENSUS_SPLIT reason. ι implementation OUT OF SCOPE.
blockers: none
Common gotchas
- Error isolation in fire() — wrap each handler call in try/catch. One buggy handler MUST NOT prevent others from running. Log errors via ζ thought_record if available.
- The
defaultRegistrysingleton — RoundState wiresdefaultRegistryby default; tests should create freshnew ForkHookRegistry()to avoid cross-test leakage. - Phase 5 ι will REPLACE the no-op handler with a real fork-creation handler. Phase 3’s job is only to ensure the call site exists, the event payload is well-shaped, and the no-op default doesn’t break the build.
- The reason enum has 5 values in state-fork.md (CONSENSUS_SPLIT, PARTITION_RECOVERY, RULE_UPGRADE, EMERGENCY, EXPERIMENTAL) but Phase 3 only emits CONSENSUS_SPLIT. The type widens later in Phase 5+.
Back to index
See also
- agent-bootstrap.md — Master bootstrap prompt (read FIRST)
- agent-handoff-protocol.md — Multi-agent handoff spec
- task-breakdown.md — Canonical 63-task breakdown (all phases)
- docs/spec/s06-consensus.md — Authoritative BFT spec
- docs/spec/s08-gossip.md — Gossip protocol spec
- docs/3-world/physics/laws/consensus.md — θ concept doc with worked examples
- docs/3-world/physics/laws/state-fork.md — ι concept doc (Phase 5)
- docs/architecture/decisions/ADR-002-vrf-implementation.md — VRF library (PROPOSED)
- docs/architecture/decisions/ADR-003-bft-library.md — BFT library (PROPOSED)
- docs/architecture/decisions/ADR-005-multi-model-defer.md — Phase 0 stub precedent
- docs/5-time/roadmap.md — Phase 3 schedule (R121+)
- docs/agents/executor-contract.md — T3 5-step chain
- CLAUDE.md — §3 worktree, §5 gate, §6 5-step chain, §7 writeback