Lição 17 · Fusão v3 · Conhecimento e ingestão · @alembic/automation ← índice
Alembic × Hermes — O Curso de Fusão v3 · Conhecimento e ingestão

@alembic/automation: manifesto, journal e o runner A4c

O codex do usuário guarda rotinas em ~/.codex/automations/<id>/: um automation.toml (o quê, quando, com qual modelo) e um memory.md (o diário do que aconteceu). O Alembic clonou esse domínio como data-model tipado + journal append-only — deliberadamente SEM daemon — e o PR #159 adicionou a perna que faltava: alembic automation run <id>, com dry-run default, --online gated e UM registro honesto por execução. PAUSED é o cinto de segurança de gasto.

1

A grande ideia: modelar primeiro, agendar nunca (ainda)

O escopo do pacote é declarado em maiúsculas no próprio código: "this is the DATA MODEL and the append JOURNAL. There is NO cron runner / scheduler / process spawner here" (packages/automation/src/types.ts:7–10). Por isso rrule e status viajam como escalares opacos — carregados verbatim, nunca interpretados. A fidelidade é com a fonte real: os sete automation.toml capturados do codex compartilham um núcleo escalar fixo (types.ts:12–15), e tudo que é comportamento de domínio vive no prompt de texto livre.

Pense como… digitalizar o fichário de rotinas de um escritório: primeiro você transcreve as fichas para um formulário validado (o manifesto) e abre um livro de ocorrências (o journal) — só depois, em outra reforma, instala a campainha que dispara as rotinas sozinha. Onde quebra: a campainha (cron daemon) continua fora por decisão, não por atraso — quem toca a rotina é o operador, à mão.

produto · 0005-ai-employee · s3
2

O manifesto: subset TOML + passthrough

Não existe parser TOML como dependência do workspace — e dependência nova é stop-condition (manifest.ts:4–11). Então o pacote lê um subset escalar de TOML feito à mão, modelado no parser de frontmatter do hermes: pares key = value de topo, inteiros bare, strings entre aspas duplas com escapes \"/\\/\n/\t/\r, e um único array de strings em linha (cwds) — exatamente os construtos que os arquivos reais usam (manifest.ts:13–21). Tabelas, floats, datetimes e strings multilinha estão FORA do escopo: valor não reconhecido vira err, nunca throw.

packages/automation/src/manifest.ts:39–49
export const parseAutomationManifest = (text: string): Result<AutomationManifest, Error> => {
  const scanned = scanScalars(text);          // estágio 1: léxico → err "toml: …"
  if (!scanned.ok) return scanned;
  const parsed = automationManifestSchema.safeParse(scanned.value);
  if (!parsed.success) {                      // estágio 2: contrato → err "manifest: …"
    return err(new Error(`manifest: ${parsed.error.issues[0]?.message ?? 'invalid'}`));
  }
  return ok(parsed.data);
};

Os dois estágios de erro são distinguíveis pelo prefixo — toml: para falha léxica, manifest: para falha de contrato (manifest.ts:36–38). O schema (automationManifestSchema, types.ts:49–79) fixa o núcleo: version, id, kind, name, prompt, status, rrule, model, reasoning_effort, execution_environment, created_at, updated_at + o array cwds — chaves snake_case espelhando o TOML verbatim, "so a round-trip is lossless" (types.ts:44–48). E a decisão mais importante: .passthrough() — chave desconhecida é PRESERVADA, "the manifest must never fail-closed on a field a future codex version adds" (types.ts:22–24, 78–79). status fica aberto como string: PAUSED em todas as amostras, mas ACTIVE/outros nunca quebram o parse (types.ts:19–21).

3

O journal: memory.md, append validado, leitura tolerante

<dir>/<id>/ automation.toml 12 escalares + cwds memory.md JSONL · 1 run/linha parseAutomationManifest subset TOML à mão (:4–11) schema .passthrough() Manifest rrule/status opacos AutomationJournal appendRun: valida ANTES, at = clock injetado (:80) readJournal: tolerante (:102) invariantes (journal.ts:15–23) APPEND-ONLY · VALIDATED · BEST-EFFORT READS · NEVER THROWS FsPort injetado (@alembic/etl) — testável em memória
Um diretório por automação: o manifesto é parseado fail-closed com passthrough; o journal só aceita registro validado e lê tolerando linha corrompida.

AutomationJournal.at(fs, dir, clock) liga um journal a um diretório; o arquivo é <dir>/memory.md (JOURNAL_FILENAME, journal.ts:35), espelhando o codex. appendRun tem um detalhe de single-source-of-truth: o caller fornece tudo MENOS o timestamp — o at é carimbado do Clock injetado e "any caller-provided at is overwritten" (journal.ts:71–80). O registro passa por automationRunRecordSchema ANTES de qualquer byte ir a disco (journal.ts:81–86), com status num enum fechado ok | noop | error (types.ts:104). Já readJournal é o oposto deliberado: arquivo ausente → ok([]); linha em branco ou corrompida → pulada em silêncio — "a single bad line never sinks the whole read" (journal.ts:20–23, 102–117).

Por que epoch-ms e não ISO? A fonte usa cabeçalhos ## <timestamp> em prosa; o clone guarda at em epoch-ms vindos do Clock injetado porque a plan-VM proíbe Date.now()/new Date() diretos e o replay determinístico exige o relógio como única fonte de tempo (types.ts:26–29, 89–92).
4

