Passo 4 · Alembic Completo · Camada L3 · Swarm
Alembic Completo · Camada L3

Camada L3 · Swarm

O orquestrador 3-tier, depth-bounded e dependency-gated do Alembic. É a peça que faz o Factory rodar vários agentes em paralelo — de forma isolada, resumível e segura — sem nunca deixar trabalho irreversível rodar sozinho.

Cada seção tem um modo Simples e um Técnico. Abra o que precisar.
01

A grande ideia

Ao fim desta lição você consegue
  • Explicar os três papéis do swarm — orchestrator → lead → worker — e por que MAX_DEPTH = 2 impede recursão infinita.
  • Dizer exatamente quando uma tarefa fica ready (a regra do dependency-gating).
  • Descrever o T4 park: que trabalho nunca roda sozinho, e como classifyPark decide.
  • Explicar o protocolo do report file (renomear = commit) e os dois modos de isolamento (worktree e background).
  • Entender por que um run é resumível: journal append-only + checkpoint no Store.
Pressupomos pouco de você
  • Você já viu as camadas anteriores (L0 Contracts/ETL, L1 Adapters, L2 Council) — mas vamos relembrar o que importar aqui.
  • Sabe o que é um processo e um arquivo. Não precisa conhecer git worktree nem teoria de filas: explicamos.
  • "Tier" aqui é a faixa de risco/autonomia de uma tarefa (T1 a T4), vinda de @alembic/contracts.

Imagine que você precisa tocar dez tarefas ao mesmo tempo: algumas dependem de outras, algumas são perigosas demais para fazer sem confirmar, e o computador pode travar no meio. Você quer um gerente que: distribui o trabalho, só libera uma tarefa quando o que ela depende terminou, segura o que é arriscado para um humano olhar, e — se cair a energia — retoma de onde parou sem refazer tudo.

Esse gerente é o Swarm, a camada L3 do Alembic, no pacote @alembic/swarm. Ele não inventa o trabalho; ele coordena quem faz. E faz isso com três garantias duras, escritas no próprio código.

É como a cozinha de um restaurante movimentado. O chef (orchestrator) não frita nada — ele organiza. Os chefes de praça (leads) dividem cada pedido em passos. Os cozinheiros (workers) cozinham, e só eles encostam na panela. Ninguém pula nível, ninguém serve um prato que depende de outro ainda não pronto, e o caderno de comandas (o journal) deixa retomar o serviço se a luz piscar.

O que o pacote anuncia de si mesmo

O docblock de index.ts (linhas 7–21) é o melhor resumo de uma frase: "um orquestrador 3-tier depth-bounded (orchestrator → lead → worker) sobre uma fila com dependency-gating, com estado durável filesystem-as-truth (JSONL append-only + índice opcional node:sqlite), um hook de reward estilo PARL gated por HITL, isolamento por git-worktree com portas determinísticas, runs resumíveis crash-safe por diretório de run endereçado por conteúdo (re-rodar reproduz o journal e recupera tarefas órfãs em vez de reiniciar), um monitor de progresso read-only por polling de arquivo (readRunProgress) para observabilidade AFK, e um T4 park rígido para trabalho irreversível/legal/security que nunca pode auto-executar."

As três regras duras

Do docblock de orchestrator.ts (linhas 32–47): (1) a profundidade é limitada — um nó em MAX_DEPTH é leaf e não pode spawnar; (2) dependency-gating — só tarefas ready rodam; (3) o T4 park — trabalho irreversível/legal/security/T4 vai para o ledger e nunca auto-executa. Toda transição é journaled pelo Store, e um run é resumível a partir do último checkpoint.

Fonte primária (leia isto primeiro)
packages/swarm/src/orchestrator.ts:32 — o docblock de arquitetura é o documento de design mais importante da camada. Tudo nesta lição decorre dele.
02

Em uma imagem

