Curso / Lição 19
Lição 19 · Motor & método · 6 de 8

O swarm: orchestrator → lead → worker

O swarm L3 é como o Alembic roda muitas unidades de trabalho ao mesmo tempo e sobrevive a crashes. Pense num canteiro de obra: um mestre de obras (o orchestrator) divide o serviço entre alguns chefes de equipe (os leads), e cada chefe coloca pedreiros (os workers) para executar. A graça está nas regras que impedem o caos: profundidade limitada, uma fila que só libera quem pode rodar, o disco como fonte da verdade, isolamento por git-worktree e um freio de mão para trabalho perigoso. Fonte: packages/swarm/.

Leia primeiro (fonte primária)
packages/swarm/src/index.ts (6–21) — o resumo do próprio pacote

Esta lição destila esse pacote, lido linha a linha. Cada número e cada nome de função têm fonte (rodapé). Por que importa pra missão: é o pacote que a matriz de fusão (§2.4) cita para tornar o delegate_tool.py do Hermes um IGNORE — o Alembic já delega, nativamente e melhor.

Objetivos desta lição
  • Explicar a hierarquia orchestrator → lead → worker e por que MAX_DEPTH = 2 impede fan-out recursivo.
  • Ler a fila com portão de dependências: quando uma task vira ready e o ciclo blocked → ready → running → done/failed.
  • Descrever o resume seguro a crash: replayInto + recuperação de órfãs = execução pelo-menos-uma-vez.
  • Distinguir as duas fronteiras fail-closed — isolamento de worktree e o park T4 — do reward heurístico (PARL, não RL).
0
MAX_DEPTH · o teto de recursão
0
papéis: orchestrator/lead/worker
0
estados da task (incl. parked)
0
razões de park (conjunto fechado)

01 · Três camadas, um nível de aninhamento

A hierarquia de papéis é orchestrator → lead → worker, e é deliberadamente curta. O orchestrator pode spawnar leads; um lead pode spawnar workers; um worker faz o trabalho-folha e nunca spawna. O número que prende tudo isso é MAX_DEPTH = 2: a profundidade de cada papel (ROLE_DEPTH) é orchestrator=0, lead=1, worker=2, e a profundidade máxima é justamente 2.

Imagine uma empresa com só três níveis: diretorgerentesfuncionários. Um funcionário executa a tarefa; ele não pode virar gerente e montar a própria equipe no meio do expediente. Esse "não pode" é o MAX_DEPTH = 2: a organização tem fundo, então ninguém cria uma cadeia infinita de subequipes que ninguém consegue acompanhar.
ROLE_DEPTH = { orchestrator: 0, lead: 1, worker: 2 } e MAX_DEPTH = 2 (types.ts). A regra que decide se um nó pode ter filhos é uma linha: export const canSpawn = (depth) => depth < MAX_DEPTH (orchestrator.ts:107). Um worker está em depth === 2, então canSpawn(2) === false: é folha. O comentário do código chama isso de "the load-bearing recursion bound" — o limite que sustenta a estrutura.
A ÁRVORE DE PAPÉIS · orchestrator → lead → worker, limitada a MAX_DEPTH = 2
orchestrator profundidade 0 · canSpawn ✓ lead profundidade 1 · canSpawn ✓ lead profundidade 1 · canSpawn ✓ worker (folha) prof. 2 · canSpawn ✗ worker (folha) prof. 2 · canSpawn ✗ worker (folha) prof. 2 · canSpawn ✗ um worker não tem filhos — é o fundo subtasks são tasks-folha — então o aninhamento é estruturalmente de UM nível. Guarda contra fan-out recursivo descontrolado.
orchestrator (d0)lead (d1)worker / folha (d2)

Como o aninhamento é limitado por construção e não só por convenção? No tipo. Uma task pode carregar subtasks — e quando carrega, ela é um lead: o orchestrator roda os subtasks como uma sub-run um nível mais fundo. Mas os subtasks são do tipo taskSpecBaseSchema, que não tem campo subtasks. Logo um subtask não pode carregar subtasks — o aninhamento é travado em um nível pelo próprio sistema de tipos, além da checagem em runtime por canSpawn.

O bound de uma linha: canSpawn(depth) = depth < MAX_DEPTH

A regra inteira que sustenta a árvore é uma única função, e o melhor jeito de internalizá-la é como um fluxograma de decisão sobre a profundidade: dado o depth de um nó, ela responde "spawna um filho?" comparando com o teto. Siga as setas — só a profundidade 0 e 1 passam; a 2 (worker) é o fundo.

