Passo 5 · Alembic Completo · Alembic Completo · Camada L4 · Harness
Alembic Completo · Camada L4 · Harness

Camada L4 · Harness — o maestro

É a camada do topo. Um único núcleo HarnessCore conduz council + swarm, emite eventos e relata — e tudo o mais (CLI, HTTP+SSE, MCP) é apenas um adaptador fino sobre ele. Aqui a engenharia interna vira um produto utilizável.

Cada bloco tem uma versão Simples e uma Técnica. Abra a técnica quando quiser o código real.
1

A grande ideia


Ao terminar esta lição você vai saber
  • Por que o HarnessCore é transport-neutral e o que isso compra.
  • Os quatro verbos do core — start, poll, fanout, report — e o que cada um faz.
  • O que é o RunSnapshot e como os eventos viajam em swimlanes.
  • O funnel T0→T3 e o bridge distillAndMarket que fecha o ciclo destilar→marketing.
  • Por que CLI, HTTP+SSE e MCP são adaptadores finos sobre o mesmo núcleo.
O que assumimos de você
  • Você já viu as camadas L0→L3 (contracts, adapters, council, swarm). Aqui elas são conduzidas, não reapresentadas.
  • Você sabe que no Alembic erros recuperáveis viram Result (ok/err) em vez de exceção.
  • Nenhum conhecimento de servidores HTTP ou MCP é pré-requisito — explicamos do zero.

Imagine uma orquestra. Os músicos são o council (a deliberação) e o swarm (a execução). O maestro não toca nenhum instrumento: ele dá as entradas, lê o andamento e fecha a peça. No Alembic, esse maestro é o HarnessCore — e a sala de concerto (CLI, navegador, outro programa via MCP) é só onde a música é ouvida, nunca onde ela é composta.

Diagrama do HarnessCore como maestro central com os quatro verbos start, poll, fanout e report, recebendo um PhaseJob, conduzindo council e swarm, e emitindo HarnessEvents pelo EventBus em três swimlanes.

O HarnessCore recebe um PhaseJob, conduz council + swarm e emite um fluxo de HarnessEvent por três raias (swimlanes). Ele não conhece nenhum transport.

Por que L4 é o topo. Sem ela, council e swarm seriam apenas bibliotecas internas. Com ela, viram um motor de destilação completo, exposto por múltiplos transports. É a camada que transforma engenharia em produto: dos contratos (L0) até a superfície de uso (L4), o sistema inteiro passa a fazer sentido.
Guarde isto Harness ≠ CLI. A confusão número um é achar que "o harness é a linha de comando". Não: o núcleo é neutro de transporte, e o CLI é só um dos clientes possíveis — existem também HTTP+SSE e MCP.
maestro HarnessCore council (delibera) swarm (executa) a plateia ouve, não rege: CLI HTTP + SSE MCP
O maestro rege os músicos (council + swarm). A plateia (CLI, HTTP, MCP) ouve a mesma peça por superfícies diferentes — mas nenhuma delas rege.
2

HarnessCore — o núcleo neutro


O que "transport-neutral" significa

O arquivo core.ts define uma única classe, HarnessCore, que não sabe como é invocada. Ela não abre socket, não lê argv, não formata saída para um terminal. Tudo o que ela faz é pura orquestração: conduzir o council, conduzir o swarm e emitir um fluxo de eventos. Quem decide como o usuário fala com ela mora fora — nos adaptadores.

CLI HTTP + SSE MCP transports — adaptadores finos, FORA do núcleo HarnessCore core.ts · nunca lança · devolve Result council · runDebate swarm · runSwarm EventBus → stream de HarnessEvent
O núcleo é o coração do motor. Todo cliente é um adaptador fino sobre este objeto; nada aqui importa um transport.
O comentário do próprio core (core.ts)

"The TRANSPORT-NEUTRAL orchestration core. This is the single conductor of the engine, and it knows nothing about how it is invoked. Every transport (CLI, HTTP+SSE, MCP) is a thin adapter over THIS object; nothing here imports a transport, binds a port, or formats output. Fallible steps return Result; the core never throws."

Construção e injeção de dependências

O HarnessCore recebe um HarnessCoreOptions com seams injetáveis: um relógio now() (para timestamps determinísticos em testes), um EventBus opcional (compartilhável com um hub SSE), e os runners runDebate e runSwarm que por padrão são os reais, mas que um teste troca por stubs. É assim que o mesmo núcleo é testado sem rede e sem custo.

