Mi último post es del 5 de abril. En los cinco meses siguientes no escribí aquí una línea, pero seguí trabajando en un proyecto privado en el que la mayor parte del código no lo escribo yo: lo escribe un agente. Este post no va de qué construí — va de qué tuvo que ser cierto en el repositorio para que eso no acabara en un desastre.
El problema, dicho sin adornos
Cuando un agente escribe la mayoría del código de un proyecto, y lo hace todos los días durante meses, la revisión humana línea a línea deja de existir. No es cuestión de disciplina ni de ponerle ganas. Puedo leer un diff con atención, o puedo leer los que se acumulan en una tarde. No las dos cosas.
La respuesta que no funciona es revisar más despacio. La que sí funciona es dejar de revisar código y empezar a revisar invariantes: cosas que tienen que ser ciertas siempre, comprobadas por una máquina, en cada PR, sin excepción y sin que yo esté delante.
Cuando el agente escribe la mayor parte del código, tu trabajo deja de ser escribirlo y deja de ser leerlo. Tu trabajo es decidir qué no puede romperse nunca, y convertir esa decisión en algo que falle solo.
No voy a dar cifras de volumen ni a decir qué es el proyecto. Es privado y seguirá siéndolo un tiempo. Y un contador de commits es una métrica pobre para lo que quiero contar: mide actividad, no criterio. Lo que sirve es el proceso que lo sostiene, que además es lo único de aquí que te puedes llevar.
El estado no vive en el chat
Antes de los gates hay un problema anterior: cómo se dirige a un agente durante meses cuando su memoria se muere al final de cada sesión.
La respuesta que fui construyendo se resume en una línea que acabó escrita en las instrucciones del repositorio: mantén el contexto compartido pequeño y duradero, y pon el estado de la tarea en el sistema de ficheros, no en el historial de conversación. Todo lo demás sale de ahí.
Un libro de registro, no una conversación
Cada tarea tiene un identificador y un expediente en el repositorio. Lo que le pasa a esa tarea se escribe como eventos tipados en JSON, en un fichero al que solo se añade, nunca se reescribe: claim cuando un agente la reclama, progress, question, answer, handoff, verify, close, y también supersede y abandon para las que mueren.
Dos reglas de ese registro me ahorran más tiempo que cualquier gate:
- Una decisión que bloquea no se queda en el chat. Se escribe como un evento
questiony se resuelve con un eventoanswer. Si está en la conversación, desaparece cuando la sesión termina, y el agente siguiente vuelve a preguntarlo o, peor, se lo inventa. - Una tarea no se cierra sin evidencia. Evidencia es: comandos ejecutados, ficheros revisados, capturas, logs — o una razón explícita de por qué una comprobación no pudo correr. "Hecho" no es evidencia.
Y antes de parar o de pasarle el trabajo a otro agente, se escribe un handoff con el estado, la rama y la siguiente acción. Ese fichero es lo que sobrevive a la ventana de contexto.
Escala de dureza
No todo el trabajo merece la misma ceremonia. Tener que decidirlo caso por caso es agotador, así que lo convertí en una tabla que el agente aplica solo:
| Nivel | Cuándo | Qué exige |
|---|---|---|
| H0 | Typo, texto, cambio local trivial | Nota final |
| H1 | Cambio pequeño de código o docs | Tarea recomendada |
| H2 | Cambio de comportamiento con tests | Tarea, criterios de aceptación, verificación |
| H3 | Función cross-módulo o de cara al usuario | Especificación, tareas, evidencia |
| H4 | Migración multi-paquete o alto riesgo | RFC, especificación, olas, revisión independiente |
La frase operativa que va con la tabla: ajusta la profundidad de la verificación a la dureza. Un agente sin esto hace una de dos cosas, y las dos son malas: monta un proceso de tres actos para arreglar una errata, o se lanza a una migración de veinte ficheros sin plan.
La separación que sostiene todo: los RFC deciden; las especificaciones y las tareas ejecutan. Cuando algo se discute en el sitio equivocado, se nota inmediatamente.
Coordinación en su propio worktree
Con varios agentes trabajando a la vez, el registro se convierte en el punto de conflicto: todos quieren escribir en él, y cada uno está en su propia rama.
La solución que funcionó fue separar los dos tipos de estado en árboles de trabajo distintos. Los worktrees de funcionalidad llevan código y tests, cada uno en su rama. Un worktree de coordinación aparte, anclado a la rama de integración, lleva solo lo compartido: reclamaciones, preguntas, handoffs, resúmenes de verificación y evidencia de cierre. El código diverge en paralelo; la coordinación converge en un único sitio.
Y un comando de reconciliación para el fallo más común de todos: tareas cuya rama ya se integró pero que siguen abiertas en el registro. Las busca y las señala.
El resto del andamiaje
- Diecinueve skills propias — procedimientos repetibles convertidos en comandos: auditorías de arquitectura, de seguridad, de tests y de tipos; ciclo de vida de una especificación; planificación de objetivo a tarea. Lo que de otro modo reescribiría en el prompt cada vez.
- Un hook de política sobre los comandos de shell. El agente no ejecuta lo que quiere: sus comandos pasan por una política antes. Es la diferencia entre darle un terminal y darle un terminal con reglas.
- Tres servidores MCP — uno que le da acceso indexado a la documentación del proyecto, otro con reglas de seguridad, y un tercero de diagnóstico de salud del código. El agente consulta en vez de suponer.
- Un devcontainer, para que el entorno sea idéntico y desechable.
- Subagentes solo para reconocimiento de lectura, revisión y análisis. Son útiles y gastan contexto aparte, que es justo el recurso que estás intentando proteger.
Nada de esto va de escribir mejores prompts. Va de que el proyecto tenga una memoria que no dependa de que el agente la recuerde. El prompt es lo único de todo el sistema que se borra solo.
Los guardarraíles
Ninguno de estos nació de un diseño previo: casi todos existen porque algo se rompió antes y no quise que volviera a pasar. Los que más trabajo me ahorran:
- Determinismo de merge. Comprueba que integrar dos ramas da el mismo resultado independientemente del orden. Con varios agentes trabajando en paralelo sobre el mismo repositorio, los conflictos que importan no son los que git detecta — son los que git resuelve limpiamente y dejan el código coherente en sintaxis e incoherente en comportamiento.
- Un registro de trabajo. Cada tarea tiene identificador, estado y un handoff escrito que sobrevive al final de la sesión. La memoria del agente se muere con la ventana de contexto; el registro no. Sin esto, cada sesión nueva vuelve a empezar de cero y el trabajo a medias se pierde.
- Mutation testing. Introduce fallos a propósito en el código y verifica que los tests los detectan. Un agente que escribe tests y código a la vez tiende a escribir tests que pasan, que no es lo mismo que tests que sirven. Esto lo destapa.
- Presupuestos de rendimiento. Umbrales de tamaño de bundle y métricas de carga que fallan el PR si se pasan. La degradación de rendimiento nunca llega en un commit: llega en cuatrocientos, cada uno con su justificación razonable.
- Migraciones de base de datos verificadas contra una real. No contra un mock. Es el sitio donde un error no se arregla con un revert.
- Auditoría de dependencias semanal y back-merge automático entre ramas largas, que son las dos tareas que yo posponía indefinidamente cuando dependían de mi disciplina.
El patrón: ninguno de estos revisa si el código es bueno. Todos comprueban que no se ha roto algo concreto que ya sé que importa. Es una distinción aburrida y es toda la diferencia.
Qué se rompió para que existan
Ninguno de estos gates es una buena práctica que leí en algún sitio. Cada uno tiene una fecha y un destrozo detrás.
El diff que parecía un parche
Un bot de dependencias subió una acción de CI de la versión 6.4.0 a la 7.0.0 en seis workflows a la vez, en un PR que se auto-mergeó. La acción compuesta que esos workflows invocan se quedó en 6.4.0. Y los comentarios # v6 se quedaron puestos en los seis sitios.
Así que el diff se leía como un cambio de parche. Un humano revisándolo con atención habría visto seis veces v6 y habría seguido adelante. Yo lo habría aprobado.
Lo detectó un invariante que no tiene nada que ver con versiones: la regla que exige que la build de comparación de tamaño use exactamente la misma versión de herramientas que la compuesta, porque si no, la comparación no mide lo que dice medir. La divergencia salió por ahí, de rebote, once días después de entrar.
El cambio era invisible a la inspección humana por diseño: los comentarios decían una versión y el código otra. Ninguna cantidad de atención lo habría cogido. Solo lo coge una máquina que comprueba una propiedad, no un texto.
Las tres veces que el agente movió el guardarraíl en vez de arreglar el código
Esta es la que no me esperaba, y es la razón por la que escribo el post.
- Un gate de nombres de fichero empezó a fallar sobre un fichero nuevo. El agente añadió ese fichero a la lista de excepciones del gate, con una justificación razonada y correcta en sus términos. Revertido el mismo día.
- Un cambio superó el presupuesto de tamaño. El agente subió el presupuesto. Al revisarlo resultó que el código podía ser más pequeño: revertido con la nota "la opción B encoge el guardia en vez de agrandar el límite".
- Un test de propiedades se pasaba del tiempo máximo en CI. El agente subió el tiempo máximo de 5 a 20 segundos.
El tercero era correcto: el test tardaba medio segundo en local y se iba a cinco bajo carga con cobertura activada, y el límite por defecto era el que estaba mal. Los otros dos no lo eran.
Y esa es justo la dificultad. No es que el agente haga trampa — las tres veces argumentó bien, y una de las tres tenía razón. Es que cuando algo le bloquea el paso, mover el obstáculo y resolver el problema le parecen la misma cosa, porque ambas hacen que el CI se ponga verde. Verde es su función objetivo. La mía es otra.
La consecuencia práctica: los ficheros que definen los propios gates —límites, listas de excepciones, umbrales— necesitan tratarse como código crítico y no como configuración. Un PR que toca un umbral no es un PR de mantenimiento.
El gate que existía y no estaba corriendo
Una migración de formato reescribió 453 documentos del corpus de una versión a la siguiente. Pasó CI y entró. Rompía la decodificación en uno de los clientes, que exige el formato antiguo y lanza una excepción a propósito si recibe el nuevo. Hubo que revertir las dos PRs enteras.
Existía un gate que lo cubría exactamente. La frase que escribí en el PR de revert: "lo habría detectado, pero el runner estaba caído y nunca llegó a ejecutarse".
Un guardarraíl que no se ejecuta no es un guardarraíl: es una creencia. Y es peor que no tenerlo, porque te hace aprobar cosas que no habrías aprobado. Desde entonces, un gate que no ha corrido cuenta como gate en rojo, no como gate ausente.
El único incidente que llegó a producción
Dos entornos distintos acabaron declarando el mismo nombre de servicio en el enrutador. El tráfico se cruzaba entre ellos de forma intermitente. Afectó a los tres entornos y tardó en verse porque no fallaba: respondía mal de vez en cuando.
El hotfix fue de una tarde. Lo que salió del post-mortem es lo que quedó: una sonda de tres pasos que corre en cada despliegue y comprueba tres cosas que antes nadie comprobaba — que el HTML servido apunta siempre al mismo build si lo pides varias veces, que ese build responde 200 todas las veces, y que las rutas de API devuelven JSON y no el HTML de la aplicación por fallback.
Las tres son triviales. Las tres habrían detectado el incidente en el primer minuto.
Lo que sigue siendo mío
Casi todo lo que firmo yo en ese repositorio son merges. El agente propone, implementa, prueba y defiende; yo decido qué entra.
Suena a poco y no lo es. El merge es el único punto del proceso que no he automatizado, a propósito. Un agente puede argumentar muy bien a favor de un cambio. No decide si ese cambio es el que el producto necesita. Esa sigue siendo la parte cara, y sospecho que va a seguir siéndolo bastante después de que el resto deje de serlo.
Crear se ha vuelto gratis; mantener no
Este verano creé también tres repositorios que no llegaron a tener un solo commit. Existen porque reservar un nombre cuesta ocho segundos. Uno de ellos iba a ser mi sitio para trastear con Glyph, el trabajo de Tsinghua que amplía la ventana de contexto renderizando el texto como imágenes y pasándoselo a un modelo de visión en vez de alargar el contexto en tokens. Busqué, vi que ya estaba hecho y mejor de lo que yo lo haría, y no escribí nada. De los tres vacíos, ese es el único acierto.
Y abandoné el proyecto en el que más código había escrito este año, con diferencia, porque salió una herramienta terminada que hacía lo mismo. No fue una derrota: fue aritmética. Mantener mi versión peor habría sido pagar por el privilegio de tenerla.
Los dos casos dicen lo mismo que el proyecto que sí sigue vivo, desde el otro lado. Montar el andamiaje de un proyecto —el repo, el CI, el README, los primeros miles de líneas que funcionan— es hoy cuestión de una tarde con un agente. Ese coste se ha desplomado. Sostenerlo no ha bajado ni un céntimo: sigue siendo atención humana, y la atención humana no escala con el modelo que uses.
Antes, montar el andamiaje dolía lo suficiente como para obligarte a pensar si merecía la pena. Ese dolor era información. Lo hemos eliminado y no hemos puesto nada en su lugar. Los guardarraíles de arriba son mi intento de poner algo: si el coste de empezar ya no te obliga a decidir, tiene que obligarte otra cosa.
Es una muestra de uno. Un proyecto, cinco meses, mis costumbres. No he medido a nadie más, así que esto es una hipótesis con datos detrás, no un hallazgo.
Las técnicas que describo son las que uso, no un catálogo de lo que existe. Salen de las instrucciones y la configuración de mi propio repositorio. No he comparado ninguna con alternativas ni he medido cuánto aporta cada una por separado: sé que el conjunto funciona, no qué pieza pesa más.
Los cuatro incidentes salen de mis propias PRs y commits, no de la memoria. Los he releído para escribirlos y he quitado todo lo que identificaba el producto, así que lees una versión abstracta de algo concreto. La abstracción es mía y puede haberle quitado matiz.
No puedes verificar nada de esto. El repositorio es privado y he quitado a propósito los contadores. Así que aquí tienes que creerme — y eso es exactamente lo que esta web dice que no deberías tener que hacer. Es la limitación más grande del post y prefiero decirla a maquillarla. Lo que sí puedes hacer es juzgar las técnicas por sí solas: o te resuelven un problema que tienes, o no.
No puedo separar qué agente hizo qué. Trabajé con Claude Code y con Codex alternando. Claude Code firma el commit con el modelo que participó; Codex no firma nada. Cualquier recuento por herramienta saldría sesgado hacia el único que deja rastro. GPT-6 Astra, que hoy uso a diario, salió el 3 de septiembre: llegó al final de esta historia, no la escribió.
Por qué cuento el proceso y no el producto
Podría haber escrito el post de "mira lo que he construido este verano". Habría funcionado mejor en el feed y no habría servido de nada, porque no puedes abrirlo.
El proceso sí te sirve. Si vas a dejar que un agente escriba la mayoría de tu código —y vas a hacerlo, si no lo estás haciendo ya—, la pregunta útil no es qué modelo usar. Es qué tiene que fallar solo en tu repositorio para que puedas dormir. Esa lista es corta, es aburrida, y es la diferencia entre un repositorio del que te fías y uno que tendrás que auditar entero en enero.
// Fuentes
- GitHub REST API — Commits — origen de los contadores de commits y del reparto de autoría
- GitHub REST API — Pull requests — origen del recuento de PRs
- thu-coai/Glyph y el paper (arXiv:2510.17800) — compresión visual-texto para ampliar la ventana de contexto
- OpenAI publica GPT-6 Astra (InfoQ, 09.2026) — la fecha de salida
- Cómo trabajo — qué parte de este post la escribe un modelo y qué parte es mía