É 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.
HarnessCore é transport-neutral e o que isso compra.start, poll, fanout, report — e o que cada um faz.RunSnapshot e como os eventos viajam em swimlanes.distillAndMarket que fecha o ciclo destilar→marketing.Result (ok/err) em vez de exceção.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.
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.
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.
"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."
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.
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.
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.| Verbo | Papel | Muta o estado? |
|---|---|---|
start | Valida a fase, reseta os campos por-fase, emite run-started + phase-started. | sim |
poll | Devolve o RunSnapshot imutável atual — barato, sem efeitos. | não |
fanout | Delibera (council) e/ou executa (swarm), emitindo o ciclo de vida por tarefa. | sim |
report | Emite 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.
poll(). Ele só devolve o RunSnapshot imutável. Por isso um painel de status pode chamá-lo num loop sem medo de efeitos colaterais.start() faz com os campos por-fase quando reusamos um core?runId e bus estáveis — então o mesmo core conduz várias fases sem perder a identidade do run.report() ser idempotente importa?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' | …
idle → deliberating → verified → fanning-out → done. Qualquer passo falível pode desviar para failed — gravado no snapshot, sem lançar.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 — 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.
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.
BudgetGuard: uma projeção de estouro bloqueia a chamada em vez de gastar. Falha fechando, nunca abrindo.FunnelReport não devolve só os verifiedSignals: traz contadores por tier (incluindo quantas chamadas o orçamento bloqueou) e o custo total medido.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.
Uma esteira em quatro estações: runFunnel → verifiedSignals → runMarketingBatch → persistManifests. A dependência só anda numa direção.
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.
// 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.)
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.
Três superfícies, um só núcleo. Trocar de transport nunca duplica a lógica — ela mora só no core.
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.
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 /runs | cria um run (via seam) |
GET /runs/:id/status | dobra o snapshot |
GET /runs/:id/events | streama SSE (ou buffer) |
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.
start ou fanout?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.Vamos seguir um run de uma fase pelo core, do start ao report, e ver quais eventos cada verbo solta.
new HarnessCore({ runId: 'r-001', now }). O now injetado deixa os timestamps determinísticos. O snapshot nasce idle.start(job). O core valida a FactoryPhase, reseta os campos por-fase e emite run-started (raia orchestrator) + phase-started. Devolve um Result<RunSnapshot>.fanout(job). Se há council, ele delibera (emite council-started → council-decided → council-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.poll() a qualquer momento. Uma tela de status lê o RunSnapshot — autonomousCount, parkedCount, eventCount — sem tocar nos subsistemas.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 abre, fanout dispara o ciclo de council e swarm, report fecha. poll() lê a foto entre eles sem forçar nada.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ê?
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."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.
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.
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.
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.HarnessCore é transport-neutral: conduz, não fala.start · poll · fanout · report.RunSnapshot achata council + swarm numa foto; eventos viajam em swimlanes.BudgetGuard falha fechando nos tiers pagos.distillAndMarket é uni-direcional e acíclico; os transports são adaptadores finos.Responda na ordem. O placar abaixo acompanha seus acertos.
HarnessCore?BudgetGuard falhando fechado?distillAndMarket garante sobre o grafo de dependências?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.