O runner A4c (PR #159): dry-run default, --online honesto

O A4 (lição dos AI Employees) mapeia schedule[] do employee para manifestos registráveis — alembic employee schedule --out escreve cada um como <dir>/emp-<id>-<i>/automation.toml, com status PAUSED de fábrica para nada rodar sozinho. Mas nada no motor EXECUTAVA um manifesto. O PR #159 fecha essa perna com alembic automation run <id> como comando explícito, não-daemon (commit 8d9ed2a, apps/cli/src/commands.ts):

run <id> lê automation.toml parse fail-closed automationTurnInput model = manifest.model system: nome + id + origem employee_id (se A4 marcou) valida modelRunInputSchema DEFAULT: dry-run ($0) imprime o ModelRunInput exato NENHUMA chamada · NENHUMA escrita nota PAUSED: "--online executa" --online (founder-gated) preflight do gateway → falhou? err, $0 adapter.run(input) → imprime o texto sucesso → journal ok (resumo ≤200 ch) falha do adapter → journal error + err journal falhou? logado — run NÃO cai PAUSED ainda roda à mão: invocar run <id> É o override do operador; cron continua fora por decisão (não-daemon)
Duas saídas do mesmo input validado: preview $0 (default) ou turno real com UM registro honesto no journal que o pacote sempre teve — e nada escrevia.

Os detalhes que valem prova: automationTurnInput é puro e determinístico — requestId = automation-<id>, o modelo é o model do próprio manifesto (campo REQUIRED do schema, sem fallback), o prompt do manifesto vira user prompt, e o system prompt declara qual automação está rodando, incluindo a origem employee_id quando o mapper A4 marcou uma. No --online, sucesso appenda status: 'ok' com resumo de até 200 caracteres; falha do adapter appenda status: 'error' com o código — e nos DOIS casos uma falha de escrita do journal é logada mas "never fails the run — the model already answered" (doc de recordAutomationRunOutcome, commit 8d9ed2a). Provado na fronteira: o dry-run no manifesto real emp-failure-historian-0 deixou o diretório intocado, e o --online gerou o primeiro registro de journal de automação da história do motor.

Caveat de honestidade (custou caro)O turno do runner não tem tool-loop: pedir "varra o ledger X" produz uma varredura FABRICADA pelo modelo — foi provado e documentado no próprio commit. Até a perna de tool-loop existir, dados verificados vão DENTRO do prompt; a automação relata, não coleta.
5

A superfície CLI: list · show · run

comandoo que fazescreve algo?
automation listparseia cada <id>/automation.toml sob --dir (default ~/.codex/automations) e imprime a tabela; dir ausente = zero automações, não erronão
automation show <id>imprime UM manifesto por inteiro (--json para a forma estruturada)não
automation run <id>dry-run: preview do ModelRunInput exato; --online: executa o turno e appenda 1 registrosó no --online (memory.md)

O ciclo completo A4→A4c em uma linha de raciocínio: o employee DECLARA (schedule[]) → o mapper MATERIALIZA (employee schedule --out, manifests PAUSED, id determinístico emp-<id>-<i>) → o operador EXECUTA (automation run <id>, à mão) → o journal LEMBRA (memory.md, 1 linha honesta por run). Cada seta é um comando inspecionável; nenhuma dispara sozinha.

6

Cheque seu modelo mental

Um manifesto está com status = "PAUSED". O que alembic automation run <id> faz com ele?
Correto: "A PAUSED manifest still runs: invoking run <id> by hand IS the operator override" (doc de runAutomationExec, commit 8d9ed2a). PAUSED é spend-safe contra agendamento automático — que nem existe ainda (não-daemon por decisão) — não contra o operador. O dry-run inclusive imprime a nota explicando isso.
Uma versão futura do codex adiciona a chave priority = 3 ao automation.toml. O que o parse do Alembic faz?
Correto: automationManifestSchema termina em .passthrough() — "the manifest must never fail-closed on a field a future codex version adds" e chaves extras são PRESERVADAS, não descartadas (types.ts:22–24, 78–79). Fail-closed fica para o que importa: valor léxico inválido (toml:) ou núcleo obrigatório ausente (manifest:).
Camada técnica — comandos copy-paste

Leitura e dry-run, $0:

# listar as automações reais do codex
alembic automation list --dir ~/.codex/automations

# ver um manifesto inteiro (estruturado)
alembic automation show <id> --json

# A4 → A4c: materializar o schedule de um employee como manifests PAUSED
alembic employee schedule <employee-id> --out /tmp/autos
alembic automation list --dir /tmp/autos

# o runner: preview exato do que seria enviado (sem modelo, sem escrita)
alembic automation run emp-<employee-id>-0 --dir /tmp/autos

# o escopo deliberado e os invariantes, na fonte
sed -n '7,15p' packages/automation/src/types.ts
sed -n '15,24p' packages/automation/src/journal.ts

O --online é founder-gated: preflight do gateway falha fechado sem gasto; sucesso appenda 1 linha em <dir>/<id>/memory.md.

O que levar desta lição
Pergunta de acompanhamento sugerida: "o que o digest do failure-historian escreveu no memory.md na primeira execução real?" — Na próxima lição, a matéria-prima do conhecimento: @alembic/sessions lê as sessões salvas de Claude, Codex e ChatGPT e as normaliza para virar memória de transcript (A4b).