Skip to content

v0.3.0: memória entre sessões, adapter claude-code, e os cinco achados do primeiro teste de campo - #3

Merged
Bulletdev merged 12 commits into
mainfrom
release/v0.3.0
Aug 25, 2026
Merged

v0.3.0: memória entre sessões, adapter claude-code, e os cinco achados do primeiro teste de campo#3
Bulletdev merged 12 commits into
mainfrom
release/v0.3.0

Conversation

@Bulletdev

Copy link
Copy Markdown
Owner

A v0.3.0 foi escrita em 23/08 e nunca chegou a ser publicada. Entre escrever e
publicar, ela foi a campo pela primeira vez: um projeto real que chama LLM de
verdade, com ai-jail 1.20.1, verificador gastando API da Anthropic e da OpenAI, e
uma sessão real do Claude Code sob os cinco hooks.

Tudo o que o marco promete se sustentou, e voltaram dez achados. Cinco estão
corrigidos aqui. Os outros cinco ficam declarados no laudo e no README, porque
nenhum deles produz valor de portão errado - foi esse o critério para não segurar
a versão.

O laudo completo está em DEVDOCS/CAMPO-01-multilingo.md, e o roteiro que
reproduz tudo no terminal em .verificacao/campo-multilingo/run.sh.

O que vem nos commits

A jaula de uma corrida não contamina a corrida seguinte (achados 1 e 2).

buildArgv não passava --clean --no-save-config, então o ai-jail gravava a
configuração da corrida no .ai-jail do projeto e a lia na corrida seguinte.
Config de projeto do ai-jail é política monotônica: ela só aperta, e a linha de
comando não reabre o que ela fechou.

Um verificador com network: true rodava sem rede logo depois de um sem rede. O
promptfoo contabilizava oito erros de conexão e ainda assim produzia relatório
com successes: 0, o psh extraía esse zero e o portão decidia em cima dele.
Métrica de corrida cujo contrato de sandbox não foi cumprido, sem nada na saída
dizendo isso.

O psh exec já passava as duas flags: os dois pontos de entrada da jaula
discordavam entre si, e o errado era justamente o que produz valor de portão.

O frescor volta a medir a árvore (resíduo do achado 2, e achado 4).

O artefato de runtime do próprio harness contava como mudança do workspace: um
psh memory consolidate entre a medição e o portão derrubava a evidência de quem
observa **, citando arquivo que verificador nenhum escreveu.

E isGitRepo era existsSync(join(root, ".git")), então todo app dentro de um
repositório maior caía para caminhada e o .gitignore deixava de valer para o
hash. Na cobaia entraram no hash o .env com chave real e um binário de node de
100 MB.

psh audit reanchor --reason "..." (achado 10).

A recusa de escrever em trilha que não bate com a âncora travava o projeto sem
saída, e a única alternativa real era apagar o .harness na mão - que é como
evidência de adulteração desaparece.

O relatório de CI diz o estado real da fronteira (achado 3).

boundary_engine era o literal "absent" desde a v0.1. Quem consome o JSON no CI
concluía o contrário do que estava acontecendo.

A regra que os achados cristalizaram

A infraestrutura usada para observar um workspace não pode modificar o conjunto
de arquivos que está sendo observado. Em termos do harness: executar verificador
não é mutar workspace, e sandbox(corrida atual) não depende de
sandbox(corrida anterior).

Verificação

524 testes, zero falhas e zero pulados, rodando com ai-jail real e com rede.
Cobertura de linha em 98,43%.

Os testes de integração da jaula ficavam sempre em skip nesta máquina porque o
AppArmor do bun instalado por snap nega exec de binário de fora do
confinamento. Com um bun fora do snap eles rodam, e é a primeira vez que a
suíte fecha inteira.

Cada correção vinda do campo foi exercitada contra o próprio defeito antes de
valer como regressão: com a correção revertida, os testes falham. Sem isso um
teste de regressão é só um teste a mais.