// core.ts
export class HarnessCore {
  readonly runId: string;
  readonly bus: EventBus;
  // seams injetáveis: now(), bus, runDebate, runSwarm, adapters…
  constructor(options: HarnessCoreOptions) { /* … */ }

  poll(): RunSnapshot { return this.snapshotState; }
  start(job: PhaseJob): Result<RunSnapshot, string> { /* … */ }
  async fanout(job: PhaseJob): Promise<Result<RunSnapshot, string>> { /* … */ }
  report(): RunSnapshot { /* … */ }
}

Reusar um core entre fases é permitido: cada start() reseta os campos por-fase mas mantém o runId e o bus estáveis.

3

Os quatro verbos


Toda a orquestração do core cabe em quatro métodos. Clique em cada um para ver o que ele faz e qual raia ele acende.

Antes de revelar — arrisque

Dos quatro verbos, apenas um dispara o swarm (a execução em paralelo). Qual você acha que é?

fanout é o único que parte o swarm por autonomia e o drena. start abre a fase, poll só lê, report só fecha. Separar "abrir", "ler", "executar" e "fechar" é o que deixa um transport mostrar progresso sem nunca tocar na lógica.
start

Abrir a fase

start — abre a fase poll — lê o snapshot fanout — dispara o swarm report — fecha o run
VerboPapelMuta o estado?
startValida a fase, reseta os campos por-fase, emite run-started + phase-started.sim
pollDevolve o RunSnapshot imutável atual — barato, sem efeitos.não
fanoutDelibera (council) e/ou executa (swarm), emitindo o ciclo de vida por tarefa.sim
reportEmite run-finished e devolve o snapshot final. Idempotente.não*

* report() é idempotente: chamá-lo duas vezes emite um segundo frame, mas não altera o resultado.

Flashcard
Qual verbo é o único que NÃO muta o estado e é seguro chamar a todo instante?
clique para virar
Resposta
poll(). Ele só devolve o RunSnapshot imutável. Por isso um painel de status pode chamá-lo num loop sem medo de efeitos colaterais.
Flashcard
O que start() faz com os campos por-fase quando reusamos um core?
clique para virar
Resposta
Reseta-os, mas mantém runId e bus estáveis — então o mesmo core conduz várias fases sem perder a identidade do run.
Flashcard
Por que report() ser idempotente importa?
clique para virar
Resposta
Um transport pode chamá-lo mais de uma vez (ex.: retry de uma resposta) sem corromper o resultado — só emite outro frame de evento.
4

RunSnapshot & eventos


O RunSnapshot é uma foto achatada do run: ele dobra o progresso do council e do swarm numa única visão plana. Assim, uma tela de status nunca precisa "entrar" nos subsistemas para saber em que pé as coisas estão — ela pergunta ao core, recebe a foto, e desenha.

Em paralelo, o core emite um fluxo de eventos. Cada evento carrega uma raia (swimlane): orchestrator, council ou task. Uma raia por tarefa deixa um console desenhar atividade paralela como pistas lado a lado — você vê o council deliberando enquanto três tarefas correm, cada uma na sua linha.

O ciclo de vida de um run é um RunPhaseStatus: idle → deliberating → verified → fanning-out → done (ou failed). O snapshot expõe decision, verification, autonomousCount, parkedCount, o swarm result, um error? e um eventCount (sinal barato de progresso).

// events.ts — as raias e os tipos de evento
swimlaneKind = 'orchestrator' | 'council' | 'task'
harnessEventKind = 'run-started' | 'council-started' |
  'council-decided' | 'council-verified' |
  'swarm-partitioned' | 'swarm-task-started' |
  'swarm-task-finished' | 'swarm-finished' | 
orchestrator council task:t1 run-started phase-started swarm-partitioned run-finished council-started council-decided council-verified task-started task-finished
Três raias, um relógio. O orchestrator abre e fecha; o council delibera no meio; cada tarefa corre na sua própria linha — atividade paralela legível.
idle deliberating verified fanning-out done failed (ramo de erro)
O ciclo de vida de um run: idle → deliberating → verified → fanning-out → done. Qualquer passo falível pode desviar para failed — gravado no snapshot, sem lançar.
Analogia Pense num quadro de aeroporto: cada portão (raia) mostra seu próprio voo na sua linha, mas todos compartilham o mesmo relógio. Você lê o paralelismo de relance, sem abrir cada portão.
5

O funnel T0→T3


