En el artículo anterior contaba cómo montamos un quality gate sobre el diff para el código que escriben los agentes. Eso protege el código nuevo, pero deja fuera la deuda que ya existía. Este artículo va de esa deuda.
Hablo de una app Ionic grande y con algunos años de vida, con la deuda que acumula cualquier proyecto así: sobre todo code smells de mantenibilidad que Sonar marca y que nunca entran en un sprint, porque la deuda siempre pierde contra un evolutivo en el planning.
Un workflow de Claude Code con 246 subagentes limpió casi toda esa deuda y subió bastante la cobertura de tests, en una PR de más de mil ficheros. Fueron 19 horas de reloj y unos $600 en tokens. Yo le dediqué un par de horas como mucho. La PR lleva semanas mergeada y no hay ningún bug ni revert atribuidos a ella.
El dato que más me sorprendió está en el título: los agentes que arreglaban el código y escribían los tests costaron unos $44. Todo lo demás se fue en poder fiarse del resultado. De esa parte va el resto, con cifras por capa, porque en mi opinión es la que casi nadie publica.
Qué estaba optimizando
Que un agente sepa cambiar un || por un ?? ya lo sabemos. La pregunta de verdad es otra: si al final tengo una PR de más de mil ficheros escrita por una IA, ¿cómo la apruebo? Nadie va a leer ese diff, y una PR que no se puede revisar es vibe coding descontrolado.
Así que el objetivo no era cuántas incidencias se cerraban. Era llegar a una PR que una persona pudiera aprobar sin leer el código y sin que aparecieran regresiones después. La revisión humana no desaparece: pasa de leer líneas a comprobar las pruebas de cada cambio y leer con calma los pocos que alteran el comportamiento.
El equipo de Claude Code cuenta algo parecido: optimizan el mínimo tiempo humano de revisión que no aumente reversiones ni incidentes. Aquí lo aplico a un caso concreto, una limpieza masiva, y añado lo que ellos no publican: cuánto cuesta cada capa de verificación y cuántos de sus hallazgos son reales.
Un lote, de principio a fin
Darle la lista de incidencias a una sola sesión no funciona: no cabe en un contexto, no paraleliza y no deja rastro por cambio. Los workflows de Claude Code son un script JavaScript que lanza subagentes. La diferencia con pedirle a un agente que los lance es que el control de flujo es código: qué se paraleliza, qué se revisa y qué pasa si algo falla no depende de que un modelo se acuerde.
La unidad de trabajo es el lote. Un script reparte el escaneo de Sonar en 52 lotes por carpeta, pesando incidencias, líneas sin cubrir y specs que faltan. Un lote tiene esta forma (simplificado):
{
"id": "14",
"scope": "src/app/common/validators",
"agentType": "angular-implementer",
"specsToCreate": 3,
"files": [
{
"path": "src/app/common/validators/greater-than.validator.ts",
"needsSpec": true,
"issues": [
{"rule": "typescript:S6606", "line": 16, "coverage": "covered"},
{"rule": "typescript:S6582", "line": 28, "coverage": "covered"}
]
}
]
}
El implementer sigue una escalera de garantías en cada fichero:
- Si no tiene spec, escribe una de caracterización que pase sobre el código sin tocar. Fija lo que el código hace hoy, no lo que debería hacer.
- Si la línea que va a cambiar no está cubierta (el campo
coverage), añade antes un test que fije su salida. - El refactor tiene que ser equivalente.
- Si el cambio es intencionado porque hay un bug real, lleva un test que pasa de rojo a verde y una nota con la prueba manual.
- Si algo queda en rojo, revierte ese cambio y lo marca como skipped. No se debilita ningún test.
En el historial de ese lote queda así, de más antiguo a más nuevo:
test(validators): caracterizar emailValidator y su regex
test(validators): caracterizar greaterThan y lessThan validators
refactor(validators): quitar escape innecesario en regex de email
refactor(validators): usar ?? y optional chaining en greaterThan y lessThan
- const value = control?.value?.toString() || ''
+ const value = control?.value?.toString() ?? ''
- if (compareControl && compareControl.invalid) {
+ if (compareControl?.invalid) {
Cada lote deja una nota con cuatro secciones: cambios de comportamiento, TODOs pendientes, falsos positivos y skipped. Un cambio de comportamiento anotado tiene esta pinta:
getData():signature ?? ''sustituye asignature ? signature : ''. Idéntico paranull,undefined,''y cualquier string. Difiere solo para0/false, que no puede llegar desde el componente que lo llama.
Hay más de 150 notas así, listadas en la PR. El patrón no es perfecto: en tres casos el test y el cambio tuvieron que ir en el mismo commit, porque el test ya llamaba a la API nueva, y la nota lo dice.
Una advertencia sobre la cobertura: sube unos 20 puntos, pero la mayor parte son tests de caracterización, que fijan también los bugs que el código ya tuviera. Sube la red para detectar cambios, no la confianza en que el comportamiento sea el correcto.
Cuánto cuesta fiarse, capa por capa
Cada capa de verificación encontró cosas que las anteriores no habían visto. Esta es la tabla que me habría gustado leer antes de empezar: cuántos hallazgos entregó cada capa, cuántos eran reales y cuánto costó, a precios de lista de la API con la caché de 1 hora de Claude Code.
| Capa | Agentes | Hallazgos | Reales | Coste aprox. | $ por hallazgo real |
|---|---|---|---|---|---|
| Implementers (arreglo + tests) | 66 | — | — | 44 $ | — |
| Revisores por lote (Sonnet) | 124 | sin medir | 1 bloqueante | 30 $ | 30 $ |
| Integración, gates y reescaneo | 5 | 6 incidencias nuevas | 6 | 39 $ | 7 $ |
| E2E contra preproducción | 1 | 10 fallos | 0 regresiones | 10 $ | — |
| Auditoría con otro modelo (Fable) | 8 | 30 | 7 | 69 $ | 10 $ |
/code-review al máximo |
31 | 15 | 13 | 375 $ | 29 $ |
El plan, el autofix mecánico y la sesión principal suman otros 34 $. Total: unos 600 $.
Lo que leo en la tabla:
- Arreglar es barato; verificar, no. El 7 % del gasto fue en hacer el cambio. El resto, en poder demostrar que el cambio era correcto.
-
La capa más cara encontró el fallo más grave. Un agente cambió una regex por otra con lookbehind para contentar a Sonar, y el lookbehind no existe en Safari antes de la 16.4: en dispositivos antiguos habría roto un formulario. Ningún test unitario lo ve, porque los tests corren en Chrome. Lo encontró el
/code-reviewal máximo, antes del merge y después de que todas las capas anteriores dieran el visto bueno. - La precisión varía mucho. La auditoría con otro modelo acertó en el 23 % de sus hallazgos; el code-review, en el 87 %. Cada hallazgo se confirmaba o se refutaba con un test antes de tocar código, así que los falsos positivos costaron tokens, pero no cambios.
- De los revisores por lote no sé si compensan. Pararon un bloqueante real, pero no medí cuántos de sus hallazgos no bloqueantes se aplicaron. Es lo primero que mediría la próxima vez.
- La verificación también hay que verificarla. El reescaneo final cazó 6 incidencias nuevas que habían metido los propios arreglos del code-review.
No probé a repetir el mismo revisor, así que no puedo decir que no sirva. Lo que sí vi es que cada cambio de modelo o de corte (por lote, por área, por ángulo) sacó cosas nuevas.
Sobre los tokens: el 97,7 % de la entrada fue lectura de caché. Cada turno de un agente relee su contexto entero, así que lo que se paga es el número de turnos, no el tamaño del cambio. Por eso el code-review, con buscadores que encadenan cientos de turnos, es la capa más cara. Elegir el modelo por rol (Haiku para implementar UI, Sonnet para servicios y revisores, Opus para el plan y la integración) fue lo que más contuvo el coste. Con una suscripción de Claude Code lo que se paga es la cuota, no estas cifras.
Qué hace el revisor humano
La PR no pide leer el diff. Pide esto, en este orden:
- Elegir commits
refactor(...)al azar y comprobar que eltest(...)anterior toca el mismo fichero y pasa sobre el código de antes. - Leer todas las notas de cambios de comportamiento, que van en el primer comentario de la PR. Son lo único que debería cambiar algo, y cada una dice por qué es equivalente o qué cambia.
- Leer con calma los pocos puntos marcados para revisión particular: los cambios que se revirtieron a propósito y por qué, los que necesitan prueba manual en varios navegadores y los TODOs que se borraron y deberían ser tickets.
- Revisar la lista de bugs que los agentes encontraron y no tocaron, porque estaban fuera de alcance.
- Pasar el ciclo de regresión manual.
El punto 1 es por muestreo; el 2, completo. Si el muestreo encuentra un refactor sin su test previo, la confianza en el resto cae y toca revisar el lote entero.
Un Sonar en local como oráculo
Es la pieza que más subestimé. Nuestro Sonar es Community Build, que no tiene ramas, así que analizar la rama en el servidor pisaría el análisis de la rama principal. Levanté un SonarQube Community en Docker con los perfiles de fábrica y comprobé que reproducía exactamente la misma línea base que el servidor.
Con eso el plan sale de datos y no de intuición, y cada fase mide su trabajo con la misma vara que usará el CI. Si vas a pedirle a unos agentes que mejoren una métrica, dales la forma de medirla ellos mismos.
Lo que salió mal
Un agente hizo push, abrió una PR y movió el ticket de Jira. Nadie se lo pidió. Tenía instalada una skill para terminar ramas y la siguió hasta el final. La PR salió con el gate en rojo, porque era un snapshot a medias. Ahora los prompts prohíben cualquier acción externa y los worktrees tienen un pushurl inválido:
git config --worktree remote.origin.pushurl no-push://bloqueado
Prohibirlo en el prompt no basta, hay que impedirlo.
Reanudar el run relanzó lotes que ya estaban hechos. resumeFromRunId reutiliza el prefijo más largo de llamadas que no han cambiado. Con dos carriles dentro de un parallel(), el orden no es estable, las claves de caché de los revisores no coincidieron y volvió a lanzar lotes terminados. Lo paré al minuto. Para continuar un run a medias, me ha funcionado mejor escribir un script nuevo solo con lo pendiente. Por eso fueron cuatro runs y no uno.
Los carriles iban desequilibrados. Repartí los lotes por orden y uno de los carriles terminó horas antes que el otro. La versión que dejé en el repo los alterna y deja las deprecaciones, que son lo más pesado, para el final.
Datos de prueba. Al escribir specs, los agentes tienden a reutilizar valores que ven en el código en lugar de inventarlos. El code-review lo marcó y se sustituyeron por datos sintéticos. Ahora hay una regla explícita en las instrucciones del repo, pero ninguna comprobación automática.
Las prohibiciones viven en el prompt. Pedí no usar NOSONAR, eslint-disable, @ts-ignore, as any ni it.skip, y justificar cada falso positivo en sonar-project.properties. Se cumplió, pero lo vigilaban los revisores, no el gate. Es el mismo error que el del push, solo que esta vez no explotó.
Un arreglo que no arreglaba nada. Un agente anotó como bug corregido un cambio que, con los datos reales de entrada, no tenía ningún efecto. Un test en verde puede estarlo por el motivo equivocado.
Casos de QA duplicados. Para la regresión manual, Claude creó en la herramienta de test 7 casos que ya existían. Los existentes no estaban enlazados a requisitos ni tenían tags. Cuando le señalé la carpeta de plantillas, reutilizó 40 y creó uno solo para lo que de verdad faltaba. Un agente solo reutiliza lo que puede encontrar; si no, se inventa la regresión.
Cambios que ningún test reproduce. Algunos arreglos de seguridad que pide Sonar dependen del navegador y no se pueden probar en un test unitario que corre en Chrome. Esos se marcan en la PR y se validan a mano en cada navegador antes del merge.
Cómo aplicarlo en tu repo
Nada de esto depende de Sonar ni de Claude Code. Sirve para cualquier cambio masivo hecho por agentes: deprecaciones, un salto de versión, una regla de lint nueva.
Antes de lanzar nada
- Monta un oráculo que reproduzca la métrica del CI en local y comprueba que da la misma línea base que el servidor.
- Reparte el trabajo en lotes pequeños por carpeta y elige el modelo por rol: barato para implementar, otro distinto para auditar.
- Bloquea por infraestructura cualquier acción externa: push, PRs, tickets. No basta con prohibirlo en el prompt.
- Deja escrita la regla de datos de prueba sintéticos antes de que el primer agente escriba un fixture.
- Ten la regresión manual localizable: casos en carpetas por funcionalidad, enlazados a requisitos y con tags.
En cada lote
- Escalera de garantías: caracterización, test de la línea, refactor equivalente, rojo → verde solo para bugs reales y revertir lo que quede en rojo.
- Un commit
test(...)antes de cadarefactor(...). - Una nota por lote con cambios de comportamiento, TODOs, falsos positivos y skipped.
- Nada que esconda el problema (
NOSONAR,eslint-disable,as any,it.skip), y que lo compruebe el gate, no solo el revisor.
Antes de abrir la PR
- Gates completos y reescaneo con el oráculo, también después de aplicar los arreglos de la auditoría.
- E2E repitiendo cada fallo sobre la rama base, para separar regresiones de lo que ya fallaba.
- Al menos una revisión con otro modelo y otra con otro corte (por área, por ángulo). Cada hallazgo se confirma o se refuta con un test antes de tocar código.
- Presupuesta la verificación: aquí costó más de diez veces lo que el arreglo.
En la PR y después
- La descripción lleva las notas de comportamiento, los puntos de revisión particular, lo revertido y los bugs encontrados sin tocar.
- La revisión humana es por muestreo de commits, lectura completa de las notas y QA.
- Mide reversiones y bugs atribuidos a la PR, y qué aportó cada capa, para recortar la siguiente vez.
Lo que todavía no sé
- Si los revisores por lote compensan sus 30 $, porque no medí cuántos de sus hallazgos no bloqueantes se aplicaron.
- Si lotes más pequeños o un code-review menos exhaustivo habrían dado la misma garantía por menos dinero.
- Cuánto tardó de verdad la revisión humana de la PR. No lo cronometré, y es justo la métrica que todo esto pretende bajar.
El workflow ha quedado en el repo, parametrizado para repetirlo con otras migraciones masivas. La próxima ejecución me dirá si lo aprendido aquí se sostiene.
Si has hecho algo parecido, me interesa sobre todo cómo lo revisó tu equipo y cuánto te costó cada capa de verificación. Es el dato que menos se publica y el que más ayuda a decidir.

Top comments (0)