Antes do detalhe, a forma geral. Um run entra como um RunSpec (uma meta + uma lista de tarefas). O orchestrator monta a fila, libera o que está pronto, despacha workers, escreve cada passo no Store, e segura o que precisa de humano. Quando tudo chega a um estado terminal, o run termina — e pode ser retomado a qualquer momento.

RunSpec goal + tasks[] orchestrator dirige a TaskQueue TaskQueue o que pode rodar agora? workers adapter.run ou subprocess T4 park t4-parked.jsonl Store journal + checkpoint + park ledger grava tudo
Visão de pássaro do @alembic/swarm. O orchestrator é o único que decide; o Store é a única fonte de verdade.
Árvore de três níveis: orchestrator (depth 0) acima, lead (depth 1) no meio, worker (depth 2, leaf) na base, com a régua MAX_DEPTH = 2 à direita.

A árvore de spawn tem exatamente três níveis. O teto MAX_DEPTH = 2 não é um número mágico: é a razão de a recursão nunca explodir.

Faça um palpite antes de seguir

Uma tarefa B declara dependsOn: ["A"]. A tarefa A terminou, mas falhou (status failed). A tarefa B fica ready e roda?

Não. A regra é estrita: B só vira ready quando todo id em dependsOn chegou a done — o sucesso terminal. failed não satisfaz a aresta (em queue.ts:25, SATISFYING = 'done'). Se um pré-requisito falha, o que dependia dele permanece blocked. Falha não se propaga como "pode rodar"; ela trava o que vinha depois.
03

Três papéis, profundidade limitada

O swarm tem exatamente três papéis, ordenados por profundidade (depth). O orchestrator (depth 0) abre o run e dirige a fila. Ele pode criar leads (depth 1). Um lead faz fan-out: pega uma tarefa grande e a quebra em subtarefas. Cada subtarefa é um worker (depth 2), que faz o trabalho de verdade — e é leaf: não pode criar mais ninguém.

Por que parar em três? Porque um worker que pudesse criar outro worker criaria uma cadeia sem fim. O Alembic corta isso com uma constante: MAX_DEPTH = 2. Quem está na profundidade 2 é folha. Ponto.

Pense em uma empresa enxuta: diretor → gerente → executor. O executor entrega; ele não abre uma nova camada de subordinados. Três níveis bastam para dividir qualquer trabalho, e o teto evita uma hierarquia que se reproduz para sempre.

Onde isso vive

types.ts:21 define os papéis: swarmRoleSchema = z.enum(['orchestrator', 'lead', 'worker']). types.ts:25–29 dá a profundidade de cada um em ROLE_DEPTH = { orchestrator: 0, lead: 1, worker: 2 }. E types.ts:32 fixa o teto: export const MAX_DEPTH = 2; — "A worker (depth 2) is a leaf and cannot spawn."

Lead = a tarefa que carrega subtasks

Não há uma "classe Lead". Um lead é simplesmente um TaskSpec que tem o campo subtasks (types.ts:122–124). Quando presente, o orchestrator roda essas subtasks como um sub-run filho, depth-bounded e dependency-gated, e dobra o resultado no resultado do lead — "done iff no subtask failed or stayed blocked". As subtasks são leaf (taskSpecBaseSchema) e não podem ter subtasks, então o aninhamento é estruturalmente limitado a um nível, casando com MAX_DEPTH.

O teto também é checado em runtime por canSpawn (citado em types.ts:120): mesmo que alguém forje uma estrutura mais profunda, o orchestrator se recusa a spawnar além de 2.

