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.
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.
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
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.
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.
README: "o CLI tem 32 comandos" (verdade de 3 semanas atrás)Drift silencioso: ninguém re-conta ao adicionar comando.
node scripts/derive-counts.mjs → 33 comandos · 47 formas (do USAGE)A fonte gera o número; o doc cita a derivação.
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.
alembic distill …/Resources/Bookmarks → família Unknown → residue 0 (mudo)
alembic distill /Users/acf/Documents → "Bookmarks" é o 1º segmento → roteia
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.
ln -s /repo/skill ~/.hermes/skills/x → import --skills não a vê (silêncio)
cp -R /repo/skill ~/.hermes/skills/x → diretório real + SKILL.md → listada
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.
registry: [modelo-inacessível, modelo-ok] empate → 1º declarado → AccessDenied
registry: [modelo-ok, modelo-inacessível] empate → 1º declarado → serve ✓ (PR #157)
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).
pnpm --filter @alembic/novo test → exit 0 · "No test files found" ignorado
novo/vitest.config.ts criado → exit 0 COM "N passed" no output
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.
kill <pid-do-vitest> → forks órfãos (PPID=1) pinando núcleos
node scripts/safe-test.mjs pnpm -w test → kill(-pgid) + pkill -9 -f vitest
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.
git checkout feat/nova && pnpm -r typecheck → Cannot find module '@alembic/X'
git checkout feat/nova && pnpm install → symlinks re-materializados → typecheck ✓
É 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.
editar doc: "… são 32 comandos …" (re-contado nunca)
node scripts/derive-counts.mjs → cola 33/47 (o número nasce da fonte a cada edição)
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.
git merge-base --is-ancestor feat main → exit 1 → "não entrou" (falso)
git log main --oneline | grep "feat(…)" → o squash está lá → ENTROU
Todo run escreve num run-dir (events.jsonl append-only + artefatos por unidade). As janelas só variam o vidro:
| comando | janela | onde no CLI |
|---|---|---|
alembic runs list | inventário dos run-dirs | commands.ts:1767 · runRunsList |
alembic tui <run-id> | terminal interativo do run | commands.ts:1906 · runTui |
alembic tail <run-id> -f | o events.jsonl ao vivo | commands.ts:2290 · runTail |
alembic cockpit | dashboard web sobre os run-dirs | commands.ts:1789 · runCockpit |
alembic serve | harness HTTP+SSE stateful (POST /runs) | commands.ts:1816 · runServe |
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
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.
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
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).
# 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
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).
# 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
pnpm --filter @alembic/novopacote test saiu verde. O que o operador conclui?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).