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:


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)/3 faulty 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_hash is 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_root voted 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_ACCEPTED events 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/curves ECVRF 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 #229

ADR-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_record MUST precede merkle_finalize for 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

  1. 64-bit signed integer arithmetic — no floating point anywhere (inherited from κ; required for cross-node parity)
  2. No wall-clock reads in vote / aggregation logic — only Lamport logical clocks in signed payloads
  3. Ed25519 for arbiter signatures — public-key cryptography per s06 §Event validation step 1
  4. Canonical JSON serialization — deterministic key order, no whitespace, single-byte separators (P1.5.4 from κ)
  5. Aggregate-all-errors validator pattern — multi-error per pass (inherited from P1.2.3)
  6. Idempotent slashing — proof-hash dedup (P3.5.1 acceptance)
  7. No external side effects before HARD finality — rollback safety
  8. Single-arbiter compatibility — every state-machine slice MUST degrade gracefully for n=1 so Phase 0 deployment continues to work
  9. AST cap inherited — κ’s MAX_INTEGER_OPS, MAX_CALL_DEPTH bounds apply when rules are evaluated during vote validation
  10. 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-serialize
  • src/__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) plus sender_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
  • Buffer values serialize as hex strings (canonical); the canonical form is what gets signed
  • EquivocationProof carries 2 conflicting signed_vote_a + signed_vote_b plus attacker_id, evidence_hash, submitter
  • Hash determinism: SHA-256 over canonical bytes produces same digest across 1000 iterations
  • No Date.now(), Math.random(), or wall-clock in 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.1
  • docs/spec/s06-consensus.md §Event validation + §Quorum
  • docs/3-world/physics/laws/consensus.md §What the arbiters vote on + §Gossip message envelope + §Equivocation proof structure
  • docs/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_type discriminator
  • All bigint for ids/counts; Buffer for 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 test pass

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 clockstimestamp_logical increments per local send; it is NOT epoch ms. Initialize to 0n at module load; provide nextLogical() helper.
  • Discriminator unions in TS — use msg_type literally 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/O
  • src/__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) / 3n
  • hasQuorum(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, not number
  • Zero Math.* / Date.* / I/O in source

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P3.1.2
  • docs/3-world/physics/laws/consensus.md §Quorum formula + §Worked example (4-node BFT)
  • docs/spec/s06-consensus.md §Quorum
  • src/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) === 1n tested
  • 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 test pass

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 handler
  • src/__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 = 10000n
    • T_reveal_phase = 10000n
    • T_timeout = 60000n (2 × T_round)
  • Commit-reveal protocol:
    1. Each arbiter computes salt = random_bytes(32) (seeded for determinism in tests)
    2. Sign vote tuple; compute c = SHA-256(canonicalSerialize(vote) || salt)
    3. Broadcast COMMIT(round_id, arbiter_id, c)
    4. Wait until commit_count >= quorum OR elapsed > T_commit_phase
    5. Broadcast REVEAL(round_id, arbiter_id, vote, salt)
    6. Verify each reveal: SHA-256(canonicalSerialize(vote_i) || salt_i) === c_i
    7. Aggregate signatures → threshold signature σ
    8. Emit QUORUM(round_id, merkle_root, σ)
  • View-change triggers:
    • timeout: proposer fails to emit proposal within T_timeout
    • equivocation_observed: detectDoubleVote returns non-empty
    • malformed_proposal: proposal fails schema validation
  • View-change collects VIEW_CHANGE messages; if count >= quorum, computes next_leader = VRF(prev_merkle_root, round_id) via P3.6.1
  • Emits VIEW_CHANGE_ACCEPTED event into ζ Decision Trail (uses existing thought_record interface; 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.md
  • docs/guides/implementation/task-breakdown.md §P3.1.3
  • docs/3-world/physics/laws/consensus.md §Voting protocol + §View-change procedure + §Worked example (4-node)
  • docs/spec/s06-consensus.md §Quorum + §Equivocation
  • src/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 test pass

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 use crypto.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. Avoid setTimeout for tests (non-determinism).
  • VIEW_CHANGE_ACCEPTED into ζ must use the existing thought_record interface — do not introduce a new domain. Set thought_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 FSM
  • src/__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) === true for current round
    • QUORUM → 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 throw FinalityRollbackError
  • No external side effects before HARD — implement an externalEffect guard that throws PrematureExternalEffectError if 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.md
  • docs/guides/implementation/task-breakdown.md §P3.2.1
  • docs/3-world/physics/laws/consensus.md §The five finality levels
  • docs/spec/s06-consensus.md §Finality levels
  • src/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 test pass

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 validator
  • src/__tests__/domains/consensus/gossip-wire.test.ts — triple-anchor pass/fail scenarios + retention horizon

Acceptance criteria

  • Wire types exported: IHAVE, IWANT
  • IHAVE carries: event_ids: Buffer[], state_root_pre: Buffer, rule_version_hash: Buffer, fork_id: Buffer, sender_id: string, timestamp_logical: bigint, signature: Buffer
  • IWANT carries: 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_hash must match receiver’s active rule set
    • state_root_pre must be reachable from last known checkpoint (no gap > 1 epoch)
    • fork_id must 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.md
  • docs/guides/implementation/task-breakdown.md §P3.3.1
  • docs/spec/s08-gossip.md §IHAVE/IWANT + §Triple-Anchor validation
  • docs/3-world/physics/laws/consensus.md §Gossip + §Gossip message envelope
  • docs/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 test pass

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_pre is not in receiver’s known_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 epochtimestamp_logical is a Lamport counter, not an epoch index. Retention is computed in epochs, so the helper needs both; pass current_epoch separately.