PapelDepthPode spawnar?O que faz
orchestrator0sim → leadsAbre o run, monta e dirige a TaskQueue, journaliza tudo.
lead1sim → workersFan-out: roda subtasks como sub-run filho e agrega o resultado.
worker2não (leaf)O trabalho real: chamada ao adapter ou subprocesso. Reporta via arquivo.
Guarde istoMAX_DEPTH = 2 é a fronteira contra recursão infinita. Orchestrator(0) → lead(1) → worker(2, leaf). Três níveis, nunca quatro.
canSpawn(depth) = depth < 2 depth 0✓ spawn depth 1✓ spawn depth 2✗ leaf o teto é checado em runtime, não só por tipo
orchestrator.ts:107 — canSpawn
lead + subtasks[] worker worker worker done iff nenhuma subtask falhou ou ficou blocked
types.ts:122 — fan-out do lead
Flashcard · papéis
Qual papel é leaf e por quê?
clique para virar
O worker (depth 2). Como MAX_DEPTH = 2, um nó nessa profundidade é folha e não pode spawnar — é o que limita a recursão. Orchestrator(0) e lead(1) podem criar o nível abaixo; o worker apenas executa.
04

A fila com dependency-gating

A TaskQueue é o cérebro de "o que pode rodar agora?". Ela é pura: só mexe em estados na memória, não executa nada, não toca em disco. Toda a durabilidade é trabalho do Store. Manter a fila pura assim deixa a lógica de liberação 100% testável, sem adapters, sem worktrees, sem filesystem.

A regra de liberação é uma frase: uma tarefa fica ready somente quando todos os ids do seu dependsOn chegaram a done. Sem dependências? Começa ready. Tem dependências não satisfeitas? Começa blocked. Precisa de park? Começa parked.

É a fila de embarque com prioridade: você só é chamado (ready) quando todas as suas conexões anteriores pousaram (done). Enquanto uma escala estiver no ar, você espera (blocked). E quem tem uma pendência de documento fica retido à parte (parked) — não embarca por conta própria.

Os seis estados

types.ts:43–50taskStatusSchema = z.enum(['blocked', 'ready', 'running', 'done', 'failed', 'parked']). blocked = dependências não cumpridas; ready = tudo cumprido, elegível; running = reivindicada por um worker; done/failed = terminais; parked = T4, retida da execução autônoma.

A regra, no código

queue.ts:14–22 é explícito: "A task is ready only when every id in its dependsOn has reached done. The queue is pure in-memory bookkeeping over TaskState; durability is the store's job. It never executes anything — it answers 'what may run now?' and 'is the run finished?'". O que satisfaz uma aresta é só o sucesso terminal: queue.ts:25const SATISFYING: TaskStatus = 'done';.

O estado inicial

queue.ts:47–59 (initialState): se classifyPark(spec) não for undefined → começa parked (com o parkReason); senão, dependsOn.length === 0 ? 'ready' : 'blocked'.

Fila de tarefas com estados ready, blocked, running e done passando por uma cancela de dependência cuja regra é: ready só quando todo dependsOn chegou a done.

A cancela (gate) só abre quando todo dependsOn chegou a done. A fila não executa — ela só responde "o que pode rodar agora?".

Ciclo de vida de uma tarefa (os seis estados)
blocked ready running done failed parked (T4) deps done claim
regate: A vira done → reavalia B Adone Bblocked → ready satisfaz aresta só 'done' satisfaz — 'failed' deixa B em blocked
queue.ts:156 — re-gate de dependentes
isComplete()? ✗ algum blocked ✗ algum ready ✗ algum running ✓ tudo terminaldone/failed/parked
queue.ts:173 — fim do run
Por que a TaskQueue é mantida "pura" (in-memory, sem executar nada)?
É a (b). O docblock (queue.ts:14–22) diz: a fila "never executes anything — it answers 'what may run now?'". Manter a decisão de liberação separada da execução deixa o dependency-gating testável em isolamento. Durabilidade não é dela: é do Store.
05

O T4 park — a cancela de irreversibilidade

Algumas tarefas nunca devem rodar sozinhas: qualquer coisa de tier T4, qualquer coisa explicitamente irreversível, ou com marcador legal/security. Para essas, o swarm não executa — ele estaciona (park). A tarefa vai para um ledger append-only (t4-parked.jsonl) e exige conselho + adjudicação humana antes de poder rodar.