Além do core de orquestração, o L4 abriga o funnel (funnel.ts): o orquestrador de destilação em quatro tiers. Para a maioria dos usuários, funnel.ts é o ponto de entrada principal, via o comando distill no CLI. Ele empurra um corpus por quatro estações, cada uma mais cara e mais criteriosa que a anterior.

T0 · determinístico · $0 — score / dedupe / route T1 · modelo LOCAL — extrai BusinessSignal (free) T2 · FRONTIER (budget-gated) — refina os fortes T3 · council + verifier → GO BudgetGuard fail-closed em T2/T3
Cada tier é mais estreito: muitos arquivos entram em T0, pouquíssimos sinais saem verificados em T3. O orçamento é checado antes de toda chamada paga.
As quatro estações

T0 — o runT0Pipeline determinístico e de custo $0: pontua, deduplica e roteia. O que não merece subir vira "resíduo".

T1 — um modelo local (o adapter injetado) extrai BusinessSignals do resíduo. É free-tier, então nunca é bloqueado por orçamento.

T2 — uma shortlist frontier, gated por orçamento, refina os sinais mais fortes do T1.

T3 — um debate de council + verifier maker-checker decide o GO final.

O relatório — FunnelReport

Ao fim, o FunnelReport traz contadores por tier (extraídos, bloqueados por orçamento, debates que chegaram a GO), o custo total em USD metrificado, e — o que mais importa — os verifiedSignals: os sinais que limparam o painel verifier do council.

Fail-closed Toda chamada de tier pago (T2/T3) passa por um BudgetGuard: uma projeção de estouro bloqueia a chamada em vez de gastar. Falha fechando, nunca abrindo.
FunnelReport — contadores que afunilam T0 arquivos T1 sinais T2 shortlist T3 GO verifiedSignals[] custo total (USD) metrificado + bloqueados por orçamento, por tier
O FunnelReport não devolve só os verifiedSignals: traz contadores por tier (incluindo quantas chamadas o orçamento bloqueou) e o custo total medido.
Lembrete: "verificado" aqui é o mesmo conceito do council (L2) — só passa quem sobreviveu ao maker-checker. O funnel apenas conduz essa peça dentro de um pipeline de quatro andares.
6

O bridge distill→market


O bridge.ts é a peça que fecha o ciclo: ele liga a saída verificada-GO do funnel à fábrica de marketing. A função distillAndMarket roda o funnel sobre um corpus, pega cada verifiedSignal, transforma em um AssetsManifest versionado via runMarketingBatch, e persiste os manifests num store append-only que deduplica.

Esteira do bridge distillAndMarket com quatro estações em ordem (runFunnel, verifiedSignals, runMarketingBatch, persistManifests), o cadeado do BudgetGuard sobre os tiers pagos, e a seta de dependência acíclica de mão única.

Uma esteira em quatro estações: runFunnelverifiedSignalsrunMarketingBatchpersistManifests. A dependência só anda numa direção.

A regra de ouro do bridge

A composição é uni-direcional: harness → marketing-factory → contracts. O grafo de dependências fica acíclico — a marketing-factory nunca volta a chamar o harness. É isso que mantém o sistema desmontável e testável peça a peça.

harness (L4) marketing-factory contracts (L0) ✗ nenhuma seta volta — grafo acíclico
A seta só anda para a direita. A marketing-factory nunca chama de volta o harness: é isso que mantém o grafo acíclico e cada peça testável sozinha.
Degradação graciosa. Os dois estágios degradam sozinhos: o funnel degrada por-tier (bloqueio de orçamento, falha de parse) e o batch de marketing registra falhas por-sinal. Um sinal ruim nunca aborta o run inteiro. Um conjunto verificado vazio simplesmente flui para um batch vazio.

A composição, em código

// bridge.ts
export const distillAndMarket = async (corpusDir, options) => {
  const funnel    = await runFunnel(corpusDir, options.funnel);
  const marketing = await runMarketingBatch(
    funnel.verifiedSignals, options.marketing,
  );
  const manifestsWritten = await persistManifests(
    marketing.manifests, options.funnel,
  );
  return { funnel, marketing, manifestsWritten };
};

Sob dryRun, persistManifests devolve 0 e não escreve nada. Uma re-execução sobre entradas idênticas converge para 0 escritos: o store deduplica por identidade do manifest. (O funnel ainda propaga erros de I/O duros — ex.: um diretório de corpus ilegível — lançando, como faz por conta própria.)

7

Os três transports


