Lição 09 · Alembic Completo v3 · Playbook do operador · gotchas · observabilidade
Alembic Completo v3 · Visual Course

O operador

O dia a dia de quem mantém o motor honesto: o baseline que nunca se negocia, a lei do derive-counts, as OITO gotchas que já custaram noites (cada uma com o erro e o acerto lado a lado), e as superfícies de observar, dispor, lembrar e buscar.

Leia primeiro (fonte primária)
scripts/safe-test.mjs — o porquê está no cabeçalho do próprio script

Esta lição destila a disciplina operacional: safe-test, derive-counts, o rol oficial de gotchas do spec v3, e os comandos de observabilidade/disposição/memória/RAG do CLI.

Você vai conseguir
  • Rodar o baseline canônico e explicar POR QUE ele usa safe-test
  • Aplicar a lei "contagens derivadas, nunca digitadas"
  • Reconhecer as 8 gotchas pelo sintoma e aplicar o acerto de cada uma
  • Observar um run (runs/tui/tail/cockpit/serve) e dispor um T4 parkeado
  • Alimentar a memória multi-store (import A4b) e buscar via RAG
Leia a versão simples, ou abra a camada técnica em qualquer seção.
1

O baseline — e por que ele usa safe-test


Toda mudança de código passa pelo MESMO portão, sem exceção:

# o baseline canônico (spec v3)
pnpm -r typecheck && pnpm -r build && node scripts/safe-test.mjs pnpm -w test
200
arquivos *.test.ts
1.834
testes verdes (pré-#159; #159 soma +4)
10min
teto do safe-test (SAFE_TEST_TIMEOUT_MS)
124
exit code de um hard-timeout

O safe-test.mjs não é frescura: workers tinypool do Vitest podem ÓRFANAR (reparentar ao PID 1) e pinar um núcleo por horas. O script roda a suíte em process group próprio (detached: true ⇒ setsid), aplica teto de relógio, mata o GRUPO inteiro (kill(-pid) alcança o vitest main + todos os forks) e ainda varre sobras com pkill -9 -f vitest — tudo documentado no cabeçalho do próprio scripts/safe-test.mjs.

2

Em uma imagem — a rotina do operador


launch-academy · 06-operator-playbook · s0
① BASELINE typecheck · build safe-test ② OBSERVAR runs list · tui · tail cockpit · serve ③ DISPOR (T4) approve · reject propose ④ LEMBRAR memory add|list import (A4b) ⑤ BUSCAR search context-pack o que a busca revela vira a próxima mudança — que volta pro baseline regra transversal: NUNCA digite uma contagem — derive (scripts/derive-counts.mjs)
As cinco estações do dia: provar (baseline) → ver (observabilidade) → decidir (T4) → registrar (memória) → recuperar (RAG). O ciclo fecha de volta no baseline.
3

A lei — contagens derivadas, nunca digitadas


O pior ofensor recorrente do repositório (eleito pelo digest #1 do failure-historian, Lição 10) era o drift-de-contagem: números escritos à mão em docs que a fonte já desmentia. A mudança de sistema: scripts/derive-counts.mjs (PR #158) — deriva as contagens DA FONTE (o USAGE exportado, o workspace real). Na PRIMEIRA execução pegou drift vivo: docs diziam 32 comandos; o real era 33 top-level / 47 formas documentadas.

Regra prática: número em doc/curso/README ou vem de derive-counts ou vem de um comando citado ao lado. Digitou de cabeça, está errado — talvez hoje, com certeza em duas semanas.

errado — número de cabeça
README: "o CLI tem 32 comandos"
(verdade de 3 semanas atrás)
Drift silencioso: ninguém re-conta ao adicionar comando.
certo — número derivado
node scripts/derive-counts.mjs
→ 33 comandos · 47 formas (do USAGE)
A fonte gera o número; o doc cita a derivação.
4

Gotcha 1 — family-prefix (residue 0 silencioso)


A família vem do 1º segmento do path relativo ao corpus (classifyFamily, packages/etl/src/priors.ts:70). Apontar o distill na raiz da FAMÍLIA rebaixa tudo a Unknown e o residue sai 0 — sem nenhum erro. A Lição 07 conta a história completa.

erro
alembic distill …/Resources/Bookmarks
→ família Unknown → residue 0 (mudo)
acerto
alembic distill /Users/acf/Documents
→ "Bookmarks" é o 1º segmento → roteia
5

Gotcha 2 — symlinks não são seguidos


Dois caminhantes do motor ignoram links simbólicos de propósito: o walkCorpus do distill e o SkillStore (via FsPort readDir) — uma skill de plugin symlinkada sob a base NÃO é importada/lida; só diretórios reais <nome>/SKILL.md contam.

erro
ln -s /repo/skill ~/.hermes/skills/x
→ import --skills não a vê (silêncio)
acerto
cp -R /repo/skill ~/.hermes/skills/x
→ diretório real + SKILL.md → listada
6

Gotcha 3 — empate de custo = ordem de declaração


pickCheapestForTier (packages/contracts/src/registry.ts:244) desempata mantendo o PRIMEIRO declarado no registry. Foi o bloqueio da rota Learnings até o PR #157 (empate T2 de 0.0005 resolvia para um modelo AccessDenied). Ordem no registry é decisão de produção.

erro
registry: [modelo-inacessível, modelo-ok]
empate → 1º declarado → AccessDenied
acerto
registry: [modelo-ok, modelo-inacessível]
empate → 1º declarado → serve ✓ (PR #157)
7

Gotcha 4 — o falso verde do --filter


pnpm --filter X test pode "passar" SEM RODAR NADA: um pacote sem vitest.config.ts próprio não resolve teste nenhum e o comando sai 0. Verde de zero testes é verde de mentira — todo pacote novo @alembic/* precisa do seu config (com setupFiles absoluto).

erro
pnpm --filter @alembic/novo test
→ exit 0 · "No test files found" ignorado
acerto
novo/vitest.config.ts criado
→ exit 0 COM "N passed" no output
8

Gotcha 5 — órfãos tinypool ⇒ SEMPRE safe-test


Matar só o PID do vitest deixa os forks tinypool reparentarem ao PID 1 e fritarem CPU por horas. Defesa em profundidade: timeouts + pool: 'forks' no vitest.config.ts (anti-hang) + scripts/safe-test.mjs (grupo de processo + teto + sweep). Em loop de agente, o baseline SEMPRE via safe-test.

fusao · 25-test-safety-engineering · s1
erro
kill <pid-do-vitest>
→ forks órfãos (PPID=1) pinando núcleos
acerto
node scripts/safe-test.mjs pnpm -w test
→ kill(-pgid) + pkill -9 -f vitest
9

Gotcha 6 — branch nova com dep workspace ⇒ pnpm install


Checkout de uma branch que ADICIONA uma dependência workspace deixa o symlink de node_modules defasado: o tsc falha com Cannot find module '@alembic/X' — enquanto o vitest, resolvendo por caminho, ainda passa. O par confunde; o fix é um comando.

erro
git checkout feat/nova && pnpm -r typecheck
→ Cannot find module '@alembic/X'
acerto
git checkout feat/nova && pnpm install
→ symlinks re-materializados → typecheck ✓
10

Gotcha 7 — contagens à mão NUNCA


É a lei da seção 3 promovida a gotcha oficial: o drift-de-contagem foi a classe de falha mais repetida do repositório (o mesmo padrão em 6+ docs, diz o digest #1). O antídoto tem nome e caminho: scripts/derive-counts.mjs.

erro
editar doc: "… são 32 comandos …"
(re-contado nunca)
acerto
node scripts/derive-counts.mjs → cola 33/47
(o número nasce da fonte a cada edição)
11

Gotcha 8 — squash-merge engana is-ancestor/cherry


Depois de um squash-merge, o commit da branch NÃO é ancestral de main: git merge-base --is-ancestor diz "não mergeado" e git cherry lista tudo como pendente — mentira estrutural. A verdade sobre o que está em main é o LOG de main (procure o título do squash), não a genealogia da branch.

erro
git merge-base --is-ancestor feat main
→ exit 1 → "não entrou" (falso)
acerto
git log main --oneline | grep "feat(…)"
→ o squash está lá → ENTROU
12

Observabilidade — cinco janelas para o mesmo run


Todo run escreve num run-dir (events.jsonl append-only + artefatos por unidade). As janelas só variam o vidro:

run-dir (a fonte única) events.jsonl (append-only) units/<id>/proof-results.jsonl approvals.jsonl · rejections.jsonl meta.json · checkpoint.json runs list — inventário tail -f — o jsonl ao vivo tui — terminal interativo cockpit — dashboard web serve — HTTP + SSE mesmos bytes, nenhuma janela "inventa"
Cinco vidros sobre o MESMO run-dir: a verdade é o events.jsonl append-only; cada janela só muda a lente (linha de comando, terminal, web, SSE).
comandojanelaonde no CLI
alembic runs listinventário dos run-dirscommands.ts:1767 · runRunsList
alembic tui <run-id>terminal interativo do runcommands.ts:1906 · runTui
alembic tail <run-id> -fo events.jsonl ao vivocommands.ts:2290 · runTail
alembic cockpitdashboard web sobre os run-dirscommands.ts:1789 · runCockpit
alembic serveharness HTTP+SSE stateful (POST /runs)commands.ts:1816 · runServe
Camada técnica — copy-paste
alembic runs list
alembic tui run-617ca163
alembic tail run-617ca163 -f
alembic cockpit
alembic serve   # POST /runs aceita goal/plan/contract/yes; eventos via SSE
13

Disposição T4 — o humano no portão certo


Tarefa T4 (legal/segurança/irreversível) não roda: PARKEIA. A disposição é sua, e fica REGISTRADA no run-dir: alembic approve <run-id> --task-id <unit> apensa em approvals.jsonl (commands.ts:2125), reject em rejections.jsonl (commands.ts:2166), e propose <run-id> reabre as parkeadas como um run de proposta (commands.ts:1957). Lembrete vivo da Lição 10: existem 2 tasks t4-parked esperando disposição há 9–14 dias — parkear sem dispor é dívida, não segurança.

Camada técnica — copy-paste
alembic propose run-617ca163                     # reabre T4 parkeadas como proposta
alembic approve run-617ca163 --task-id u-pub-01  # → run-dir/approvals.jsonl
alembic reject  run-617ca163 --task-id u-pub-02  # → run-dir/rejections.jsonl
14

Memória multi-store + import A4b


A memória do motor é JSONL append-only por substore em <dataDir>/memory/<substore>.jsonl — os mesmos cinco nomes do binding de employee: alembic memory <episodic|semantic|procedural|decision|transcript> add|list (commands.ts:5845, runMemory).

Import A4b (commands.ts:5982, runImport): sessões Claude/Codex/ChatGPT viram registros de TRANSCRIPT — formato inferido do path, dry-run $0 por default, --write apensa só os NOVOS (dedupe por id determinístico ⇒ re-importar é no-op). O degrau seguinte, --distill --online, extrai semantic/decision — mas marcado como INTERPRETAÇÃO, nunca fato: proveniência session:<id> + distilled:<modelId> nas associations (tags spoofadas pelo modelo são REMOVIDAS — session-distill.ts:78-89), confidence ≤ 0.5, e resposta malformada ⇒ resultado VAZIO. --distill sem --online falha fechado. --skills importa SKILL.md para o SkillStore (dry-run default; colisão de nome = skip idempotente; symlink não conta — gotcha 2).

Camada técnica — copy-paste
# memória direta
alembic memory decision add --agent failure-historian --record '{"decision":"…"}'
alembic memory episodic list --agent failure-historian --limit 5

# A4b: preview $0 → depois --write (idempotente)
alembic import ~/.claude/projects/…/session.jsonl --agent claude-main
alembic import ~/.claude/projects/…/session.jsonl --agent claude-main --write

# destilar sessão → semantic/decision (marcado model-distilled, conf ≤ 0.5)
alembic import session.jsonl --agent claude-main --distill --online --write

# importar skills (SKILL.md) para o SkillStore
alembic import ~/.claude/skills --skills --write
15

RAG — search e o context-pack --retrieve


O caminho de LEITURA do RAG é offline, $0 e determinístico: alembic search "<q>" <família> (commands.ts:803, runSearch) embeda a query com o MESMO backend offline do índice, carrega embeddings-index/<família>.jsonl e ranqueia por cosseno com desempate determinístico por chunk_id (searchChunkIndex, packages/embeddings/src/query.ts:203). Índice ausente = resultado vazio, não erro.

E a ponte para o trabalho: alembic context-pack <files…> --retrieve "<q>" --family <F> (commands.ts:5663, runContextPack) injeta os chunks mais próximos na camada L3 do pack — opt-in e aditivo (sem --retrieve o pack é BYTE-IDÊNTICO ao anterior); --rerank mmr re-ordena por diversidade via o PURO mmrRerank (packages/embeddings/src/rerank.ts; --lambda 1 = pura relevância, 0 = pura diversidade).

Camada técnica — copy-paste
# cadeia típica: bridge → distill → índice → busca
alembic wiki-bridge ~/Documents/Resources/Bookmarks
alembic embed-index ~/Documents/Resources/Bookmarks
alembic search "budget fail-closed" Bookmarks --top-k 5 --json

# retrieval dentro de um context-pack (L3), com re-rank de diversidade
alembic context-pack src/a.ts src/b.ts --brief "auditar gates" \
  --retrieve "proof gate fail-closed" --family Bookmarks --rerank mmr --resolve-text
Quiz — pnpm --filter @alembic/novopacote test saiu verde. O que o operador conclui?
Gotcha 4: pacote sem vitest.config.ts próprio pode "passar" sem rodar NADA. Verde só vale com "N passed" visível — e o baseline completo roda via safe-test.mjs (gotcha 5).
Recall: baseline com safe-test (1) · números derivados (3) · oito gotchas com sintoma e antídoto (4–11) · cinco janelas de observabilidade (12) · disposição T4 registrada em JSONL (13) · memória idempotente com proveniência anti-spoof (14) · RAG determinístico opt-in (15).
Teaser da Lição 10 (final): o motor virado para si mesmo — o ciclo declarar→mapear→executar→registrar→aprender, o digest que elegeu seu pior defeito e o corrigiu por PR, e o ledger honesto das 7 provas de fronteira.