Blog de Vibe Arcade

Historias de desarrollo, diseño de juegos y el arte de construir con IA

De Local a Remoto: La Migración de Pipeline Que Rompió Todo

La pipeline nocturna funcionaba. Luego intentamos hacerla verdaderamente autónoma — y vimos meses de comportamiento sólido como roca desmoronarse en una sola semana.

· Vibe Arcade

ingeniería pipeline IA arquitectura máquinas de estado lecciones aprendidas

La pipeline nocturna funcionaba. De tres a cinco juegos nuevos de navegador se lanzaban por semana, las puertas pasaban, los auto-merges limpios, las mañanas se pasaban revisando en lugar de reparando. Sobre el papel no había nada que arreglar. Sin embargo, había un compromiso silencioso, y era del tipo que molesta: la pipeline en realidad solo se ejecutaba cuando el ordenador estaba despierto y la app de Claude abierta. Cierra la tapa o sal de la app y la pipeline "nocturna" era solo una definición de skill en disco.

Así que tratamos de arreglar eso. Esta es la historia de cómo arreglarlo rompió casi todo lo demás — y lo que la reconstrucción nos enseñó sobre adoptar infraestructura nueva.

La Base Que Funcionaba

La pipeline local era estructuralmente simple. Una sola sesión de Claude Code de larga duración dirigía toda la ejecución. Un modelo de planificación producía una spec detallada del juego. Un modelo de implementación construía el juego desde esa spec a lo largo de una a cuatro horas, generando sub-agentes para módulos paralelos cuando era necesario. Tres puertas automatizadas — lint estructural, comprobaciones de seguridad, rúbrica de jugabilidad — corrían en línea. Si todo pasaba por encima del umbral, la build se auto-mergeaba. Si algo se marcaba, esperaba a la revisión matinal.

La razón por la que era robusta es casi vergonzosa en retrospectiva: todo vivía en un proceso, en memoria, durante toda la ejecución. La salida de planificación se pasaba a la implementación como contexto en memoria, no como un artefacto serializado. Los fallos de puerta podían reintentarse en la misma sesión con el estado previo intacto. La "pipeline" era realmente una sola conversación continua con mucha estructura. El entorno de alta capacidad — máquina siempre encendida, sin límites de reloj de pared, estado persistente — absorbía mucha holgura de diseño que no notábamos que aprovechábamos.

El único problema era que no era realmente autónoma. Requería una máquina despierta y una app abierta. Para un flujo de trabajo llamado "nocturno," ese era un compromiso que valía la pena eliminar.

El Intento de Migración

El arreglo obvio era mover el trigger fuera de la máquina local por completo. Dejar que un trabajo programado en un runner en la nube disparara toda la pipeline a las 2 AM. El ordenador puede dormir. La app puede estar cerrada. El trabajo ocurre en otro lugar, en el metal de otra persona, y solo bajamos las escaleras por la mañana a revisar pull requests.

La categoría de enfoque era directa: un runner alojado en la nube, disparado por cron, invocando la misma skill que la sesión local había estado ejecutando. Un único punto de entrada que, una vez disparado, planificaría, construiría, evaluaría y abriría un pull request — exactamente como el flujo local, solo en un host remoto. La tecnología nueva estaba disponible. Las credenciales estaban cableadas. La primera ejecución programada arrancó como se diseñó.

Esa fue la última cosa que salió como se diseñó por un buen tiempo.

El Muro del Tiempo de Cómputo

Una ejecución nocturna real son horas de cómputo. La planificación es corta, pero la fase de implementación — especialmente cuando bifurca sub-agentes para módulos paralelos — rutinariamente corre más allá de lo que la mayoría de runners remotos toleran antes de matar el trabajo. Las puertas de seguridad y la rúbrica de jugabilidad cada una añaden tiempo propio. A lo que había sido una sesión local relajada de varias horas ahora se le estaba pidiendo que terminara dentro de un presupuesto restringido de reloj de pared en un host remoto.

Peor, el modo de fallo del runner no es elegante. No te devuelve cortésmente un resultado parcial. Termina. Lo que estaba en memoria se va. La rama está a medio poblar. Nadie envió una señal. El siguiente trigger programado no tiene idea de que hubo siquiera una ejecución previa para reanudar.

La Regresión

Todo lo que había funcionado impecablemente durante meses se vino abajo de golpe. No una desaceleración gradual — una ruptura categórica.

Las builds expiraban a mitad de implementación, dejando ramas con andamiaje pero sin juego real. El estado que había vivido cómodamente en memoria — el plan, la implementación parcial, las decisiones en vuelo — desaparecía en cada reinicio porque no había representación en disco. Los reintentos no "reanudaban"; volvían a correr desde cero, quemando el presupuesto de nuevo y golpeando el mismo muro aproximadamente en el mismo punto.

La ruta de auto-merge, que había sido la pieza más orgullosa del sistema local, se volvió peligrosa. Había sido diseñada para un mundo donde cada puerta corría hasta completarse antes de la decisión de merge. En el mundo remoto, un runner podía morir entre "puerta pasada" y "merge ejecutado," y el siguiente trigger no tenía forma limpia de saber si una ejecución previa había realmente terminado. Los artefactos parciales producían problemas en cascada de pull requests — ramas huérfanas, locks rancios, reclamos de concepto duplicados.

El shock de confianza fue real. Un humano que había dejado de cuidar las mañanas porque la pipeline era aburridamente fiable estaba de pronto de vuelta triando una clase diferente de fallo cada día. La nota en CLAUDE.md que ahora dice "esto es por diseño, no un bug a arreglar" sobre el comportamiento no-op del workflow cron no era un comentario casual. Era una verdad ganada con esfuerzo que sobrevivió a varios intentos fallidos de reconstrucción.