Agora a parte que fecha a camada: os adaptadores. Os três falam com o mesmo HarnessCore. Nenhum deles contém orquestração — eles só traduzem entre o mundo externo (terminal, HTTP, JSON-RPC) e os verbos do core.

Diagrama de três colunas (CLI, HTTP+SSE, MCP) convergindo por setas para um único HarnessCore central, mostrando que todo transport é um adaptador fino sobre o mesmo núcleo de orquestração.

Três superfícies, um só núcleo. Trocar de transport nunca duplica a lógica — ela mora só no core.

CLI · cli.ts

Um parser total (parseCli) vira o argv num comando tipado e dispatchCli chama os verbos. Três verbos: distill, run, status. Verbo desconhecido ou fase ruim viram um err tipado — nunca uma exceção.

argvparseCliverbo
HTTP + SSE · server.ts + http.ts

Um bind fino de node:http monta uma tabela de rotas tipada sobre um socket real. Ele não tem orquestração: um RunRegistry resolve um HarnessCore por run, e POST /runs delega para um seam CreateRun injetado. Todo caminho resolve para um status HTTP tipado; o servidor nunca lança.

POST /runscria um run (via seam)
GET /runs/:id/statusdobra o snapshot
GET /runs/:id/eventsstreama SSE (ou buffer)
cliente POST/GET RunRegistry core status (JSON) · ou stream SSE
MCP · mcp.ts

Um servidor MCP expõe ferramentas tipadas a um host externo (outro agente), via JSON-RPC. Ele é estritamente read-only: de propósito não há ferramenta start ou fanout — um host MCP pode observar uma orquestração, nunca dispará-la. As ferramentas: harness_status, harness_events, harness_lane (e, num segundo tier scoped ao run, context_pack e artifact_read). invokeTool valida os argumentos crus pelo schema antes de executar.

host MCP ✓ permitido (read-only): harness_status harness_events harness_lane ✗ ausentes de propósito: start fanout observar é seguro · comandar é founder-gated
Por que só leitura Dar a um agente externo o poder de iniciar e fanout uma orquestração seria uma superfície de risco que o MCP não precisa expor. Observar é seguro; comandar é founder-gated e fica fora daqui.
Por que o servidor MCP não expõe uma ferramenta start ou fanout?
(b) É uma escolha de design declarada no mcp.ts: "deliberately no start/fanout tool here". O protocolo até poderia carregar uma escrita (a), e o motivo não tem a ver com o CLI (c) — é redução consciente da superfície de risco. Iniciar/fanout fica founder-gated, fora do MCP.
8

Exemplo guiado — uma fase, ponta a ponta


Vamos seguir um run de uma fase pelo core, do start ao report, e ver quais eventos cada verbo solta.

Conduzir uma fase com o HarnessCore
1
Construir o core. new HarnessCore({ runId: 'r-001', now }). O now injetado deixa os timestamps determinísticos. O snapshot nasce idle.
2
start(job). O core valida a FactoryPhase, reseta os campos por-fase e emite run-started (raia orchestrator) + phase-started. Devolve um Result<RunSnapshot>.
3
fanout(job). Se há council, ele delibera (emite council-startedcouncil-decidedcouncil-verified). Se há swarm, ele particiona por autonomia (swarm-partitioned) e dispara cada tarefa (swarm-task-started/finished na raia task:tN), fechando com swarm-finished.
4
poll() a qualquer momento. Uma tela de status lê o RunSnapshotautonomousCount, parkedCount, eventCount — sem tocar nos subsistemas.
5
report(). Emite run-finished e devolve o snapshot final. Se algo falhou no caminho, ele já está gravado como error no snapshot — o core nunca lançou.
start() fanout() report() emite ↓ run/phase-started council-* · swarm-* task-started/finished run-finished poll() pode ser lido a qualquer momento ↑
Cada verbo solta seus eventos: start abre, fanout dispara o ciclo de council e swarm, report fecha. poll() lê a foto entre eles sem forçar nada.
Agora você — antes de ler a resposta

Um cliente HTTP faz POST /runs e depois GET /runs/r-001/status antes de qualquer fanout ter rodado. Qual status o snapshot vai mostrar, e por quê?

Provavelmente idle ou deliberating — nunca done. O POST /runs materializa o run via o seam CreateRun e tipicamente chama start() (que abre a fase), mas o fanout é assíncrono. O GET …/status só dobra o poll() atual: ele reporta a foto naquele instante, sem forçar progresso. É exatamente por isso que poll e fanout são verbos separados.
A lição do exemplo O transport (HTTP) só fez duas coisas: materializar o run e ler o snapshot. Toda a orquestração de verdade aconteceu no core. Troque o transport por CLI ou MCP e a história do core não muda uma linha.
9