Isso é uma fronteira de segurança dura, não um palpite. A filosofia, escrita no código: na dúvida, park.

É o cofre com dupla chave do banco. Movimentos rotineiros o caixa faz sozinho. Mas certas operações — apagar um registro, mexer em algo jurídico, uma ação sem volta — exigem que duas pessoas autorizadas estejam presentes. O sistema não decide isso sozinho de madrugada.

Os cinco motivos

types.ts:57–63parkReasonSchema = z.enum(['tier-t4', 'irreversible', 'legal', 'security', 'manual']). O motivo explica por que o orchestrator reteve a tarefa, para que um humano possa decidir a partir do ledger sozinho.

A decisão, em ordem de precedência

park.ts:38–45 (classifyPark) retorna o motivo ou undefined (elegível para execução autônoma). A precedência é exata: (1) marcadores de metadata legal/security/irreversible (a primeira chave verdadeira vence); (2) a flag irreversible do spec → 'irreversible'; (3) a regra de tier: spec.tier === Tier.T4 || isParked(spec.tier)'tier-t4'. "The first matching reason wins so the ledger records the most specific cause."

Para onde vai

park.ts:13–21: tarefas parked são roteadas para t4-parked.jsonl (append-only) pelo Store e exigem "council + human adjudication before they can run". makeParkEntry (park.ts:68–81) re-valida o motivo via Zod — um motivo malformado nunca chega ao ledger — e nunca lança exceção.

Bifurcação de decisão classifyPark: caminho autônomo que roda à esquerda e caminho parked para t4-parked.jsonl à direita, com os motivos legal, security, irreversible e tier-t4.

A função classifyPark é a cancela. Se ela devolve um motivo, a tarefa nasce parked e espera por um humano. Se devolve undefined, segue autônoma.

precedência (o 1º que casa vence) 1 · metadata legal / security 2 · flag irreversible 3 · tier === T4 senão → undefined (roda)
park.ts:38 — ordem das checagens
parked = imóvel replay ✗ · report perdido ✗ recuperação de órfã ✗
queue.ts:147 — fronteira estrutural
Exemplo guiado — a tarefa "apagar a tabela de produção"
1
O autor da plan marca a tarefa com irreversible: true (ou metadata { legal: true }). Tier dela é T2.
2
Ao montar a fila, initialState chama classifyPark(spec). As chaves de metadata são checadas primeiro; depois a flag irreversible casa → motivo 'irreversible'.
3
A tarefa não nasce ready — nasce parked, com parkReason: 'irreversible', mesmo sendo "só" T2. A flag vence o tier.
4
O Store grava um ParkEntry em t4-parked.jsonl. O run continua com as outras tarefas; esta fica esperando aprovação humana.
5
Agora você: uma tarefa é T1 (baixo risco) mas tem metadata { security: true }. Ela roda sozinha? Responda antes de virar o flashcard abaixo.
Flashcard · park
T1 + metadata { security: true }: roda sozinha?
clique para virar
Não. O marcador security é checado antes da regra de tier e força o park com motivo 'security'. O tier baixo é irrelevante: qualquer marcador legal/security/irreversible estaciona a tarefa. Na dúvida, park.
CuidadoNão confunda park com falha. Uma tarefa parked não deu errado — ela foi retida de propósito, com motivo registrado, à espera de um humano. É segurança, não erro.
06

O worker e o protocolo do report file

Quando uma tarefa finalmente roda, quem faz é um worker. Ele tem dois modos: chamar um modelo de IA (adapter.run) ou rodar um subprocesso real — um comando do sistema — quando a tarefa carrega o campo command. Esse segundo modo é a primitiva "build" do ADW: roda algo como pnpm test, e o resultado vira o desfecho (exit 0 = done, qualquer outro = failed).

