Saltar al contenido principal
Volver al blogCopiar Markdown
·13 min de lectura
·claude codeagentsworkflowworktreestesting

Claude Code sin humo: el flujo de agentes de esta web

Cómo hago esta web con agentes de Claude Code: worktrees, verificadores, cola de noche y un smoke test. Y el 500 de tres meses que explica el smoke test. →

Óscar Gallego

Ingeniero de software · Málaga

Un plano reparte trabajo entre varios puestos separados que convergen en una integración comprobada
En este artículo

Respuesta corta: las tareas que delego en lote las hace cada una un agente en su propio worktree, con roles en modelos de distinto tamaño y precio: uno investiga, otro implementa, un tercero intenta romper lo hecho, otro arregla lo que sobreviva y un asesor mira el conjunto. Las tareas grandes entran por la noche como issues con una etiqueta. No hay CI: los checks los ejecuto yo en local y, después de los deploys que tocan el enrutado, lanzo a mano un smoke test contra producción que prueba lo que el local no puede ver. Ese último paso lo monté porque el formulario de contacto estuvo devolviendo un 500 durante tres meses con todos los checks en verde.

Una aclaración antes de seguir: este post lo redactó un agente a partir del repo; los incidentes los viví yo. Cómo se hizo lo cuento más abajo.

El formulario falló en cada envío desde el 28 de junio de 2026 hasta el 6 de octubre. Tests, tipos y build, todo en verde en mi máquina durante esos tres meses. Yo no vi nada porque no había nada que ver desde donde miraba. Lo encontré el 6 de octubre; cómo exactamente no lo recuerdo, y no me lo voy a inventar. Lo que sí sé es que nada me avisó, porque no había ningún check contra producción.

La causa tiene gracia, de la mala. El middleware de borde reenvía las peticiones dinámicas pidiendo /_render en el mismo host. Con trailingSlash: "always", Vercel contestaba a esa petición con un 308 hacia /_render/. Un GET sigue una redirección sin problema. Un POST con el cuerpo en streaming no puede, porque el cuerpo ya se consumió. El GET /api/geo/ funcionaba, el POST del formulario no. El arreglo, el 6 de octubre, fue una pequeña integración de build que pone la ruta de /_render por delante de la redirección de la barra final en la config de salida de Vercel.

Ni un test unitario ni el build ven esa redirección: es enrutamiento de Vercel, que existe en cualquier deploy de Vercel, previews incluidas, y nunca en mi máquina.

Todo lo que viene ahora es lo que monté después, y lo que ya tenía montado antes de que esto pasara.

Sin CI, el filtro soy yo antes del push

No hay GitHub Actions en este repo. Todo se ejecuta en local y Vercel construye main cuando hago push. Lo documento tal cual en docs/CI_CD.md.

El peso lo lleva un hook de pre-push que ejecuta, en este orden, astro check, el lint de regex, docs:check (que falla si un documento cita un fichero que no existe), la comprobación de la config de Vercel, la de imágenes públicas, el lint y la validación de los posts, y los tests unitarios. Si falla algo, no hay push.

La suite unitaria tiene hoy 475 tests en 33 ficheros; medida el 28 de octubre de 2026, tarda entre 3 y 7 segundos según lo cargada que esté la máquina. Esa velocidad es lo que permite que cada agente la ejecute tras cada cambio sin que nadie lo piense dos veces.

El build completo no lo lanza cada agente. Se lanza una vez, al final, sobre el resultado ya fusionado.

El smoke test cubre lo que mi máquina no puede ver

pnpm smoke pide a producción las rutas que dependen de cómo enruta Vercel: la home y el blog, el endpoint de geolocalización, el formulario de contacto y el firewall. En el trabajo de día lo lanzo a mano después de cada deploy que toque src/pages/api/, astro.config.mjs o vercel.json; el agente de noche lo lanza por su cuenta, antes y después de sus propias publicaciones.

Un par de decisiones que salieron de usarlo:

  • No envía correos. Los dos POST al formulario o caen en la trampa de tiempo (no llevan el campo ts) o no pasan la validación. Compruebo que la ruta responde; que Resend entregue es otra cuestión.
  • Un 429 del formulario se anota como «saltado». El firewall deja 5 POST al formulario por hora y por IP, y cada ejecución del smoke gasta 2. Si no, a la tercera ejecución en la misma hora un sitio sano daría rojo.

Solo funciona desde una IP con un System Bypass del firewall: con la protección contra bots activa, a un script le sueltan el reto de verificación sea cual sea su user agent, y un reto no te dice si el sitio está bien o mal. Para mirar una ruta suelta a mano sin bypass queda vercel curl --deployment. Además, el script hace pocas peticiones, porque una ráfaga de curl desde mi IP activa la mitigación de DDoS de Vercel y me deja fuera de mi propia web durante un rato.

Cada tarea delegada en su worktree, con cinco roles

