Uma ordem, um worktree: isolando agentes de código com git worktree
O jeito mais barato de reduzir o raio de explosão de um agente é não deixá-lo tocar no seu checkout. Os detalhes que fazem isso ser seguro de verdade.
Read in English Versão em Markdown

Por que isolar agentes de código
Um agente de código trabalhando no mesmo diretório que você cria três problemas, e todos pioram quando há mais de um agente:
- Colisão. Dois agentes editando o mesmo checkout sobrescrevem o trabalho um do outro, e o
git statusvira uma mistura que ninguém sabe separar. - Contaminação. Você está no meio de uma mudança, o agente roda os testes, falha por causa do seu código incompleto e "corrige" o que não era dele.
- Raio de explosão. Um
git checkout .ou umrmmal pensado do agente atinge o seu trabalho não commitado.
Um container resolve os três, mas custa imagem, volume e credenciais montadas. Um clone separado resolve também, mas duplica o .git e perde o compartilhamento de objetos. O git worktree fica no meio: diretórios de trabalho independentes, um único repositório por trás.
O básico em um comando
git worktree add -b feat/TASK-0042 .factory/worktrees/app/TASK-0042 main
Isso cria o diretório, cria o branch a partir da main e faz o checkout nele. O agente recebe esse diretório como cwd e só isso. Quando a tarefa termina e o trabalho está commitado, git worktree remove tira o diretório.
Se fosse só isso, este post acabaria aqui. O resto é o que dá errado quando uma fábrica cria centenas desses automaticamente.
Os guards que fazem isso ser seguro
O WorkspaceManager do T25 é tratado como código sensível. Cada item abaixo existe porque o caso sem ele é plausível.
1. Git sem shell
Todas as chamadas de git passam por execFile com uma lista de argumentos, nunca por uma string de shell. O ID da tarefa e o nome do branch nunca são interpolados num comando, então um título de tarefa com ; rm -rf é só texto. O processo também tem timeout duro e é morto com SIGKILL, para que um git travado não deixe o repositório num estado intermediário enquanto alguém tenta limpar.
2. ID e branch validados
O ID da tarefa precisa casar com ^[A-Za-z0-9][A-Za-z0-9._-]*$ antes de virar parte de um caminho. O branch base passa por git check-ref-format --branch. Nada que tenha .. ou barra entra no caminho.
3. O caminho tem que ficar dentro da raiz
Antes de usar qualquer caminho de worktree, ele é resolvido com realpath e comparado com a raiz configurada. Se o caminho relativo começa com .., é absoluto ou está vazio, a operação falha. Isso pega o caso do symlink: um diretório que parece estar dentro da raiz mas aponta para fora.
4. Dono registrado, e registrado duas vezes
Um diretório existir no lugar certo não prova que a fábrica o criou. Antes de reusar um worktree, o T25 exige duas coisas:
- um arquivo de metadados (
.factory-metadata/<TASK-ID>.json) com o ID da tarefa, o branch e o caminho canônico, e os três precisam bater; - o caminho precisa aparecer em
git worktree list --porcelaincom o mesmo branch.
Sem as duas, a fábrica se recusa a adotar o diretório. Isso impede que ela "herde" uma pasta que alguém criou à mão ou que sobrou de outra ferramenta.
5. Lock por tarefa
Dois processos preparando o mesmo worktree ao mesmo tempo é uma corrida clássica. O lock é um arquivo criado com a flag wx (falha se já existe) contendo o PID de quem pegou. Se o lock existe e o processo dono está vivo, a segunda tentativa falha com a mensagem certa. Se o processo morreu, o lock é considerado velho e é assumido.
6. Base sincronizada antes de ramificar
Este guard veio de um erro real. Numa rodada de dogfood, uma tarefa nasceu de uma main local atrasada em relação ao origin: um pull request tinha sido mesclado direto no GitHub, fora da fábrica. O resultado foram três ciclos de QA e implementação perseguindo uma asserção de teste que já estava corrigida no remoto.
Agora, antes de criar um worktree novo, o T25 faz git fetch origin main seguido de git merge --ff-only origin/main. O --ff-only é a parte importante: se o clone local divergiu por qualquer motivo, a operação falha alto em vez de seguir com base errada.
7. Limpeza que não apaga trabalho
cleanup() confere o dono de novo, roda git status --porcelain --untracked-files=all e se recusa a continuar se houver qualquer mudança, incluindo arquivo não rastreado. Nunca usa --force. Perder o trabalho de um agente por uma limpeza automática é pior do que deixar um diretório sobrando.
Nome de branch que passa no seu repositório
O branch segue o prefixo do tipo da tarefa, no padrão de Conventional Commits: feat/, fix/, refactor/, docs/, chore/, seguido do ID. A escolha é prática: muitos repositórios têm hooks de pre-push ou regras de proteção que recusam nomes fora do padrão, e um branch factory/... morreria no push.
Worktree resolve tudo?
Não. Worktree isola o sistema de arquivos do repositório, não o processo. O agente ainda roda com o seu usuário, enxerga o resto da máquina e usa as credenciais do CLI dele. Por isso o T25 empilha outra camada por risco: tarefas de risco médio para cima podem rodar o agente dentro de um sandbox Docker, com uma política assinada que o worker valida antes de executar. Restringir a rede desse sandbox de verdade ainda está na nossa lista. Worktree é a primeira camada, não a única.
| Opção | Isola arquivos do repo | Isola processo | Custo de setup | Compartilha objetos git |
|---|---|---|---|---|
| Checkout principal | Não | Não | Nenhum | Sim |
git worktree |
Sim | Não | Baixo | Sim |
| Clone separado | Sim | Não | Médio | Não |
| Container + volume | Sim | Sim | Alto | Depende |
Checklist para fazer o mesmo
Se você está montando isolamento para os seus agentes:
- Um worktree e um branch por tarefa, criados a partir de uma base sincronizada com
--ff-only. - Git chamado sem shell, com timeout e
SIGKILL. - ID de tarefa validado por regex antes de virar caminho;
realpathe checagem de raiz em todo uso. - Dono gravado em metadados e confirmado em
git worktree list. - Lock por tarefa com PID e detecção de lock velho.
- Limpeza que recusa diretório sujo e nunca força.
Perguntas frequentes
Posso rodar vários agentes em paralelo no mesmo repositório?
Sim, desde que cada um tenha o próprio worktree. Eles compartilham o banco de objetos do git, mas cada um tem diretório de trabalho, índice e branch próprios.
O worktree é apagado quando a tarefa termina?
Só se estiver limpo. A limpeza do T25 se recusa a remover um worktree com mudanças não commitadas ou arquivos não rastreados, e nunca usa --force.
Por que não usar um container para tudo?
Container isola mais, mas custa imagem, volume e credenciais montadas em cada tarefa. Worktree resolve colisão e contaminação com custo quase zero. O T25 usa os dois em camadas: worktree sempre, sandbox Docker a partir do risco médio.
Onde ficam os worktrees do T25?
Numa pasta local e ignorada pelo git, por padrão dentro de .factory/worktrees/, separada por projeto. Nada disso é commitado.