Curso / Lição 04
Lição 04 · O resultado-chave

O loop fechado auto-evolutivo

A Lição 3 nomeou o ciclo de aprendizado como a pedra angular da fusão. Aqui está como ele realmente é entregue: três subsistemas — memory, learning, curator — que juntos deixam uma run finalizada tornar a próxima mais esperta, sem nunca auto-escrever uma lição não validada na memória durável.

Leia primeiro (fonte primária)
ADR-0018 — Internalize a validator-gated self-improvement loop

Esta lição destila docs/adr/0018-internalize-validator-gated-self-improvement-loop.md + o código real em packages/hermes/src/ + o docs/hermes-complete-map.md. Todo número e linha de código tem fonte no rodapé. Por que importa pra missão: é o mecanismo que faz o Alembic aprender entre runs — a diferença entre um agente que repete erros e um que sedimenta acertos.

Objetivos desta lição
  • Explicar por que o snapshot de memória é congelado no início e o que isso faz pelo cache de prompt.
  • Distinguir propor (o revisor) de dispor (o Validador) — a decisão central da ADR-0018.
  • Rastrear uma proposta pelo kernel reviewAndLearn até um dos três baldes: applied / rejected / failed.
  • Justificar por que auto-aplicar foi rejeitado de propósito (o modo de falha da ADR-0006) e ler a máquina de estados do curador.
0
subsistemas no loop (memory · learning · curator)
0.0
piso de score padrão (scoreThresholdGate)
0
baldes de resultado (applied/rejected/failed)
0
transições do curador (nunca deleta)

01 · Três partes, um loop

Antes do código, o modelo mental. "Auto-evolutivo" soa mágico, mas é um circuito de três peças concretas. Pense numa oficina que melhora entre turnos: ela trabalha com o caderno de notas do jeito que estava ao abrir de manhã (memory), no fim do dia um revisor propõe anotações novas e um inspetor decide quais entram (learning), e periodicamente alguém arruma a estante de ferramentas — sem jogar nada fora (curator). A oficina de amanhã abre com o caderno mais rico.

O LOOP FECHADO · resumo do turno → revisão com portão → o snapshot de amanhã fica mais rico
1 · memory/ snapshot congelado no início 2 · learning/ propor → portão → aplicar 3 · curator/ active→stale→archived resumo do turno telemetria escritas aprovadas sedimentam → o snapshot da próxima sessão fica mais rico o curador mantém o lado de skills limpo (nunca deleta)
A peça que torna o loop "fechado": a seta de baixo. O que o learning aprova num turno não some — sedimenta no arquivo, e o próximo início de sessão lê esse arquivo como snapshot. É essa realimentação que faz "a próxima run é mais esperta" ser literal, não retórica.

02 · Memory — o snapshot congelado

Dois stores limitados, em arquivo, persistem entre sessões: MEMORY.md (as próprias notas do agente) e USER.md (o que ele sabe sobre o usuário). Ambos são injetados no system prompt como um snapshot congelado no início da sessão. A analogia: é uma fotografia do caderno tirada quando a sessão abre. Você pode escrever no caderno durante o dia (e a tinta seca de verdade, no disco), mas a fotografia colada na parede não muda até você tirar uma nova — amanhã.

Por que congelar? O cache de prompt

Parece estranho não atualizar o snapshot na hora. A razão é puramente de engenharia, e vale entender:

ESCRITA NO MEIO DA SESSÃO · vai pro disco, mas o prefixo do prompt fica intacto (cache quente)
início da sessão próximo início load → snapshot escrita (meio) novo load prefixo do prompt = o snapshot · IMUTÁVEL a sessão inteira → cache fica quente escrita → disco (durável já) prefixo NÃO muda → ← só o novo load reflete

Modelos cacheiam o prefixo do prompt: se o começo não muda, a parte cara já está computada. Se cada escrita reescrevesse o snapshot, o prefixo mudaria e o cache esquentaria do zero a cada nota. Por isso a disciplina:

// packages/hermes/src/memory/memory-store.ts:50-57
export const ENTRY_DELIMITER = '\n§\n';
/** Limite de caracteres padrão para o store MEMORY.md (padrão do Hermes). */
export const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
/** Limite de caracteres padrão para o store USER.md (padrão do Hermes). */
export const DEFAULT_USER_CHAR_LIMIT = 1375;

Este subsistema é um CLONE fiel de tools/memory_tool.py (1089 LOC). Os desvios são deliberados: IO é injetado via FsPort, e toda op falível retorna Result<T,Error> em vez de um dict Python. O mergulho completo no MemoryStore é a Lição 07.

Dois stores, dois limites

São dois arquivos distintos, com tetos de caracteres distintos — porque servem a propósitos distintos. As notas do agente cabem mais (2200); o que ele sabe do usuário é mais enxuto (1375):

MEMORY.md vs USER.md · os dois stores e seus limites de caracteres (não tokens)
MEMORY.md notas do agente 2200 chars USER.md o que sabe do usuário 1375 chars Limite em CARACTERES = independente do modelo (tokens variariam por tokenizer). Mesma escala nas duas barras.

A anatomia de uma op de memória

A op memory tem três ações, e o detalhe que importa: replace/remove acham o alvo por uma substring curta e única — sem IDs. As entradas ficam separadas por § numa linha própria:

OP memory · ações add / replace / remove · entradas delimitadas por § · alvo por substring (sem IDs)
add replace remove o arquivo (MEMORY.md / USER.md) entrada 1 … § <- delimitador (linha própria) entrada 2 … <- alvo achado por SUBSTRING única § entrada 3 …
Lembre-se: limite em caracteres, não tokens. Tokens variam por modelo; caracteres não. É a mesma disciplina de determinismo que vai voltar no curador (seção 08), onde o tempo também é injetado em vez de lido do relógio do sistema.
POR QUE CARACTERES, NÃO TOKENS · o mesmo texto, contado de dois jeitos
um mesmo trecho de memória, dois jeitos de medir CARACTERES mesmo nº em QUALQUER modelo → determinístico ✓ TOKENS varia por tokenizer/modelo → limite imprevisível ✗ Por isso o limite é em caracteres: o mesmo arquivo "cabe ou não cabe" igual, troque o modelo que quiser.

03 · A escolha de design mais importante (ADR-0018)

A escolha de design mais importante — ADR-0018

O Hermes auto-escreve na memória após um turno. O Alembic não. O revisor apenas propõe; o Validador existente do Alembic dispõe. As escritas são com portão do Validador, nunca auto-aplicadas.

Em uma frase: propor ≠ dispor. Quem sugere uma lição (o revisor) não é quem decide se ela entra (o Validador). Em quase todo o resto a fusão clona o Hermes "quase literalmente"; aqui ela diverge de propósito. Por que a mudança? Duas razões do ADR, ambas fundamentadas:

FIDELIDADE À FONTE · memory e curator são CLONE quase literal · learning DIVERGE de propósito
CLONE (quase literal) memory/ = tools/memory_tool.py (1089 LOC) curator/ = apply_automatic_transitions DIVERGE (de propósito) Hermes: auto-escreve (daemon após o turno) learning/ propõe → portão (síncrono) A fusão clona o que é bom como está; só muda onde a fonte conflitaria com um invariante (aqui, o Validator Gate).
PROPOR ≠ DISPOR · dois papéis separados, não um só (a divergência consciente da ADR-0018)
REVISOR propõe · sugere · pontua { target, op, rationale, score } VALIDADOR dispõe · aprova / rejeita só o aprovado MEMÓRIA escrita durável no Hermes essas duas caixas são UMA SÓ (auto-escreve) — a ADR-0018 as separa Uma sugestão sozinha NUNCA vira escrita. Tem que passar pelo Validador no meio.

Então o ciclo são três portas injetadas e um kernel. As portas são as "tomadas" onde produção e teste plugam implementações diferentes — exatamente o padrão de injeção da Lição 05:

PortaPapel
ReviewProposerRetorna ReviewProposals a partir do resumo do turno — cada um um { target, op, rationale, score }. Em produção encapsula uma chamada de ModelAdapter; em testes, um fake.
ReviewGateDispõe cada proposta (aprova/rejeita). O padrão é scoreThresholdGate(0.7); o Validador real do coda conecta depois fornecendo seu próprio gate — sem mudar o kernel.
MemoryStoreO store onde escritas aprovadas se aplicam — reusando seu dedup, então rever um fato reforça em vez de duplicar.
A FORMA DO LEARNING · um kernel pequeno + três portas injetadas (cada uma com fake em teste)
reviewAndLearn o kernel (orquestra) ReviewProposer prod: 1 ModelAdapter · teste: fake ReviewGate padrão: scoreThresholdGate(0.7) MemoryStore reusa o dedup (reforça, não duplica) LearnOutcome applied / rejected / failed trocar qualquer porta (ex.: gate do coda) não toca o kernel — a costura é a injeção (Lição 05)

04 · O kernel reviewAndLearn

Aqui está o coração do learning — pequeno de propósito. Leia primeiro o fluxo de cada proposta, depois o código exato. A regra de ouro: tudo que pode falhar de verdade falha fechado (para o passo inteiro), mas uma rejeição normal não é falha — é um resultado esperado que vai para um balde.

CAMINHO DE UMA PROPOSTA · validar (Zod) → portão → aplicar, e os três baldes de destino
proposta crua do revisor (saída de modelo, NÃO confiável) Zod valida na fronteira? NÃO failed (registra, não lança) SIM portão aprova? (score ≥ 0.7) NÃO rejected (ok normal) SIM store aceita a escrita? SIM applied escrita durável NÃO (rejeição do store) → failed erro ≠ rejeição erro de proposer ou portão → falha o PASSO INTEIRO fechado
// packages/hermes/src/learning/review.ts:54-69 — o kernel
export const reviewAndLearn = async (summary, deps) => {
  if (summary.trim().length === 0) return ok(emptyOutcome());   // "Nada a salvar."
  const proposed = await deps.proposer(summary);
  if (!proposed.ok) return proposed;                          // erro do proposer → falha fechada
  if (proposed.value.length === 0) return ok(emptyOutcome());
  const acc = { applied: [], rejected: [], failed: [] };
  for (const raw of proposed.value) {
    const stepErr = await processOne(raw, deps, acc);   // validar → portão → aplicar
    if (stepErr) return stepErr;                            // erro do portão → falha fechada
  }
  return ok({ applied: acc.applied, rejected: acc.rejected, failed: acc.failed });
};

Três baldes de resultado — applied / rejected / failed — então nada é descartado em silêncio. A saída do proposer é validada por Zod na fronteira (é saída de modelo não confiável em produção). Um erro de proposer ou portão falha o passo inteiro fechado; uma rejeição do store a uma escrita aprovada é registrada em failed, nunca lançada.

Rejeição (dado) ≠ erro (falha)

A distinção que confunde mais gente: uma proposta reprovada não é uma falha. Reprovar é resultado esperado e vem como ok(...); só uma quebra real (proposer ou portão estourando) vira err(...) e fecha o passo. Veja os dois mundos lado a lado:

DUAS COISAS QUE PARECEM IGUAIS · rejeição volta como ok(verdict) · erro volta como err e fecha tudo
REJEIÇÃO = dado esperado portão diz "não" (score < piso) ok({ approved:false }) → balde rejected · laço CONTINUA ERRO = quebra real proposer ou portão estoura err(Error) → passo INTEIRO falha fechado A decisão vive em verdict.approved, não no Result. Confundir os dois é o erro nº 1 de quem lê o kernel.
Em uma frase: o revisor entrega uma lista de sugestões; o kernel passa cada uma por um filtro e coloca em uma de três caixas — "entrou", "não passou" ou "deu erro ao gravar". Se o filtro em si quebrar (não só reprovar), ele para tudo e reporta — nunca finge que deu certo.
No detalhe: reviewAndLearn retorna Result<LearnOutcome, Error>. Resumo vazio ou zero propostas → ok(emptyOutcome()) (curto-circuito barato). Um !proposed.ok propaga o erro tal e qual (fail-closed). processOne retorna Error | undefined: undefined = continua o laço (a proposta já foi rotulada num balde); um Error = falha o passo inteiro. Distinção crucial: rejeição de gate é dado (ok), erro de gate é falha (err).