Quebra de contrato

Quem consumia boundary_engine no JSON do adapter ci passa a ler
boundary.mode.

Michael D. and others added 12 commits August 23, 2026 03:52
… no projeto

Medido contra o ai-jail 1.19.2, ao investigar se a marca de qual agente está
executando sobrevive dentro da jaula.

Por padrão o ai-jail grava um `.ai-jail` na raiz do projeto e o lê na execução
seguinte. O arquivo mora dentro da árvore que o agente edita, então parte da
montagem passaria a vir de algo que o próprio enjaulado escreve, que é o G4 que
o PRD aponta no harness de referência.

Na prática ele também acumulava lixo: cada corrida somava os deny paths de novo,
guardados na forma `~/...`, e o ai-jail os reabria como `<raiz>/~/...`, avisando
"rule not applied" para regra que não existia. A regra que valia continuou sendo
a do argv, e isso está medido, mas o ruído escondia o aviso de verdade.

O argv passou a levar `--clean --no-save-config`.

Com a escrita desligada, a exceção que perdoava o `.ai-jail` no relatório de
violação perdeu o motivo de existir e só servia para deixar passar uma cópia
feita pelo agente. O conjunto de arquivos perdoados ficou vazio e o arquivo
entra no deny como qualquer outro.

A terceira coisa é que a jaula zera o ambiente do processo filho: `PSH_AGENT` no
env do spawn chega vazio lá dentro. A marca vai explícita no argv, por `--env`,
para que o núcleo saiba quem pediu a ação também dentro da jaula.

Quatro casos novos na suíte de integração com o binário real: nenhuma
configuração deixada no projeto, a terceira corrida enjaula igual à primeira,
configuração plantada na raiz não muda a fronteira, e o id do agente atravessa
a jaula.
Primeira metade do C5, a faixa transitória de conhecimento entre sessões.

A página de memória é markdown em `.harness/memory/pages/`, com cabeçalho
validado por JSON Schema. O arquivo é a versão canônica e o SQLite é só índice,
que pode ser apagado e reconstruído sem perda: a mesma divisão da evidência.

Toda busca sincroniza o índice antes de responder, comparando o hash do arquivo.
Página editada fora do psh entra na resposta seguinte, página corrompida sai do
índice com aviso, e o número de páginas examinadas vai na saída (R2.13). Sem
FTS5 no SQLite a busca cai para varredura e diz que caiu, no resultado e no
`psh doctor`.

O `psh handoff` monta o bloco de retomada a partir do estado e da evidência em
disco, nunca de resumo de modelo: fase, tentativa, última decisão, o que reprova
o portão agora, memória fixada e o próximo comando. É o UC3, fechar a sessão por
limite de plano e reabrir depois, possivelmente em outro runtime.

A memória entra no deny duro da fronteira, e isso está medido contra o ai-jail
real. O bloco de handoff é injetado no início da sessão seguinte, então memória
que o agente escreve à mão é texto que ele injeta em si mesmo depois, sem passar
por nenhuma porta do núcleo.

Falta para fechar a v0.3: a captura automática (R5.1) depende dos hooks do
adapter claude-code, e a consolidação por LLM (R5.2) depende do Maestro.

Três coisas que a revisão achou e que não são da memória:

- Quebra de linha em título ou origem gravava a página, reportava sucesso e
  deixava a anotação ilegível para sempre. Tag com vírgula voltava partida em
  duas. As duas param na porta de escrita, com o campo nomeado.

- `psh memory promote --to` deixaria um agente escrever fora da própria
  allowlist com a assinatura do núcleo. O destino passa pela fronteira de quem
  promove.

- Flag de traço simples nunca existiu no parser: `psh audit log -n 5` virava
  dois posicionais ignorados e o comando respondia com o limite padrão, sem
  erro. Agora falha dizendo a forma certa, e `--n abc` não chega mais ao LIMIT
  do SQLite como NaN.
