Lição 14 · Fusão v3 · VM: o executor que recusa o relógio ← índice
Alembic × Hermes — O Curso de Fusão v3 · Execução e planejamento

VM: o executor que recusa o relógio

O @alembic/vm é quem executa seu alembic.plan.ts — mas antes de importar UMA linha, ele lê o FONTE e recusa qualquer plano que toque Date.now(), new Date() ou Math.random(). O determinismo não é recomendação: é imposto na porta, com erro nomeando o construto. Nesta lição: o pipeline de runPlan, o erro real que o teste do pacote exige, o "sandbox" honesto de plan modules (capabilities via hooks, não jaula de SO) e por que isso compra cache e replay de graça.

1

runPlan: cheque primeiro, importe depois

runPlan (packages/vm/src/run-plan.ts:53–88) segue uma ordem inegociável: ① lê o texto do módulo (readFile); ② roda o cheque de determinismo — .ts passa por checkDeterminismTs (sweep por regex, run-plan.ts:16–28), JS passa por checkDeterminism de @alembic/mission (parse AST com Acorn, packages/mission/src/determinism.ts:51–101); ③ só então registra o loader tsx (ensureTsx, :32–44) e importa; ④ exige export const run função — senão, erro "must export a run(hooks) function" (:74–78); ⑤ injeta os hooks (createHooks) e devolve o summary. E o envelope: nunca lança — todo caminho de falha vira { ok: false, error } (:84–87).