E como o orchestrator sabe que o worker terminou? Por um truque simples e robusto: o arquivo de report. O worker escreve num arquivo "em progresso" e, no fim, faz um rename atômico para um nome terminal. O rename é o "commit": o orchestrator só observa os nomes finais, então nunca vê um report pela metade.

É a comanda na roda da cozinha. O cozinheiro escreve o pedido num papel e só pendura na roda (.complete) quando o prato está pronto. O garçom (orchestrator) olha só a roda — nunca pega um prato meio-feito. E se o cozinheiro desmaiar, o papel pendurado continua lá: o serviço sabe o que ficou pronto.

Renomear é o commit

worker.ts:23–32: "A worker writes its findings to an in-progress file (<taskId>.report.md), then atomically renames it to a terminal name — <taskId>.complete.md or <taskId>.failed.md. The orchestrator watches only for the terminal names, so it never observes a partially-written report: the rename is the commit. This is filesystem-as-IPC and survives process death, which a shared in-memory channel would not."

worker.ts:35–39REPORT_SUFFIX = { inProgress: '.report.md', complete: '.complete.md', failed: '.failed.md' }. O corpo do report é Markdown com um bloco JSON cercado (worker.ts:56–60): o bloco é a carga legível por máquina; a prosa ao redor torna o arquivo auditável num diff.

Subprocesso real (a primitiva build)

types.ts:92–98: quando command (um array de argv, sem shell — sem superfície de injeção) está presente, o worker roda um subprocesso em vez de uma chamada de modelo. "exit 0 → done, anything else → failed". Com isolate, o comando roda com cwd apontando para o checkout do worktree.

O protocolo: rename atômico = sinal de pronto
<id>.report.md em progresso (escrevendo) <id>.complete.md <id>.failed.md rename atômico = commit
worker adapter.run(input)chamada ao modelo command (argv)subprocesso, sem shell
worker.ts:113 / :200 — dois modos
exit code → outcome exit 0 exit ≠ 0 complete failed
types.ts:92 — a primitiva build
Por que isso é tão robusto? Um rename é atômico no sistema de arquivos: ou aconteceu, ou não. Não existe "meio renomeado". Por isso o orchestrator pode simplesmente poll o diretório e confiar: se o nome terminal apareceu, o trabalho realmente acabou — e o arquivo sobrevive à morte do processo, coisa que um canal em memória não faria.
07

Isolamento: worktree & background

Rodar vários agentes em paralelo cria dois riscos: eles podem pisar no mesmo código ao mesmo tempo, e um pode morrer junto com o orchestrator. O swarm tem um mecanismo para cada.

Para o primeiro: worktree. Com a flag isolate: true, a tarefa roda num git worktree dedicado — um checkout próprio, numa branch própria, com uma porta de rede determinística. Dois workers nunca tocam a mesma árvore de trabalho, e o teardown é garantido.

Para o segundo: background. Com background: true, o worker roda num processo destacado (detached). Ele sobrevive se o pai morrer; o orchestrator coordena só pelo arquivo de report, e um resume re-attacha ao report em vez de refazer o trabalho.

Worktree é dar a cada cozinheiro a sua própria bancada e suas próprias panelas: ninguém esbarra no preparo do outro. Background é o forno de cocção lenta que continua assando mesmo se o chef sair da cozinha — você volta horas depois e o prato está lá, com o bilhete de pronto.

Worktree (isolamento espacial)

worktree.ts:15–27: "Each task runs in its own git worktree on a dedicated branch so concurrent workers never touch the same working tree." A porta é derivada de forma determinística do nome da branch — portForBranch (worktree.ts:38–42) devolve um valor em [PORT_BASE, PORT_BASE + PORT_SPAN) = [4000, 5000). A branch é branchForTask (worktree.ts:45–46) = swarm/<runId>/<taskId>. withWorktree garante o teardown; os comandos git passam pelo defaultGitRunner endurecido (argv, sem shell, com timeout/AbortSignal/cap de buffer). Uma tarefa que pede isolate sem config de worktree no orchestrator falha fechado (types.ts:86–91, orchestrator.ts:72–75).

