Los agentes escriben código más rápido de lo que nadie puede revisarlo. Aquí está el trinquete que evita que el código se pudra


Le pides a un agente que refactorice un módulo. Te devuelve un diff de 800 líneas, lo ojeas, lo fusionas. Tres meses después abres ese fichero: 4.000 líneas, una docena de ramas por función, y una hora de lectura antes de poder cambiar una línea con seguridad. El problema no es que el agente escribiera mal código — es que la velocidad de generación ha superado a la velocidad de revisión. Algo puede añadir 5.000 líneas al día; nadie puede leer 5.000 líneas al día. Los codebases rara vez se arruinan por un commit terrible; los aplasta un millar de commits que cada uno añadía “solo un poquito”.

El 5 de octubre de 2026, el repositorio de Hermes fusionó el PR #132646, que instala un mecanismo interesante: la salud del código solo puede mejorar. No persigue un ideal. Funciona como un trinquete — cada función y cada fichero conserva su nivel actual, y solo puede moverse en una dirección. Esto es cómo funciona y qué merece la pena copiar.

La regla central: techos separados, sin préstamos

La regla es corta y dura:

  • Lo que ya está por encima del objetivo no puede crecer. Una función con complejidad ciclomática (CC) 40 — un recuento aproximado de caminos independientes por el código — puede quedarse en 40 o bajar a 20, pero nunca llegar a 41.
  • El código nuevo tiene que cumplir el objetivo. Las funciones y ficheros nuevos deben satisfacer: CC 20, 300 líneas propias por función, anidamiento 6, 2.000 líneas por fichero.

La mitad más sutil es que no puedes tomar prestado presupuesto. Las violaciones de reglas de patrón (digamos, una excepción tragada) se rastrean por huella — la regla, la función contenedora y el texto de la línea. Arreglar una violación nunca paga por añadir otra, y no se acumula nada entre ficheros. Sin eso, un trinquete se convierte en un marcador que se puede amañar.

Sin fichero de línea base — la parte ingeniosa

La mayoría de herramientas de salud del código quieren una línea base: registrar cada violación existente y prohibir superarla. Ese fichero se convierte en un imán de conflictos, porque dos PR en paralelo quieren editarlo y gana quien fusione primero.

Hermes no guarda ninguna línea base. Mide al vuelo: toma los ficheros que toca un cambio, tanto en la base (el primer padre del commit de merge) como en el head, y los compara. Tres consecuencias:

  • una mejora que aterriza en main se convierte de inmediato en el nuevo tope, sin que nadie tenga que acordarse de “apretar el trinquete”;
  • los PR en paralelo nunca chocan por el estado del trinquete;
  • ambos lados se miden siempre con la misma versión de la herramienta, así que la comparación es significativa.

El código movido conserva su techo; el código copiado no

Esto es lo que mantiene viable la refactorización:

  • Las funciones se emparejan por un hash del cuerpo que ignora el nombre. Una función movida a otro fichero y editada ligeramente (similitud de cuerpo ≥ 0,6, uno salió y uno llegó) conserva su techo, y las sentencias a nivel de módulo movidas tal cual conservan el suyo.
  • Una copia que deja el original en su sitio cuenta como código nuevo, y tiene que cumplir el objetivo.
  • Los ficheros renombrados siguen la detección de renombrados de git en lugar de tratarse como nuevos.
  • Las funciones anidadas son unidades propias, así que editar un closure no se carga a la función que lo contiene. La complejidad de Python usa el C901 de ruff menos las defs anidadas directas.

Qué más bloquea

El tamaño es una categoría. El conjunto completo de reglas (con el alcance y el texto de arreglo que ve el agente) vive en scripts/code_health/config.py:

Categoría Reglas
Excepciones tragadas S110, BLE001, E722 (except: pelado); un logger.warning("...: %s", e) solo de mensaje sigue contando como tragada
Corrección F821/F823/F811/F841, RUF006
Puede colgarse ASYNC230/ASYNC240, S113, HX006 (subprocess/urlopen sin timeout), HX007 (I/O de configuración síncrona dentro de async def), HX008 (get_event_loop), HX009 (resultados de gather comparados contra Exception)
Alcance de perfil HX001 (~/.hermes hardcodeado), HX002 (una nueva variable de entorno HERMES_*), HX004 (UnscopedSecretError cayendo a os.getenv), HX005 (captura en tiempo de import), HX012 (threading.Thread en crudo), más PS-P05/PS-P06
Otras HX003 (identidad de proceso por argv), HX010 (bool() de una cadena de entorno), HX011 (escaleras if/elif largas)

Un subconjunto es solo de advertencia: HX001, HX003, HX010, HX012, PS-P05, PS-P06. Las muestras de los revisores sitúan su precisión en torno al 12–60%, así que se imprimen en cada ejecución pero nunca bloquean un commit. Convertir una en bloqueante es un cambio de una línea una vez hay evidencia de repetición.

La vía de escape: renúnciala, pero di por qué

Toda regla necesita excepciones. La sintaxis es:

# health: allow BLE001 -- <why>

El motivo es obligatorio, y cada allow usado en una ejecución se lista en la salida, así que un revisor ve cuántas exenciones añade un cambio y qué dicen. Es una mejora real sobre # noqa, que normalmente es silencioso.

Un comando, exactamente lo que ejecuta CI

