Copilot genera código rápido. SonarQube te dice que ese código tiene problemas. Nada conecta los dos. El desarrollador tiene que leer el reporte de Sonar, interpretarlo, volver al editor y pedirle manualmente a Copilot que arregle la cosa que Copilot escribió en primer lugar. Esto es una brecha de workflow, no una brecha de tooling, y es el agente de alto impacto más barato que puedes construir ahora mismo.
El patrón es fácil de imaginar. Una PR recibe issues marcados, algunos se arreglan, el resto pasa con un 'no es bloqueante', y uno o dos se leen mal. Para cuando un senior la revisa, la PR ya trae deuda de calidad incorporada que el tooling detectó y un humano descartó. El humano no la ignoró por pereza. La ignoró porque el feedback era un link a un dashboard, no un cambio accionable.
El ciclo que falta
El agente tiene un solo trabajo: convertir un issue de Sonar en un prompt que el desarrollador pueda pegar en Copilot sin pensar. Eso es todo. No está reescribiendo el código. No está abriendo PRs automáticamente. Está cerrando la brecha entre 'aquí hay un problema' y 'aquí hay una petición de corrección con contexto completo'.
Un chequeo de licencia antes de que planifiques el fin de semana. El análisis de pull request no está en SonarQube Community Build. Necesita Developer Edition o superior, y Community rechaza sonar.pullrequest.key de plano. Así que el camino gratuito es el análisis de branch: corre el scanner sobre main, deja que el agente lea los issues que aparecieron desde el último análisis, y postea los prompts en el commit de merge en lugar de en la PR abierta. Si quieres el trigger de PR sin pagar, el plugin comunitario de branches (mc1arke/sonarqube-community-branch-plugin) agrega análisis de branch y de PR a Community Build. Lo mantiene la comunidad y SonarSource no le da soporte, así que lee la matriz de compatibilidad antes de conectar a tu equipo a eso. Todo lo que sigue es el mismo agente en cualquiera de los dos casos. Sólo cambian el trigger y el lugar donde comenta.
Por qué este primero
Cuando alguien me pregunta cuál debería ser su primer agente, esto es lo que recomiendo para cualquier equipo que ya use Sonar y Copilot. Tres razones:
- La entrada es estructurada. SonarQube tiene una API limpia que devuelve issues con rule ID, severidad y ubicación de archivo. No estás parseando HTML ni haciendo scraping.
- La salida es un blob de texto. El agente produce un prompt. Sin ejecución de código, sin riesgo de producción, sin dependencias de runtime.
- El valor es obvio en la primera semana. Los desarrolladores sienten el ahorro de tiempo en la primera PR que tiene warnings de Sonar.
Compara eso con un agente de triaje de bugs o un agente de code review. Ambos son valiosos. Ambos requieren 10x el esfuerzo para alcanzar calidad utilizable. El puente a SonarQube es un proyecto de fin de semana que se usa todos los días.
Lo que realmente hace el agente
Trigger: el webhook de SonarQube que dispara cuando el análisis termina. En una PR si tu edición hace análisis de pull request, en la branch si no. Una GitHub Action que corre después de que Sonar termina funciona igual.
Pasos, en orden:
- Trae la lista de issues de esta PR desde Sonar. Filtra INFO, mantén BLOCKER, CRITICAL, MAJOR.
- Para cada issue, lee el archivo en el rango de líneas marcado. Incluye 10 líneas de contexto arriba y abajo.
- Agrupa los issues por archivo. Un prompt de Copilot por archivo, no por issue. Los desarrolladores quieren corregir por archivo.
- Genera un prompt estructurado: la regla, por qué importa, el snippet, el patrón de corrección. Incluye cualquier convención del proyecto desde tu base de conocimiento.
- Postea un único comentario en la PR con los prompts, colapsables por archivo.
Cómo se ve el prompt
## Fix prompt for `src/orders/order-summary.component.ts`
SonarQube flagged 3 issues in this file. Paste this into Copilot or Claude Code:
---
In `order-summary.component.ts`, fix the following issues while preserving existing behavior:
1. **Line 42 (typescript:S1481)**: unused local variable `totals`.
Remove the variable. If it was intended for debugging, delete it, do not comment it out. The team convention is to remove dead code rather than leave it behind.
2. **Line 67 (typescript:S4123)**: `await` should only be used with promises.
`formatAmount` returns a string, not a Promise. Drop the `await` keyword on that call.
3. **Line 88 (typescript:S6544)**: Promise returned in a context expecting `void`.
The async callback passed to `addEventListener` returns a Promise, which is unhandled. Wrap the body in a non-async IIFE or handle the returned Promise explicitly. Prefer the IIFE pattern since the team avoids bare async handlers on DOM events.
Do not modify any file other than `order-summary.component.ts`. Keep imports sorted alphabetically. Return only the full updated file content, no explanation.Nota lo que hay en ese prompt que no está en un comentario estándar de Sonar: la descripción de la regla traducida a lenguaje humano, la convención del equipo, la restricción dura sobre qué no modificar, el formato de salida. Esta es la parte que el desarrollador solía hacer en su cabeza. Ahora la hace el agente.
Cableando todo
Este post se empareja con el post del monorepo. El agente reutiliza ReActAgent de libs/base_agent/ y vive en agents/sonar_bridge/. Menos de 100 líneas de código real, la mayor parte pegamento HTTP. Lo que importa es la forma: un puñado de herramientas tipadas, un playbook y una función de build que el runtime llama.
# libs/tools/sonar.py
import os
import requests
SONAR_URL = os.environ["SONAR_URL"]
SONAR_TOKEN = os.environ["SONAR_TOKEN"]
PROJECT_KEY = os.environ["SONAR_PROJECT_KEY"]
def get_issues_for_pr(pr_key: str) -> list[dict]:
r = requests.get(
f"{SONAR_URL}/api/issues/search",
params={
# componentKeys is not optional. Without a project scope the
# search does not come back with this PR's issues.
"componentKeys": PROJECT_KEY,
"pullRequest": pr_key,
# Legacy taxonomy, still accepted. On SonarQube 10.2 and up
# the current filter is impactSeverities=HIGH,MEDIUM.
"severities": "BLOCKER,CRITICAL,MAJOR",
},
auth=(SONAR_TOKEN, ""),
timeout=15,
)
r.raise_for_status()
return r.json()["issues"]
# libs/tools/github.py
import os
import requests
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
def read_file_range(repo: str, ref: str, path: str, start: int, end: int) -> str:
# Returns the flagged range plus 10 lines of context on each side.
r = requests.get(
f"https://api.github.com/repos/{repo}/contents/{path}",
params={"ref": ref},
headers={
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github.raw",
},
timeout=15,
)
r.raise_for_status()
lines = r.text.splitlines()
lo = max(0, start - 11)
hi = min(len(lines), end + 10)
return "\n".join(lines[lo:hi])
def comment_on_pr(repo: str, pr_number: int, body: str) -> None:
r = requests.post(
f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
json={"body": body},
timeout=15,
)
r.raise_for_status()Tres herramientas, dos archivos. La de Sonar acota la búsqueda al proyecto y filtra por severidad antes de que el agente vea la lista, así el ruido de baja prioridad nunca llega al prompt ni al presupuesto de tokens. El read de GitHub devuelve una ventana alrededor de las líneas marcadas para que el agente pueda razonar sobre la corrección en contexto, no sólo sobre el rule ID. El comentario de GitHub es una única escritura al final de la corrida, forzada por el playbook en lugar de por el código, así el comportamiento es visible y cambiable sin un redeploy.
# agents/sonar_bridge/playbook.py
PLAYBOOK = """You are the Sonar-to-Copilot bridge agent.
Goal: turn a Sonar analysis report on a pull request into prompts a
developer can paste into Copilot or Claude Code without context-switching.
Process, in order:
1. Call get_issues_for_pr with the PR key from the trigger. Project
scope and severity are already handled by the tool.
2. Group the remaining issues by file path.
3. For each file, call read_file_range on the flagged line ranges to
pull the code into context.
4. For each file, synthesize one Copilot-ready prompt. Apply project
conventions retrieved from the knowledge base.
5. Call comment_on_pr once with all per-file prompts in a single
collapsible comment.
Hard rules:
- You generate prompts. You do not generate code.
- comment_on_pr is called at most once per run.
- If there are zero qualifying issues, exit without commenting.
"""El playbook es donde se especifica el comportamiento del agente. Fíjate lo que hay en él: los pasos del proceso, las reglas duras, el formato de salida. Lo que no hay: la guía de traducción regla por regla. Eso vive en la base de conocimiento, recuperado en tiempo de ejecución, porque las convenciones del equipo cambian y no quiero redeployar el agente cada vez que cambian.
# agents/sonar_bridge/agent.py
from libs.base_agent import ReActAgent, AgentConfig
from libs.memory import MemoryStack
from libs.telemetry import Telemetry
from libs.tools import sonar, github
from .playbook import PLAYBOOK
def build() -> ReActAgent:
return ReActAgent(
config=AgentConfig(
name="sonar_bridge",
version="0.1.0",
playbook=PLAYBOOK,
tools=[
sonar.get_issues_for_pr,
github.read_file_range,
github.comment_on_pr,
],
# Confidence in the prompt it drafted for a file. Below
# this, the agent lists the issue and writes no prompt.
confidence_threshold=0.85,
),
memory=MemoryStack.default(),
telemetry=Telemetry.default(),
)La última pieza es el trigger. Un handler FastAPI en apps/api/ recibe el webhook de Sonar, extrae el PR key y llama a build().run({"pr_key": ...}). El agente base hace el resto: trae corridas pasadas similares del vector store, ejecuta el loop ReAct, escribe telemetría en Postgres, postea el comentario en GitHub. El threshold queda por encima del default de la librería porque un prompt de corrección equivocado cuesta más que uno faltante. Un desarrollador que pega en Copilot un prompt equivocado y seguro de sí mismo entrega el cambio equivocado creyendo que Sonar lo aprobó. Si construiste la estructura del monorepo primero, agregar un nuevo agente es básicamente escribir los tres archivos de arriba y los tests que van con ellos.
La parte no obvia: convenciones del equipo
Un issue de Sonar fuera de contexto es una violación de regla. Un issue de Sonar con contexto del equipo es una directiva específica. El agente necesita saber cosas como 'preferimos async/await sobre observables crudos', 'nunca usamos var', 'nuestro patrón de null-check es optional chaining, no ternario'. Ahí es donde entra tu base de conocimiento, ya sea un vault de Obsidian, una carpeta de markdown en el repo o un espacio de Notion.
El agente lee la regla, busca la convención del equipo para ese tipo de regla y las fusiona. Esta es la diferencia entre 'agrega un null check' (genérico) y 'agrega un null check usando optional chaining, el patrón que usamos en services/' (específico y consistente con el codebase).
Qué medir
Si pones esto en producción, sigue dos números.
- Tiempo promedio desde el reporte de Sonar hasta el commit de corrección. Antes del agente, esto probablemente se medía en días o nunca. Después, debería estar en horas.
- Porcentaje de issues marcados que efectivamente se corrigen antes del merge. Antes del agente, mide tu línea base, porque suele ser más baja de lo que el equipo supone. Después, compara contra esa línea base y contra nada más. Un número del equipo de otra persona no es una meta.
No sigas 'issues resueltos'. Los desarrolladores pueden cerrar issues marcándolos como won't fix, y ese número miente. Sigue cambios de código reales contra las líneas marcadas.
Qué puede salir mal
Dos modos de falla para los que conviene prepararse.
Primero, el agente genera prompts para issues que en realidad son falsos positivos. Sonar los tiene. Si el agente no sabe qué reglas tu equipo suprime habitualmente, produce ruido. El arreglo es alimentar al agente con tu historial de supresión para que aprenda qué reglas degradar o saltear por completo. Mismo vector store, query diferente.
Segundo, los desarrolladores empiezan a tratar la salida del prompt como si fuera la corrección. La pegan en Copilot, aceptan la sugerencia, commitean. Sin review. Esto derrota todo el propósito. El agente es una herramienta de borrador, no un tomador de decisiones. Fija la expectativa temprano de que el desarrollador sigue siendo dueño de la corrección.
Copilot aceleró la escritura de código. SonarQube aceleró la detección de sus defectos. La brecha entre los dos era un workflow, no una herramienta. Ahí es donde vive el agente útil más barato.
Referencias
- SonarQube Web API: https://docs.sonarsource.com/sonarqube/latest/extension-guide/web-api/
- Documentación de GitHub Copilot: https://docs.github.com/en/copilot
- Documentación de Claude Code. https://docs.claude.com/en/docs/claude-code/overview