FLUXOGRAMA DEPTH-BOUND · canSpawn(depth) = depth < MAX_DEPTH(2)
nó na profundidade depth orchestrator=0 · lead=1 · worker=2 depth < 2 ? (canSpawn) SIM spawna o filho childRole(role): orch→lead, lead→worker NÃO (depth=2) é FOLHA — só executa childRole(worker) = undefined → sem filhos "the load-bearing recursion bound"

Uma comparação < é todo o anti-fan-out. Fonte: canSpawn (orchestrator.ts:107), childRole (orchestrator.ts:110).

02 · A fila — com portão de dependência, pronto significa pronto

Rodar concorrente sem regra é receita para desastre: uma task que depende de outra rodaria antes do pré-requisito existir. A fila do swarm resolve isso com um portão de dependências. Só tasks ready rodam, e uma task só vira ready quando todos os ids em seu dependsOn alcançaram sucesso-terminal (done). O grafo de dependências é honrado, não torcido.

Pense num checklist de cozinha: você não monta o prato (servir) enquanto o arroz (cozinhar) não estiver pronto. "Pronto para montar" só acende quando o passo anterior terminou de verdade — não quando ele só "começou".

A fila é pura contabilidade em memória sobre o estado das tasks; ela nunca executa nada — só responde "o que pode rodar agora?" e "a run terminou?". Antes de qualquer agendamento, validateGraph (estilo Kahn, redução de indegree) rejeita ciclos e dependências para ids inexistentes. Há três tipos de worker, e a diferença entre eles é o que de fato roda:

Tipo de workerO que rodaSinal de "done"
model workeruma chamada de adapter (o run() da lição 14)o report do adapter
command workertaskSpec.command → um subprocesso real (argv, sem shell)saída 0 → done; qualquer outra → failed
background workertaskSpec.background → um filho destacado que sobrevive à morte do paire-anexa ao seu report no resume (requer command, proíbe isolate)
TRÊS TIPOS DE WORKER · o que cada um executa por baixo
model worker adapter.run() — lição 14 report do adapter → done command worker subprocesso (argv, sem shell) exit 0 → done qualquer outra → failed background worker filho destacado (detached) sobrevive ao pai · re-anexa requer command · proíbe isolate Os três compartilham o mesmo protocolo de report-file + rename; muda só QUEM faz o trabalho.

O sinal atômico de "done": renomear o report

Como o orchestrator sabe, sem corrida, que um worker terminou? Pelo rename do arquivo de report. O worker escreve o corpo num arquivo em progresso e, ao terminar, o renomeia para o nome terminal (.complete.md ou .failed.md). O rename é o sinal atômico — ou o arquivo final existe inteiro, ou não existe. Não há estado meio-escrito que o orchestrator possa ler errado.

PROTOCOLO DE REPORT · escreve em progresso, renomeia para o nome terminal (atômico)
<task>.md em progresso (parcial) rename atômico <task>.complete.md outcome: complete <task>.failed.md outcome: failed orchestrator ingere WorkerReport (Zod) → estado O nome final é o "done"; o corpo é parseado por Zod. Por isso um background re-anexa no resume: ele procura o report já renomeado.

O portão em ação: uma task espera as duas dependências

Veja três tasks. C declara dependsOn: [A, B]. Enquanto A e B não estiverem ambas done, C fica blocked — o orchestrador não a toca. Quando a última delas conclui, a fila re-avalia (a função regate) e promove C a ready:

PORTÃO DE DEPENDÊNCIA · C só vira ready quando A E B chegam a done
A · done ✓ dependsOn: [] B · done ✓ dependsOn: [] A∧B? C · blocked → ready dependsOn: [A, B] regate() promove C só quando AMBAS as deps estão done se só A estivesse done, C seguiria blocked

03 · O ciclo de vida de uma task (a máquina de estados)

Toda task vive numa pequena máquina de estados de seis posições. Entender as setas dela é entender o swarm inteiro: o portão de dependências, o resume e o park são todos transições nesta máquina. As terminais (done, failed, parked) não voltam atrás — salvo a recuperação de órfãs, a única seta que reabre um estado, e que é exatamente o coração do resume (seção 05).

