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.
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:
extractGoalFromPlanHtml(html)— lê o<h1>ou a primeira sentença da seção Purpose.extractValidationFromPlanHtml(html)— colhe cada<li>da seção Validation Commands; é daí que saem os critérios de prova.
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:
run(opts)— uma iteração de agente dentro de um sandbox descartável.createSandbox(opts)— um sandbox nomeado, com branch persistente, que múltiplos agentes podem compartilhar (ex.: implementador + revisor).createWorktree(opts)— um worktree git isolado, mais leve que o sandbox completo.
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
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):
- Tokens — todo o
:root { ... }sai deWARM_NEUTRALemthemes.ts: ivory#FAF9F5, slate#141413, clay#E75533, olive#788C5D, oat#E3DACC. Os mesmos nomes ganham valores escuros sob:root[data-theme="dark"]. - Tipografia — body 20.5px (≥35% maior que o default web),
h1serif 42px,h2mono-uppercase 17px,h3serif 25px. As regras de nível impedem pular hierarquia. - Dark toggle — script de pre-paint evita flash; lê
localStorage["vc-theme"]antes do primeiro frame. - Sidebar opcional — quando
navestá presente, renderiza um aside fixo com scrollspy que destaca a seção corrente. Semnav, vira coluna centrada. - Animação opcional — qualquer elemento com classe
revealouflowganha intersection-observer; respeitaprefers-reduced-motion.
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:
tokenDeclarations(preset)achatapalette + typography + spacingem--name: value;ordenadas.renderThemeCssembrulha em:root { ... };renderDarkThemeCssfaz o mesmo sob[data-theme="dark"]só sepaletteDarkexistir (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).- O
bodyé injetado como HTML cru depois do<h1>auto-emitido, garantindo que nenhuma página pule doh1direto para umh3(regra que o detectorimpeccablecoloca em CI). - 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
- Quando você roda
alembic plan, sai um HTML pronto para anexar a um issue. É o pacoteplanf3. - Quando você roda
alembic run --goal --plan, o forge cria o diretório, a fábrica orquestra os agentes, e o coda fecha com um curso da execução. Os três pacotes em sequência. - Quando você roda
alembic course corpus.json, o coda gera um curso multi-lição sobre um corpus distilado. Mesma renderização. - Quando você roda
alembic forge publish <course-dir>, ele publica um gist privado (e Cloudflare Pages com--pages). É o Publish Gate.
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.