R5.2, a segunda metade do C5: a sessão vira uma página de memória.

A captura já existia e ninguém tinha reparado. Decisão de fase, resultado de
verificador, violação de fronteira, decisão humana e anotação entram na trilha
desde o R4.3, encadeadas por hash e conferíveis por `psh audit verify`. Guardar
uma segunda cópia dos mesmos fatos num buffer paralelo criaria duas versões da
mesma sessão, e a segunda não teria como provar que é verdadeira. Então a
consolidação lê a trilha, e cada linha da página carrega o número da entrada que
a originou.

O que R5.2 pede e esta versão não faz é a reescrita como narrativa, porque isso
é chamada de modelo e precisa passar pelo Maestro, que é v0.4. Chamar modelo por
fora do roteador seria furar a própria regra. A página diz, nela mesma, que foi
montada sem modelo. Quando o C6 entrar, a narrativa vira uma reescrita por cima
deste texto.

A marca d'água mora em `.harness/memory/consolidation.json`, não no SQLite: o
índice é descartável por construção, e se a marca morasse nele, jogar o índice
fora faria a próxima consolidação varrer a trilha inteira e despejar meses de
histórico numa página só. Ela também só avança depois de a página existir e a
trilha registrar, porque na ordem inversa uma falha no meio perderia a sessão
para sempre.

Duas coisas que a primeira execução mostrou:

- A consolidação se auto-alimentava. Ela grava uma entrada `memory.write` ao
  terminar, e essa entrada virava material da corrida seguinte: rodar o comando
  três vezes produzia três páginas, e as duas últimas só falavam da anterior.
  Agora ela pula as próprias entradas, e a marca d'água avança mesmo quando não
  há página, senão o bookkeeping ficaria sendo relido para sempre.

- Trilha comprometida não vira memória. A cadeia é verificada antes de resumir,
  porque assinar como memória um relato que a própria cadeia não sustenta seria
  fabricar prova.

A página de sessão nasce solta, não fixada: o que precisa sobreviver a qualquer
corte é o que o humano fixou com `psh remember` (R5.5), e um resumo não entra
nessa categoria só por ser recente. Ela aparece em "Memória recente" no
`psh handoff` seguinte, que é a seção que até agora nunca enchia.

Tipo de entrada que esta versão não sabe resumir sai contado por tipo na própria
página (R2.13). Entrada que some sem número vira sessão que parece menor do que
foi.
A âncora existe para pegar reescrita coordenada da cadeia (R4.1), mas quem a
conferia era só o `psh audit verify`.

Como todo `append` regrava a âncora com o topo novo, bastava uma escrita
qualquer depois da adulteração para a cadeia voltar a fechar: remova a última
linha da trilha, rode `psh remember`, e o `verify` volta a dizer "íntegra" para
uma trilha de onde uma entrada foi arrancada.

Agora o `append` confere antes de escrever e recusa com erro de auditoria. A
conferência é por contagem de linha e hash do topo, sem re-hashear a cadeia
inteira: remoção no meio muda a contagem, e edição ou religamento mudam o hash
do topo. É o mesmo alcance que a âncora já prometia, cobrado antes da escrita e
não só depois dela.

Achado por um teste do adapter que esperava a consolidação recusar uma trilha
quebrada e viu ela passar, porque o próprio hook de fim de sessão tinha gravado
uma entrada antes e consertado a âncora no caminho.
C8 para o runtime que a casa usa, e a peça que fechava o R5.1: o prompt do
usuário, que é a única coisa da captura que a trilha não vê sozinha.

Cinco pontos de extensão. SessionStart injeta as regras do harness e o bloco de
retomada; UserPromptSubmit registra o pedido; PreToolUse aplica a fronteira
antes da escrita acontecer; PostToolUse avisa quando a escrita derrubou o
frescor de uma evidência; SessionEnd consolida a sessão.

