Curso · produto

Alembic — Factory, Forge & cursos

Esta lição mostra as superfícies de construção do Alembic — a fábrica que orquestra agentes em sandboxes (@alembic/factory), o front-end que vira um prompt em escopo executável (forge/plan), e o motor de cursos que renderiza o que você está lendo agora (course + @alembic/design).

Mapa dos quatro pacotes

Os pacotes formam uma linha de montagem com uma única direção: um prompt vira um plano, o plano vira uma execução em sandbox, a execução vira um curso publicável. Cada estação é um pacote isolado com I/O por Result<T,Error> — falha fechada, nunca lança.

@alembic/planf3 createPlan(prompt) → plan.html @alembic/forge loadScope(goal,plan) → runDir/ @alembic/factory run() + createSandbox() → branches + commits @alembic/coda generateCourse() → index.html + en/ @alembic/design WARM_NEUTRAL · renderHtmlPage() (shell desta página)

plan — do prompt ao HTML

O comando

alembic plan "venture: previdência factory MVP"

Por baixo, isso chama createPlan do pacote @alembic/planf3. O pacote tenta primeiro o modelo mais barato de T2 (ou um --model pinado) para extrair um PlanContent tipado; se falhar, ou se rodando offline, cai num template determinístico com cinco fases: Scope → Design → Build → Proof → Ship. Em qualquer caso, você sai com um arquivo HTML auto-contido em .alembic/plans/<slug>/plan.html.

Dois helpers acompanham o plano e são usados pelo runner:

Por que HTML em vez de Markdown? O plano é o primeiro artefato visível: ele tem que renderizar bem no navegador, ser legível em modo escuro, e poder ser anexado a um issue ou compartilhado por link. Markdown forçaria um conversor. HTML auto-contido é a entrega final.

forge — do plano ao escopo executável

O Scope Gate

Antes do runner executar qualquer agente, o Scope Gate de @alembic/forge materializa o diretório do run. O verbo é loadScope:

// packages/forge/src/scope.ts
export const loadScope = async (
  input: ScopeInput,
): Promise<Result<ScopeArtifact, Error>> => {
  // 1. lê e valida GOAL.md (não pode estar vazio)
  // 2. confirma que o plan module existe
  // 3. parseia validation-contract.md via Zod
  // 4. cria runDir/ com index.json, meta.json, tasks.json, run-state.json
  // 5. cria subdirs: council/ units/ workflows/ park/ course/ reports/
  // 6. seeda LOOP-LOG.md e review.md
};

Toda execução começa do mesmo esqueleto. O runId é endereçado por conteúdo (hash do goal + contract + planPath), então rodar o mesmo escopo duas vezes resolve para o mesmo diretório — base do --resume e do cache.

Resume sem driftar

Quando você passa --resume <run-id>, o forge não só reabre o diretório: ele valida que o GOAL.md, o planPath e o validation-contract.md batem com o meta.json original. Se algum mudou, o resume falha fechado com "resume mismatch". Você não consegue acidentalmente trocar de escopo no meio do caminho.

factory — o software factory por trás dos agentes

O @alembic/factory é o sandcastle internalizado: a fábrica MIT do Matt Pocock que vendoramos e adaptamos. Ele dá ao Alembic três primitivas:

Provedores de agente embutidos: claudeCode, codex, copilot, cursor, opencode, pi. Provedores de sandbox: Docker, Podman, ou "no sandbox" (bind-mount direto).

Os cinco templates de fábrica

blank esqueleto vazio prompt + main.mts simple-loop pega issues um por um sequential- reviewer implementa → revisa, mesmo sandbox parallel- planner opus planeja, N sonnets executam parallel-planner- with-review plan → exec+review → merge (4 fases) ↑ menos opinativo mais opinativo ↑ o Alembic dogfooda o parallel-planner-with-review para shippar seus próprios PRs

Em parallel-planner-with-review, a coreografia é literal: um agente opus roda uma iteração com maxIterations: 1 para devolver um JSON <plan> tipado por Zod com os issues não-bloqueados; cada issue ganha um sandbox próprio criado por createSandbox() sob a branch determinística sandcastle/issue-<id>; o implementador roda 100 iterações, o revisor roda uma, e Promise.allSettled garante que uma falha em pipeline não cancela os outros.

Como você desce até a fábrica

Em um repo que já fez sandcastle init (cria .sandcastle/), o template está em .sandcastle/main.mts e você o invoca como:

npx tsx .sandcastle/main.mts

O binário próprio do pacote é alembic-factory (declarado no package.json), expondo subcomandos do CLI Effect com --template, --agent, --model e --image-name. [a verificar] a lista completa de subcomandos do binário não está coberta pelo proof-pack do contrato; consulte packages/factory/src/cli.ts para o conjunto vigente.

course + design — o motor desta página

O Course Gate

Depois que alembic run termina (Scope → Council → Proof → Validator), o Course Gate de @alembic/coda gera o curso da execução. generateCourse escreve dois arquivos:

// packages/coda/src/course.ts
// PT-BR é PRIMÁRIO (raiz do curso, dir=''); inglês espelha sob en/.
const LANGUAGES = {
  pt: { lang: 'pt-BR', dir: '',   title: 'Curso da Execução Alembic', ... },
  en: { lang: 'en',    dir: 'en', title: 'Alembic Run Course',        ... },
};

Ambos renderizam pelo mesmo renderHtmlPage de @alembic/design. É a regra de ouro do design system: uma função, um shell, em todos os lugares — o gerador alembic course, o Course Gate pós-run, e esta lição.

O comando do gerador autônomo

alembic course corpus.json --out /tmp/curso-demo
# → Local-LLM Course … index: /tmp/curso-demo/index.html

O corpus pode ser um arquivo JSON/JSONL de registros, ou uma raiz de família wiki, ou um pai multi-família. O comando escolhe os melhores pacotes via @alembic/coda/corpus-select (theme-argmax sobre o vocabulário canônico de 12 temas), gera o courseManifest tipado em @alembic/contracts, e renderiza tudo via @alembic/design.

O shell em uma página

O renderHtmlPage é uma única função, ~190 linhas em packages/design/src/render.ts, que produz HTML auto-contido (zero requisições externas):

Como funciona por dentro — a anatomia de uma página

O renderHtmlPage é determinístico: mesmas entradas, mesma saída byte-a-byte. Por isso a Course Gate aceita um clock injetável; o lib path nunca chama Date.now() diretamente. A montagem é linear:

  1. tokenDeclarations(preset) achata palette + typography + spacing em --name: value; ordenadas.
  2. renderThemeCss embrulha em :root { ... }; renderDarkThemeCss faz o mesmo sob [data-theme="dark"] só se paletteDark existir (a função é "darkMode default true mas opt-out automático" quando o preset não tem palette escuro — uma regra de proteção do contrato).
  3. O body é injetado como HTML cru depois do <h1> auto-emitido, garantindo que nenhuma página pule do h1 direto para um h3 (regra que o detector impeccable coloca em CI).
  4. O script de toggle e o intersection-observer são inlinados em texto literal — nada de bundle, nada de CSP-quebrável.

O renderHtmlPage com darkMode: false produz a "marca LIGHT pura" — exatamente o artefato que o impeccable detect docs/ roda sem nenhum finding. Por isso o produto e a skill visual-teach usam o mesmo motor: o gate é cego ao consumidor.

Onde isso aparece no seu trabalho

Veja a lição de marketing factory para um exemplo de pipeline construído sobre a mesma narrow-waist (Result, FsPort, contrato), e casos de uso para vê-los amarrados ponta a ponta.

Quiz

1. Por que parallel-planner-with-review usa maxIterations: 1 só no agente planner?

Porque o planner precisa entregar saída estruturada (JSON dentro de <plan>, validado por Zod via Output.object). O comentário no template é explícito: "structured output requires maxIterations: 1". Os implementadores rodam 100 iterações porque estão escrevendo código real; o revisor roda 1 porque o output dele é o próprio diff revisado.

2. O que acontece se você passar --resume run-abc com um GOAL.md diferente do original?

O loadScope falha fechado retornando err(new Error('resume mismatch: GOAL.md differs from original run')). O mesmo vale para planPath e validation-contract.md: todos são comparados contra o meta.json escrito na criação do runDir. Você nunca troca de escopo no meio.

3. Por que o course e a Course Gate usam exatamente a mesma função renderHtmlPage?

Para garantir que o gate de governança da marca (impeccable detect) seja cego ao consumidor: a função produz uma "marca LIGHT pura" que passa o detector com zero findings. Se cada gerador renderizasse à sua maneira, qualquer drift de hex ou tipografia escaparia. Centralizando em @alembic/design, o brand-fail vira erro de build, não erro de revisão visual.