Lição 12 · Fusão v3 · Factory: a fábrica de software em sandboxes ← índice
Alembic × Hermes — O Curso de Fusão v3 · Execução e planejamento

Factory: a fábrica de software em sandboxes

O @alembic/factory orquestra agentes de código em sandboxes isolados (Docker, Podman ou worktree git puro) para planejar, implementar, revisar e abrir PRs AFK. Não nasceu aqui: é vendorizado de @ai-hero/sandcastle (MIT, © Matt Pocock) e adaptado ao stack do Alembic — a proveniência está gravada no próprio package.json. Nesta lição: a primitiva run({ agent, sandbox, prompt }), o scaffold .factory/ que dirige a fábrica do próprio repo, e onde o vocabulário factoryPhaseSchema aparece no distill --from/--to.

1

Proveniência declarada no package.json

Regra de ouro da fusão: o que é útil se internaliza — com a fonte citada. O campo description de packages/factory/package.json declara a origem, e o README dedica uma seção "Attribution": as primitivas de orquestração (run, createSandbox, createWorktree, os sandbox providers, o prompt resolver, os agent providers) são trabalho do Matt Pocock, sob MIT, com o copyright original retido verbatim em LICENSE.sandcastle.

packages/factory/package.json — campo description
// trecho real, não editado
"description": "Alembic software factory — orchestrates AI coding agents in
  isolated sandboxes. Vendored and adapted from @ai-hero/sandcastle
  (MIT, (c) Matt Pocock); see README.md."

A adaptação ao Alembic (README, §Attribution): pnpm em vez de npm e o layout de workspaces; os gates pnpm typecheck && pnpm build && pnpm test + impeccable detect docs/; commits convencionais; e o fluxo de main protegida — a fábrica abre PRs, nunca faz push em main. Os providers de sandbox em nuvem proprietários (Vercel, Daytona) não foram vendorizados: o Alembic usa o provider Docker, e aqueles exigiriam SDKs externos.

Pense como… comprar a planta de uma fábrica alemã e montá-la no seu terreno: a linha de montagem é do projetista original (e a placa na entrada diz isso), mas a voltagem, as normas e o portão de expedição são os do seu país. Onde quebra: uma fábrica física se compra uma vez; aqui o vendoring permite EVOLUIR a linha localmente — o código passou a ser de manutenção própria, com os testes do monorepo cobrindo cada peça.

2

A primitiva: run({ agent, sandbox, prompt })

Todo o resto se compõe a partir de UMA função: run (packages/factory/src/run.ts:360–371, com 3 overloads — Output.object devolve output: T tipado via extractStructuredOutput, Output.string devolve texto, sem output devolve o RunResult padrão). Você entrega um agente (claude code, codex, opencode, copilot, cursor, pi — os providers em AgentProvider.ts), um sandbox (sandboxes/docker.ts, podman.ts ou no-sandbox.ts) e um prompt (inline ou promptFile); a fábrica cria worktree + branch, roda o agente isolado e sincroniza o resultado de volta.

Duas decisões de engenharia merecem zoom: (a) estratégia de branch derivada do isolamento — sandbox isolatedmerge-to-head; a estratégia head é RECUSADA com provider isolado (run.ts:386–400: "head branch strategy is not supported with isolated providers") — um agente isolado nunca escreve direto na sua árvore; (b) fan-out por fork de sessãoRunResult.fork(prompt) re-executa a partir da sessão capturada do pai, com id de sessão próprio; forks concorrentes exigem branches distintos (run.ts:339–352, nota do ADR 0018).

backlog GitHub issues ready-for-agent 1 · Planner run({sandbox: docker(), promptFile: plan-prompt.md}) <plan> JSON issues + branches 2 · N em PARALELO (≤4) cada issue: sandbox próprio, branch próprio → implement → review ADVERSARIAL (quem revisa não construiu) 3 · Publisher → abre PR NUNCA push em main (protegida) sandbox isolated ⇒ merge-to-head; 'head' com isolated ⇒ erro (run.ts:386–400) sweepStrayVitest entre iterações: pkill -9 -f vitest — nada de órfãos tinypool acumulando CPU (.factory/run.ts:24–35)
.factory/run.ts: MAX_ITERATIONS=10, MAX_PARALLEL=4, AGENT=factory.claudeCode(…). O plano vem numa tag <plan> parseada do stdout do Planner.
alembic-completo · 0006-hot-paths · s2
A fábrica que constrói a si mesma. O scaffold do repo-raiz .factory/ usa a biblioteca contra o PRÓPRIO backlog do Alembic: plan-prompt.md → implement-prompt.md → review-prompt.md → merge-prompt.md, um agente por fase, cada um em sandbox Docker. E carrega uma cicatriz de guerra: sweepStrayVitest() (.factory/run.ts:24–35) mata vitest órfão entre iterações — a lição do vazamento tinypool (PPID=1, ~90% CPU) aprendida no host, aplicada como rede defensiva.
3

factoryPhaseSchema: o vocabulário das fases

