The L2 decision engine is a layered kernel: a DebateEngine that preserves dissent, a quantitative 0–10 scoring system, an independent maker-checker Verifier, and an N-lens panel that is the T3+ emission gate. The trick that makes it trustworthy: contrarian-last and read-only verification are enforced structurally, not by prompts. Source: packages/council/src/{debate,verifier,board}.ts, governed by ADR-0003.
Phases run in the board's declared order (serial), and members within a phase run in parallel. Because phases are serial, a later phase receives the accumulated contributions of earlier ones. This is what makes "the contrarian sees everything and speaks last" real sequencing, not a prompt claim — the engine's own header comment says so:
// packages/council/src/debate.ts:30-44 — the engine's contract, verbatim intent // Phases are run in the board's declared order (SERIAL between phases) and the // members within a phase are dispatched IN PARALLEL. … The contrarian phase is // ordered last by the board loader, so "contrarian sees everything and speaks // last" is REAL sequencing — earlier phases have already produced their // statements before the contrarian phase's model inputs are even built.
Adapter calls obey the never-throws invariant (lesson 14): each run() resolves to a ModelRunResult discriminated on ok, so the engine branches on the result instead of wrapping calls in try/catch. A member that fails to bind, errors, or returns an unparseable vote is recorded as a MemberFailureReason — never a thrown exception.
Each member emits axis scores over SCORING_AXES (feasibility / revenue / cx / ttm / risk) weighted .25/.25/.20/.15/.15. GO_THRESHOLD = 7, PIVOT_THRESHOLD = 5. The non-obvious correctness detail: the risk axis is scored so 10 = low risk, adding in the same direction as every other axis — there is no 1 - risk normalization to get backwards:
// packages/council/src/scoring.ts — direction is uniform; axis names are one source of truth SCORING_AXES = ['feasibility', 'revenue', 'cx', 'ttm', 'risk']; // typo can't silently zero an axis // high score → GO; risk 10 = low risk; NO inversion — all axes add the same way
Fail-closed quorum. aggregateConsensus returns NO_GO before consulting any score when fewer than MIN_VALID_AGENTS = 3 valid votes are present (consensus.ts:101-125). "A sparse or degraded board must never green-light." A board that lost two members to adapter failures can't accidentally pass on one optimistic vote.
The maker-checker Verifier is the embodiment of invariant ④ (lesson 16). It accepts only readonly views, exposes no adapter or mutation surface, and proves atomic claims with deterministic oracles over the structured evidence — never over the maker's prose:
rejected.MAX_LOOPS = 3 → park to T4 (escalate-after-N), not an infinite retry.For T3 and above, emission is gated by verifyPanel / isPanelEmissionApproved — three perspective-diverse lenses aggregated by quorum with a preserve-dissent veto:
| Lens | Checks |
|---|---|
| COHERENCE | = verifyDecision reused — quorum, verdict↔score, self-consistency |
| FAITHFULNESS | evidence is present and clears confidence/strength floors |
| DOMAIN | a GO needs a validation signal; evidence is not a monoculture |
The panel approves by quorum (default 2 of 3) but a hard rejection in any single lens vetoes the whole panel (preserve-dissent), and escalate-after-N propagates. So a confident two-lens majority can still be stopped by one lens that found a fatal flaw. This conjunction — majority to pass, any-veto to block — is what the funnel's verified-GO (lesson 15) depends on. It is deliberately harder to pass than a simple vote.
MemberFailureReason, not a throw.risk = 10 means low risk, so it adds in the same direction as feasibility/revenue. There is deliberately no 1 - risk step; getting that backwards would invert every decision, so the axes are kept uniform and the axis names are a single source of truth.