Meus agentes agendados rodaram código velho por dias e nunca me avisaram
Uma quebra você percebe. Um agente que roda código velho em silêncio e reporta OK é o que te custa quatro dias. O conserto é um passo de sincronização que é barulhento quando falha.
Um agente de revisão de código que rodo toda manhã relatou “nenhum commit novo nas últimas 36 horas.” Eram dez. O agente estava lendo uma cópia do meu repositório que estava congelada havia quatro dias, e nada no sistema inteiro dizia isso. Ele não quebrou. Não me avisou. Apenas fez seu trabalho em silêncio sobre código velho e relatou que deu tudo certo.
Esse é o modo de falha sobre o qual ninguém te avisa quando você começa a rodar agentes em um agendamento. Uma quebra você percebe. Trabalho desatualizado que reporta OK, não.
O que de fato quebrou
Rodo várias tarefas do Aldeia, um dos meus projetos, em um agendamento via Cowork: um autopublicador de avisos, uma revisão de código diária, uma passada de segurança. Todas executam a partir de um único checkout na minha máquina, em /Users/edra/Documents/Claude/aldeia.
Dois dias antes eu tinha implantado uma reescrita da autopublicação de avisos. Ela “nunca havia disparado.” Quando rodei a publicação na mão para depurar, a execução bateu no script velho e escreveu uma linha com status pending em avisos_submissions, em vez de colocar o aviso no ar em avisos como o código novo faz. O código novo estava em origin/main. O checkout não estava rodando ele.
A pista veio de outro agente. O artefato de revisão de código daquela manhã citava um hash de commit de quatro dias atrás e concluía que não havia tido “nenhum commit em 36 horas.” Um agente agendado que descreve com confiança um commit velho como o topo está te dizendo que o runner dele está desatualizado, se você estiver prestando atenção. Eu não estava, por mais ou menos uma hora.
A causa raiz
O checkout estava preso em f4adaf8, dez commits atrás de origin/main. Todo git pull e git commit ali vinham falhando, em silêncio, desde que um arquivo de lock obsoleto apareceu:
$ git -C /Users/edra/Documents/Claude/aldeia rev-parse --short HEAD
f4adaf8
$ git -C /Users/edra/Documents/Claude/aldeia log --oneline HEAD..origin/main | wc -l
10
$ ls -la /Users/edra/Documents/Claude/aldeia/.git/index.lock
-rw-r--r-- 1 edra staff 0 Jun 19 05:34 .git/index.lock
Um .git/index.lock de zero bytes, datado de 19 de junho às 05:34, bem no meio da janela matinal do Cowork. Uma operação de git morreu no meio da escrita (o Mac dormiu durante uma tarefa) e deixou o lock para trás. O git trata esse lock como “outro processo de git está trabalhando aqui, se afaste,” então toda escrita posterior é abortada. Como o lock nunca foi o lock ativo de um processo vivo, nada o limpou. O checkout ficou congelado por quatro dias enquanto os agentes agendados seguiam rodando contra ele.
O conserto que não basta
O conserto óbvio é uma linha:
rm -f .git/index.lock && git pull --ff-only
Isso resolve este incidente. Não faz nada para evitar o próximo, e é perigoso como reflexo. Apagar index.lock enquanto um processo de git está legitimamente segurando ele corrompe seu índice. Então a regra real é: só limpe o lock quando não houver nenhum processo de git vivo.
# obsoleto SÓ se o arquivo existir E não houver processo de git rodando
[ -f .git/index.lock ] && ! pgrep -f '[g]it ' && rm -f .git/index.lock
Mas o problema de fundo nunca foi o lock. Foi que o congelamento era silencioso. O sistema precisa se sincronizar antes de cada execução, e precisa ser barulhento quando não consegue.
O conserto que basta
Coloquei um cowork-sync.sh no repositório e fiz dele o primeiro passo de toda tarefa agendada. Ele se auto-cura levando o checkout para origin/main, e sai com código diferente de zero em qualquer falha, para que uma sincronização quebrada alerte em vez de enviar código velho.
A parte que quero que você copie é o desenho de segurança. Existem dois tipos de checkout, e eles precisam de tratamento oposto:
- Um checkout descartável que existe só para o agente. Nada meu vive ali, então o certo é forçá-lo a bater com o remoto exatamente.
reset --hardmaisclean -fdnão travam em sujeira local do jeito quepull --ff-onlytrava. - Um checkout compartilhado que também guarda meu trabalho sem commit. Aqui
reset --harddestruiria esse trabalho. O certo émerge --ff-only, que nunca pode sobrescrever mudanças locais, e uma parada barulhenta se estiver bloqueado.
Separo os dois modos com um arquivo marcador ignorado pelo git, .cowork-checkout. Marcador presente significa descartável, force. Marcador ausente significa compartilhado, sincronize com cuidado. Um reset destrutivo, portanto, só pode tocar um checkout que eu marquei explicitamente como descartável.
#!/usr/bin/env bash
set -euo pipefail
REPO="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
cd "$REPO"
# Limpa um lock OBSOLETO só quando não há processo de git vivo.
if [ -f .git/index.lock ] && ! pgrep -f '[g]it ' >/dev/null 2>&1; then
rm -f .git/index.lock
fi
PREV="$(git rev-parse --short HEAD)"
git fetch origin --quiet
if [ -f "$REPO/.cowork-checkout" ]; then
# Checkout descartável do agente — force ao remoto.
git reset --hard origin/main --quiet
git clean -fd --quiet
else
# Checkout compartilhado — só sincronização segura, parar BARULHENTO se bloquear.
if ! git merge --ff-only origin/main --quiet; then
echo "cowork-sync: ff-only blocked on $REPO — NOT forcing." >&2
exit 4
fi
fi
echo "cowork-sync: OK ${PREV}→$(git rev-parse --short HEAD) (${REPO})"
Dois detalhes que vale a pena manter. clean -fd apaga arquivos não rastreados mas mantém os ignorados pelo git, então nunca apaga seu .env.local nem node_modules. Nunca adicione -x. E se um commit sincronizado mudou o lockfile, avise mas não instale automaticamente. npm ci apaga node_modules, o que é destrutivo se o seu for um symlink.
A regra
Um agente que quebra é um bom agente. Ele te avisa que algo está errado. Um agente que roda código velho e reporta sucesso é o que te custa quatro dias, porque parece exatamente igual a estar tudo bem.
Se você roda qualquer coisa em um agendamento a partir de um checkout que também mexe na mão, faça da sincronização o primeiro passo, faça a falha ser barulhenta, e nunca aponte um reset --hard para uma árvore que guarde trabalho que você sentiria falta. O congelamento vai acontecer. A única coisa que você controla é se ele fica em silêncio.
Deixe seu e-mail e eu te aviso quando o próximo sair, com as ferramentas e fluxos que eu de fato uso. Sem spam.
Mais tutoriais ↗