El flujo por cada tarea es una cadena corta. Primero investiga un agente con un modelo pequeño, que devuelve hechos con fuente, fecha de lectura y una lista de lo que no pudo confirmar. Después implementa otro con un modelo intermedio, en una rama agency/<id> dentro de su propio worktree. Luego revisa otro con un modelo más grande, que tiene el encargo explícito de encontrar lo que está mal (cuánto caza de verdad no lo tengo medido; vuelvo a eso abajo). Solo si queda algún fallo bloqueante o grave, entra un agente que lo arregla en ese mismo worktree. Al final, un asesor mira el lote entero y me dice qué mantener, qué cortar y qué hacer después.

Tres reglas que no se negocian:

  • Los verificadores son de solo lectura. Miran el worktree, no lo tocan.
  • En el pipeline normal, los agentes hacen commit en su rama y nada más. No hay push ni merge.
  • Ningún agente del pipeline toca el checkout principal: se queda en main y limpio, y ahí trabajo yo. Fusiono yo, lanzo el build completo una vez, borro los worktrees y las ramas ya fusionadas. Solo conserva su rama lo que se queda sin fusionar.

Esa regla ya existía, y un agente se la saltó. La mañana del 28 de octubre de 2026 lancé una tanda de día con tres issues y una limpieza de tipos. La primera pasada usaba el aislamiento por worktree del propio harness, que se negó a arrancar porque en este repo .claude es un enlace simbólico. Mientras tanto, un agente al que le había pedido que solo investigara implementó un arreglo y lo dejó en una rama del checkout principal. Lo vi al revisar la tanda. En la segunda pasada, el prompt de investigación decía de forma explícita que era de solo lectura, y cada constructor creó su propio worktree a mano en una carpeta hermana. Aun así, «no toques el principal» lo compruebo con git status.

La cola de noche: issues con una etiqueta

Lo pequeño va en .skeptic/backlog.md, una línea por tarea con cómo se comprueba. Lo grande es un issue de GitHub con la etiqueta night. Un agente de noche recoge ambos, construye cada elemento con la misma cadena de arriba y deja un informe para la mañana. Este sí publica lo que sobrevive: vigila producción un rato con las rutas del smoke y revierte si falla una ruta o suben los errores.

Donde más cuidado puse es en qué se queda fuera. Variables de entorno, secretos, configuración de deploy, dominios y facturación no se construyen de noche: pasan al backlog como propuesta para que las haga una persona. Un test no puede demostrar que sean seguras, y algunas no se pueden deshacer. Tampoco se acepta un issue de alguien sin permiso de escritura en el repo: mi repo es privado, pero la regla está pensada para uno público, donde cualquiera puede abrir un issue con instrucciones dentro.

Cada criterio de aceptación tiene que poder comprobarse con un test o con un comando que el repo ya tenga. Si no, la tarea vuelve al backlog con «necesita criterios comprobables» en vez de construirse a ciegas.

Un ejemplo concreto: la noche del 27 de octubre de 2026 abrí seis issues con la etiqueta night. El agente de noche construyó y publicó tres hacia la 1:15 UTC: un test que comprueba que cada pareja relatedSlug y cada ancla de la home apuntan a algo que existe, la cobertura de accesibilidad de varias páginas nuevas y el módulo de consentimiento de cookies. Los otros tres no los tocó. Dos de ellos, quitar los restos de Sentry y una opción de configuración obsoleta, tocan la config de build o de deploy, así que decido yo. Pasar Vitest a la versión 5 es un salto de versión mayor, y como ningún e2e cubre el propio runner de tests, tampoco lo construyó. Por la mañana entraron en la tanda de día de la que hablaba arriba: la cadena hizo lo de Sentry y lo de Vitest, y la opción de configuración es justo el arreglo que el agente de investigación implementó sin que se lo pidiera. Lo revisé (el HTML generado salía idéntico byte a byte) y lo fusioné tal cual. Una noche no es una estadística: no he contado cuántas veces la cola de noche publica algo que no debía o deja fuera algo que podía hacer.

Quité BotID del formulario

Esto es otro incidente, aunque su segunda parte cayera el mismo 6 de octubre que el arreglo del 500. Probé Vercel BotID en el formulario dos veces y las dos lo rompió: la primera bloqueó a visitantes reales; la segunda, el 6 de octubre, estuvo puesto unos 17 minutos y me bloqueó a mí, con «Message blocked» desde un navegador normal. Lo quité. Se quedan el honeypot, la trampa de tiempo, el filtro de enlaces y el límite por IP del firewall.

De ahí saqué una regla para el flujo: una protección que no puedo probar desde un navegador normal no es una protección, es una apuesta.

Lo que este flujo no arregla

Cuatro pegas, y la primera es la que más me escuece.