packages/vm/src/run-plan.ts:16–28 — o sweep conservador para .ts
// trecho real, não editado
const checkDeterminismTs = (source: string): { ok: true } | { ok: false; reason: string } => {
  const forbidden = [
    { pattern: /\bDate\.now\s*\(/, name: 'Date.now()' },
    { pattern: /\bnew\s+Date\s*\(\s*\)/, name: 'new Date()' },
    { pattern: /\bMath\.random\s*\(/, name: 'Math.random()' },
  ];
  for (const { pattern, name } of forbidden) {
    if (pattern.test(source)) {
      return { ok: false, reason: `Non-deterministic construct detected: ${name}` };
    }
  }
  return { ok: true };
};
fusao · 28-determinism-and-replay · s1

Pense como… o raio-X da esteira do aeroporto: a mala (seu plano) é escaneada ANTES de embarcar — não existe "abre depois que decolar". Onde quebra: o raio-X procura o proibido com margem de erro humana; aqui o sweep de .ts é assumidamente paranoico — pega o padrão até dentro de comentário, porque "a false positive is safer than allowing non-determinism into a resumable plan" (run-plan.ts:12–14).

2

O erro real, exigido por teste

Isto não é doc aspiracional: o teste do pacote ESCREVE um plano com Date.now() e exige a recusa, com as duas substrings no erro. A mensagem que você verá na prática é "plan module <path> is non-deterministic: Non-deterministic construct detected: Date.now()" (montada em run-plan.ts:61–65).

packages/vm/src/vm.test.ts:125–152 — 'rejects a non-deterministic plan before execution'
// trecho real, não editado (núcleo do teste)
writeFileSync(
  planPath,
  `export const run = async () => ({ now: Date.now() });`,
  'utf8',
);
// …
expect(result.ok).toBe(false);
if (!result.ok) {
  expect(result.error).toContain('non-deterministic');
  expect(result.error).toContain('Date.now');
}
A nuance que separa os dois checadores. O caminho AST (Acorn) permite datas DETERMINÍSTICAS: Date.parse(…) e new Date(isoString) passam, porque o input é fixo — só new Date() com ZERO argumentos é proibido (determinism.ts:5–9, 80–88). O regex de .ts preserva a mesma fronteira: /\bnew\s+Date\s*\(\s*\)/ só casa parênteses VAZIOS. Precisa de "agora" ou de aleatório? Receba clock/seed injetados — nunca do ambiente.
3

O sandbox honesto de plan modules

Chame de sandbox, mas com precisão: não é uma jaula de SO (isso é a lição 12, com Docker/Podman). O plan module roda in-process, importado como ESM (pathToFileURL → import, run-plan.ts:71–72). O confinamento da VM é de OUTRO tipo, em três camadas: ① porta de entrada — o cheque de determinismo recusa o fonte antes do import; ② superfície de capacidades — o plano recebe UM objeto h (AlembicHooks, packages/vm/src/types.ts:58–89) com exatamente 8 verbos: phase, agent, parallel, pipeline, mission, swarm, council, log — modelo, journal e council só existem ATRAVÉS deles, com adapters e store injetados por RunPlanOptions (types.ts:94–119); ③ envelope de errorunPlan captura tudo e devolve { ok:false }, nunca derruba o chamador (run-plan.ts:84–87).

envelope: nunca lança — toda falha vira { ok:false, error } (run-plan.ts:84–87) readFile o FONTE do plano determinismo? .ts: regex · .js: Acorn plan module … is non-deterministic: Non-deterministic construct detected: Date.now() ensureTsx loader p/ .ts import(mod) run é função? run(createHooks) h = 8 verbos → summary sem export run ⇒ "must export a run(hooks) function" (run-plan.ts:74–78) o cheque roda sobre o TEXTO, antes de qualquer import — código proibido nunca chega a executar (vm.test.ts:125–152)
run-plan.ts:53–88 — recusar antes de importar; importar antes de executar; executar só com capacidades injetadas.
4

O que o determinismo compra: cache e replay

A recusa do relógio não é purismo — é o alicerce de duas features. Cache: h.agent/h.swarm calculam computeCacheKey = sha256(JSON estável de {prompt, opts}) (packages/vm/src/cache.ts:20–21) e persistem em <runDir>/workflows/<wf-id>/cache.json (cache.ts:23–24) — só faz sentido porque um plano determinístico gera as MESMAS chamadas a cada execução; --no-cache pula leitura e escrita (RunPlanOptions.noCache, types.ts:117–118). Identidade e replay: o próprio runId é run-<hash> do spec canônico (runIdFor, packages/swarm/src/ids.ts:19–30) — re-submeter o mesmo escopo cai no MESMO run-dir, e alembic replay reaplica events.jsonl + cache em vez de recomputar o mundo.

fusao · 28-determinism-and-replay · s0
alembic.plan.ts export const run = async (h) => … h = AlembicHooks (types.ts:58–89) h.phase / h.log → events.jsonl (phase-started/run-log) h.parallel concorrência limitada, in-process h.pipeline h.agent · h.swarm sha256({prompt,opts}) → cache.json h.mission specs/mission-N.json + runSwarm h.council → CouncilDecision (GO/NO_GO) modelo, journal e council SÓ existem através do h — adapters e store chegam injetados por RunPlanOptions (types.ts:94–119)
hooks.ts: 8 verbos, uma superfície. agent/swarm consultam o cache antes do motor; --no-cache pula leitura e escrita.
A regra prática do CLAUDE.md "Plan modules (alembic.plan.ts) must not use Date.now(), new Date() or Math.random(). The VM rejects them." Se o troubleshooting da sua run diz "Non-determinism error", a correção é sempre a mesma: apague o construto (até de comentários, no caminho .ts) ou mova a necessidade para dentro de um hook — o journal carimba at em cada evento por você (swarm/types.ts:225–231).
3
construtos proibidos: Date.now, new Date(), Math.random
8
verbos no AlembicHooks — a superfície inteira do plano
1.834
testes verdes na suíte que exige esta recusa
5

Cheque seu modelo mental

new Date('2026-01-01') dentro de um alembic.plan.ts — a VM aceita?
Correto: o padrão é /\bnew\s+Date\s*\(\s*\)/ — parênteses vazios (run-plan.ts:19). E o checador AST formaliza a mesma fronteira: new Date(isoString) e Date.parse(…) são permitidos "because the input is fixed" (determinism.ts:5–9); só a chamada com zero argumentos é recusada (determinism.ts:85–88). Determinismo proíbe LER o ambiente, não representar datas.
Seu plan module não exporta run. O que runPlan devolve ao chamador?
Correto: run-plan.ts:74–78 valida typeof mod.run !== 'function' e lança internamente, mas o catch do envelope (:84–87) converte QUALQUER falha em { ok:false, error } — o contrato do executor é nunca derrubar o chamador. O mesmo envelope cobre o erro de determinismo e um plano que explode no meio.
Camada técnica — comandos copy-paste

Provoque a recusa você mesmo, depois leia as duas implementações do cheque:

# 1) um plano deliberadamente sujo
cat > /tmp/dirty.plan.ts <<'EOF'
export const run = async (h) => ({ now: Date.now() });
EOF

# 2) rodar e ver o erro real (a run morre ANTES de executar)
alembic run --goal GOAL.md --plan /tmp/dirty.plan.ts --yes
# → plan module /tmp/dirty.plan.ts is non-deterministic:
#   Non-deterministic construct detected: Date.now()

# 3) as duas portas do cheque
sed -n '16,28p'  packages/vm/src/run-plan.ts          # regex (.ts, conservador)
sed -n '51,101p' packages/mission/src/determinism.ts  # Acorn/AST (.js, permite new Date(iso))

# 4) o teste que exige a recusa
sed -n '125,155p' packages/vm/src/vm.test.ts
pnpm --filter @alembic/vm test

# 5) os dividendos: cache por chamada + replay do disco
cat <runDir>/workflows/agent-1/cache.json
alembic replay <run-id>

Regra de leitura: se você precisa de tempo/aleatório, a pergunta certa não é "como escondo do regex?" — é "qual hook deveria receber isso injetado?".

O que levar desta lição
Pergunta de acompanhamento sugerida: "e QUEM escreve um bom alembic.plan.ts — existe um método para transformar prompt em plano com fases e validação?" — Próxima lição: @alembic/planf3 — a meta-skill de planejamento internalizada, alembic plan "<prompt>" e as 7 imagens de teaching do pacote.