La parte más práctica es la experiencia local. python scripts/check ejecuta ruff, las comprobaciones guard (footgun / shebang / no-tmp / config-writer / OS-marker / case-collision / lazy-deps / profile-archive) y el propio trinquete en paralelo, usando el ruff fijado por CI — tomado del grupo dev de pyproject, cayendo a uvx ruff@<pin> en lugar de usar en silencio el ruff que tengas. Unos diez segundos sobre el árbol.

python scripts/check                # todo, contra el merge-base con origin/main
python scripts/check --only health  # solo el trinquete de salud del código
python scripts/check --staged       # juzga el índice (lo que ejecuta el hook de pre-commit)
python scripts/check --commit REV   # juzga un commit (lo que ejecuta el hook de pre-push)
python scripts/check --install-hook pre-push
python scripts/check --uninstall-hook

Los hooks son opt-in y se pueden saltar con --no-verify; CI no. scripts/check necesita Python 3.11+ (para tomllib) y lo dice claramente, apuntando al virtualenv del repositorio, en lugar de morir con un traceback en cada commit con hook.

También hay un interruptor maestro: ENFORCEMENT = "blocking" | "advisory" | "off" en scripts/code_health/config.py. Y lo crucial es que siempre se lee desde main — en CI, del primer padre del commit de merge; en local, de origin/main — nunca de la rama bajo prueba. Un commit de una línea a main baja el trinquete para todos los PR abiertos, y ningún PR puede relajar su propia comprobación.

Se validó sobre una repetición congelada antes de bloquear nada

El peor modo de fallo para una nueva barrera es bloquear cosas que en realidad están bien, así que primero se repitió contra el historial: todos los PR fusionados entre el 2026-10-02 y el 2026-10-03.

Comprobación Resultado
Repetición congelada (318 PR) Se bloquearían 59. 38 solo por crecimiento de ficheros; 21 por otras reglas: crecimiento de CC o una función nueva por encima de CC 20 en 10 PR, 13 excepts ciegos en 9 PR, cinco F841 en un fichero de test, un F811 de test (un test ensombrecido que nunca se ejecutó), un S110, una función nueva de 300+ líneas
Trinquete de tamaño de fichero El único bloqueante para 38 de 318 PR (12 %) en esa ventana, sobre todo +1–50 líneas en ficheros que ya superaban 2.000 (main.ts, auxiliary_client.py, cron/scheduler.py, el test_tui_gateway_server.py de 23 mil líneas)
Informe del árbol completo en main Unidades por encima del objetivo hoy: 451 CC, 55 de longitud de función, 28 de anidamiento; 119 ficheros; BLE001 6.516, S110 828, HX012 227, HX006 209, HX005 53 — deuda existente que solo bloquea cuando crece

Esa última fila merece una pausa. El mecanismo no le pide a nadie que pague la deuda. Las 6.516 excepciones tragadas que ya existen están congeladas, no programadas; solo importan si crecen. Eso es lo que hace que una barrera así sea sobrevivible — exigir una hoja en blanco desde el primer día garantiza que nadie encienda el interruptor.

Qué copiar para tu propio repo

Si tu equipo también deja que los agentes escriban mucho código, varias de estas decisiones se transfieren directamente:

  1. Un trinquete, no un objetivo. No “de ahora en adelante la CC debe quedarse por debajo de 20”, sino “lo que ya está por encima no puede empeorar, y el código nuevo debe cumplir el listón”. La primera versión bloquea toda edición al código heredado.
  2. Mide al vuelo, no guardes fichero de línea base. Sin conflictos entre PR paralelos, y sin el problema de disciplina de acordarse de apretarlo.
  3. Pon huella a las violaciones de patrón para que un arreglo nunca compre una violación nueva.
  4. Publica las reglas nuevas como advertencias primero, demuestra su precisión en repeticiones históricas, y luego hazlas bloqueantes.
  5. Exige un motivo para cada exención, e imprime las exenciones — una excepción silenciosa es lo mismo que ninguna regla.
  6. Haz que el comando local sea idéntico a CI, para que nadie tenga que adivinar qué dirá el pipeline.

Como contexto, este es el mismo repositorio que un agente refactorizó a escala en septiembre: 1.393 subagentes recortaron el Python no de test un 34,4 %. Eso demostró que los agentes pueden reescribir un código en bloque. El trinquete responde a la pregunta siguiente — cómo evitar que vuelva a crecer poco a poco. Si te importa que las restricciones de ingeniería aterricen en CI en lugar de en una wiki, las comprobaciones de cadena de suministro del catálogo de plugins son otra versión de “escribe la regla en el pipeline”.

La lección de diseño

Quienes construyeron esto tomaron una decisión deliberadamente modesta: no perseguir el ideal, solo mantener la línea. Los objetivos — CC 20, funciones de 300 líneas, anidamiento 6, ficheros de 2.000 líneas — no son un estándar de virtud; son la barrera en la que se detiene el “irse a peor”. Lo que lo hace sobrevivir en la práctica son tres detalles: la deuda existente se congela en lugar de liquidarse, el interruptor de aplicación siempre se lee desde main y no desde la rama que se juzga, y cada regla tiene que demostrar sobre datos históricos que no producirá falsos avisos. Cuando generar código se acerca a ser gratis, mecanismos como este ganan valor, no lo pierden — porque lo único que se vuelve más caro es el tiempo que una persona necesita para leerlo.