Background (sobrevivência ao pai)

background.ts:12–21 — "the tac-9 /background pattern": o dispatcher lança o worker e o orchestrator coordena apenas pelo report file (poll + re-attach), nunca pelo handle do processo. Por isso ele sobrevive à morte do pai; num resume, re-attacha em vez de re-rodar. O payload vai por env: BACKGROUND_PAYLOAD_ENV = 'ALEMBIC_BG_PAYLOAD' (background.ts:24). Resolver ok significa "launched", não "finished" (background.ts:35–38). Background exige command e proíbe isolate — um filho destacado não pode compartilhar o ciclo de vida do worktree do pai (types.ts:99–107).

portForBranch — determinística swarm/<run>/<task> hash % 1000 + 4000 porta fixa[4000, 5000)
worktree.ts:38 — mesma branch, mesma porta
orchestrator⨯ morre filho detached unref .complete.md resume re-attacha ao report (não re-roda)
background.ts:12 — sobrevive ao pai
isolate: worktree

Branch dedicada swarm/<runId>/<taskId> + porta determinística em [4000, 5000). Teardown garantido por withWorktree. Isolamento no espaço.

background: detached

Processo destacado que sobrevive ao pai; coordenação só pelo report file; resume re-attacha. Exige command, proíbe isolate. Isolamento no tempo.

O que acontece se uma tarefa pede isolate: true mas o orchestrator não recebeu config de worktree?
É a (c). types.ts:86–91 e orchestrator.ts:72–75 são claros: a config de worktree é "required when any task sets isolate: true; such a task fails closed if this is absent." O Alembic prefere falhar de forma segura a degradar o isolamento sem avisar — coerente com o princípio fail-closed de @alembic/contracts.
08

Store, journal & resume

A única fonte de verdade do swarm é o sistema de arquivos — filesystem-as-truth. Quem cuida disso é o Store. Cada coisa que acontece num run vira uma linha num diário append-only (o journal, events.jsonl). De tempos em tempos, um checkpoint guarda o estado completo. Tarefas estacionadas vão para o park ledger.

É isso que torna um run resumível. Se o processo cair, basta re-rodar: o swarm reproduz o journal a partir do último checkpoint, recupera tarefas que estavam no ar (órfãs) e continua — em vez de começar do zero. E como o diretório do run é endereçado por conteúdo (um hash do RunSpec), o mesmo run sempre aponta para o mesmo lugar.

É o livro-caixa de um contador: cada lançamento é registrado em ordem e nunca apagado. Se faltar luz no meio do fechamento, ele não recomeça o mês — ele lê do último ponto conferido (o checkpoint) e segue dali. O caderno é a verdade; a memória da pessoa, não.

Por que parsear na leitura

O docblock de types.ts:4–12 explica a disciplina: "Every value that crosses a durability boundary (JSONL line, checkpoint, worker report file) is defined here as a Zod schema... Parsing on read is mandatory — the filesystem is the source of truth, so a corrupt or hand-edited line must be rejected at the boundary rather than trusted."

Os eventos do journal

types.ts:203–217swarmEventKindSchema: run-started, run-resumed, task-enqueued, task-state, task-parked, reward, worktree, checkpoint, run-finished (e eventos de nível de run emitidos pela VM acima do swarm: phase-started, phase-finished, run-log). Cada linha (swarmEventSchema, types.ts:225–231) tem um seq monotônico por run para replay determinístico.

Checkpoint & resume

