Council & Verifier: debater, pontuar, provar
O motor de decisão é um kernel em camadas: um DebateEngine que preserva o dissenso, um sistema quantitativo de pontuação 0–10, um Verifier maker-checker independente e read-only, e um painel N-lentes que é o portão de emissão para T3+. O truque que o torna confiável: contrarian-last e verificação read-only são impostos estruturalmente, no carregamento e na arquitetura — nunca por um prompt.
Esta lição destila esses arquivos, lidos literalmente, governados pela ADR-0003 (o dissenso é uma propriedade do sistema). Por que importa pra missão: é o gate que decide, com prova determinística, se uma decisão do council pode emitir — o GO-verificado de que o funil (Lição 15) depende.
- Explicar por que "o contrarian fala por último" é sequenciamento real (fases seriais), não um claim de prompt.
- Calcular o agregado 0–10 com os pesos reais e mapeá-lo a GO / PIVOT / NO_GO pelos limiares 7 e 5.
- Distinguir o quórum fail-closed (
MIN_VALID_AGENTS = 3) da pontuação — e por que ele vem primeiro. - Descrever o Verifier read-only e o painel N-lentes: quórum e veto de preservação-de-dissenso.
01 · O kernel em quatro camadas
Antes dos detalhes, o mapa. Uma decisão atravessa quatro componentes, cada um num arquivo próprio em packages/council/src. O segredo de design é que cada camada tem um trabalho só e passa adiante uma estrutura imutável — não a prosa do modelo:
02 · DebateEngine — fases seriais, membros paralelos
As fases rodam na ordem declarada do board (serial entre fases), e os membros dentro de uma fase rodam em paralelo. Como as fases são seriais, uma fase posterior recebe as contribuições acumuladas das anteriores via buildModelInput. É isso que faz "o contrarian vê tudo e fala por último" ser sequenciamento real — o próprio comentário de cabeçalho do motor diz isso, literalmente:
// packages/council/src/debate.ts:30-44 — o contrato do motor, intenção literal // 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.
Repare na assimetria que dá poder ao desenho: paralelo dentro da fase (rápido — os membros não esperam uns aos outros) mas serial entre fases (correto — a fase seguinte enxerga o que a anterior disse). O contrarian, sozinho na última fase, recebe o debate inteiro como contexto. Tirar a serialidade quebraria a garantia; por isso ela é estrutural, não opcional.
contrarian para o meio (não é mais a última). O que acontece quando o board é carregado?
validateBoardIntegrity (board.ts:145-166) acha o índice da fase contrarian; se ele não for o último, retorna err({ kind: 'contrarian_not_last' }). Não há board parcialmente formado: ou o contrarian é o último, ou não há board. A garantia "fala por último" é verificada no load, antes de qualquer modelo rodar. Se você chutou "um prompt avisa o modelo", caiu na armadilha que a arquitetura existe justamente para eliminar.03 · Um membro nunca lança — ele vira um resultado tipado
Despachar um membro é uma pequena pipeline: bind → run → parse → buildVote. Cada passo pode falhar, mas dispatchMember nunca lança — ele honra o invariante nunca-lança (Lição 14) ramificando no resultado. Uma falha vira um MemberFailureReason tipado, não uma exceção que derruba o debate inteiro:
As três falhas formam a união discriminada MemberFailureReason = bind | adapter | parse (debate.ts:47-50); o despacho devolve um MemberOutcome (53-60) carregando ou um voto ou uma falha. Um membro que cai não vira exceção: vira um voto a menos — e é exatamente aí que o quórum fail-closed da seção 08 entra em cena.
04 · Pontuação — 0–10 sobre cinco eixos fixos
Cada membro emite uma nota por eixo sobre SCORING_AXES — cinco eixos numa escala 0–10 — e o motor combina tudo num único agregado 0–10 com pesos fixos. Os pesos somam exatamente 1.0, então o agregado fica na mesma escala dos eixos. Veja a fórmula peça por peça:
A nota e o voto de um membro vêm do mesmo agregado (buildVote, scoring.ts:122-135): o score é o agregado e a decision é decisionFromScore desse mesmo agregado. Por construção, a nota e o veredito de um membro nunca podem discordar — uma propriedade que o Verifier depois confere (seção 10).
A tabela de eixos e pesos (comparativo)
| Eixo | Peso | O que mede | Direção |
|---|---|---|---|
| feasibility | 0.25 | É construível com o que temos? | 10 = alto / melhor |
| revenue | 0.25 | Potencial de receita | 10 = alto / melhor |
| cx | 0.20 | Experiência do cliente | 10 = alto / melhor |
| ttm | 0.15 | Time-to-market (rapidez) | 10 = alto / melhor |
| risk | 0.15 | Risco do empreendimento | 10 = BAIXO risco (soma igual!) |
05 · O detalhe não-óbvio: o risco não é invertido
Aqui mora o bug clássico que a arquitetura recusa cometer. Quatro eixos são "mais é melhor" — fácil. Mas risco também é codificado assim: 10 = baixo risco, 0 = alto risco. Ele soma na mesma direção de todos os outros. Não existe em lugar nenhum um passo 1 - risk — e é justamente esse passo que inverteria todo veredito:
// packages/council/src/scoring.ts:17-27 — KNOWN-BUG GUARDS (do not regress) // No GO/NO_GO inversion: a HIGH score yields GO, a LOW score yields NO_GO. The // `risk` axis is scored such that 10 = low risk / 0 = high risk, so it adds to // the aggregate in the same direction as every other axis. There is NO // `1 - risk` normalization anywhere; doing so is what flips the verdict.
risk = 10 significa baixo risco. Por isso não há passo 1 - risk: ele faria o eixo subtrair e inverteria toda decisão. Os eixos são mantidos uniformes e os nomes (SCORING_AXES) são fonte única — o comentário no código existe para que ninguém "conserte" o que não está quebrado.06 · Faça a conta na mão (passo a passo → agora você)
Você viu a fórmula. Agora aplique-a devagar, com um caso concreto — depois um exercício é seu. Recuperar o procedimento (não só ver o número) é o que fixa.
GO_THRESHOLD (7) → GO. (Se desse, digamos, 6.2, seria PIVOT; abaixo de 5, NO_GO.)07 · Calculadora de veredito — sinta os limiares
Arraste os cinco eixos. A barra cresce na escala 0–10 e cruza as linhas de PIVOT (5) e GO (7); o veredito recalcula ao vivo com os pesos reais. Experimente afundar só o risk e veja que ele baixa a nota como qualquer outro eixo — não há inversão:
weightedMeanScore) e aplica os mesmos limiares — board e membros falam uma escala só. Mas antes de olhar qualquer nota, vem o quórum (próxima seção).08 · Quórum fail-closed — checado ANTES de qualquer nota
Esta é a propriedade de segurança que carrega o sistema. aggregateConsensus retorna NO_GO imediatamente quando há menos de MIN_VALID_AGENTS = 3 votos válidos — antes de consultar qualquer score. Um board esparso ou degradado nunca deve dar sinal verde:
A ordem é a garantia: o quórum é a primeira coisa que a função olha. Um board que perdeu dois membros por falha de adapter (seção 03) tem só um voto válido — e não pode passar nem com a nota mais otimista do mundo. Junto disso, o board também mede o spread dos votos (CONSENSUS_SPREAD_THRESHOLD = 3): se as notas dos membros divergem muito, o board é marcado como "dividido" (consensus: false) mesmo que a média cruze um limiar — o dissenso não some na média.
O medidor de quórum
Arraste o número de votos válidos. Abaixo de 3, o veredito é NO_GO fail-closed independentemente de qualquer nota:
Consenso ≠ média alta: o spread também conta
Com quórum atingido, o board ainda mede o spread = (maior nota − menor nota) dos membros. Se ele passa de CONSENSUS_SPREAD_THRESHOLD = 3, o board é marcado consensus: false (dividido) — mesmo que a média cruze um limiar. Dois boards com a mesma média de 7 podem contar histórias opostas:
É a mesma filosofia da ADR-0003: o dissenso é uma propriedade do sistema, não ruído a ser apagado pela média. O Verifier depois prova a claim mole consensus-spread (seção 10) exatamente sobre esse número.
09 · O Verifier — read-only por arquitetura
O Verifier maker-checker é a encarnação do invariante ④ (Lição 16). Ele aceita apenas vistas readonly — o DebateResult do maker e o ContextPack — e não expõe adapter, registry, nem nenhuma superfície de mutação. Não pode re-rodar um modelo, não pode editar um voto, não pode mudar uma decisão. Só pode inspecionar evidência e emitir um veredito. É essa separação que o torna um checker, não um segundo maker:
VerifierEvidence = { consensus, votes, pack } (tudo readonly); cada claim tem um ClaimOracle = (evidence) => { proven, evidence } — uma função PURA, sem I/O e sem modelo, que lê só a estrutura. verifyDecision (verifier.ts:216-251) decompõe a decisão em claims, roda cada oráculo, e dobra os resultados num VerificationReport com veredito verified / needs-review / rejected.10 · As cinco claims atômicas — duras e moles
O Verifier decompõe uma decisão em cinco claims atômicas, cada uma provada por um oráculo. Três são duras (falhou → rejected, a decisão é insustentável na cara); duas são moles (sinais consultivos — se não provam, no máximo levam a needs-review):
buildVote já deriva nota e voto do mesmo agregado, isso normalmente passa de graça — mas a claim existe para pegar um voto adulterado ou malformado que tentasse dizer "nota 3, mas voto GO". O oráculo é o guardião de uma invariante que o construtor já tenta garantir.11 · escalate-after-N — não há retry infinito
E quando a verificação não converge — quando uma decisão é re-litigada vez após vez? O Verifier não tenta para sempre. Acima de MAX_LOOPS = 3 (loopCount > 3), ele para de re-decidir e estaciona o trabalho para T4 (revisão humana), via escalateTier(Tier.T3):
É o mesmo padrão escalate-after-N que o board declara em workflowRulesSchema.escalateAfter (default 3, board.ts:101-103): uma decisão contestada sobe para humano em vez de queimar ciclos. A T4 é o estacionamento — de onde alembic propose/approve/reject retoma o trabalho parado.
12 · O painel N-lentes — o portão de emissão T3+
Para T3 e acima, a emissão tem portão por verifyPanel / isPanelEmissionApproved. Por que um painel, e não só o Verifier? Porque um verificador determinístico rodado N vezes dá o mesmo veredito N vezes — o fan-out só compra sinal se cada lente olhar por uma perspectiva diferente. São três:
| Lente | Pergunta | Checa |
|---|---|---|
| COHERENCE | A decisão é internamente sólida? | = verifyDecision reusado — quórum, verdict↔score, auto-consistência, spread |
| FAITHFULNESS | Está ancorada na evidência que recebeu? | evidência presente (dura); confiança média ≥ 0.5; força de pico ≥ 3 |
| DOMAIN | Respeita os invariantes de negócio? | um GO precisa de sinal validation; evidência não é monocultura (≥ 2 tipos) |
A agregação é a peça inteligente — quórum E um veto, ambos de propósito. O painel aprova por quórum (padrão DEFAULT_PANEL_QUORUM = 2 de 3) mas uma rejeição dura em qualquer lente única veta o painel inteiro (preservação-de-dissenso), e o escalate-after-N da coherence propaga:
13 · Maker vs checker — por que são coisas diferentes
A lição inteira gira em torno de uma separação: quem decide (o maker — o debate) não é quem prova (o checker — o Verifier/painel). Veja o contraste lado a lado:
• Roda modelos (tem adapter).
• Produz votos, notas, prosa.
• Pode discordar, errar, alucinar.
• Não-determinístico (depende do modelo).
• Saída:
DebateResult + ConsensusResult.• Não roda modelo (sem adapter).
• Prova claims com oráculos puros.
• Só lê estrutura, nunca a prosa.
• Determinístico (mesma evidência → mesmo veredito).
• Saída:
VerificationReport / aprovação de emissão.14 · Como isso se encaixa
Você dissecou o kernel por dentro. Agora dê um passo atrás: onde ele liga no resto da máquina? O Council & Verifier não decide no vácuo — ele é o portão de emissão que o funil (Lição 15) atravessa quando uma decisão sobe para T3. À esquerda entra o contexto que alimenta o debate (o ContextPack em camadas); no meio roda esta peça — debate → pontuação → Verifier → painel N-lentes — opcionalmente espelhada pelo Validador coordenado; à direita só sai o que o painel emitiu (o GO-verificado), que segue para o swarm executar.
Onde você está na metodologia: as Lições 14–17 montaram a cintura estreita, o funil, as invariantes e a pipeline de gates que executam uma run; esta Lição 18 é o cérebro do Council Gate dentro dessa pipeline — o componente que, com prova determinística, decide se uma decisão de T3+ pode emitir. A Lição 19 (o swarm) é quem pega o que emitiu e roda. Para ver as 30 peças no mesmo mapa e como cada uma liga na seguinte, abra o mapa interativo da metodologia.
- Lição 15 · O funil — a esteira T0→T3 que desemboca aqui: quando uma ideia chega ao topo do funil (T3), é este painel N-lentes que decide se ela emite. O "GO-verificado" da Lição 15 é literalmente a saída desta peça.
- Lição 17 · A pipeline de gates — o Council Gate desta lição é um dos cinco portões dessa pipeline (Scope → Council → Proof → Validator → Publish); aqui você vê o Council Gate por dentro, lá você vê a ordem dos cinco e como o run os encadeia.
- Lição 16 · As quatro invariantes — o Verifier read-only é a encarnação do invariante ④ (maker ≠ checker) e a preservação-de-dissenso vem da ADR-0003 — as invariantes que esta peça impõe estruturalmente, não por prompt.
- Lição 27 · Tiers, custo & orçamento — o tier (T3+) é o que liga o painel N-lentes: um trabalho de tier baixo nem aciona o gate. É a Lição 27 que define como uma decisão ganha seu tier e por que só T3+ paga o custo do fan-out de lentes.
- Lição 19 · O swarm — o destino do que emitiu: uma decisão verificada vira trabalho que o swarm distribui e executa. Esta peça é o "sim, pode" antes do swarm gastar um único token de execução.
15 · Na prática
Chega de diagrama — eis a peça como comandos reais. O Council Gate roda dentro de alembic run sem flag; o que você liga à mão é o Validador coordenado, o espelho multi-lente desta lição. Ele é opt-in (--coordinated), aditivo e nunca altera a decisão do council: roda a verificação risk-tier multi-lente sobre cada unit e grava o veredito em units/<id>/coordinated-verdict.json — offline e $0 por padrão.
# Roda a run e LIGA o passe do Validador coordenado (opt-in, aditivo). # --yes pula a confirmação; offline = adapter determinístico, $0. alembic run --goal GOAL.md --plan alembic.plan.ts --coordinated --yes # … a run executa as units e passa os gates como sempre … # para CADA unit, o passe coordenado imprime UMA linha de resumo: # coordinated validator [u1]: grade VERIFIED · verdict pass · tier full · 6 lens(es) · 0 finding(s) # e grava o veredito completo (findings, lentes, grade) em: # .alembic/runs/<run-id>/units/u1/coordinated-verdict.json # (evento de log por falha não-fatal: run:coordinated-pass-error)
Fonte da forma exata: apps/cli/src/index.ts:118 (a flag --coordinated) e apps/cli/src/commands.ts:921-953 (runCoordinatedPassForSpec) — o passe seleciona as units com proof ou de tier T4, roda runCoordinatedValidatorGate por unit, imprime coordinated validator [<id>]: <note> e persiste o JSON; ele nunca falha a run (o Council Gate continua sendo o único portão de emissão). A linha <note> é montada em packages/coda/src/coordinated-validator.ts:521-529.
E para ver o contexto em camadas que o council recebe antes de debater — o mesmo ContextPack L0–L7 que entra no kernel — monte um pack à mão com alembic context-pack:
# Monta o ContextPack de 8 camadas (L0–L7) sobre arquivos reais. alembic context-pack packages/council/src/verifier.ts --brief "como o Verifier prova uma decisão" # saída (cabeçalho + as camadas + o orçamento de tokens): # context-pack: pack-… (role default, model …) # manifest: man-… (1 file(s)) # layers: L0 brief 35 chars | L1 repoMap … | L2 files 1 | L3 snippets … | # L4 summaries … | L5 debate … | L6 artifacts …/constraints … | L7 manifest 1 # budget: … estimated / … max (within budget)
Fonte: apps/cli/src/commands.ts:3356-3423 (runContextPack) — imprime context-pack: <id> (role <role>, model <model>), uma linha de manifest:, a linha de layers: L0–L7 e a de budget: com within budget ou OVER BUDGET; --json emite o pack inteiro. Atenção à camada: este é o pack que alimenta o debate (o upstream do diagrama acima) — não o veredito que sai dele.
- Clone e entre no repo, depois garanta o build verde:
git clone <repo> alembic && cd alembic·pnpm -r typecheck && pnpm -r build && pnpm -w test - Gere um escopo mínimo (cria
GOAL.md+alembic.plan.ts+ contrato):alembic plan "um passo trivial que só roda um teste" - Rode com o passe coordenado:
alembic run --goal GOAL.md --plan alembic.plan.ts --coordinated --yes - O que procurar na saída: uma linha
coordinated validator [<unit-id>]: grade … · verdict … · tier … · N lens(es) · M finding(s)por unit elegível (com proof ou T4). - O que procurar no dir da run: abra
.alembic/runs/<run-id>/units/<unit-id>/coordinated-verdict.jsone leia o veredito completo (grade,verdict,riskTier,lensesRan,findings). - Prove o "aditivo": rode a MESMA run sem
--coordinated. Nenhuma linhacoordinated validatoraparece e nenhumcoordinated-verdict.jsoné escrito — mas o resultado da run (o que o Council Gate decidiu) é idêntico. O passe só observa; nunca muda o veredito.
--coordinated ao alembic run. (É o espelho aditivo desta lição — só grava.)alembic context-pack <arquivos…> --brief "<objetivo>". É o upstream do diagrama (o que ALIMENTA).units/<id>/coordinated-verdict.json no dir da run (o passe sempre persiste o JSON além de imprimir a linha-resumo).alembic run … --coordinated --yes, uma das lentes do passe falhou, mas a run terminou verde e o council emitiu normalmente. Bug ou esperado? Decida antes de revelar.
degraded (com um WARNING no note) e segue — o erro vira o evento de log run:coordinated-pass-error, nunca um throw. Por que assim: o único portão de emissão é o Council Gate (a peça desta lição); este espelho observa, ele não decide. Deixá-lo falhar a run daria a um observador o poder de um portão — exatamente o que o desenho recusa.Fixe os conceitos (flashcards)
Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.
contrarian_not_last é erro duro de board-load. Não é um prompt.1 - risk — ele inverteria todo veredito.MIN_VALID_AGENTS = 3, antes de consultar qualquer nota.Revisão cumulativa — recupere de memória
Antes de clicar: responda de cabeça. As quatro opções têm tamanho parecido de propósito — sem pista pela forma.
failClosed=true. a e c consultam a nota — mas a nota nem é olhada abaixo do quórum. d erra o invariante nunca-lança: a falha de bind virou um MemberFailureReason tipado (um voto a menos), não um throw.VerifierEvidence e prova claims com predicados puros — mesma evidência entra, mesmo veredito sai. b contradiz o design: ele NÃO roda modelo nenhum. c confunde-o com um membro do council — ele não vota nem tem peso. d inventa uma pré-condição de unanimidade que não existe; ele confere qualquer decisão, inclusive uma que falhou o quórum.