O registro é em forma exec: o runtime executa o binário direto, com os
argumentos em lista, sem shell no meio. Caminho de instalação com aspas, cifrão
ou crase nunca chega a um parser de shell, que é a mesma postura do R2.14 para
expressão regular.

Nada disso foi escrito de memória.

O R8.6b manda validar contra o runtime instalado, e a seção 3.2 do PRD registra
duas vezes em que essa regra foi quebrada nesta casa: 881 linhas de plugin morto
no harness de referência, e os três mecanismos do prostaff-hooks que nunca
responderam porque o campo se chamava `prompt` e o código dizia `message`. Então
os nomes saíram do artefato do Claude Code 2.1.238, ficam em `contract.json`, e
`psh adapter claude-code contract` lê o binário e reprova o que não existe.

O que o binário mostrou e a documentação não diria: o runtime aceita 31 eventos
de hook, `PreToolUse` tem `defer` além de allow, deny e ask, a entrada de hook
aceita a forma exec, e `SessionEnd` existe. Este último mudou o desenho: `Stop`
dispara ao fim de cada turno, e consolidar ali geraria uma página por resposta
do modelo.

A validação foi conferida numa sessão real, pelo ponto de entrada do usuário e
sem gastar turno de modelo (R8.6d). O log do próprio runtime registra
"Successfully parsed and validated hook JSON output" e "provided
additionalContext (1253 chars)", e o efeito foi conferido em disco (R8.6e): as
entradas na trilha e a página de sessão gerada.

Falha do adapter nunca vira sessão travada nem escrita liberada em silêncio: em
PreToolUse a decisão degradada é `ask`, com o motivo na tela, e quem decide
passa a ser o humano.

A poda é de duas pontas e por handler (R8.6f): instalar remove o registro antigo
do psh, preserva hook de terceiro no mesmo evento, e conta quantos apontavam
para artefato ausente. `psh doctor` diz quantos pontos estão ativos, porque
quatro de cinco significa uma responsabilidade do R8.1 que simplesmente não
acontece.

Fica declarado o que não entrou: servidor MCP com as tools do núcleo (R5.6),
compressor de saída (R8.7), e o registro de toda chamada de tool na trilha, que
somaria um fsync por chamada e precisa de medição antes de virar padrão.

Também fica declarado o limite da varredura de símbolo: ela procura bytes, então
símbolo que é prefixo de outro é dado como presente quando só o mais longo
existe.
Memória entre sessões e adapter Claude Code, os dois marcos que o roadmap
declarava para esta versão.

Fica registrado no README o que a versão não faz: a página de sessão é montada
da trilha e não escrita por modelo, a memória não é fonte canônica, o adapter
não expõe tools por MCP, e a atribuição de quem fez o quê é melhor esforço
dentro da jaula.
Achado medindo o harness contra um projeto real, com o binário compilado.

`psh --version` levava 185 ms; o `bun` cru leva 2 ms. Os 181 ms de diferença
iam embora antes de a primeira linha de lógica rodar, porque o ajv compilava os
sete schemas do projeto no escopo de módulo, mesmo em comando que não valida
nada.

Isso não era só cosmético: o hook do adapter roda uma vez por chamada de tool,
então essa partida entrava no caminho crítico de cada escrita da sessão.

Os validadores passaram a compilar sob demanda, mantendo a interface do ajv,
inclusive `.errors`. Nenhum ponto de uso mudou.

Depois: `psh --version` em 53 ms e o hook de PreToolUse em 156 ms.

O que sobra está atribuído e registrado no CHANGELOG: a decisão de fronteira
leva 1 ms, o `loadBoundary` leva 38 ms compilando o schema da allowlist sob
demanda, e o resto é partida do binário. Baixar mais exige validador
pré-compilado em tempo de build.
O `buildArgv` não passava `--clean` nem `--no-save-config`, então o ai-jail
gravava a configuração da corrida no `.ai-jail` do projeto e a lia na corrida
seguinte. Config de projeto do ai-jail é política monotônica: ela só aperta, e a
linha de comando não reabre o que ela fechou.

