Mis agentes programados ejecutaron código viejo durante días y nunca me avisaron
Un fallo lo notas. Un agente que corre código viejo en silencio y reporta OK es el que te cuesta cuatro días. El arreglo es un paso de sincronización que es ruidoso cuando falla.
Un agente de revisión de código que ejecuto cada mañana informó “ningún commit nuevo en las últimas 36 horas.” Había diez. El agente estaba leyendo una copia de mi repositorio que llevaba cuatro días congelada, y nada en todo el sistema lo decía. No falló. No me avisó. Simplemente hizo su trabajo en silencio sobre código viejo e informó que todo había salido bien.
Ese es el modo de fallo del que nadie te advierte cuando empiezas a ejecutar agentes en un horario. Un fallo lo notas. El trabajo desactualizado que reporta OK, no.
Qué se rompió de verdad
Ejecuto varias tareas de Aldeia, uno de mis proyectos, en un horario a través de Cowork: un autopublicador de avisos, una revisión de código diaria, un pase de seguridad. Todas se ejecutan desde un único checkout en mi máquina, en /Users/edra/Documents/Claude/aldeia.
Dos días antes había desplegado una reescritura de la autopublicación de avisos. “Nunca se había disparado.” Cuando ejecuté la publicación a mano para depurarla, la corrida golpeó el script viejo y escribió una fila con estado pending en avisos_submissions, en lugar de publicar el aviso en vivo en avisos como hace el código nuevo. El código nuevo estaba en origin/main. El checkout no lo estaba ejecutando.
La pista vino de otro agente. El artefacto de revisión de código de esa mañana nombraba un hash de commit de cuatro días atrás y concluía que no había habido “ningún commit en 36 horas.” Un agente programado que describe con seguridad un commit viejo como la punta te está diciendo que su runner está desactualizado, si prestas atención. Yo no la presté, durante una hora más o menos.
La causa raíz
El checkout estaba clavado en f4adaf8, diez commits por detrás de origin/main. Cada git pull y git commit ahí venían fallando, en silencio, desde que apareció un archivo de bloqueo obsoleto:
$ 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
Un .git/index.lock de cero bytes, fechado el 19 de junio a las 05:34, justo en mitad de la ventana matutina de Cowork. Una operación de git murió a mitad de escritura (el Mac se durmió durante una tarea) y dejó el lock atrás. Git trata ese lock como “otro proceso de git está trabajando aquí, apártate,” así que toda escritura posterior se aborta. Como el lock nunca fue el lock activo de un proceso vivo, nada lo limpió. El checkout quedó congelado durante cuatro días mientras los agentes programados seguían corriendo contra él.
El arreglo que no alcanza
El arreglo obvio es una línea:
rm -f .git/index.lock && git pull --ff-only
Eso resuelve este incidente. No hace nada para evitar el siguiente, y es peligroso como reflejo. Borrar index.lock mientras un proceso de git sí lo está sosteniendo legítimamente corrompe tu índice. Así que la regla real es: solo limpia el lock cuando no haya ningún proceso de git vivo.
# obsoleto SOLO si el archivo existe Y no hay proceso de git corriendo
[ -f .git/index.lock ] && ! pgrep -f '[g]it ' && rm -f .git/index.lock
Pero el problema de fondo nunca fue el lock. Fue que el congelamiento era silencioso. El sistema necesita sincronizarse antes de cada corrida, y necesita ser ruidoso cuando no puede.
El arreglo que sí
Puse un cowork-sync.sh en el repositorio y lo hice el primer paso de cada tarea programada. Auto-sana el checkout hacia origin/main, y sale con código distinto de cero ante cualquier fallo para que una sincronización rota alerte en vez de enviar código viejo.
La parte que quiero que copies es el diseño de seguridad. Hay dos tipos de checkout, y necesitan trato opuesto:
- Un checkout desechable que existe solo para el agente. Ahí no vive nada mío, así que lo correcto es forzarlo a coincidir con el remoto exactamente.
reset --hardmásclean -fdno se pueden atascar en basura local como sí puedepull --ff-only. - Un checkout compartido que además guarda mi trabajo sin commitear. Aquí
reset --harddestruiría ese trabajo. Lo correcto esmerge --ff-only, que nunca puede sobrescribir cambios locales, y una parada ruidosa si está bloqueado.
Distingo los dos modos con un archivo marcador ignorado por git, .cowork-checkout. Marcador presente significa desechable, fuérzalo. Marcador ausente significa compartido, sincroniza con cuidado. Un reset destructivo, por lo tanto, solo puede tocar un checkout que marqué explícitamente como descartable.
#!/usr/bin/env bash
set -euo pipefail
REPO="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
cd "$REPO"
# Limpia un lock OBSOLETO solo cuando no hay proceso 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 desechable del agente — fuérzalo al remoto.
git reset --hard origin/main --quiet
git clean -fd --quiet
else
# Checkout compartido — solo sincronización segura, parar RUIDOSO si bloquea.
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})"
Dos detalles que vale la pena conservar. clean -fd borra archivos sin trackear pero conserva los ignorados por git, así que nunca borra tu .env.local ni node_modules. Nunca agregues -x. Y si un commit sincronizado cambió el lockfile, avisa pero no instales automáticamente. npm ci borra node_modules, lo cual es destructivo si el tuyo es un symlink.
La regla
Un agente que falla es un buen agente. Te avisa que algo anda mal. Un agente que corre código viejo y reporta éxito es el que te cuesta cuatro días, porque se ve exactamente igual a que todo está bien.
Si ejecutas algo en un horario desde un checkout que además tocas a mano, haz de la sincronización el primer paso, haz que el fallo sea ruidoso, y nunca apuntes un reset --hard a un árbol que guarde trabajo que extrañarías. El congelamiento va a pasar. Lo único que controlas es si se queda en silencio.
Déjame tu correo y te aviso cuando publique el próximo, con las herramientas y flujos que de verdad uso. Sin spam.
Más tutoriales ↗