En el trabajo de día el smoke test lo lanzo yo, a mano, después del deploy a producción, así que un fallo de enrutado lo sigo descubriendo con la web ya publicada. No está automatizado ni se lanza todavía contra las previews. Y solo lo lanzo cuando el cambio toca src/pages/api/, astro.config.mjs o vercel.json: un cambio fuera de esas tres rutas podría traer otro 500 y nadie lo vería, porque ni siquiera lanzaría el check. Y el smoke test solo corre cuando yo lo lanzo: nada lo ejecuta de forma periódica ni avisa de errores en producción, así que un fallo que no venga de un deploy (un cambio de la plataforma, una clave que caduca) pasaría igual de desapercibido.

Sin CI, mi máquina es la única puerta, y el hook se salta con --no-verify. Si un día tengo prisa y lo uso, o lo usa un agente, no hay nada detrás que lo pare.

Tampoco tengo medido cuántos fallos cazan los verificadores en tareas reales. El flujo existe, pero en el repo no hay un registro por lote de qué encontró cada uno.

Y los checks que escribo hoy siguen siendo los que se me ocurren a mí. Los que no se me ocurren los descubro cuando algo se rompe, como pasó con el formulario.

Cómo lo hice

Al agente, que usó la skill write-article, le pedí en esencia: escribir primero en español y después en inglés, pensando cada versión en su idioma y sin traducir; no meter nada interno de mi trabajo; no meter ninguna cifra que no esté medida en este repo y fechada; y dejar una lista de afirmaciones con su prueba.

El agente leyó el historial de git, la documentación del repo, el script del smoke test, el hook de pre-push, el backlog de noche y los issues con la etiqueta night, y ejecutó la suite unitaria para medirla. Lo que no pudo confirmar en esas fuentes lo dejó fuera. Después pasó por un panel de revisores que lo leyeron sin contexto, y uno de ellos pilló que el relato de la noche del 27 estaba mal contado: se corrigió con el informe de esa noche delante.

Los roles del pipeline son prompts cortos: en el flujo de día van dentro del script del workflow, y en el de noche son ficheros roles/ de la skill. Lo que cambia entre ellos es el modelo y la frase que dice qué se espera: el investigador devuelve hechos con fuente, el verificador tiene que buscar fallos.

Fuentes

Todo sale del repo privado de esta web: historial de git, documentación, el script del smoke test, el hook de pre-push y los issues. Eso no lo puedes abrir, pero los incidentes que salen aquí (el 500 del formulario, BotID y el agente que implementó cuando solo tenía que investigar) están en el laboratorio, cada uno con qué pasó, cómo se detectó y qué cambió. Las cifras de los tests y de los issues están medidas el 28 de octubre de 2026.

Por dónde empezar

No copies mi pipeline. Haz una sola cosa esta semana: escribe un script de unas veinte líneas que pida a producción las dos o tres rutas que tu entorno local no puede reproducir (redirecciones, endpoints dinámicos, reglas de firewall) y que falle si algo no devuelve lo esperado. El esqueleto cabe en esto:

const BASE = process.env.SMOKE_URL ?? "https://tu-dominio.com";

const post = (path: string, fields: Record<string, string>) => {
  const body = new FormData();
  for (const [k, v] of Object.entries(fields)) body.append(k, v);
  // El navegador manda Origin en un POST de formulario; sin él, un check CSRF da 403.
  return fetch(BASE + path, { method: "POST", body, headers: { origin: BASE }, redirect: "manual" });
};

const checks: [string, () => Promise<Response>, number][] = [
  ["home", () => fetch(`${BASE}/`, { redirect: "manual" }), 200],
  // Un POST con cuerpo: aquí es donde estaba mi 500. Con un `ts` válido y un
  // email malo pasa la trampa de tiempo y falla la validación: no sale ningún correo.
  ["form", () => post("/api/contact/", { ts: String(Date.now() - 60_000), name: "x", email: "bad", message: "short" }), 400],
];

let failed = 0;
for (const [name, run, want] of checks) {
  const res = await run();
  const ok = res.status === want;
  if (!ok) failed++;
  console.log(`${ok ? "ok  " : "FAIL"} ${name}: ${res.status}`);
}
process.exit(failed ? 1 : 0);

export {}; // lo convierte en módulo, para que funcione el await de nivel superior

Cambia las rutas y los estados por los tuyos: el POST malo debería dar el error de validación de tu endpoint, no un 500. Este mismo script lo lancé contra esta web el 28 de octubre de 2026 y los dos checks salieron ok. Si tienes protección contra bots, al script le va a plantar un reto: añade tu IP al bypass del firewall o solo verás retos y 429. Lánzalo después de cada deploy y también programado, para que un fallo que no venga de un deploy salga igual. Si tienes agentes trabajando en el repo, añade una segunda cosa: una etiqueta night y la norma de que cada issue que la lleve diga cómo se comprueba.

Lectura relacionada: el reparto de subagentes por stories y worktrees y por qué un verificador también puede mentir.


P.D. ¿Has tenido alguna vez un fallo que tus checks locales no podían ver y del que te enteraste por un usuario? Cuéntamelo en LinkedIn.

Comparte este artículo

Artículos relacionados