MÁQUINA DE ESTADOS DA TASK · blocked → ready → running → done/failed, mais parked
blocked deps abertas deps done ready pode rodar reivindicada running worker ativo done (terminal) satisfaz deps de outras failed (terminal) dependentes nunca acendem órfã no resume: running → ready (recoverOrphans) parked T4 · imóvel T4/irreversível → park direto (seção 07)

A run é completa (isComplete) quando nenhuma task está blocked, ready ou running. Uma task failed ou parked é terminal e não trava a conclusão — seus dependentes simplesmente nunca viram ready. Fonte: queue.ts.

04 · Monte a árvore de execução na mão (passo a passo → agora você)

Você viu a hierarquia e a fila. Agora trace uma run pequena na mão, devagar — recuperar o procedimento fixa mais que ver o resultado pronto. Considere uma run com quatro tasks: setup, build (dep: setup), test (dep: build) e um deploy marcado tier: T4.

Exemplo resolvido · que roda primeiro, e o que nunca roda sozinho?
1
Classifique cada task no início. initialState diz: sem deps → ready; com deps → blocked; deve ser parkada → parked. Então setup = ready; build/test = blocked; deploy (T4) = parked.
2
Drene os ready.setup está ready, então só ele roda. Ao concluir, ele vira done.
3
Re-avalie o portão. regate roda: build depende só de setup, que agora está donebuild vira ready. test ainda depende de build → segue blocked.
4
Repita. build roda → doneregate promove test a readytest roda → done.
5
O T4 nunca se move. deploy permanece parked a run inteira — nenhuma seta o tira de lá. A run termina "completa" mesmo com ele parado, porque parked é terminal (não bloqueia isComplete).
Agora você: adicione docs com dependsOn: [build] (não depende de test). Em que momento docs fica ready, e ele pode rodar concorrente com test? Responda antes de revelar.
docs vira ready no mesmo instante que build conclui (passo 4), porque sua única dependência é build. E sim: com maxConcurrency > 1, docs e test ambos dependem só de build e ficam ready juntos, então rodam em paralelo. A regra é sempre a mesma: uma task acende quando o conjunto de suas deps — e só ele — está todo done.

05 · Resume seguro a crash — o filesystem é a verdade

Aqui está a peça que distingue um brinquedo de um motor de produção. O que acontece se a máquina morre no meio de uma run de 8 horas? O swarm não perde o trabalho. O estado é durável no disco: um journal append-only (events.jsonl) mais um checkpoint.json. Re-rodar a mesma run não recomeça do zero — ele reproduz o journal e continua.

É como um diário de bordo de navio: cada evento é anotado em ordem, em tinta permanente (append-only). Se o capitão muda no meio da viagem, o novo lê o diário inteiro e sabe exatamente onde o navio está — não precisa refazer a rota. E se um marinheiro saiu para uma tarefa e nunca voltou (o processo morreu), o diário mostra "saiu mas não reportou", e a tarefa dele é redistribuída.
O resume passa por um único caminho compartilhado, replayInto (orchestrator.ts:706): aplica o último checkpoint, depois reproduz todo evento task-state (idempotente, latest-per-id vence — o checkpoint é pré-seed, não corte), depois recupera órfãs. A mesma função serve à inspeção (resumeQueue) e à execução (runSwarm), então as duas nunca divergem na interpretação do journal.

A parte engenhosa é como ele trata um worker que morreu no meio do voo. A função recoverOrphans (orchestrator.ts:747) percorre os estados: toda task cujo último estado durável é running teve seu worker morto sem evento terminal — então é rebaixada de volta a ready e re-tentada. Isso dá execução pelo-menos-uma-vez, e como o enqueue é idempotente, o denominador do monitor cobre toda task mesmo após um crash no meio da declaração.

// packages/swarm/src/orchestrator.ts:747-755 (condensado) — recoverOrphans
const recoverOrphans = (queue, now) => {
  let count = 0;
  for (const state of queue.states()) {
    if (state.status !== 'running') continue;
    queue.applyState({ ...state, status: 'ready', updatedAt: now() }); // órfã → re-tentada
    count += 1;
  }
  return count;
};

A anatomia do diário: events.jsonl append-only + checkpoint.json

O que exatamente é gravado no disco? Um journal append-only (events.jsonl): uma linha por evento, cada uma com seq (contador monotônico por run), at e um payload validado por Zod contra o schema do kind. Periodicamente, um checkpoint.json condensa o estado reconstituível num ponto — o pré-seed do replay, nunca um corte. As espécies de evento (swarmEventKindSchema) são um conjunto fechado; veja a linha do tempo de uma run típica:

O JOURNAL APPEND-ONLY · sequência de eventos de uma run (seq monotônico)
seq crescente · append-only (tinta permanente) → run-started task-enqueued task-state task-state task-parked reward checkpoint run-finished checkpoint.json estado condensado = PRÉ-SEED do replay (não um corte) cada task-state é idempotente latest-per-id vence — replay reaplica TODOS, inclusive os de subtasks de um lead Toda linha é parseada por Zod na leitura: uma linha corrompida ou editada à mão é rejeitada na fronteira (invariante ③, lição 16). Há ainda eventos de nível-run acima do swarm: phase-started / phase-finished / run-log (emitidos pela VM do Alembic). Fonte: swarmEventKindSchema / swarmEventSchema (types.ts:203–231), runCheckpointSchema (types.ts:238–245).

A run-id é content-addressed — e é por isso que o resume "encontra" o estado

Por que re-submeter a mesma run cai exatamente no diretório de estado já existente em vez de criar uma árvore nova? Porque o id da run é o hash do seu spec imutável: runIdFor(spec) = run-${shortHash(spec)} (ids.ts:30), um SHA-256 de 16 caracteres. Crucialmente, o hash usa canonicalJson (chaves ordenadas recursivamente), então a ordem dos campos no objeto não muda o digest — o mesmo goal + as mesmas tasks sempre resolvem para o mesmo runId, e portanto para o mesmo diretório on-disk.

PIPELINE DO RESUME · do spec ao estado recuperado (content-addressed)
spec imutável goal + tasks canonicalJson + SHA-256 (16 hex) run-id run-<hash> → diretório replayInto 1· checkpoint.json (pré-seed) 2· todo evento task-state 3· recoverOrphans → ready mesmo spec → mesma run-id → mesmo diretório → estado recuperado. Re-submeter NÃO forka uma árvore nova. Todo valor que cruza a fronteira de durabilidade é parseado por Zod na leitura (invariante ③ da lição 16).

A unicidade global de id por toda a árvore (checkUniqueIds, orchestrator.ts:141) é validada antes de qualquer coisa ser jornalada, porque a run compartilha um único journal chaveado por id — um id duplicado deixaria o estado de uma task sobrescrever o de outra e poderia envenenar um resume.

06 · Decisão: "este nó spawna ou é folha?" (fluxograma)

Junte a profundidade, os subtasks e o portão num único fluxograma de decisão — exatamente o que o orchestrator executa para cada task. Siga as setas: cada losango é uma pergunta que escolhe o caminho entre "vira um lead (sub-run)" e "roda como worker-folha".

FLUXOGRAMA · o que o orchestrator faz com uma task (spawn vs folha)
task chega à fila classifyPark(spec) primeiro deve ser parkada? (T4/irrev/legal/sec) SIM → PARK nunca auto-roda NÃO tem subtasks E canSpawn(depth)? SIM vira LEAD sub-run 1 nível + fundo NÃO roda como WORKER-folha canSpawn(2) = false → o fundo da árvore model · command · background model worker (adapter) · command worker (subprocesso, exit 0 → done) · background worker (filho destacado) caminho do worktree: se isolate=true e há config → roda em git-worktree; se isolate=true sem config → ERRO (fail-closed, seção 07)
Por que classifyPark vem primeiro: a segurança precede a execução. Antes de decidir como rodar, o orchestrator decide se pode. Trabalho perigoso nunca chega ao ramo de execução — ele desvia para o park no primeiro losango.
O ramo "vira lead": uma task com subtasks roda uma sub-run dependency-gated um nível mais fundo (orchestrator → lead → worker), e seu resultado é dobrado no resultado do lead — done sse nenhum subtask falhou ou ficou blocked. O ramo "folha" é o caso comum.

O fold do lead: como os subtasks viram um único veredito

Quando uma task vira lead, seus subtasks rodam como uma sub-run — mas o orchestrator precisa de um resultado para a task-lead. Ele dobra (fold) os estados dos subtasks numa regra estrita: o lead é done se e somente se nenhum subtask ficou failed ou blocked. Um único subtask que falha (ou que nunca acendeu por uma dep aberta) já derruba o lead:

FOLD DO LEAD · done sse nenhum subtask failed/blocked (senão failed)
cenário A · todos done s1 done s2 done s3 done fold ∧ lead → done ✓ cenário B · um falhou s1 done s2 failed s3 blocked um basta lead → failed ✗ É um E lógico sobre os subtasks: basta um failed/blocked para o lead inteiro falhar. Fonte: taskSpecSchema/fold do lead (types.ts:111–124).

07 · Isolamento e o park T4 — fail-closed, ambos

Duas fronteiras do swarm são fail-closed: quando em dúvida, elas param em vez de seguir em silêncio. São o mecanismo de segurança do motor.

Isolamento de worktree fail-closed

isolate: true sem config de worktree é um erro, nunca uma run sem-isolamento em silêncio. O código é explícito (orchestrator.ts:509-514): se !deps.worktree, retorna err("task '…' requires isolation but no worktree config was provided"). Um command worker que não pode tocar o checkout principal recebe um git worktree via withWorktree (branch + porta determinística, derrubado depois), ou não roda. Se você pediu separação e o ambiente não pode prover, a run para — ela não executa quietamente sua task contra a working tree compartilhada.

Park T4 duro

Tasks irreversíveis / legais / de segurança / T4 são roteadas para o ledger do park (t4-parked.jsonl, append-only) e nunca auto-executadas. O park é idempotente entre resumes (parkIneligible não re-anexa o que já está no ledger) e estruturalmente protegido: em applyState (queue.ts:150), uma task que deve ser parkada é imóvel — nenhuma linha reproduzida, report perdido ou varredura de órfãs consegue tirá-la de parked. As razões são um conjunto fechado: tier-t4 / irreversible / legal / security / manual. É a expressão em nível de swarm do portão humano: o motor recusa dar o passo perigoso por conta própria, e você o reabre com alembic approve/reject/propose.

A precedência de classifyPark (park.ts:38) é deliberada — o primeiro motivo que casa vence, para o ledger registrar a causa mais específica: marcadores legal/security nos metadados primeiro, depois a flag/metadado irreversible, e por último a regra do tier T4. "When in doubt, park."

PRECEDÊNCIA DO classifyPark · primeiro motivo que casa vence (mais específico → mais genérico)
metadata.legal/security? marcadores explícitos casa → retorna PARK reason = 'legal' / 'security' não casa ↓ irreversible (flag/meta)? força park, qualquer tier casa → retorna PARK reason = 'irreversible' não casa ↓ tier === T4 / isParked? a regra do tier casa → retorna PARK reason = 'tier-t4' nenhum casa ↓ undefined → elegível p/ execução autônoma Conjunto fechado de motivos: tier-t4 / irreversible / legal / security / manual

A ordem é o que dá ao ledger a causa mais específica. Fonte: REASON_KEYS + classifyPark (park.ts:24–45).

08 · O reward — heurístico, não aprendizado por reforço

O swarm tem um sinal de reward, e o nome engana de propósito. computeReward (reward.ts:74) é um escalar heurístico moldado — estilo PARL — e não é aprendizado por reforço: não há gradiente de política, não há replay buffer, não há atualização online de pesos. É uma conta determinística e explicável sobre três componentes, com um humano no loop.

PESOS DO REWARD HEURÍSTICO · completion 0,6 · latency 0,2 · output 0,2 (somam 1)
completion 0,6 · concluiu? latency 0,2 · rápido vs budget output 0,2 · produziu saída? score = clamp(0..1) da soma ponderada · sem budget de latência → componente neutro 0,5 (não dá pra julgar velocidade sem budget)

E o sinal é inerte até aprovado: requiresApproval nasce true, então nada pode influenciar o roteamento futuro antes de um humano assinar (HITL). Só sinais aprovados (ou que nunca precisaram de aprovação) contam — isActionable. O reward molda a priorização com um humano no loop; não há treino.

Bônus do código: o lead-dispatcher (lead-dispatcher.ts) lança subtasks num ramp estilo Kimi — começa com initialLimit (default 5) tarefas e libera +1 vaga a cada intervalMs (default 700 ms) até maxConcurrency; tarefas que terminam não liberam vagas (o ramp controla o total lançado ao longo do tempo). Ele também emite um stream de consult/update por subtask, validado por Zod, para um observador AFK seguir cada passo.

O ramp do lead: arranque controlado, não enxurrada

