Recientemente hemos montado un quality gate que el agente de IA ejecuta él mismo antes de dar una tarea por terminada y que vuelve a correr en cada pull request. Solo mira las líneas que ha cambiado la PR y comprueba ocho cosas: reglas de SonarJS, plantillas Angular, SCSS, reglas de arquitectura, código muerto, secretos, vulnerabilidades de dependencias y que cada fichero tocado tenga su test al lado. A eso se suma la cobertura del diff. Si algo falla, el agente no puede cerrar la tarea y recibe las incidencias para corregirlas.
Todo lo hemos hecho con SonarQube Community Build, que no analiza pull requests, y sin pagar licencias nuevas. El repo es una app Angular 21 + Ionic 8 con arquitectura hexagonal, unas 184.000 líneas, Bun como gestor de paquetes, Azure Pipelines como CI y Bitbucket para las PR.
Por qué hace falta
La IA no arregla un equipo, amplifica lo que ya hay. Lo dice el informe DORA de 2025, que además relaciona la adopción de IA con menos estabilidad en las entregas cuando no hay controles fuertes, como tests automáticos sólidos: "Without robust control systems, like strong automated testing … an increase in change volume leads to instability" (DORA 2025). En el informe de 2024, cada 25 % más de adopción de IA iba acompañado de una caída estimada del 7,2 % en la estabilidad de las entregas (DORA 2024).
Los datos sobre el código generado van en la misma dirección:
- GitClear midió que el código copiado pasa del 8,3 % al 12,3 % de las líneas cambiadas entre 2021 y 2024, mientras el código refactorizado o movido baja del 25 % a menos del 10 % (GitClear 2025).
- Veracode encontró fallos de seguridad en el 45 % de las pruebas con código generado por más de 100 modelos (Veracode 2025).
- En un estudio de Stanford, quienes usaban un asistente escribieron código menos seguro y a la vez confiaban más en que era seguro (Perry et al.).
- CodeRabbit analizó 470 PR y las escritas con IA tenían unas 1,7 veces más incidencias (CodeRabbit).
Nuestra lectura es simple: si el agente va a escribir mucho código, la comprobación tiene que ser automática, determinista y estar dentro del propio bucle del agente, no solo en la revisión humana.
La pieza central: un gate sobre el diff
SonarQube Community Build solo analiza la rama principal después del merge; el análisis de pull requests empieza en la Developer Edition (docs de SonarSource). Es decir, Sonar te avisa cuando el problema ya está en development.
Lo resolvimos con un script de Node (bun run quality:pr) que calcula el merge-base contra la rama base, saca las líneas añadidas o modificadas con git diff --unified=0 y filtra todas las comprobaciones a esas líneas. Esto sigue la idea de Clean as You Code de SonarSource: el gate se aplica solo al código nuevo para concentrar el esfuerzo ahí (SonarSource, New code). Con un repo de 184.000 líneas y deuda legacy, un gate sobre todo el código fallaría siempre y el equipo dejaría de mirarlo.
Lo que comprueba:
- Reglas de SonarJS con eslint-plugin-sonarjs, las mismas reglas JS/TS de Sonar ejecutadas en local. El propio README avisa de que no están todas las del analizador del servidor, así que el análisis de Sonar después del merge sigue teniendo sentido.
- Plantillas Angular con angular-eslint, cuyo parser usa
@angular/compiler. Encontró atributos duplicados en plantillas que llevaban tiempo en el repo. - SCSS con Stylelint:
remen vez depxy variables del design system para colores y fuentes. - Cobertura del diff: el porcentaje de líneas nuevas cubiertas por tests, no la cobertura global. El Google Testing Blog defiende objetivos por commit (90 % como suelo razonable) frente a objetivos de proyecto (Code Coverage Best Practices), y es la misma idea de diff-cover. Nosotros empezamos en un 60 %, configurable por variable de entorno.
El resultado se publica en la PR con Bitbucket Code Insights: un informe y anotaciones en las líneas afectadas (la API admite hasta 1.000 por informe). Hay un precedente en dev.to de este enfoque con ESLint (Julian Cook).
Meter el gate dentro del bucle del agente
El cambio que más ha pesado en la práctica es este. Claude Code tiene hooks, comandos que se ejecutan en momentos concretos del ciclo del agente. El hook Stop corre cuando el agente cree que ha terminado, y si sale con código 2, Claude no puede parar y recibe la salida para seguir trabajando (Hooks reference).
La propia documentación de Anthropic lo explica mejor que nosotros: "Claude stops when the work looks done", y "Unlike CLAUDE.md instructions which are advisory, hooks are deterministic" (Best practices). Poner en el CLAUDE.md "pasa el linter antes de terminar" es una sugerencia; un hook es una obligación.
Nuestro hook hace tres cosas:
- Si
stop_hook_activeviene atrue, sale sin bloquear. Así el agente recibe el gate una vez por parada y no entra en bucle. - Desactiva la cobertura, porque el
lcov.infolocal suele ser de otra rama y daría falsos fallos. En el hook solo cuentan las reglas, los secretos y las dependencias. - Si el gate falla, devuelve las incidencias junto con instrucciones concretas: corregir sin
eslint-disable, sacar los secretos a.envo al Key Vault, subir la dependencia en vez de ignorar el aviso, crear la spec si falta.
Ese último punto importa. Si no se lo dices, el agente toma el camino corto y silencia la regla. Cursor tiene un mecanismo equivalente con sus eventos stop y afterFileEdit (Cursor hooks).
Reglas de arquitectura que el agente no puede saltarse
El repo tiene una arquitectura hexagonal con prohibiciones claras: Firebase solo se importa en un servicio, PouchDB solo en otro, y los componentes de UI no inyectan servicios de infraestructura. Estaban escritas en el CLAUDE.md, pero eso no garantiza nada.
Las convertimos en reglas forbidden de dependency-cruiser, que valida el grafo de imports contra tus propias reglas. Son tres reglas, cada una con un comment que explica qué usar en su lugar, porque ese texto es lo que lee el agente cuando falla. En Xebia cuentan algo parecido: al activar las reglas encontraron muchos imports incorrectos incluso en un código base nuevo y cuidado (Xebia).
Con knip detectamos ficheros, exports y dependencias sin usar en lo que cambia la PR. Un agente que refactoriza tiende a dejar el código viejo vivo al lado del nuevo, y GitClear lo ve en sus datos con el aumento de código duplicado. Como dice la documentación de knip, el código muerto es engañoso y menos código es menos superficie para fallos (Why use Knip?).
Secretos y dependencias
gitleaks revisa los cambios en busca de credenciales. GitGuardian midió que el 6,4 % de los repos que usan Copilot filtró al menos un secreto, frente al 4,6 % del total de repos públicos (GitGuardian). Un aviso: el README de gitleaks dice ahora que el proyecto está "feature complete" y solo recibirá parches de seguridad.
bun audit (docs) falla si aparece una vulnerabilidad nueva de nivel alto o superior. Las que ya existían en la rama base no bloquean la PR, porque si no ninguna PR pasaría. Con IA hay además un riesgo propio: los paquetes alucinados. Un estudio presentado en USENIX Security 2025 encontró que al menos el 5,2 % de los paquetes recomendados por modelos comerciales, y el 21,7 % en modelos open source, no existen (Spracklen et al.). Un atacante puede registrar ese nombre, y es lo que se ha llamado slopsquatting (Socket).
Quien toca un fichero sin test, escribe el test
Al preparar el mutation testing vimos que cientos de ficheros con lógica no tenían su .spec.ts al lado. La guía de estilo de Angular es clara: los tests unitarios viven en el mismo directorio que el código que prueban (Angular style guide).
La primera idea fue abrir un ticket para crearlos todos con tests mínimos. La descartamos porque eso produce tests de relleno que solo suben la cobertura. Preferimos la regla del boy scout (Robert C. Martin), que el Google Testing Blog aplica justo a la cobertura: dejarlo un poco mejor cada vez y llegar de forma incremental a un estado sano.
La regla colocated-spec falla si la PR cambia líneas de un .ts con lógica que no tiene spec al lado. Excluye rutas, módulos, mocks, ficheros index y .d.ts. Para el agente supone que, si toca el fichero, tiene que escribir el test de su comportamiento en la misma tarea.
Mutation testing: comprobar que los tests prueban algo
La cobertura dice que una línea se ejecutó durante un test, no que alguna aserción fallaría si esa línea estuviera mal. Loiane Groner lo explica con Angular y Stryker, y señala que los tests generados por IA tienden a usar toBeTruthy() y a optimizar la cobertura (Loiane Groner). Meta usa justo mutantes para guiar la generación de tests con LLM: su herramienta ACH generó 571 tests y los ingenieros aceptaron el 73 % (Meta, ACH).
Añadimos StrykerJS bajo demanda con bun run quality:mutation <fichero|carpeta>, no dentro del gate. Ponerlo en marcha con Angular 21 costó más de lo previsto:
- Stryker necesita un config de Vitest propio, y
ng testusa Vitest por dentro del builder de Angular. Usamos el plugin de Analog para compilar los componentes en un config de Vitest normal. - Con AOT, los mutantes dentro de decoradores y signal inputs rompían la compilación con el error "statically analyzable". Lo resolvimos compilando en JIT solo cuando corre Stryker y con el ignorer oficial
angularde Stryker. Al principio escribimos un plugin propio sin buscar si ya existía uno. - Seis specs daban otro resultado con Analog que con
ng test. El builder de Angular convierteasync/awaiten generadores porque zone.js no puede parchear elasyncnativo; añadimos un paso de esbuild que hace lo mismo. - Sobre todo
src/appsalen 94.523 mutantes, inviable. Lo lanzamos por fichero o carpeta y solo con la spec hermana de cada fichero.
El matiz académico existe: un estudio de 2026 concluye que la utilidad de la cobertura y la mutación sobre suites generadas por LLM depende mucho del contexto (Zhao et al.). Para nosotros es una herramienta de revisión, no un número que perseguir.
Darle al agente acceso a Sonar con MCP
El SonarQube MCP Server expone Sonar como herramientas que el agente puede llamar: estado del quality gate, incidencias de un proyecto, cobertura de un fichero (Tools). Funciona con Community Build desde la 25.1 (GitHub).
Lo dejamos versionado en el .mcp.json del repo, en local con Docker, en modo solo lectura y con el token personal de cada desarrollador en una variable de entorno. Así cada uno ve solo lo que su cuenta ve y no hay usuario técnico compartido. Dos lecciones:
- Usa la imagen oficial
sonarsource/sonarqube-mcp. La que aparece en el catálogo MCP de Docker era una copia desfasada. - Fija la imagen por digest, no solo por tag. Una revisión de seguridad automática nos lo marcó como riesgo de cadena de suministro: un servidor MCP corre con tu token y puede ejecutar herramientas, así que es una dependencia como cualquier otra.
La versión alojada por SonarSource exige una edición de pago, así que hemos pedido a DevOps que evalúe desplegarlo como servicio compartido dentro de la red interna.
Lo que encontramos en el propio Sonar
Revisar la configuración del servidor sacó problemas que no veíamos:
- El "New Code" llevaba dos meses fijo en una versión antigua. No era la configuración, que ya estaba en Previous version: era que no llegaban análisis con versión nueva.
- El checkout del pipeline usaba
fetchDepth: 1. Con un clon superficial Sonar no tiene blame, en nuestro caso para 2.321 ficheros, y sin blame no puede saber bien qué líneas son nuevas. Lo cambiamos a historial completo solo en la rama que analiza Sonar. - Un spec tenía dos caracteres corruptos (
U+FFFDen lugar de una "ñ") que el scanner marcaba como problema de encoding. - El gate asignado solo exigía un 10 % de cobertura en código nuevo. Lo cambiamos por uno más estricto.
Un quality gate sobre código nuevo depende de que Sonar sepa qué es código nuevo. Revisa el blame y la versión antes de endurecer las condiciones.
Resultados y lo que todavía no sabemos
En el código nuevo, la fiabilidad está en A según Sonar. La global sigue en E por el código legacy, que es justo lo que esperábamos de un enfoque sobre el diff.
No hemos medido todavía cuántas incidencias evita el hook antes de llegar a la PR, ni el impacto en tiempo de entrega. Tampoco hemos validado el flujo en Windows, que usa parte del equipo. El estudio de METR de 2025 encontró que desarrolladores con experiencia tardaban un 19 % más con IA mientras creían ir un 20 % más rápido (METR); su actualización de 2026 rebaja la conclusión, con intervalos que incluyen el cero (METR 2026). La lección para nosotros es no fiarnos de la sensación y medir.
Si tuviera que quedarme con tres ideas:
- Gate sobre el diff, no sobre el repo. Es la única forma de que un código con deuda adopte un gate exigente.
- El gate dentro del bucle del agente, con un hook determinista y mensajes que digan cómo arreglar, no solo qué falla.
- Reglas que el agente lee: arquitectura, tests colocados y secretos como comprobaciones automáticas, no como párrafos en un markdown.
Top comments (0)