← Tutoriales
BUILD · AGENTES

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.

Para
Cualquiera que ejecute agentes en un horario desde un checkout en su propia máquina
Necesitas
Un agente programado (Cowork, cron, CI) y un repositorio git desde el que corre
Tiempo
20 minutos

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 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:

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.

¿TE SIRVIÓ? RECIBE EL PRÓXIMO

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 ↗