Por que um lead não dispara todos os subtasks de uma vez? Para não saturar a máquina (e o gateway) no instante zero. O ramp começa com initialLimit (5) tarefas e solta +1 vaga a cada 700 ms até maxConcurrency. Detalhe que confunde: tarefas que terminam não devolvem vagas — o ramp governa o total lançado ao longo do tempo, como abrir um registro aos poucos:

RAMP ESTILO KIMI · 5 no início, +1 vaga a cada 700 ms até maxConcurrency
vagas tempo → maxConcurrency (teto) t=0 · 5 +700ms · 6 +700ms · 7 +700ms · 8 … até o teto A escada SÓ sobe (vagas liberadas não voltam quando uma tarefa termina). Fonte: lead-dispatcher.ts (initialLimit=5, intervalMs=700).

E o lead transmite cada passo: o stream consult/update

Enquanto fan-outa, o lead emite um stream de lifecycle por subtask — uma união discriminada por kind, validada por Zod, para um observador AFK (ou um log de transcript compartilhado) acompanhar sem o dispatcher vazar internals. São quatro espécies; hoje o dispatcher emite o par terminal, e update fica reservado para quem fia progresso intermediário:

STREAM CONSULT/UPDATE · ciclo de vida por subtask (started → result | failed)
started lançou (1 por launch) update progresso (reservado) result launch resolveu ok failed resolveu err (+ mensagem) timestamps do MESMO clock injetado (determinístico / plan-safe) Mesmo idiom de fronteira do resto do swarm: z.discriminatedUnion('kind', …). Fonte: consultUpdateSchema (lead-dispatcher.ts:39–64).

09 · Por que delegate_tool.py do Hermes é IGNORE (comparativo)

A matriz de fusão (§2.4) classifica o delegate_tool.py do Hermes como IGNORE — não porque delegar seja ruim, mas porque o Alembic já delega nativamente e com mais garantias. Veja o trade-off lado a lado: o que o swarm L3 oferece que uma tool de delegação simples não tem.

Dimensãodelegate_tool.py (Hermes)swarm L3 (Alembic)
Profundidadedelegação ad-hoc, sem limite estruturallimitada por MAX_DEPTH = 2 (anti-fan-out)
Ordenaçãochamada direta, sem grafo de depsfila com portão de dependsOn (ready = ready)
Crash mid-runtrabalho perdido se o processo morreresume via replayInto + órfãs → ready
Isolamentocompartilha o ambiente do paigit-worktree fail-closed por task
Trabalho perigosoexecuta o que for pedidopark T4 duro (nunca auto-roda)
DELEGAÇÃO SIMPLES vs SWARM L3 · o mesmo objetivo, garantias diferentes
delegate_tool.py — chamada direta pai → sub-agente → resultado sem rede de segurança crash = perde · sem deps · sem park sem isolamento swarm L3 — delegação com garantias orchestrator → lead → worker portão · resume · isolamento · park crash = retoma · deps honradas T4 nunca auto-roda Mesmo objetivo (dividir trabalho) — o swarm envolve a delegação em invariantes de produção. Por isso a tool do Hermes é IGNORE: redundante e mais fraca.

10 · Como isso se encaixa

O swarm não age sozinho — ele é o braço executor no meio da máquina. Antes dele, os gates decidem se e o que rodar; o swarm então roda exatamente as unidades aprovadas, em paralelo e com resume; e o que ele não pode tocar (T4/irreversível) ele devolve ao portão humano. Veja a peça (em destaque) dentro do pipeline real do Alembic:

O SWARM NO PIPELINE · gates aprovam → swarm executa as unidades → T4 volta ao humano → aprendizado
UPSTREAM · gates scope → council → proof → validator (Lição 17) decidem SE e O QUÊ rodar unidades aprovadas ESTA PEÇA · o swarm (L3) orchestrator → lead → worker orchestr. lead worker executa as unidades · fila c/ portão · resume DOWNSTREAM unidades done/failed + reward heurístico (sob portão HITL) T4 / irreversível PORTÃO HUMANO · park T4 t4-parked.jsonl · nunca auto-roda (Lições 10 & 17) você reabre: alembic approve / reject / propose LOOP DE APRENDIZADO o aprovado sedimenta na memória → a próxima run abre mais rica (Lição 04) Os gates dizem o que rodar; o swarm roda; o T4 volta ao humano; o aprovado realimenta a próxima run.

Clique em "Percorrer o fluxo" para acender cada estágio em ordem: gates → swarm → resultados → portão T4 → aprendizado.