Ou seja, o que o psh gravava numa corrida só podia tirar capacidade da corrida
seguinte, nunca devolver.

Um verificador com `network: true` rodava sem rede logo depois de um sem rede. O
curl saía com 6, o promptfoo contabilizava oito erros de conexão e ainda assim
produzia relatório com `successes: 0`, o psh extraía esse zero e o portão decidia
em cima dele. Métrica de corrida cujo contrato de sandbox não foi cumprido, e
nada na saída dizia isso.

O `psh exec` já passava as duas flags. Os dois pontos de entrada da jaula
discordavam entre si, e o errado era justamente o que produz valor de portão.

A mesma escrita sujava a árvore observada durante a corrida: qualquer verificador
com `watch: ["**"]` reprovava por `workspace-mutated-during-run`, incluindo o
`secrets` que vem no pacote. Um verificador que roda `/bin/true` reprovava.

A invariante que isso cristaliza: sandbox(corrida atual) não depende de
sandbox(corrida anterior).

Cinco testes de integração cobrem as duas ordens de execução com rede real, e
foram exercitados contra o próprio defeito: com a correção revertida, os cinco
falham.

Achados 1 e 2 do Campo 01.
…vore

Dois defeitos do mesmo mecanismo, achados no primeiro teste de campo.

O artefato de runtime do próprio harness contava como mudança do workspace. A
lista de sempre-excluídos cobria quatro caminhos enquanto o `Layout` já tinha
sete diretórios de runtime, então `memory/`, `approvals/`, `reviews/` e `tmp/`
entravam no hash. Um `psh memory consolidate` entre a medição e o portão
derrubava a evidência de quem observa `**`, citando arquivo que verificador
nenhum escreveu. Num projeto Git o `.harness/.gitignore` mascarava parte disso.

A classificação agora é explícita nos dois sentidos, em `HARNESS_RUNTIME_PATHS` e
`HARNESS_OBSERVABLE_PATHS`: contrato e documento de fase moram no mesmo diretório
e continuam observáveis, porque escondê-los abriria a classe dos arquivos
invisíveis ao observador. Há teste cobrando que todo caminho do `Layout` esteja
numa das duas listas, porque o defeito não foi a lista estar errada, foi ela ter
envelhecido calada enquanto o `Layout` crescia. Pelo mesmo motivo o `tmpDir`
entrou no `Layout`, em vez de continuar como a string "tmp" repetida no
`execute.ts`, que era como ele escapava da classificação.

A exclusão passou a aparecer no manifesto da evidência, contada e nomeada:
decisão de medição que não fica no registro não pode ser auditada depois.

O segundo: `isGitRepo` era `existsSync(join(root, ".git"))`, que só acerta o
projeto que é a raiz do repositório. Todo app dentro de um repositório maior caía
para caminhada, e o `.gitignore` deixava de valer para o hash. Na cobaia entraram
no hash o `.env` com chave real, o diretório de relatório e um binário de node de
100 MB. O `psh doctor` chamava isso de `[ok] enumeracao por caminhada`, com o
detalhe "projeto nao e repositorio Git", que é falso, enquanto o `checkSecrets`
do mesmo doctor usava `git ls-files` no mesmo diretório sem problema.

Quem responde agora é o Git, por `git rev-parse --is-inside-work-tree`. O
fallback pelo diretório continua para um caso só, o `git` não ter respondido,
porque é ele que mantém o `walk-fallback` visível em vez de virar caminhada
silenciosa.

A regra que os dois casos compartilham: a infraestrutura usada para observar um
workspace não pode modificar o conjunto de arquivos observado.

Resíduo do achado 2, e achado 4 do Campo 01.
A recusa de escrever numa trilha que não bate com a âncora funciona e está certa,
mas ela travava o projeto para qualquer operação que escreva na trilha, ou seja
praticamente todas. A mensagem mandava rodar `psh audit verify`, que só confirma
o diagnóstico, e nenhum comando resolvia. Sobrava mexer no `.harness` na mão, que
é exatamente como evidência de adulteração desaparece.

