Um run não apenas "executa e termina". Ele atravessa uma sequência de gates — Scope, Council, Proof, Validator, Publish — e cada um pode pará-lo. Alguns são de pré-voo, um fecha o run, um é opcional, e o último exige um humano. Juntos, eles são a disciplina que transforma "o agente produziu algo" em "o agente produziu algo verificado". É exatamente aqui também que encaixa o ReviewGate do loop de aprendizado (lições 4 e 8). Fonte: @alembic/coda + @alembic/mission + @alembic/forge.
Esta lição é citada de arquivo real: packages/forge/src/scope.ts (Scope), packages/mission/src/council-gate.ts + packages/harness/src/core.ts (Council), packages/coda/src/proof.ts (Proof), packages/coda/src/validator.ts (Validator) e packages/coda/src/publish.ts (Publish). Nada aqui é inventado — toda afirmação tem um arquivo atrás.
ReviewGate do aprendizado a esta mesma família de gates.Result em vez de lançar exceção — e que "falha" é um valor, não um susto.Um gate é uma cancela. Antes de o run seguir adiante, ele precisa passar — e se não passar, a cancela fica fechada. O segredo do Alembic é que fechado é o estado padrão: nenhum gate "deixa passar na dúvida".
Pense em cinco catracas numa esteira de fábrica. A peça (o run) entra pela esquerda e precisa cruzar cinco catracas para sair pela direita. A primeira só monta a estação de trabalho. A segunda pode vetar tudo antes de começar. A terceira é a mais dura — confere, com um comando real, se o trabalho de fato funcionou. A quarta chama um juiz independente (quem construiu não pode ser quem aprova). A quinta é a única que precisa de uma pessoa assinando, porque o passo dela é irreversível: publicar para fora.
Pense como… as comportas de um canal (as eclusas que elevam um barco de um nível para outro): a água só sobe quando a comporta anterior está travada e a verificação foi feita. Ninguém abre a próxima comporta "torcendo para dar certo". A analogia quebra num ponto: numa eclusa a falha é rara; aqui o sistema assume que o modelo vai errar — e desenha as comportas em volta dessa certeza.
Os cinco gates vivem em três pacotes: o Scope Gate em @alembic/forge (scope.ts); o Council Gate em @alembic/mission (council-gate.ts), com o HarnessCore.fanout em @alembic/harness aplicando a mesma ideia internamente; e os três últimos — Proof, Validator, Publish — em @alembic/coda (proof.ts, validator.ts, publish.ts). Cada um devolve um Result<T, Error>: o sucesso é ok(...), e a recusa é um err(...) que o chamador usa para falhar o run fechado.
Não é coincidência que sejam Result e não exceções: o pipeline inteiro é controle de fluxo uniforme. Quem orquestra ramifica em result.ok — nunca precisa de try/catch para descobrir que um gate fechou.
Os cinco gates em volta de um run. A peça atravessa da esquerda para a direita; qualquer cancela fechada para o run no lugar.
Os cinco aparecem sempre na mesma sequência. Dois são de pré-voo (rodam antes do trabalho), um é a espinha determinística, um é a revisão independente e o último é a porta humana para o passo de fora.
De cara: dos cinco gates, quantos você acha que são opcionais (podem ser ligados ou desligados)? E qual é o único que exige uma pessoa?
--council / config do plano). Os outros quatro fazem parte do caminho normal. E o único que exige um humano é o Publish Gate — porque seu passo (publicar para fora) é irreversível. Os demais decidem com lógica; o Publish decide com uma assinatura humana.Monta o diretório do run e copia GOAL.md, o plano e o contrato para dentro dele.
forge/src/scope.ts fecha quando: um resume tenta trocar o escopo → err("resume mismatch…")A rampa de entrada. Antes de qualquer trabalho, o run precisa de um lugar para morar.
loadScope(input) (em forge/src/scope.ts) copia o GOAL.md, o módulo de plano e o contrato de validação para um diretório do run e semeia o esqueleto dele: as subpastas council/, units/, workflows/, park/, course/ e reports/, mais os arquivos meta.json, o LOOP-LOG.md e o review.md.
A regra de segurança chave: um resume não pode trocar de escopo. Ao retomar um run existente, o GOAL.md, o caminho do plano e o contrato são comparados contra o meta.json salvo — e qualquer diferença devolve um err("resume mismatch: …"). Você não consegue, sem querer, re-escopar um run que está apenas continuando.
Pense como… abrir uma pasta de obra numa construtora: tem a planta aprovada (GOAL), o cronograma (plano) e o contrato — e cada gaveta já rotulada (as subpastas). Se alguém tentar reabrir a pasta de uma obra antiga e enfiar uma planta diferente, o arquivo trava: aquela pasta é daquela obra.
O cabeçalho de loadScope documenta literalmente: "writes index.json, meta.json, tasks.json, run-state.json; seeds LOOP-LOG.md and review.md; creates subdirectories: council/, units/, workflows/, park/, course/, reports/". Na ramificação de resume, ele lê meta.json e devolve err("cannot read meta.json for resumed run") se não conseguir, e três err distintos de resume mismatch — um para o GOAL.md, um para o caminho do plano e um para o contrato de validação.
units/<id>/proof-results.jsonl, t4-parked.jsonl…). Sem ele, não há onde os outros gates deixarem rastro.Result, não exceções: controle de fluxo uniforme — quem orquestra só olha result.ok.A pergunta que vem antes do trabalho: "isto deve sequer começar?"
runCouncilGate (em mission/src/council-gate.ts) roda um conselho antes de o trabalho começar e devolve uma CouncilDecision agregada. NO_GO significa que a missão não deve prosseguir. É opcional (ligado por --council / config do plano) — um veto de pré-voo, não uma revisão depois do fato.
O núcleo do harness reforça a mesma ideia por dentro: no fanout do HarnessCore (harness/src/core.ts), o conselho roda primeiro e um NO_GO faz curto-circuito antes de qualquer worker ser despachado. O comentário no código é direto: "um board que não dá sinal verde não deve gerar trabalho autônomo".
Pense como… a reunião de "lançar ou não lançar" antes de o foguete acender os motores. Se a sala diz NO_GO, ninguém abasteceu nada à toa — a decisão veio antes do gasto. Comparar com a revisão pós-voo (que é o Validator Gate): uma decide se decola; a outra examina os destroços ou o sucesso depois.
O cabeçalho de runCouncilGate documenta: "returns a GO / NO_GO verdict. NO_GO is fail-closed: the mission aborts before any worker is dispatched." Em core.ts, dentro de fanout: if (job.council) { ... if (decided.value === 'NO_GO') ... emite 'phase halted: council returned NO_GO' e devolve sem entrar na fase de partição/drain dos workers.
// harness/src/core.ts — fanout: o conselho roda primeiro; NO_GO faz curto-circuito async fanout(job) { if (job.council) { const decided = await this.deliberate(job.council); if (decided.value === 'NO_GO') return halt('phase halted: council returned NO_GO'); // nenhum worker despachado } // só aqui começa a partição + drain dos workers… }
Este é o gate que faz "funciona" significar alguma coisa. Sem ele, "deu certo" é só uma opinião.
Cada string em unit.proof[] de uma missão vira uma tarefa de comando bash -c que depende da tarefa da unidade. O Proof Gate lê o estado dessas tarefas e falha o run fechado se qualquer uma saiu com código diferente de zero. Não há "aviso e segue": uma prova que falha derruba o run.
Um detalhe sutil e importante: ele lê o estado das tarefas a partir do event journal (o events.jsonl, append-only), e não do checkpoint. Por quê? Porque um store compartilhado pode ter seu checkpoint.json sobrescrito por uma chamada posterior de runSwarm — mas o journal, que só cresce, guarda todo evento task-state para sempre. Ler o journal torna o gate robusto através de múltiplas chamadas. Os resultados são gravados em units/<unitId>/proof-results.jsonl.
Pense como… a diferença entre alguém dizer que pagou a conta e o extrato bancário mostrando o débito. O checkpoint é como o último print que pode ter sido tirado por cima de outro; o journal é o extrato completo, linha a linha, que ninguém apaga. Em caso de dúvida, você confia no extrato.
runProofGate chama store.readEvents(), monta um Map de task-state por id, filtra as tarefas com metadata.kind === 'proof' e marca cada uma como complete só se state?.status === 'done'. Ao final, const failed = results.filter(r => r.outcome === 'failed'); se houver alguma, devolve err(new Error("Proof Gate failed: " + summary)), com um resumo por unidade (unit=… index=… command="…"). Antes disso, escreve o proof-results.jsonl de cada unidade, ordenado por proofIndex.
// packages/coda/src/proof.ts (condensado) — lê o journal, falha fechado const events = await store.readEvents(); // o journal append-only, não o checkpoint for (const e of events.value) if (e.kind === 'task-state') stateById.set(e.payload.id, e.payload); // toda tarefa com metadata.kind === 'proof' PRECISA ter saído 0 (status === 'done') const failed = results.filter(r => r.outcome === 'failed'); if (failed.length > 0) return err(new Error(`Proof Gate failed: ${summary}`)); // chamador falha o run fechado
Por que o gate lê o journal append-only e não o checkpoint: o journal é a fonte da verdade que sobrevive a múltiplas chamadas runSwarm.
u1 declara proof: ['pnpm -w test']. O compilador transforma isso numa tarefa bash -c "pnpm -w test" com metadata.kind === 'proof', dependente de u1.task-state com status diferente de 'done' no events.jsonl.done, marca-a como failed e escreve o proof-results.jsonl da unidade mesmo assim (o rastro fica).err("Proof Gate failed: unit=u1 index=0 command=\"pnpm -w test\""). O chamador falha o run fechado. Nada de "passou com ressalvas".proof: ['pnpm -r typecheck', 'pnpm -w test'] e o typecheck saiu 0 mas o test saiu 1. Quantas linhas o proof-results.jsonl da unidade tem, e o run passa? (Resposta: duas linhas — uma complete, uma failed — e o run NÃO passa, porque basta uma falha.)Provas dizem que os comandos passaram. O Validator Gate adiciona um juízo — e quem o emite nunca é quem construiu.
runValidatorGate (em coda/src/validator.ts) roda um conselho independente de validadores mais um painel verifier sobre a evidência de cada milestone. Só approved === true (o painel está verificado e não foi parqueado) permite a emissão; um NO_GO ou um painel rejeitado parqueia para revisão humana. Há um seam opcional de fusion que cuida das unidades T4 / de alto risco.
A separação é o ponto inteiro: o agente que produziu o trabalho nunca é o que assina embaixo. Provas são determinísticas (o comando passa ou não); o Validator adiciona a camada qualitativa — alguém de fora olhando a evidência.
Pense como… revisão por pares num jornal sério: o repórter (quem construiu) não é quem decide publicar; um editor independente lê a matéria e ou aprova, ou manda de volta. Em matéria sensível (T4), entra um segundo editor sênior (o fusion). A analogia quebra num ponto: aqui, "mandar de volta" não é opcional — é o estado padrão na dúvida.
O cabeçalho documenta: "the verifier panel checks the council decision. The result is fail-closed: only `approved === true` means the work may emit." A linha-chave: approved: panel.verdict === 'verified' && panel.parkedTier === undefined — verificado E sem tier parqueado. O verdict do painel mapeia para 'GO' (verified), 'NO_GO' (rejected) ou 'PIVOT'. Quando a unidade é T4 e há um fusion injetado, runValidatorGate delega a ele logo no início (if (options.t4 && options.fusion) return options.fusion(options)).
approved === true emite; o resto parqueia. T4 desvia pelo seam opcional fusion.O último passo é o irreversível: mandar algo para fora. Por isso, é o único gate cuja chave é uma assinatura humana.
Qualquer publicação para fora precisa passar por este gate (coda/src/publish.ts). Fechado (sem aprovação) → o artefato é parqueado em t4-parked.jsonl e espera a aprovação humana. Aberto (aprovado) → ele é publicado pelos gist/pages fornecidos. Um gist é obrigatório quando aprovado; o Cloudflare Pages é opcional.
Isso espelha o princípio do "humano-no-ponto-de-ship" (ADR-0005): o passo de fora, irreversível, é deliberadamente o que exige uma pessoa.
Pense como… o botão de lançar um míssil que precisa de duas chaves giradas ao mesmo tempo: o sistema pode preparar tudo, mas o ato irreversível só acontece com a mão humana na chave. Aqui a "chave" é o approved; sem ela, o artefato fica numa gaveta etiquetada (t4-parked.jsonl), pronto, mas não enviado.
Em runPublishGate: se !options.approved, ele dá appendFile de um registro { runId, artifactPath, reason: 'publish-gate', at } no t4-parked.jsonl e devolve ok({ decision: 'parked' }). Se aprovado e sem publisher de gist, devolve err('approved publish gate requires a gist publisher'). Com gist (e, opcionalmente, pages), devolve ok({ decision: 'published', gistUrl, pagesUrl? }).
// packages/coda/src/publish.ts (condensado) — park quando fechado if (!options.approved) { await appendFile(parkedPath, JSON.stringify({ runId, artifactPath, reason: 'publish-gate', at: Date.now() }) + '\n'); return ok({ decision: 'parked' }); // espera a aprovação humana } if (!gistPublisher) return err(new Error('approved publish gate requires a gist publisher'));
approved, parqueia; com ele, publica via gist (e Pages, se houver).O passo de aprendizado do pacote de fusão (lições 4 e 8) é guardado por um ReviewGate com scoreThresholdGate(0.7) — um membro desta mesma família de gates. Os cinco gates acima governam o que um run tem permissão de mandar para fora; o ReviewGate governa o que um run terminado tem permissão de sedimentar em memória e skills. Mesma filosofia, aplicada à autoevolução do motor: só aprenda de vitórias validadas. É por isso que a matriz de fusão chama o loop de aprendizado de um CLONE que "compõe com o validator gate" em vez de substituí-lo.
t4-parked.jsonl para um humano — a ação de fora, irreversível, é de propósito a que exige uma pessoa (ADR-0005).err e parar, ou parquear. Se você esperava um "passou com warnings", esqueça: aqui não existe.| Gate | Quando | O que decide | Falha = ? |
|---|---|---|---|
| ① Scope | entrada | monta o run-dir; trava o resume | err("resume mismatch") |
| ② Council | pré-voo (opcional) | se a missão começa | NO_GO → aborta antes dos workers |
| ③ Proof | após o trabalho | se os comandos saíram 0 | err → run falha fechado |
| ④ Validator | após o trabalho | se a evidência convence | parqueia p/ humano |
| ⑤ Publish | no ship | se sai para fora | park em t4-parked.jsonl |
Alterne entre a explicação leiga e a precisa. Use a aba "Técnico" quando quiser os nomes reais; a "Simples" quando quiser a intuição.
loadScope (forge) materializa o run-dir e trava o resume contra meta.json; runCouncilGate (mission) + HarnessCore.fanout fazem o NO_GO de pré-voo curto-circuitar antes de despachar workers; runProofGate (coda) lê store.readEvents() e devolve err em qualquer prova != done; runValidatorGate (coda) emite só com panel.verdict === 'verified' && parkedTier === undefined, com seam fusion para T4; runPublishGate (coda) parqueia em t4-parked.jsonl quando !approved, senão publica via gist (obrigatório) e pages (opcional).Vire cada cartão (clique, ou Enter/Espaço) e tente responder antes de ver o verso. É prática de recuperação — vale mais que reler.
checkpoint.json pode ser sobrescrito por outra chamada runSwarm; o events.jsonl é append-only e guarda todo task-state. O journal é a fonte da verdade.approved === true: o painel está verified e sem tier parqueado. Quem constrói nunca é quem aprova; T4 desvia para o seam fusion.t4-parked.jsonl (com reason: 'publish-gate') e espera a aprovação humana. O passo de fora é irreversível — por isso exige uma pessoa (ADR-0005).meta.json.unit.proof[] saindo 0.Responda os três. O placar embaixo acompanha seus acertos — é revisão espaçada, não prova.
proof[] de uma unidade sai com código diferente de zero. O que o Proof Gate faz?metadata.kind === 'proof' precisa estar done (saída 0). Qualquer falha entra num resumo e o gate devolve err, falhando o run fechado. Provas são a espinha determinística — não degradam para aviso.checkpoint.json sobrescrito por uma chamada posterior, mas o events.jsonl append-only retém todo evento task-state. Ler o journal torna o gate robusto através de runs com múltiplas chamadas.