§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 lifecycle
  • src/__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; default k ≈ 7 for n=1000, p=0.01
  • Default config: n=1000, p=0.01 → m≈9585 bits (~1.2 KB), k=7
  • insert(event_id: Buffer): void
  • mightContain(event_id: Buffer): boolean — false-positive rate ≤ p
  • Fresh filter per roundreset() 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.md
  • docs/guides/implementation/task-breakdown.md §P3.3.1 (Bloom criterion)
  • docs/spec/s08-gossip.md §Bloom filter algorithm + §Deduplication
  • src/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 test pass

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.log has 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; use byte = 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 tracking
  • src/__tests__/domains/consensus/adaptive-fanout.test.ts — table fixture + epoch-recompute test

Acceptance criteria

  • computeFanout(connectivity_score: bigint): bigint returns max(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 outcome
  • recomputeIfDue(current_epoch: bigint, last_recompute_epoch: bigint, period_epochs: bigint = 5n) returns updated score iff current - last >= period
  • Score uses live-peer count: number of peers with at least 1 successful exchange in last period_epochs epochs

Pre-flight reading

  • CLAUDE.md
  • docs/spec/s08-gossip.md §Adaptive fanout
  • docs/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 test pass

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_epochs epochs”. 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 detection
  • src/__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_ms values, 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_n AND timestamp_{n+1} >= timestamp_n); violations flagged as soft fault
  • Replay protection: anchors with epoch < current_epoch - 10 rejected
  • 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.md
  • docs/guides/implementation/task-breakdown.md §P3.4.1
  • docs/spec/s06-consensus.md §Signed time anchors
  • docs/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 test pass

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_ms IS a wall-clock unit — this is the one place in θ that names physical milliseconds. But the signing path NEVER reads Date.now() in production; an offline ticker or external time source feeds the timestamp_ms argument. 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 30000ms constant 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 slasher
  • src/__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"} if proof.evidence_hash already in alreadyApplied
    • 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)
  • 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.md
  • docs/guides/implementation/task-breakdown.md §P3.5.1
  • docs/3-world/physics/laws/consensus.md §Equivocation + §Equivocation proof structure
  • docs/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 test pass

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 critical offense (8000bps loss)”. Use the κ DAMAGE_FRAUD = 10000n constant for fraud, but s06’s equivocation text positions this between “severe” and “fraud” — task-breakdown gives the exact mapping at 8000bps which equals DAMAGE_CRITICAL per κ P1.1.3. Use DAMAGE_CRITICAL, not DAMAGE_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/curves Ed25519 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 + verify
  • src/__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/curves ECVRF-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 index output_uint32 mod n
  • Interface designed for swap: a VrfProvider interface with eval + verify methods; HmacVrfProvider implements it; future NobleCurvesVrfProvider will too

Pre-flight reading

  • CLAUDE.md
  • docs/architecture/decisions/ADR-002-vrf-implementation.md §Option A
  • docs/3-world/physics/laws/consensus.md §View-change procedure
  • docs/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 test pass

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 are Buffer so no API change.
  • selectLeader’s output.readUInt32BE(0) % arbiters.length has 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 + handlers
  • src/__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_propose accepts proposal; returns {round_id, status: "QUORUM"} immediately when n=1
    • consensus_vote signs and stores; returns immediate QUORUM
    • consensus_finality returns QUORUM when only 1 arbiter has voted (trivial finalization)
    • consensus_gossip returns empty arrays (no peers)
    • vrf_eval returns 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_INPUT error
  • Failure modes documented per tool:
    • consensus_vote returns ALREADY_VOTED if same arbiter signs same round twice (NOT equivocation — same tuple is retry)
    • consensus_finality returns ROUND_NOT_FOUND for unknown round_id
    • vrf_eval returns INVALID_KEY for malformed privKey

Pre-flight reading

  • CLAUDE.md
  • docs/5-time/roadmap.md §Phase 3 new tools
  • docs/3-world/physics/laws/consensus.md §Phase 0 posture
  • src/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 test pass

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 same registerTool helper.
  • Single-arbiter shortcut — DO NOT branch on if (n === 1) return trivial; instead, run the real state machines with n=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_votes schema-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 0x prefix 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 harness
  • src/__tests__/domains/consensus/parity-harness.test.ts — 4 default-corpus scenarios
  • src/domains/consensus/default-corpus.ts — curated test scenarios

Acceptance criteria

  • 4 scenarios in default corpus:
    1. n=1 (single-arbiter Phase 0 compat — happy path)
    2. n=4, all honest (quorum reached trivially)
    3. n=4, 1 Byzantine (D votes divergent root; A/B/C reach QUORUM on majority root)
    4. 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.md
  • docs/3-world/physics/laws/consensus.md §Worked example
  • src/__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 test pass

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.ts importable; 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-failure
  • src/__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_roots is the union of distinct merkle_roots from REVEAL phase; reason is CONSENSUS_SPLIT for vote-genuinely-split, PARTITION_RECOVERY for re-converging branches (Phase 3 only sets CONSENSUS_SPLIT; PARTITION_RECOVERY is 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.md
  • docs/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 test pass

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 defaultRegistry singleton — RoundState wires defaultRegistry by default; tests should create fresh new 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


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.