Reancorar não conserta a trilha nem finge que a divergência não houve: grava na
própria trilha, como `audit.note`, o que a âncora dizia, o que o arquivo diz,
quem decidiu e por quê, e só então move a âncora para o topo real. A divergência
vira cicatriz permanente e legível, em vez de virar diretório apagado no susto.

O `--reason` é obrigatório por natureza do comando: o valor dele está no que fica
escrito, não em silenciar o alarme.

E ele recusa quando o defeito está dentro do arquivo, ou seja linha corrompida,
seq fora de ordem, elo quebrado ou hash que não fecha. Aí a âncora não é o
defeito, e mover a âncora só trocaria um relatório vermelho por outro. Prometer
conserto nesse caso seria pior que não ter o comando.

A mensagem da recusa passou a citar a saída, em vez de só dizer que travou.

Achado 10 do Campo 01.
`boundary_engine` era o literal "absent" no tipo e no objeto, declaração da v0.1
de quando o C3 não existia, que ninguém atualizou quando o motor de fronteira
entrou na v0.2.

No mesmo projeto e no mesmo instante o `psh status` dizia `fronteira mount` e o
`psh doctor` dizia `fronteira aplicada por mount do ai-jail`, enquanto o JSON de
CI dizia que motor de fronteira não havia. Quem consome esse JSON conclui o
contrário do que está acontecendo, que é a pior falha possível num contrato de
máquina: não é ausência de informação, é informação invertida.

O campo virou `boundary: { mode, agents, detail }` e sai da `descreverFronteira`,
a mesma função que alimenta o `psh status`.

O defeito não foi o valor errado, foi haver duas declarações paralelas do mesmo
fato, e por isso o teste não confere o valor: ele exige que `ci --json` e
`status --json` digam a mesma coisa, palavra por palavra.

Muda o formato do relatório: quem consumia `boundary_engine` passa a ler
`boundary.mode`.

Achado 3 do Campo 01.
A 0.3.0 foi escrita em 23/08 e nunca chegou a ser publicada. Entre escrever e
publicar ela foi a campo pela primeira vez, contra um projeto real que chama LLM
de verdade, e voltou com dez achados. Cinco foram corrigidos antes desta
publicação e estão nos commits anteriores. Os outros cinco ficam declarados no
laudo e no README: nenhum deles produz valor de portão errado, que foi o critério
para não segurar a versão.

O bloco [Não lançado] do changelog foi absorvido na 0.3.0, que passa a ser
datada de 25/08.

O README do psh ainda abria com "Marco v0.1" e tinha uma seção dizendo que o C3
entrava na v0.2 e que o adapter `ci` devolvia `boundary_engine: absent`. Era a
mesma classe de mentira do achado 3, só que no arquivo que se lê primeiro. O
marco virou v0.3, a seção "Fora do escopo" foi reescrita para o que de fato falta,
e o layout em disco passou a mostrar `memory/` e `tmp/`, que existem desde a v0.3
e não estavam lá.

O README da raiz tinha o mesmo `boundary_engine: absent` no exemplo de JSON do
CI. A seção "O que o psh não faz" ganhou os achados que seguem abertos, incluindo
o do toolchain e credencial, que no campo custou vendorizar o binário do node
dentro da árvore.

Também entram no changelog as três decisões de projeto que os achados
cristalizaram: quem observa um workspace não o modifica, trilha divergente tem
caminho de volta declarado, e um fato tem uma fonte só.
@Bulletdev Bulletdev self-assigned this Aug 25, 2026
@Bulletdev
Bulletdev merged commit 6eb1c0e into main Aug 25, 2026
7 checks passed
@Bulletdev
Bulletdev deleted the release/v0.3.0 branch August 25, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant