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.
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.
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.
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).
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).
## <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).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):
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.
| comando | o que faz | escreve algo? |
|---|---|---|
automation list | parseia cada <id>/automation.toml sob --dir (default ~/.codex/automations) e imprime a tabela; dir ausente = zero automações, não erro | nã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 registro | só 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.
status = "PAUSED". O que alembic automation run <id> faz com ele?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.priority = 3 ao automation.toml. O que o parse do Alembic faz?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:).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.
rrule/status opacos (types.ts:7–10).toml: vs contrato manifest: (manifest.ts:4–11, 36–38)..passthrough(): chave futura do codex nunca quebra o parse — e é preservada (types.ts:78–79).at do clock injetado, leitura tolerante (journal.ts:77–117).--online = preflight + turno + 1 registro honesto ok|error; PAUSED roda só à mão (PR #159).@alembic/sessions lê as sessões salvas de Claude, Codex e ChatGPT e as normaliza para virar memória de transcript (A4b).