05 · O portão e o piso de 0.7

O portão padrão é deliberadamente simples — um check puro de score. "Com portão" não quer dizer "humano no loop"; quer dizer "tem que passar um piso de qualidade". O piso padrão é 0.7:

// packages/hermes/src/learning/gate.ts:24-36 — o portão conservador padrão
export const scoreThresholdGate = (min = DEFAULT_REVIEW_SCORE_THRESHOLD) => {
  return async (proposal) => {
    const approved = proposal.score >= min;          // limite inclusivo: score === min aprova
    const reason = approved
      ? `score ${proposal.score} ≥ threshold ${min}`
      : `score ${proposal.score} < threshold ${min} (learn only from validated wins)`;
    return ok({ approved, reason });                  // puro + total: ok(verdict) para toda entrada
  };
};

O limite padrão é 0.7 — a codificação mecânica da regra do hermes-mini-loop "aprender só com vitórias validadas". Note que a decisão vive em verdict.approved, não no Result: uma rejeição é um ok(...) normal, não um erro.

Preveja antes de continuar
Uma proposta chega com score exatamente 0.7, e o portão padrão está em uso. Ela é aprovada ou rejeitada? Pense no operador antes de revelar.
Aprovada. O check é proposal.score >= min — o >= torna o limite inclusivo: score === min passa. Se você chutou "rejeitada", provavelmente leu como > (estrito). O comentário no código deixa explícito: "limite inclusivo: score === min aprova". É um detalhe pequeno mas é justamente o tipo de fronteira que um teste de unidade trava — 0.7 aprova, 0.699… não.

A escala de score, lado a lado com o piso

Veja onde o piso corta. Tudo de 0.7 pra cima aprova (verde); abaixo, rejeita (clay) — e rejeitar é normal, não erro:

SCORE vs PISO 0.7 · ≥ 0.7 aprova (sedimenta) · < 0.7 rejeita (balde rejected, sem erro)
< 0.7 → rejected (ok normal) ≥ 0.7 → applied piso 0.7 (inclusivo →) 0.0 1.0 "aprender só com vitórias validadas" — o 0.7 é essa regra escrita em código.
Por que o piso é configurável: scoreThresholdGate(min) recebe o piso. O 0.7 é só o padrão conservador. Depois, o Validador completo do coda pode plugar um gate inteiramente diferente (uma rodada de Council, por exemplo) sem tocar uma linha do kernel — a porta ReviewGate é a costura. É o mesmo padrão "injete o comportamento, não o codifique" da seção 03.

06 · Siga uma proposta de ponta a ponta (passo a passo → agora você)

Você viu o fluxograma e o código. Agora rode-os na mão com um exemplo concreto, devagar — depois um caso é seu. Recuperar o procedimento (não só ver o veredito pronto) é o que fixa.

Exemplo resolvido · o revisor propõe duas escritas após um turno bem-sucedido
1
Há o que revisar? O summary do turno não é vazio → o kernel não curto-circuita. (Resumo vazio retornaria ok(emptyOutcome()) — "Nada a salvar".)
2
O revisor propõe. deps.proposer(summary) retorna duas propostas: P1 { op:'add', score:0.82 } e P2 { op:'add', score:0.55 }. A chamada deu ok (sem erro de modelo) → segue.
3
P1 entra no processOne. Zod valida o formato ✓ → portão: 0.82 >= 0.7 ✓ aprova → o store aceita ✓ → vai para applied.
4
P2 entra no processOne. Zod valida ✓ → portão: 0.55 >= 0.7 ✗ → ok({approved:false}) → vai para rejected. Não é erro: o laço continua normalmente.
5
Veredito do passo. ok({ applied:[P1], rejected:[P2], failed:[] }). Uma escrita sedimentou; uma foi barrada pelo piso; nada falhou. O snapshot da próxima sessão incluirá P1.
Agora você: uma terceira proposta P3 chega com score:0.9, passa o portão, mas o MemoryStore rejeita a escrita (digamos, estouraria o limite de caracteres). Em qual balde ela cai — applied, rejected ou failed? Decida antes de revelar.
failed. P3 foi aprovada (não é rejected, que é só para o portão dizer "não"), mas a aplicação no store não deu certo — e a regra é "uma rejeição do store a uma escrita aprovada é registrada em failed, nunca lançada". A pegadinha: rejected = barrada pelo portão; failed = aprovada mas o store não gravou. E nenhum dos dois lança — só um erro de proposer ou de portão faz o passo inteiro falhar fechado.