La pieza amarga: nada estaba realmente mal con la lógica de la pipeline. La construcción de juegos funcionaba. Las puertas funcionaban. Las reglas de autoría funcionaban. Lo que se rompió fue la suposición de que un modelo de ejecución construido para una sesión larga continua sobreviviría siendo cortado en piezas.

La Re-arquitectura

El arreglo no fue un parche. Era una forma diferente.

El primer movimiento fue dejar de pretender que la pipeline podía terminar en un solo disparo. La reconstruimos como una máquina de estados con una fase por trigger. Cada fase — planificar, implementar, evaluar, mergear — lee un archivo de estado serializado del repo, hace exactamente un trozo de trabajo, hace commit del estado actualizado de vuelta, y termina bien dentro de la ventana de tiempo del runner remoto. El siguiente trigger programado lee ese estado y retoma donde la última fase lo dejó. Sin handoff en memoria. Sin "sesión larga." Cada bit de progreso es visible en disco y visible en git.

Esa primera pasada de máquina de estados funcionó, pero era lenta — una fase por disparo programado significaba que un juego completo podía tomar muchas ventanas programadas en terminar. Iteramos: en lugar de una fase por disparo, el runner hace tanto progreso como pueda con seguridad por disparo, haciendo checkpoint del estado tras cada fase para que una terminación a mitad de ejecución sea recuperable en lugar de destructiva. Los disparos tempranos de planificación ahora hacen planificación más el andamiaje de implementación. Los disparos posteriores hacen implementación más puertas. El sistema apunta al progreso máximo por disparo sin arriesgar nunca un corte irrecuperable.

Añadimos un orquestador — un componente pequeño cuyo único trabajo es mirar el estado comiteado y decidir qué fase debería correr a continuación. Y envolvimos cada fase en un timeout de watchdog que mata la fase limpiamente antes de que lo haga el propio runner, para que el siguiente trigger herede un estado sano en lugar de un árbol de proceso medio muerto. Una iteración posterior dividió las escrituras grandes de archivos específicamente para esquivar un comportamiento silencioso de timeout de inactividad que solo descubrimos tras lanzar en el nuevo entorno.

La progresión, commit por commit, fue: respaldar el sistema conocido-bueno; introducir diseño multi-pasada con orquestador y timeouts de watchdog; cortar a una máquina de estados de Nivel 3 de estrictamente una-fase-por-trigger; luego relajar eso a progreso-máximo-por-trigger ahora que el checkpoint era fiable; luego parchear el chunking de escritura para cerrar el último caso edge de timeout de inactividad. Cada paso fue una respuesta a un modo de fallo específico que el diseño previo no había modelado.

Lo Que Costó

Días de depuración a través de múltiples noches. Semanas de cacerías de regresión intermitentes a medida que surgían modos de fallo nuevos que no habían existido en el modelo local. Un golpe real a la confianza: código que había sido sólido como roca durante meses estaba de pronto produciendo ramas confusas y merges parciales, y la causa raíz no estaba en el código que la pipeline había estado corriendo — estaba en el modelo de ejecución a su alrededor.

La lección general, dicha en simple: adoptar una tecnología nueva que resuelve uno de tus problemas casi siempre intercambia regresiones en otro lugar. "Siempre encendido" no es un retoque que atornillas a un sistema diseñado para una máquina siempre disponible. Es una reconstrucción. Y si no lo tratas como una reconstrucción, la reconstrucción ocurre de todos modos, solo que en forma más sucia, bajo presión, mientras producción está rota.

Lo Que Le Diría a Alguien Haciendo Esto Hoy

Tres cosas, en orden de cuánto dolor cada una habría ahorrado.

Respalda el estado conocido-bueno antes de reescribir. Cuando la reconstrucción aún está funcionando-en-teoría, haz snapshot de la versión que estaba enviando juegos reales anoche. Un directorio nuevo, un nombre claro, comiteado. El snapshot no es paranoia; es lo que te permite cortar pérdidas en una reconstrucción sin perder el sistema que estaba pagando las cuentas. Un directorio estilo skills-backup en la raíz del repo fue la diferencia entre "corregir el rumbo" y "empezar de nuevo."

Prueba en el entorno restringido temprano, no al final. El error fue validar la pipeline de extremo a extremo en el entorno local de alta capacidad y luego intentar portarla. El entorno local escondía cada suposición que solo se rompe bajo presión de reloj de pared y reintentos sin estado. Si el entorno remoto es el objetivo de producción, hazlo el entorno de prueba de primera clase desde el día uno, incluso si es más lento y más difícil de depurar.

Construye la máquina de estados primero, no segundo. Si tu pipeline tiene más de unos pocos minutos de trabajo de reloj de pared, asume desde el principio que correrá en un entorno restringido eventualmente. Modela el estado en disco, no en memoria. Haz cada fase reanudable. Pagarás un pequeño impuesto de diseño por adelantado y ahorrarás una reconstrucción de varias semanas después. También: los humanos siguen revisando cada pull request por la mañana. Esa parte no cambió en la migración, y es la parte que nunca quieres automatizar.

La tecnología nueva es seductora porque promete resolver un problema real. Usualmente lo hace. La pregunta es qué problemas introduce silenciosamente a cambio.


Lectura relacionada: La Pipeline Nocturna de Juegos · ¿Qué Es Vibe Coding? · Cómo Construimos Neon Snake Con IA · Construyendo un Leaderboard Universal