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.
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).
// 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 }; };
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).
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).
// 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'); }
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.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 erro — runPlan captura tudo e devolve { ok:false }, nunca derruba o chamador (run-plan.ts:84–87).
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.
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).new Date('2026-01-01') dentro de um alembic.plan.ts — a VM aceita?/\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.run. O que runPlan devolve ao chamador?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.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?".
run-plan.ts:53–65).new Date(iso) (determinism.ts:5–9).vm.test.ts:148–152).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.