07 · Com portão vs auto-aplicar — a opção rejeitada de propósito

A ADR-0018 não escolheu "com portão" por inércia: ela considerou e rejeitou a alternativa óbvia — auto-aplicar (o comportamento literal do Hermes). Veja os dois caminhos lado a lado. Repare onde o caminho de cima contorna o gate, e por que isso é justamente o que a ADR-0006 proíbe:

DOIS CAMINHOS · auto-aplicar (rejeitado) contorna o gate · com portão (escolhido) compõe com ele
✗ AUTO-APLICAR (literal do Hermes) — considerado e REJEITADO turno termina revisor sugere Validator Gate CONTORNA o gate → lição não validada endurece (o modo de falha da ADR-0006) memória durável (sem filtro) ✓ COM PORTÃO (escolhido) — compõe com o pipeline de gates turno termina revisor PROPÕE Validator Gate só o aprovado memória durável (passou o piso) Mesmas pontas; a diferença é o filtro no meio. "Mais rápido" não vence "não corrompe a memória".
Auto-aplicar seria mais rápido. Foi rejeitado de propósito. A ADR-0018 considerou "auto-aplicar escritas após cada run (comportamento literal do Hermes)" e rejeitou: burla o Validator Gate e deixa lições não validadas endurecerem na memória durável — exatamente o modo de falha que a ADR-0006 existe para prevenir. O ponto inteiro da fusão é que o ciclo compõe com o pipeline de portões em vez de contorná-lo.

08 · Curator — a metade do descarte

Memory e learning são a metade que acumula. O curador é a metade que mantém limpo. O agente cria skills; telemetria de uso se acumula; o curador é o passo determinístico que arruma a biblioteca. É um CLONE fiel de agent/curator.py:apply_automatic_transitions, com quatro regras clonadas exatamente:

O tempo é um Clock injetado — nunca Date.now() (a regra de determinismo do motor, e o que torna os testes de transição reprodutíveis). O curador é o mesmo Clock com que o usage store foi construído, então um evento registrado "agora" e uma transição decidida "agora" concordam.

As guardas, antes das transições

Antes de decidir qualquer transição, o curador filtra. Duas guardas eliminam a maioria das skills sem nem olhar para datas — é o que impede o curador de mexer no que não é dele:

PORTÕES DE ENTRADA DO CURADOR · proveniência e pin filtram ANTES de qualquer transição
uma skill createdBy === 'agent'? NÃO PULA (intacta) SIM pinned? SIM PULA (intacta) NÃO decide transição (pela data + Clock) duas guardas em série · só skills do AGENTE e NÃO pinadas chegam à máquina de estados

A máquina de estados

Passadas as guardas, restam quatro transições entre três estados. Repare: não há nenhuma seta saindo para "deletado" — o estado terminal é archived, e uma skill stale pode voltar para active se for usada de novo:

MÁQUINA DE ESTADOS DO CURADOR · active ⇄ stale → archived (terminal; nunca delete)
active stale archived passa staleAfter usada de novo → reativa passa archiveAfter active passa archiveAfter → archived (pula stale) terminal · sem seta de delete

O mergulho completo no UsageStore + curador (a telemetria que alimenta as datas) é a Lição 09.

09 · O ciclo de vida, acumular vs descartar (lado a lado)

As duas metades do loop fechado seguem a mesma filosofia conservadora, mas em direções opostas. Veja-as na mesma tabela — uma sedimenta conhecimento sob portão; a outra poda skills sem nunca apagar:

DimensãoLearning (acumula)Curator (descarta)
O que moveNotas de memória (MEMORY.md/USER.md)Estado de skills (active/stale/archived)
GatilhoResumo do turno (pós-unidade)Telemetria de uso + passagem do tempo
O filtroReviewGate (piso 0.7 padrão)Guardas: proveniência (agent) + pin
DecisãoProbabilística (modelo propõe, gate dispõe)Determinística (Clock injetado, sem aleatório)
Ação máximaEscrita aprovada (durável)Arquivar — nunca deletar
Reversível?Dedup reforça em vez de duplicarstale → active (reativa se reusada)
O fio comum: nenhuma das metades age sozinha de forma irreversível. O learning exige um piso antes de escrever; o curador nunca apaga. "Conservador por padrão" é a assinatura do loop inteiro — e é o que o torna seguro de deixar rodando.
A simetria: learning é a porta de entrada (o que merece virar memória), curador é a porta de saída (o que merece deixar de ser ativo). Juntos formam um circuito fechado que se mantém útil sem inchar nem corromper.
UM CIRCUITO, DUAS PORTAS · entrada (learning, com piso) e saída (curator, sem delete) fecham o loop
ENTRADA · learning admite sob piso (0.7) acumula conhecimento o que circula memória + skills úteis SAÍDA · curator arquiva, nunca deleta poda o lado de skills reativação (stale → active) realimenta o útil de volta à entrada As duas portas são conservadoras: uma exige piso para entrar, a outra recusa apagar. Por isso o loop é seguro AFK.

10 · Como isso se encaixa

Você dissecou o loop por dentro. Agora dê um passo atrás: onde ele liga no resto da máquina? O loop fechado não é um apêndice — é o que acontece depois que uma run termina e antes que a próxima comece. Ele se pluga na ponta de saída do pipeline (uma run finalizada, já passada pelos gates) e alimenta a ponta de entrada da run seguinte (o snapshot de memória mais rico).

O LOOP NA TUBULAÇÃO · run finalizada (gates passaram) → ESTE loop → snapshot da próxima run · clique para percorrer
run finalizada Proof + Validator passaram (Lição 17 · a pipeline de gates) resumo do turno ESTE loop (Lição 04) memory · snapshot congelado (Lição 07) learning · propõe → portão (Lição 08) curator · poda skills (Lição 09) só o aprovado sedimenta escrita aprovada próxima run abre com o snapshot mais rico o ciclo fecha: a próxima run executa mais esperta → vira a próxima "run finalizada" Upstream = os gates (Lição 17). Esta peça (Lição 04) destila e poda. Downstream = o início da próxima sessão. É a seta tracejada de volta que torna o loop "fechado" — sem ela, seria só um pós-processamento descartável.

Onde você está na metodologia: as Lições 01–03 montaram o motor, a engenharia reversa do Hermes e a matriz de fusão; as Lições 14–19 detalham a cintura estreita, o funil, os gates, o Council e o swarm que executam uma run. Esta Lição 04 é a peça que fecha o circuito por cima de tudo isso — ela transforma o resultado de uma run executada (sob os gates) no combustível da próxima. Para ver as 30 peças no mesmo mapa e como cada uma liga na seguinte, abra o mapa interativo da metodologia (ou volte ao hub do curso).

As peças que este loop conecta — cada link abre o mergulho na peça vizinha, e a frase diz por que elas se tocam:
  • Lição 07 · MemoryStoreo store que este loop lê no início (snapshot congelado) e onde as escritas aprovadas pousam; aqui você só viu a forma, lá você vê o dedup, os delimitadores § e os limites em caracteres por dentro.
  • Lição 08 · reviewAndLearno kernel "propor → portão → aplicar" desta seção, esmiuçado: as três portas injetadas, os três baldes e a validação Zod na fronteira, linha a linha.
  • Lição 09 · UsageStore + curadora outra metade do loop (a do descarte): a telemetria de uso que alimenta as datas e a máquina de estados active⇄stale→archived que poda sem nunca deletar.
  • Lição 17 · A pipeline de gateso que vem ANTES (upstream): é o Validator Gate dessa pipeline que o loop reusa para "dispor" — por isso auto-aplicar foi rejeitado, pra não contornar este portão (a ligação com a ADR-0006).

11 · Na prática