Onde você está na metodologia: o swarm é o estágio de execução — o único que roda trabalho de verdade. Tudo antes dele (Scope, Council, Proof, Validator) é decisão; tudo depois (park T4, reward, aprendizado) é consolidação sob portão humano. Ele é o motor que pega o plano aprovado e o transforma em unidades concluídas, sem perder trabalho num crash e sem dar sozinho o passo perigoso. Veja o mapa interativo inteiro em a metodologia completa →.

Lição 17 · A pipeline de gates
Porque conecta: os gates (scope → council → proof → validator) são o upstream do swarm — eles decidem quais unidades chegam à fila. O swarm só executa o que a pipeline aprovou.
Lição 10 · ClarifyGateway
Porque conecta: o park T4 do swarm é a expressão, em nível de execução, do mesmo portão humano — quando o motor não pode decidir sozinho, ele para e devolve a você (clarify / approve / reject).
Lição 18 · Council & Verifier
Porque conecta: o Council decide antes (GO/NO_GO) e o Verifier observa depois; o swarm é o meio que roda as unidades entre essas duas decisões, alimentando o Verifier com os resultados.
Lição 30 · Capstone
Porque conecta: no capstone você junta tudo — uma run real exercita gates + swarm + park + aprendizado de ponta a ponta. É aqui que o swarm aparece operando dentro da máquina inteira.

11 · Na prática

O swarm não tem um comando "swarm" próprio — ele é o que alembic run aciona por baixo quando uma run tem várias unidades. Você o dispara com run e o observa com runs list, tui e tail; um replay retoma uma run que caiu. Os comandos abaixo são todos canônicos (CLAUDE.md):

# 1) Disparar uma run a partir de um spec (mission.json / tasks.json):
#    o swarm vira o executor — orchestrator divide em leads, leads em workers.
alembic run mission.json

# 1b) …ou a partir de um GOAL.md + plano (h.mission() com units[]):
alembic run --goal GOAL.md --plan alembic.plan.ts --yes

# 2) Listar as runs e seus estados (encontre a run-id content-addressed):
alembic runs list
# run-3f9a1c… · finished · 7/8 done · 1 parked (T4)

# 3) Acompanhar AFK — um dashboard de terminal por run-id:
alembic tui run-3f9a1c

# 4) …ou seguir o journal append-only em tempo real (events.jsonl):
alembic tail run-3f9a1c -f
# task-state  build   running
# task-state  build   done
# task-parked deploy  tier-t4   → reabra com approve/reject/propose

# 5) Caiu no meio? Retome do disco — content-addressed, sem refazer o feito:
alembic replay run-3f9a1c
# recoverOrphans: 1 task running órfã → ready (re-tentada)

Note o ciclo que toda a lição descreve, agora no terminal: run aciona o swarm; tail -f mostra cada task-state sendo jornalado; um task-parked tier-t4 aparece e não auto-roda; e se a máquina morrer, replay reproduz o journal e a recoverOrphans rebaixa a task órfã de running de volta a ready — exatamente o resume seguro a crash da seção 05. [uncertain] a forma exata das linhas do tail é ilustrativa do conceito (task-state / task-parked do events.jsonl); o formato textual impresso pode diferir.

Experimente · dispare o swarm e assista o resume em ação
1
Posicione-se no repo. cd para o checkout do Alembic e garanta o build verde: pnpm -r typecheck && pnpm -r build && pnpm -w test. Sem build, os pacotes dependentes não enxergam os .d.ts.
2
Dispare uma run. Rode alembic run --goal GOAL.md --plan alembic.plan.ts --yes (ou alembic run mission.json). O swarm pega as unidades aprovadas e as executa — leads fan-outam workers num ramp controlado.
3
Observe. Em outro terminal, rode alembic runs list para pegar a run-id, depois alembic tail <run-id> -f. Procure as transições task-state … running → done e, se houver uma unidade T4, a linha task-parked.
4
Inspecione o disco (a fonte da verdade). No diretório da run, abra events.jsonl — uma linha por evento, seq crescente — e checkpoint.json (o pré-seed do replay). É o journal append-only da seção 05.
5
Force o resume. Interrompa a run (Ctrl-C) e rode alembic replay <run-id>. Porque a run-id é o hash do spec, o replay cai no mesmo diretório, reproduz o journal e a recoverOrphans re-tenta a task deixada running — você verá a run continuar de onde parou, não recomeçar.
Reabra um T4. Se uma unidade ficou parked, ela é o portão humano: registre sua decisão com alembic approve <run-id> --task <unit-id> (ou reject), ou reabra como proposta com alembic propose <run-id>. O motor nunca dá esse passo sozinho — você dá.

Confusões comuns

"PARL reward significa que ele aprende por reforço." Não — computeReward é um escalar heurístico moldado, explicitamente não RL, e tem portão HITL por requiresApproval. O reward molda a priorização com um humano no loop; não há gradiente, não há treino.
"Background workers rodam de fato assíncronos, liberando o slot." Ainda não — um gap conhecido: background workers ainda bloqueiam o slot da task enquanto fazem polling do report (a vitória entregue hoje é durabilidade: filho destacado + sobrevive ao pai + re-anexa no resume). O drain assíncrono é um slice futuro que a costura do dispatcher já acomoda. O curso declara gaps com honestidade em vez de exagerar.

Fixe os conceitos (flashcards)

Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.

Profundidade
Por que um worker nunca spawna?
clique pra virar ↻
Resposta
Está em depth 2 = MAX_DEPTH; canSpawn(2)=false. É folha. Subtasks são tasks-folha → aninhamento de 1 nível, travado no tipo.
Fila
Quando uma task vira ready?
clique pra virar ↻
Resposta
Quando TODOS os ids em dependsOn chegam a done. regate promove blocked → ready. Pronto significa pronto.
Resume
O que acontece com uma task running órfã?
clique pra virar ↻
Resposta
recoverOrphans rebaixa running → ready e re-tenta → execução pelo-menos-uma-vez. Enqueue idempotente cobre todo o denominador.
Fail-closed
isolate:true sem worktree config?
clique pra virar ↻
Resposta
Retorna err — nunca uma run sem-isolamento em silêncio. Pedir separação que não se pode obter para a run.

Revisão cumulativa — recupere de memória

Antes de clicar: responda de cabeça. As opções têm tamanho parecido de propósito — sem pista pela forma.

1. Um lead na profundidade 1 spawna workers. Um desses workers pode spawnar seus próprios sub-workers?
Correto: c — a profundidade é limitada em 2 e canSpawn(2)=false; subtasks são tasks-folha, então o aninhamento é de um nível, travado no tipo e no runtime. a erra o ponto inteiro: "qualquer profundidade" é justamente o fan-out recursivo que o bound existe para impedir. b inventa uma exceção por tipo de worker que não existe — o limite é de profundidade, não de tipo. d imagina uma aprovação em runtime; não há — é um invariante estrutural, não uma decisão dinâmica.
2. Um processo worker é morto no meio da task e a run é retomada depois. O que acontece com essa task?
Correto: breplayInto reproduz o checkpoint mais os eventos task-state; recoverOrphans rebaixa a task deixada running a ready e a re-roda. a confunde "worker morreu" com "task falhou": não houve evento terminal failed, então marcá-la falha seria errado. c escolheria a semântica oposta (no-mais-de-uma-vez) e travaria a conclusão para sempre — isComplete nunca fecharia. d ignora todo o ponto do journal durável: o resume continua de onde parou, não recomeça.
3. Um task spec define isolate: true mas nenhuma config de worktree é fornecida. O que o orchestrator faz?
Correto: d — o código retorna err("requires isolation but no worktree config was provided"). a e b são a falha clássica que o fail-closed existe para impedir: executar sem a separação pedida (silenciosa ou não) pode sujar o checkout compartilhado. c "auto-cria com padrões" parece prestativo, mas inventar config de isolamento que o operador não deu é justamente o tipo de suposição perigosa que o motor recusa.
4. Por que re-submeter exatamente o mesmo spec de run cai no diretório de estado já existente em vez de criar um novo?
Correto: arunIdFor(spec)=run-${shortHash(spec)} é content-addressed; canonicalJson ordena as chaves, então até a ordem dos campos é irrelevante: mesmo spec → mesmo id → mesmo diretório (filesystem-como-verdade). b inventa um prompt interativo que quebraria runs AFK. c "o mais recente" forkaria árvores erradas quando há várias runs. d é o oposto do design: um UUID aleatório jamais reencontraria o estado anterior — não haveria resume.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Por que o ramp começa em 5 e não em 1?", "Como o park sobrevive a um resume?", "Qual a diferença real entre command e background worker?". É só dizer.