runCheckpointSchema (types.ts:238–245) guarda taskStates + parkedIds num ponto no tempo; "replay starts from the last checkpoint and applies subsequent journal events." O docblock de index.ts:13–16 resume: "crash-safe resumable runs by content-addressed run directory (re-running replays the journal and recovers orphaned in-flight tasks instead of restarting)."

Crash → resume: o journal é o que torna o run recuperável
run-started task-state checkpoint task-state crash resume: lê do último checkpoint → reaplica eventos → recupera órfãs run-resumed readRunProgressmonitor read-only (AFK)
RUN_FILES (filesystem-as-truth) events.jsonl — journal autoritativo checkpoint.json — dobra periódica t4-parked.jsonl — ledger de park
store.ts:35 — RUN_FILES
seq monotônico → replay determinístico 0 1 2 3 SqliteIndex = cache (perder = ok)
store.ts:22 — o índice é só cache
0
MAX_DEPTH (orchestrator→lead→worker)
0
estados de tarefa (blocked…parked)
0
motivos de park (legal…manual)
[4000,5000)
janela de portas do worktree
09

No código

O coração da camada cabe em poucos trechos. Comece pelo docblock de arquitetura — ele é o contrato de design do swarm.

packages/swarm/src/orchestrator.ts:32 — o docblock de arquitetura
/**
 * The 3-tier, depth-bounded orchestrator: orchestrator → lead → worker.
 *
 * It drives a TaskQueue to a terminal state while honoring three hard
 * rules: (1) depth is bounded — a node at MAX_DEPTH is a leaf and may
 * not spawn; (2) dependency-gating — only `ready` tasks run; (3) the T4 park —
 * irreversible/legal/security/T4 tasks are routed to the ledger and never
 * auto-executed. Every transition is journaled through the Store
 * (filesystem-as-truth), and a run is resumable from its last checkpoint.
 */
packages/swarm/src/types.ts:21–32 — papéis, profundidade e o teto
export const swarmRoleSchema = z.enum(['orchestrator', 'lead', 'worker']);

/** Depth of each role in the spawn tree; also the hard recursion ceiling. */
export const ROLE_DEPTH = {
  orchestrator: 0,
  lead: 1,
  worker: 2,
};

/** Maximum spawn depth. A worker (depth 2) is a leaf and cannot spawn. */
export const MAX_DEPTH = 2;
packages/swarm/src/park.ts:38–45 — classifyPark, a cancela de irreversibilidade
export const classifyPark = (spec: TaskSpec): ParkReason | undefined => {
  for (const [key, reason] of REASON_KEYS) {
    if (spec.metadata[key] === true) return reason; // legal / security / irreversible
  }
  if (spec.irreversible) return 'irreversible';
  if (spec.tier === Tier.T4 || isParked(spec.tier)) return 'tier-t4';
  return undefined; // elegível para execução autônoma
};
packages/swarm/src/queue.ts:14–25 — a regra do dependency-gating
/**
 * A dependency-gated task queue.
 * A task is `ready` only when every id in its `dependsOn` has reached `done`.
 * The queue is pure in-memory bookkeeping ...; durability is the store's job.
 * It never executes anything — it answers "what may run now?"
 */
/** Terminal-success status that satisfies a dependency edge. */
const SATISFYING: TaskStatus = 'done';
Detalhe de engenhariaA superfície pública sai toda por index.ts (export * de cada módulo). Lá também mora partitionByAutonomy (index.ts:55), um utilitário leve que separa tarefas em autonomous vs parked pela regra isAutonomous antes de montar um run completo.
10

Experimente

Use as abas para ver o que acontece com cada faixa de tier ao montar a fila. O swarm decide, tarefa por tarefa, entre rodar (autônomo) e park (retido para humano).

roda

Tier T1 — baixo risco

Tarefas de baixo risco são autônomas. Sem dependências, nascem ready e o worker é despachado.

classifyPark → undefined · queue.ts:57
autônomo → roda ready → running → done parked → t4-parked.jsonl espera humano + conselho
Repare: T1, T2 e T3 acendem a faixa de cima (rodam). Só T4 — ou qualquer flag/marcador (irreversible, legal, security) — acende a de baixo (park). É a regra de classifyPark em ação.
11

Recapitulando

Cinco ideias, em sequência. Use as setas do teclado ou os botões.

Recap 1 de 5

Três papéis, teto rígido

O swarm é orchestrator → lead → worker. MAX_DEPTH = 2 faz do worker uma folha: ele não spawna. É isso que impede recursão sem fim.

orchestratorleadworker · leaf
1

Recap 2 de 5

Ready = todos os deps em done

A TaskQueue é pura e só responde "o que pode rodar?". Uma tarefa vira ready apenas quando todo dependsOn chegou a done (SATISFYING='done').

2

Recap 3 de 5

T4 park: na dúvida, park

Trabalho irreversível/legal/security/T4 nunca auto-executa. classifyPark decide por precedência e roteia para t4-parked.jsonl — espera conselho + humano.

3

Recap 4 de 5

Rename = commit

O worker escreve .report.md e renomeia atomicamente para .complete.md/.failed.md. O rename é o sinal de pronto e sobrevive à morte do processo (filesystem-as-IPC).

4

Recap 5 de 5

Journal → resume

O Store grava cada passo (journal append-only) + checkpoints. Re-rodar reproduz o journal e recupera tarefas órfãs — o run é crash-safe, não recomeça do zero.

5
Slide 1 / 5 Use
Por que o L3 importa. É a camada que transforma o "plano" do Factory em execução real, distribuída e auditável. Sem o Swarm, o Factory seria um orquestrador ingênuo de processos. Com ele, vira um sistema de execução agêntica com garantias fortes de isolamento, resumibilidade e segurança — capaz de evoluir o próprio repositório sem quebrá-lo.
12

Verifique

Dois caminhos: confirme o entendimento no quiz e prove a camada rodando os testes reais.

Auto-checagem (3 perguntas)
1. Qual frase descreve corretamente o teto de profundidade do swarm?
(b). types.ts:32: "A worker (depth 2) is a leaf and cannot spawn." Orchestrator(0) e lead(1) podem spawnar; o worker, não.
2. Uma tarefa T2 marcada com irreversible: true — o que acontece?
(a). Em classifyPark (park.ts:42), a flag irreversible é checada antes da regra de tier e força o park. Na dúvida, park.
3. Por que o protocolo de report usa rename atômico em vez de um canal em memória?
(c). worker.ts:23–32: "the rename is the commit. This is filesystem-as-IPC and survives process death, which a shared in-memory channel would not." O orchestrator só vê nomes terminais — nunca um report pela metade.
Acertos: 0/3

Prove rodando

A forma honesta de verificar a camada é rodar a suíte do pacote e ler os testes que cobrem cada garantia.

# roda só o pacote do swarm
pnpm --filter @alembic/swarm test

# o que olhar:
# - orchestrator.test.ts  → as 3 regras duras de ponta a ponta
# - queue.test.ts         → dependency-gating (a regra do 'ready')
# - park.test.ts          → classifyPark + precedência de motivos
# - worktree.test.ts      → isolate + porta determinística
# - process.test.ts       → subprocesso real (command)
Tarefa prática1) Leia orchestrator.ts:32–47 (o docblock). 2) Leia types.ts:43–63 (estados + motivos de park). 3) Rode um swarm pequeno e inspecione o journal no Store. 4) Marque uma tarefa como irreversible e observe o park. 5) Compare um run com e sem isolate: true.
Confusão comum a evitar: achar que "swarm é só um orquestrador simples". Não é. Ele tem journal completo para resume, park ledger separado, depth bounding rígido, e dois modos de execução (adapter vs subprocesso real). Na próxima lição você verá como a camada L4 · Harness expõe todo esse poder para CLI, HTTP, MCP e outros clientes.