Confusão comum


❌ O mito

"Harness é só o CLI." Quem pensa assim acha que para mudar a interface é preciso mexer na lógica de orquestração — e acaba duplicando council/swarm em cada superfície.

✓ A realidade

O core é transport-neutral. CLI, HTTP+SSE e MCP são adaptadores finos. A lógica mora uma vez no HarnessCore; adicionar uma quarta superfície não toca em council nem swarm.

Tarefa prática (no repo)

1. Leia o comentário de cabeçalho de packages/harness/src/core.ts (a definição do papel do core). 2. Leia o módulo-doc de bridge.ts (o distill→market bridge). 3. Rode um distill com --dry-run e observe o FunnelReport. 4. Inspecione como o MCP (mcp.ts) expõe as mesmas capacidades em modo read-only. 5. Compare o mesmo run via CLI e via uma chamada MCP.

Como verificar a camada. pnpm test packages/harness — com foco em funnel.test.ts, bridge.test.ts, server.test.ts e mcp.test.ts. Cada um prova uma metade da camada: o funnel destila, o bridge compõe, o servidor faz o bind, o MCP observa.
10

Recapitulando em 6 slides


A camada do topo

L4 é o maestro

Um único HarnessCore conduz council + swarm e emite eventos. É o que transforma bibliotecas internas num motor utilizável.

A regra do núcleo

Transport-neutral

O core não sabe como é invocado. Não abre socket, não lê argv, não formata saída. Etapas falíveis devolvem Result; ele nunca lança.

A interface inteira

Quatro verbos

start abre · poll lê · fanout executa (council/swarm) · report fecha. Separá-los deixa um transport mostrar progresso sem tocar na lógica.

A destilação

Funnel T0→T3

T0 determinístico $0 · T1 local · T2 frontier (budget-gated) · T3 council+verifier → GO. O BudgetGuard falha fechando.

Fechando o ciclo

O bridge distill→market

distillAndMarket: funnel → verifiedSignals → marketing → manifests. Dependência uni-direcional (harness → marketing → contracts), grafo acíclico.

As superfícies

Três transports, um core

CLI · HTTP+SSE · MCP (read-only). Todos adaptadores finos sobre o mesmo núcleo. Com L0→L4, o motor inteiro está mapeado.

1 / 6setas
As cinco verdades da camada L4
  1. O HarnessCore é transport-neutral: conduz, não fala.
  2. Quatro verbos: start · poll · fanout · report.
  3. O RunSnapshot achata council + swarm numa foto; eventos viajam em swimlanes.
  4. O funnel destila T0→T3; o BudgetGuard falha fechando nos tiers pagos.
  5. O bridge distillAndMarket é uni-direcional e acíclico; os transports são adaptadores finos.
11

Verifique o que ficou


Quiz de fechamento — 3 perguntas

Responda na ordem. O placar abaixo acompanha seus acertos.

1. O que melhor descreve "transport-neutral" no HarnessCore?
(c) Transport-neutral = o núcleo ignora a superfície. Ele não embute protocolos (a) nem formata saída (b); essas tarefas ficam nos adaptadores finos. O core só conduz council + swarm e emite eventos.
2. No funnel, qual estação é gated por orçamento com o BudgetGuard falhando fechado?
(a) Só os tiers pagos passam pelo guard. T0 é determinístico e $0 (b está errado), e T1 é free-tier — nunca bloqueado por orçamento (c está errado). O guard protege T2/T3, falhando fechado.
3. O que o bridge distillAndMarket garante sobre o grafo de dependências?
(b) O bridge é uni-direcional: a marketing-factory nunca volta a chamar o harness, então o grafo fica acíclico. Chamada mútua (a) quebraria isso; e o store é append-only com dedupe — uma re-execução converge para 0 escritos, não reescreve tudo (c).
Acertos: 0/3
Você é o aluno e também o professor: olhe a sua própria unidade de trabalho e pergunte — "qual seria o start, o fanout e o report dela?". Se a fronteira entre conduzir e falar com o usuário não estiver óbvia, esse é o sinal de que separar o core do transport vale o esforço. A seguir (0006): saímos da arquitetura por camadas e percorremos os hot paths — os caminhos quentes que a maioria dos runs realmente executa.