O contrato compartilhado das fases da fábrica vive em @alembic/contracts: factoryPhaseSchema (packages/contracts/src/domain.ts:99–108) — um enum Zod de 7 fases, "from opportunity discovery through shipped product" (domain.ts:95–98).

packages/contracts/src/domain.ts:99–108
// trecho real, não editado
export const factoryPhaseSchema = z.enum([
  'discover',
  'validate',
  'design',
  'plan',
  'build',
  'review',
  'ship',
]);
export type FactoryPhase = z.infer<typeof factoryPhaseSchema>;

Onde ele entra no distill --from/--to

O comando alembic distill aceita --from e --to exatamente com esse vocabulário: distillArgsSchema declara from: factoryPhaseSchema.default('discover') e to: factoryPhaseSchema.default('review') (apps/cli/src/args.ts:74–75) — "from/to bound the phase window of the funnel" (doc do schema, args.ts:62–63). Um valor fora do enum falha o parse na borda (custo $0, nada roda). O mesmo enum alimenta alembic status, que imprime a esteira inteira: phases: factoryPhaseSchema.optionsdiscover -> validate -> design -> plan -> build -> review -> ship (apps/cli/src/commands.ts:464, 472).

Honestidade de fronteira A janela é declarada e validada — mas o corpo atual de runDistill (commands.ts:348–389) não repassa from/to ao runFunnel: o funil executa seu T0→T3 completo. Prova: grep -n 'args\.from' apps/cli/src/commands.ts não devolve nada. Ou seja: hoje o enum é o VOCABULÁRIO canônico (validação de flag + esteira do status); o recorte de execução por fase é assento reservado, não comportamento. Não ensine (nem cobre) o que o código não faz.
discover validate design plan build review ship janela default do distill: --from discover --to review (args.ts:74–75) valor fora do enum ⇒ parse falha na borda ($0) · dentro do enum ⇒ aceito; o funil roda T0→T3 completo alembic status imprime a esteira toda: phases: discover -> … -> ship (commands.ts:472)
Um enum, três usos: validar flags do distill, imprimir a esteira no status e tipar FactoryPhase em quem componha fases.
4

A fábrica em números

7
fases no factoryPhaseSchema (discover → ship)
≤4
implementadores em paralelo no scaffold .factory/
3
sandboxes vendorizados: docker, podman, no-sandbox
28
workspaces no monorepo que a fábrica constrói

O pacote também expõe o binário alembic-factory (package.json, campo bin) e entrypoints dedicados por sandbox (./sandboxes/docker, ./sandboxes/podman, ./sandboxes/no-sandbox — campo exports), então um scaffold escolhe só o provider que usa, sem carregar os demais.

5

Cheque seu modelo mental

Qual é a origem do núcleo de orquestração do @alembic/factory?
Correto: o campo description do packages/factory/package.json declara "Vendored and adapted from @ai-hero/sandcastle (MIT, (c) Matt Pocock)", e o README (§Attribution) retém o copyright verbatim em LICENSE.sandcastle e lista o que mudou (pnpm, gates, main protegida) e o que ficou de fora (providers Vercel/Daytona).
alembic distill corpus --from build --to ship — o que acontece com a janela?
Correto: from/to validam contra factoryPhaseSchema na borda (args.ts:74–75) — só valores do enum passam —, mas runDistill (commands.ts:348–389) não repassa a janela ao runFunnel: hoje ela é vocabulário declarado, não filtro de execução. Prova de ausência: grep 'args\.from' apps/cli/src/commands.ts devolve vazio.
Camada técnica — comandos copy-paste

Ler a proveniência, dirigir a fábrica do repo e ver o enum de fases em ação:

# proveniência: package.json + README §Attribution + licença retida
grep -n '"description"' packages/factory/package.json
sed -n '1,40p' packages/factory/README.md
ls packages/factory/LICENSE.sandcastle

# a primitiva e as validações de branch/isolamento
sed -n '360,430p' packages/factory/src/run.ts

# o scaffold que constrói o próprio Alembic (prompts por fase)
ls .factory/
sed -n '1,60p' .factory/run.ts

# o enum das 7 fases + a janela do distill + a esteira no status
sed -n '99,108p' packages/contracts/src/domain.ts
sed -n '74,75p'  apps/cli/src/args.ts
alembic status        # phases: discover -> validate -> … -> ship

# prova da nota de honestidade (janela não consumida pelo funil)
grep -n 'args\.from' apps/cli/src/commands.ts || echo "sem consumo: só validação na borda"

Regra de leitura: um enum em @alembic/contracts é contrato público — quem for ligar a janela ao funil amanhã já tem o vocabulário validado esperando na borda.

O que levar desta lição
Pergunta de acompanhamento sugerida: "a fábrica isola UM agente por sandbox — e quando eu quero DEZENAS de tarefas-modelo em árvore, com dependências e retomada?" — Próxima lição: @alembic/swarmh.swarm(), o fan-out por template {{item}}, a profundidade máxima 2 e o cache que compõe com mission.