Sair de uma sessão apagou código que você nunca commitou


Você roda três sessões em paralelo. Uma delas está no meio da edição de um arquivo — sem commit ainda, você termina amanhã. Você sai da sessão e fecha o terminal. Na manhã seguinte, o diretório sob .worktrees/ sumiu, e a branch também.

Você não lembrou errado onde ele estava. A limpeza de saída do worktree: true está faltando uma checagem: ela olha para commits, nunca para arquivos.

O que o modo worktree está fazendo

Iniciar uma sessão com hermes -w (ou definir worktree: true na config) dá a cada sessão de CLI o seu próprio worktree sob .worktrees/, na sua própria branch:

worktree: false        # padrão: só quando -w é passado
worktree_sync: true    # padrão: ramifica a partir da ponta remota recém-buscada; false usa o HEAD local

É assim que vários agentes editam, commitam, fazem push e abrem PRs sem pisar uns nos outros. Ramificar a partir da ponta remota recém-buscada também é deliberado — o PR acaba limitado à sua mudança, em vez de herdar o quanto o clone local estava atrasado.

O problema está no passo de saída.

O que a documentação promete vs. o que o código faz

A documentação de configuração, na seção de isolamento por git worktree, é explícita:

Worktrees limpos são removidos na saída; os sujos são mantidos para recuperação manual.

Enquanto isso, o _cleanup_worktree no cli.py declara o seu próprio critério na docstring:

def _cleanup_worktree(info: Dict[str, str] = None) -> None:
    """Remove a worktree and its branch on exit; kept only when it has unpushed commits."""

Só commits não enviados são mantidos. O helper que decide, _worktree_has_unpushed_commits(), roda:

git log --oneline HEAD --not --remotes

Em outras palavras, ele pergunta quais commits no HEAD nenhum remoto tem. O status da working tree nunca é lido. Então:

O que a worktree contém na saída Resultado real
Um novo arquivo não rastreado, nunca commitado worktree APAGADA
Um arquivo rastreado modificado, não commitado worktree APAGADA
Um commit ainda não enviado worktree mantida

O passo final é git worktree remove <path> --force mais git branch -D <branch>. O --force significa exatamente o que parece: ignore mudanças não commitadas na working tree.

Por que existem dois critérios diferentes

O Hermes na verdade tem dois caminhos que gerenciam worktrees, e eles discordam.

Um é a limpeza de saída no cli.py, descrita acima — só commits.

O outro é o pruner de inicialização em hermes_cli/worktree_ops.py, e ele entende de “sujo”: o _worktree_is_dirty() roda git status --porcelain (arquivos não rastreados incluídos), falha em direção a True com segurança, e o seu veredito é registrado como dirty, que é um membro de _PRESERVE_REASONS. O comentário acima dele é incomumente direto:

Guardas em todos os níveis e idades: trees sujas nunca são removidas.

Então isso não é “o Hermes não sabe distinguir sujo de limpo”. São dois caminhos com dois critérios diferentes: o pruner em background protege arquivos sujos, a limpeza de saída protege commits. Os usuários batem no caminho de saída.

Como se proteger

Até esse critério ser alinhado, eis o que funciona hoje.

  1. Commite antes de sair, mesmo como WIP. Um commit não enviado faz a tree sobreviver:
    git add -A && git commit -m "wip: still working"
  2. Ou faça stash, incluindo arquivos não rastreados. O -u em git stash -u é a parte importante — sem ele, arquivos novos ainda existem só dentro do worktree.
  3. Não trate “a sessão ainda está aberta” como segurança de arquivo. Trabalho não commitado só está seguro enquanto a sessão vive; no momento em que você sai, ele está ao alcance do --force.
  4. Dê uma olhada no status ao rodar sessões paralelas. git -C .worktrees/<name> status --porcelain; saída não vazia significa que essa tree será apagada na saída.
  5. Faça a sessão commitar e fazer push. Sessões em worktree são desenhadas para deixar o agente commitar, fazer push e abrir PRs; uma vez enviado, o critério de “commits não enviados” mantém a tree, e você ganha uma segunda rede de segurança.
  6. Audite periodicamente. Vale saber o que o check-up de worktree introduzido na v0.20.5 cobre e o que não cobre.

Se já aconteceu: comece com git worktree list e git branch para ver se a branch sobreviveu. Se havia sequer um commit antes da saída, esse conteúdo ainda está no object store e dá para recuperar a partir da branch ou do reflog. Mas arquivos não commitados e não rastreados removidos com --force não têm caminho de recuperação em nível de git — se você os recupera depende de buffers do editor, snapshots do sistema ou backups, não do git. Melhor ser direto sobre isso do que insinuar um resgate que não existe.

Onde isso está

A issue #129482 foi aberta em 2026-09-30, marcada como P1, contra o código em 41755854, e continuava aberta quando isto foi escrito. A correção natural é fazer a limpeza de saída chamar _worktree_is_dirty() também, para que os dois caminhos compartilhem um único critério. Até lá, faça os passos 1 e 2 acima.

Este também não é o primeiro desses bugs do tipo “parece limpo, perdeu algo em silêncio”. A guarda de rollback de repositório aninhado cobre rollbacks que tocam diretórios que nunca foram capturados, e limites de sessão explica o que as sessões de fato compartilham. Vale incorporar os dois aos seus hábitos.

A conclusão

A lição não é “não use worktrees”. É que uma operação de delete deveria usar o critério mais conservador disponível: ao decidir se uma tree pode ser removida, o que está no diretório de trabalho importa muito mais do que o que está no grafo de commits. Um arquivo meio pronto e não commitado costuma valer mais que dez commits enviados.

Até o código concordar, a autodefesa é uma frase só: commite ou faça stash antes de sair.