Chega de diagrama — eis o loop como comandos reais. O passe de aprendizado é opt-in: uma run normal não aprende. Você liga a peça desta lição com a flag --learn numa run com escopo (ADR-0018). O passe roda depois que os gates passam, e é não-fatal — se ele falhar, a run verde continua verde.

# Liga o passe de aprendizado pós-run (opt-in, ADR-0018).
# --offline mantém tudo hermético e $0 (adapter offline determinístico).
alembic run --goal GOAL.md --plan alembic.plan.ts --learn --offline --yes

# … a run executa as units e passa os gates como sempre …
# então, no fim, o passe de aprendizado roda e imprime UMA linha:
#   learning gate: 1 applied, 1 rejected
# (evento de log: run:learning-pass-done { applied:1, rejected:1, failed:0 })
# as escritas APROVADAS sedimentam no MEMORY.md / USER.md do dir da run.

Fonte da forma exata: apps/cli/src/index.ts:117 (a flag) e apps/cli/src/commands.ts:1364-1390 — o passe só roda sob --learn, dispõe pelo gate do Validador (gate:{kind:'validator'}) e escreve learning gate: N applied, M rejected. Sem --learn, nada disso acontece: o comportamento da run é idêntico ao de antes.

E para inspecionar a memória multi-store que o agente acumula (episódica, semântica, procedural, de decisão, de transcrição), o comando é alembic memory. Liste um substore para ver o que já sedimentou:

# Lista os registros de um substore (newest-first), com filtros opcionais.
alembic memory decision list --limit 3

# saída (uma linha de cabeçalho + uma por registro):
#   memory decision: 2 record(s) under .alembic/memory/decision.jsonl
#     dec-01J… [agent cli] at 2026-06-27T16:00:00.000Z
#     dec-01J… [agent cli] at 2026-06-27T15:58:00.000Z

Fonte: apps/cli/src/commands.ts (runMemory) — list imprime memory <substore>: N record(s) under <path> e uma linha <id> [agent <agente>] at <timestamp> por registro; os substores são episodic | semantic | procedural | decision | transcript (CLAUDE.md). Atenção à camada: este alembic memory é o store multi-substore append-only em JSONL; é parente — não idêntico — ao MEMORY.md/USER.md de snapshot da seção 02, que é o store que o --learn sedimenta. Os dois são memória, em granularidades diferentes.

EXPERIMENTE · ligue o loop numa run real e veja as escritas sedimentarem (offline, $0)
  1. Clone e entre no repo, depois garanta o build verde:
    git clone <repo> alembic && cd alembic · pnpm -r typecheck && pnpm -r build && pnpm -w test
  2. Gere um escopo mínimo (cria GOAL.md + alembic.plan.ts + contrato):
    alembic plan "um passo trivial que só roda um teste"
  3. Rode com o passe de aprendizado, hermético:
    alembic run --goal GOAL.md --plan alembic.plan.ts --learn --offline --yes
  4. O que procurar na saída: a linha learning gate: N applied, M rejected perto do fim (sem ela, ou você esqueceu --learn, ou não há adapter de proposer).
  5. O que procurar no dir da run: abra .alembic/runs/<run-id>/ e veja o MEMORY.md / USER.md — só as escritas que passaram o piso do Validador estão lá. Depois rode alembic memory decision list para inspecionar o store multi-substore.
  6. Prove o opt-in: rode a MESMA run sem --learn. Nenhuma linha learning gate: aparece, e nenhum MEMORY.md é escrito pelo passe — a peça desta lição fica desligada por padrão.
Faça na mão · qual comando para cada intenção?
1
"Quero que esta run aprenda com o próprio resultado." → adicione --learn ao alembic run. (É a peça inteira desta lição, ligada por uma flag.)
2
"Quero rodar sem rede e sem custo." → adicione --offline: o passe usa o adapter offline determinístico como proposer (commands.ts:1365).
3
"Quero ver o que já sedimentou na memória multi-store."alembic memory <substore> list (ex.: decision, episodic).
Agora você: você rodou alembic run --goal GOAL.md --plan alembic.plan.ts --yes (sem --learn) e a run ficou verde, mas nenhuma linha learning gate: apareceu e o MEMORY.md não mudou. Bug ou esperado? Decida antes de revelar.
Esperado. O passe de aprendizado é opt-in: sem --learn, o bloco em commands.ts:1364 (if (args.learn)) nem executa — a run se comporta exatamente como antes da ADR-0018. Por que opt-in: aprender é um efeito durável sobre a memória; ligar por padrão faria toda run mexer no snapshot da próxima. A disciplina é "conservador por padrão" — a mesma do gate 0.7 e do curador que nunca deleta.

Fixe os conceitos (flashcards)

Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.

Memory
Por que o snapshot é congelado no início?
clique pra virar ↻
Resposta
Pra manter o prefixo do prompt imutável → cache quente a sessão inteira. Escritas vão ao disco já, mas só o próximo load reflete.
ADR-0018
Quem propõe e quem dispõe?
clique pra virar ↻
Resposta
O revisor propõe; o Validador dispõe. Escritas com portão, nunca auto-aplicadas — ao contrário do Hermes literal.
Kernel
Quais os três baldes de resultado?
clique pra virar ↻
Resposta
applied (escreveu) · rejected (portão barrou, ok normal) · failed (store recusou, não lança). Nada some em silêncio.
Curator
Qual a ação máxima do curador?
clique pra virar ↻
Resposta
Arquivar (archived é terminal) — nunca deletar. E só toca skills createdBy:'agent' e não pinned.

Revisão cumulativa — recupere de memória

Antes de clicar: responda de cabeça. As opções têm tamanho parecido de propósito — sem pista pela forma.

1. Uma escrita de memória no meio da sessão tem sucesso. O system prompt muda pelo resto daquela sessão?
Correto: b. O snapshot é congelado no início da sessão; as escritas são duráveis no disco imediatamente, mas não invalidam o prefixo do prompt — esse é o ponto inteiro (cache quente). "Próxima run mais esperta" é literal: o refresh acontece no load da próxima sessão. a descreve justamente o que a disciplina evita — reconstruir a cada escrita esfriaria o cache a cada nota. c confunde camadas: o portão é do learning; a frieza/quentura do snapshot é independente de gate. d inventa uma assimetria — os dois stores seguem a mesma regra de snapshot.
2. O revisor propõe uma escrita com score: 0.6 e o portão padrão está em uso. O que acontece?
Correto: d. O scoreThresholdGate(0.7) retorna ok({approved:false, reason}) — uma rejeição é um resultado normal, não um erro. A proposta cai em rejected e o laço segue. a e c confundem "rejeição" com "erro": só um erro de proposer ou de portão (não uma reprovação de score) falha o passo fechado. b inventa um caminho "escreve mas sinaliza" que não existe — abaixo do piso, não escreve. O humano não está no loop do gate padrão; o piso é um check puro.
3. O curador encontra uma skill há muito sem uso com pinned: true e createdBy: 'user'. O que ele faz?
Correto: c. Duas guardas se aplicam antes de qualquer data: o portão de proveniência só toca skills createdBy === 'agent' (esta é 'user'), e skills pinned nunca são transicionadas. a ignora as duas guardas — a data nem é consultada. b viola a regra terminal: o estado máximo é archived, não há caminho de delete nenhum. d aplica a transição errada (reativação é só para stale usada de novo) e, de novo, esta skill nem chega à máquina de estados.
4. Por que a ADR-0018 rejeitou auto-aplicar escritas após cada run (o comportamento literal do Hermes)?
Correto: a. Auto-aplicar é mais rápido, mas contorna o gate e deixa conhecimento não validado sedimentar — exatamente o que a ADR-0006 ("validador como portão de emissão") existe para impedir. b erra os fatos: dá para auto-aplicar em TS sem thread; o ADR cita a falta de um AIAgent-daemon como razão para o passo síncrono, não como impossibilidade. c inventa um custo que não é o argumento do ADR. d inverte a verdade: o Hermes auto-escreve na memória após um turno — é justamente o comportamento que o Alembic divergiu de propósito.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Por que rejeição é ok e não erro?", "O que o coda Validator pluga no lugar do gate 0.7?", "Como o dedup do MemoryStore reforça em vez de duplicar?". É só dizer.