SSD Nodes Learn 🎉 VPS desde $4.99/mes
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-07

Autoalojar un agente de revisión de PR en un VPS

Ejecuta un revisor de PR en tu VPS con runner propio, prompts limitados al diff, filtros por ruta y tamaño, comentarios inline y coste por PR.

Qué hace un agente de revisión de PR autoalojado

Un agente de revisión de PR autoalojado es un programa pequeño que se ejecuta en un servidor propio. Lee el diff de una pull request (PR) y envía al modelo sólo las líneas modificadas. La respuesta se publica como comentarios insertados en la revisión. Nunca hace checkout de la rama y nunca lee un archivo que la pull request no haya modificado. Las únicas credenciales que almacena son una clave de API del modelo y un token que sólo puede publicar comentarios.

Un modelo puede leer un diff. Esa parte ya está resuelta. Lo importante es adónde se envía el diff y quién controla la clave. Con un bot de revisión alojado, cada diff de cada repositorio privado sale de su red, llega a los registros de un tercero y queda sujeto a su política de retención. En un VPS propio, el diff va de GitHub a su servidor y de ahí a la API del modelo. Además, puede leer las cuarenta líneas de código que determinan qué se envía.

Qué necesita antes de empezar

  • Un VPS con Ubuntu 24.04 y un runner de GitHub Actions autohospedado ya registrado en el repositorio. Asígnele la etiqueta adicional pr-review al registrarlo, porque el flujo de trabajo siguiente selecciona los runners que tienen esa etiqueta.
  • Una clave de API de Anthropic obtenida en Claude Console.
  • Un repositorio en el que pueda controlar quién abre una pull request. Un repositorio privado es la opción más sencilla. La sección sobre forks trata el caso público, y la respuesta en ese caso es menos cómoda.

Instale el revisor en el VPS

El servicio runner se ejecuta con la cuenta sin privilegios que creó al ejecutar ./svc.sh install. Instale el revisor con esa misma cuenta para que el trabajo pueda ejecutarlo sin sudo. Sustituya runner por el nombre de su cuenta.

sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --version

gh --version muestra gh version 2.45.0 en Ubuntu 24.04 a fecha de agosto de 2026. Cualquier versión a partir de 2.20 incluye la opción --input que se utiliza a continuación. Un mensaje Command 'gh' not found indica que el componente universe no está habilitado. Ejecute sudo add-apt-repository universe y vuelva a intentarlo.

Dónde se almacenan la clave y el token

Dos secretos con dos ciclos de vida diferentes. Ninguno debe estar en el repositorio.

ANTHROPIC_API_KEY es un secreto del repositorio. Se configura en Settings, después en Secrets and variables y, por último, en Actions. GitHub lo cifra y lo inyecta en el entorno del paso durante la ejecución. Nunca es un archivo en disco ni aparece en el historial de git.

GITHUB_TOKEN funciona de otra forma. Actions genera un token nuevo para cada trabajo y lo destruye cuando termina el trabajo. El bloque permissions: del flujo de trabajo define las operaciones que puede realizar ese token. Ahí es donde se aplica realmente el principio de privilegio mínimo:

permissions:
  contents: read
  pull-requests: write

Ese token puede publicar una revisión. No puede enviar un commit, fusionar una rama, editar un archivo de flujo de trabajo ni acceder a otro repositorio. Un agente que puede comentar es un revisor. Un agente que puede enviar cambios es un committer, y nadie ha autorizado eso. Proteja la clave del modelo con el mismo cuidado, porque consume dinero de su cuenta. Encontrará más información sobre este tipo de problemas en mantener los secretos fuera del alcance de un agente de IA.

Actions reemplaza la cadena exacta del secreto por *** en los registros de los trabajos. Sólo busca la cadena exacta. Por eso, una clave codificada en base64, dividida en dos líneas o mostrada carácter por carácter aparece sin ocultar. No añada un paso de depuración que vuelque el entorno.

Por qué un pull request desde un fork nunca recibe tu clave de API

La regla de GitHub es sencilla: con la excepción de GITHUB_TOKEN, los secretos no se pasan al runner cuando un workflow se activa desde un repositorio bifurcado. Por tanto, una ejecución de pull_request desde un fork inicia el script sin ningún ANTHROPIC_API_KEY, y la primera llamada a la API falla con invalid x-api-key.

La solución tentadora es cambiar el disparador a pull_request_target, que se ejecuta en el contexto del repositorio base y sí recibe los secretos. No lo hagas aquí. La propia documentación de seguridad de GitHub indica que esos workflows «tienen privilegios, lo que significa que comparten la misma caché de la rama principal con otros disparadores de workflows privilegiados, y pueden tener acceso de escritura al repositorio y acceso a los secretos referenciados», y que el resultado «se puede explotar para tomar el control de un repositorio».

La misma documentación es clara sobre el runner: «Los self-hosted runners casi nunca deben utilizarse para repositorios públicos en GitHub, porque cualquier usuario puede abrir pull requests contra el repositorio y comprometer el entorno».

Esto determina dos decisiones de diseño. El job incluye una comprobación para que sólo se ejecute en ramas enviadas a tu propio repositorio. Además, el workflow no tiene ningún paso actions/checkout. El agente nunca tiene la rama en el disco, por lo que un pull request malicioso sólo es texto que se envía a un modelo. No puede ejecutar un script de compilación en tu VPS, porque nada de tu VPS llega a ejecutarlo.

Obtenga el diff, no el repositorio

Una solicitud obtiene todo el diff como texto sin formato.

export GH_TOKEN=your_token   # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"

El tipo de contenido Accept: application/vnd.github.diff convierte la respuesta, que normalmente es un objeto JSON con la descripción de la pull request, en el diff unificado. gh api muestra ese cuerpo sin modificar. La primera línea que aparece debe comenzar por diff --git a/. Un gh: Not Found (HTTP 404) indica que el token no puede ver el repositorio. En un token personal de permisos específicos, casi siempre significa que se dejó desactivado el permiso Pull requests.

Filtre antes de consumir un token

Esta sección marca la diferencia entre un bot que la gente lee y uno que silencia. Cada filtro siguiente se ejecuta antes de que el modelo vea un solo byte.

  • Filtros de rutas. Excluya archivos de bloqueo, directorios de dependencias incluidas, paquetes minimizados y código generado. Un comentario del modelo sobre package-lock.json sólo añade ruido, y esos archivos suelen contener la mayor parte de los bytes de un diff.
  • Un límite de tamaño. Si se supera el límite, omita la revisión y finalice correctamente. Una refactorización de 4,000 líneas recibe una explicación honesta de una línea indicando que era demasiado grande para revisarla automáticamente, en lugar de sesenta suposiciones.
  • Un umbral de gravedad y un límite de comentarios. Informe de los hallazgos de gravedad alta y media, hasta un máximo de diez, ordenados de mayor a menor gravedad. Nadie lee el comentario once.

El script

Guárdelo como /opt/pr-review/review.py. Lee su configuración desde el entorno, por lo que el flujo de trabajo puede cambiar de modelo sin modificar el código.

#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys

import anthropic

REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")

raw_diff = subprocess.run(
    ["gh", "api", f"/repos/{REPO}/pulls/{PR}",
     "-H", "Accept: application/vnd.github.diff"],
    check=True, capture_output=True, text=True,
).stdout

Dividir el diff por archivo permite filtrar por ruta. Numerar cada línea permite colocar los comentarios de revisión. GitHub sólo acepta un comentario insertado en una línea que forme parte del diff, por lo que el modelo debe indicar un número de línea real. Proporcionarle los números permite que copie uno en lugar de inventarlo.

def per_file(diff_text):
    """Split a unified diff into one string per file."""
    sections, current = [], []
    for line in diff_text.splitlines():
        if line.startswith("diff --git ") and current:
            sections.append("\n".join(current))
            current = []
        current.append(line)
    if current:
        sections.append("\n".join(current))
    return sections


def annotate(section):
    """Prefix every line that exists in the new file with its line number."""
    out, n, in_hunk = [], 0, False
    for line in section.splitlines():
        if line.startswith("@@"):
            n = int(line.split("+")[1].split(",")[0].split(" ")[0])
            in_hunk = True
            out.append(line)
        elif not in_hunk or line.startswith(("-", "\\")):
            out.append(line)
        else:
            out.append(f"{n}\t{line}")
            n += 1
    return "\n".join(out)


kept = [s for s in per_file(raw_diff)
        if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)

if not payload.strip():
    print("every changed file was filtered out")
    raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
    print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
    raise SystemExit(0)

La cabecera del hunk contiene la numeración. @@ -12,7 +12,9 @@ indica que el hunk del archivo nuevo empieza en la línea 12, por lo que el contador comienza ahí y avanza sólo en las líneas añadidas y sin cambios. Las líneas eliminadas pasan sin numerar porque no existen en el archivo nuevo. La condición para las líneas que empiezan con una barra inversa omite el marcador de ausencia de salto de línea que git escribe al final de un archivo. De lo contrario, desplazaría en uno todos los números siguientes.

Ambas salidas usan el estado 0, no el 1. Una pull request filtrada o demasiado grande debe mostrar una comprobación correcta. Una comprobación incorrecta sobre la que una persona no puede actuar se ignora, y cuando se ignora una comprobación, se ignoran todas.

SYSTEM = (
    "You review one pull request diff. Every line that exists in the new file is "
    "prefixed with its line number and a tab character. "
    "Report only defects you can see in the lines shown: a crash, a resource leak, "
    "a security mistake, a wrong boundary condition, a broken contract with code "
    "that is visible in this diff. Do not comment on style, naming or formatting. "
    "Do not guess about code you cannot see. Leave out anything you are not "
    "certain about. An empty findings list is a normal and common answer. "
    'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
    '"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
    "Every line number must be one you can see in the left column of that file."
)

client = anthropic.Anthropic()
message = client.messages.create(
    model=MODEL,
    max_tokens=2000,
    system=SYSTEM,
    messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
      f"out={message.usage.output_tokens}", file=sys.stderr)

text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]

if not findings:
    print("nothing above the severity threshold; posting no comment")
    raise SystemExit(0)

review = {
    "event": "COMMENT",
    "body": f"Automated review of the changed lines. {len(findings)} finding(s).",
    "comments": [
        {"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
         "body": f"**{f['severity']}** {f['comment']}"}
        for f in findings
    ],
}
subprocess.run(
    ["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
    input=json.dumps(review), text=True, check=True,
)

Tres detalles de ese bloque son esenciales. El JSON se recorta entre el primer { y el último } porque a veces el modelo incluye su respuesta en un bloque de código, y json.loads no puede procesar ese bloque. A path se le elimina un b/ inicial, porque ese prefijo procede de la cabecera del diff y GitHub necesita una ruta relativa al repositorio. --input - envía toda la revisión en una sola llamada a la API, por lo que diez hallazgos llegan como una notificación en lugar de diez.

Cuando no hay nada que informar, el script no publica nada. Un bot que escribe «no se encontraron problemas» en cada pull request enseña a las personas a pasar por alto el mensaje. Después también pasarán por alto el que sí era importante.

Conéctelo al flujo de trabajo

Guarde esto como .github/workflows/pr-review.yml:

name: pr-review

on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths-ignore:
      - '**.md'
      - 'docs/**'

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: pr-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  review:
    if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
    runs-on: [self-hosted, linux, pr-review]
    steps:
      - name: Review the changed lines
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REVIEW_MODEL: claude-haiku-4-5-20251001
          MIN_SEVERITY: medium
        run: /opt/pr-review/venv/bin/python /opt/pr-review/review.py

GITHUB_REPOSITORY no está en ese bloque de env: porque Actions lo establece para cada trabajo. El grupo concurrency es importante para la facturación: sin él, al enviar tres correcciones rápidas a una rama se ejecutan tres revisiones completas y se paga por las tres; con él, sólo se conserva la última.

La línea if: cumple dos funciones. La primera mitad omite las solicitudes de extracción procedentes de forks, que de todos modos fallarían porque no tienen ninguna clave. La segunda proporciona a su equipo un interruptor para desactivar el trabajo: añada la etiqueta no-ai-review a una solicitud de extracción y el trabajo no se ejecutará.

Abra una solicitud de extracción y observe lo que ocurre:

gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --comments

Una ejecución que termina en unos segundos y muestra nothing above the severity threshold; posting no comment en el registro funciona correctamente. En una solicitud de extracción pequeña y sin cambios innecesarios, ese es el resultado esperado.

¿Cuánto cuesta una revisión automatizada de pull requests?

El diff representa casi toda la entrada, por lo que su tamaño determina el precio. A continuación se muestra un diff medido de 500 líneas junto con el system prompt. El recuento se obtuvo mediante el endpoint de recuento de tokens, no mediante una estimación.

ChartTokens for one 500 line pull request diff, measured August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5",
    "input_tokens": "8,000",
    "output_tokens": "1,200"
  },
  {
    "label": "Sonnet 5",
    "input_tokens": "10,400",
    "output_tokens": "1,560"
  },
  {
    "label": "Opus 5",
    "input_tokens": "10,400",
    "output_tokens": "1,560"
  }
]

Ese diff produjo 8,000 tokens de entrada en Haiku 4.5 y 10,400 en Sonnet 5. El texto es el mismo, pero el recuento es diferente. Los modelos Claude desde 4.7 en adelante usan un tokenizador más reciente que produce aproximadamente un 30% más de tokens para la misma entrada, según la documentación de Anthropic en su página de precios. Tenga esto en cuenta al comparar el precio por millón de tokens de un modelo nuevo con el de uno antiguo.

Precios de lista en agosto de 2026: Haiku 4.5 cuesta $1 por millón de tokens de entrada y $5 por millón de tokens de salida. Sonnet 5 cuesta $2 y $10 con el precio introductorio vigente hasta el 31 de agosto de 2026; después costará $3 y $15. Opus 5 cuesta $5 y $25.

ChartCost of that one review at list prices, August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5",
    "cost_per_pr_cents": 1.4,
    "cost_200_prs_usd": "2.80"
  },
  {
    "label": "Sonnet 5",
    "cost_per_pr_cents": 3.64,
    "cost_200_prs_usd": "7.28"
  },
  {
    "label": "Opus 5",
    "cost_per_pr_cents": 9.1,
    "cost_200_prs_usd": "18.20"
  }
]

Eso equivale a 1.4 centavos por pull request en Haiku 4.5 y 9.1 centavos en Opus 5. Un equipo que integre 200 pull requests al mes paga aproximadamente $2.80 con Haiku 4.5, $7.28 con Sonnet 5 o $18.20 con Opus 5. Desde el 1 de septiembre de 2026, multiplique por 1.5 el valor de la fila de Sonnet 5.

Dos factores pueden hacer que la factura real supere esa estimación. El disparador synchronize inicia una revisión con cada push, por lo que una rama activa con ocho pushes cuesta ocho revisiones. La regla de concurrencia sólo ayuda cuando los pushes se producen con poca diferencia de tiempo. Además, las cifras suponen que los filtros de rutas funcionan: un único archivo de bloqueo sin filtrar puede duplicar por sí solo la entrada.

La caché de prompts no ayuda en este caso. El prefijo almacenado en caché debe ser idéntico byte por byte entre las llamadas, y el diff cambia cada vez. El system prompt es la única parte estable, pero su longitud está muy por debajo del mínimo necesario para almacenarlo en caché. Para consultar la regla general, vea cuándo se amortiza la caché de prompts; para elegir entre los tres modelos anteriores, consulte qué modelo Claude usar para cada tarea.

Mida sus propios diffs antes de activarlo

Añada esta línea después de construir payload y ejecute el script manualmente contra varios pull requests del mes pasado:

print(client.messages.count_tokens(
    model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)

El endpoint de recuento no ejecuta el modelo, por lo que no consume tokens de entrada ni de salida. Además, utiliza el tokenizador correspondiente al modelo que indique. Ejecútelo sobre diez pull requests reales de su propio repositorio y use la mediana en lugar de la media, para que una migración muy grande no distorsione la estimación.

Por qué los bots de revisión se silencian y cómo evitarlo

Hay dos comportamientos que destruyen la confianza en estos bots. El código anterior corrige ambos.

Revisarlo todo de una vez. Un bot que deja cuarenta comentarios no consigue que se lea ninguno. El umbral de severidad y el límite de diez comentarios no son una cuestión de cortesía. Son lo que mantiene visibles los hallazgos reales. Ordenar por severidad antes de truncar garantiza que el límite descarte los hallazgos menos importantes y no diez elementos al azar.

Comentar con seguridad sobre algo que no puede comprobar. Esto hace que los ingenieros lo desactiven definitivamente. Un modelo al que se muestran 200 líneas de una base de código de 40,000 líneas todavía puede escribir «esto rompe la invalidación de la caché en redis_client.py» sobre un archivo que nunca ha visto. El mensaje del sistema lo impide con instrucciones claras: informar sólo de los defectos visibles en las líneas mostradas y omitir todo aquello sobre lo que no haya certeza. Nombrar directamente el fallo funciona mejor que pedir precisión en general. Indicar al modelo que un resultado vacío es normal evita que invente algo que decir sobre un cambio de dos líneas.

Publique la revisión como COMMENT, nunca como REQUEST_CHANGES. La opinión de un modelo no debe poder bloquear una fusión. En cuanto pueda hacerlo, alguien con una fecha límite eliminará todo el flujo de trabajo en lugar de discutir con él.

Modos de fallo y cadenas que verá

HTTP 422 al publicar la revisión. gh muestra gh: Unprocessable Entity (HTTP 422) y el cuerpo de la respuesta indica el campo: Pull request review thread line must be part of the diff. GitHub no puede anclar ese comentario. Las causas habituales son un número de línea inventado por el modelo, un path que todavía conserva el prefijo b/ o un comentario sobre una línea eliminada, que requiere establecer side en LEFT en lugar de RIGHT. Muestre el JSON de la revisión antes de publicarla y compruebe manualmente un comentario comparándolo con el diff.

invalid x-api-key de la API del modelo. El paso falla en la primera llamada a messages.create. El secreto ANTHROPIC_API_KEY no está definido en el repositorio, o la solicitud de incorporación de cambios procede de un fork y Actions no ha pasado ningún secreto. La protección contra forks de la línea if: debería haber omitido el paso, así que compruebe primero esa línea.

gh: Resource not accessible by integration (HTTP 403). El token del trabajo no puede escribir en las solicitudes de incorporación de cambios. Añada pull-requests: write al bloque permissions:. Si ya está presente, vaya a Settings, después a Actions y luego a General, donde una política de la organización puede limitar lo que cualquier token del flujo de trabajo puede solicitar.

json.decoder.JSONDecodeError. El modelo no devolvió un JSON que se pueda analizar. La causa habitual es que la respuesta alcanzó el límite de tokens y quedó interrumpida a mitad de un objeto. La línea del registro muestra stop_reason precisamente para este caso: un valor de max_tokens significa que debe aumentar max_tokens o reducir MAX_COMMENTS.

El flujo de trabajo nunca se ejecuta. gh run list no muestra nada para la solicitud de incorporación de cambios. Compruebe que paths-ignore no haya filtrado todos los archivos modificados. Después, compruebe la protección contra forks y la protección por etiqueta. Por último, compruebe que el runner esté activo con sudo systemctl status 'actions.runner.*' en el VPS. Un runner desconectado deja el trabajo en cola sin mostrar ningún mensaje de error en la solicitud de incorporación de cambios.

Todas las revisiones llegan vacías. Establezca MIN_SEVERITY en low para una ejecución. Si aparecen resultados, el umbral funciona correctamente. Si no aparece nada, muestre payload y confirme que los filtros no hayan eliminado todo el diff.

Ejecutarlo junto con los demás agentes

El revisor es pequeño, por lo que puede parecer conveniente instalarlo en el servidor que ya ejecuta todo lo demás. Manténgalo separado si el repositorio es importante. Este proceso contiene un token que puede comentar su código y una clave que puede gastar su dinero, y un self-hosted runner está diseñado precisamente para ejecutar código de workflows. Una cuenta sin privilegios dedicada y sin permisos de sudo, en un host que no ejecute nada más, es el nivel básico recomendado. Si también ejecuta agentes interactivos que hacen checkout del código, una VM desechable por agente es el patrón más fiable, y ejecutar un agente de programación en un VPS explica la configuración general. Si la API de Anthropic es nueva para usted, una primera aplicación de Claude API en un VPS es un punto de partida más sencillo que este.

FAQ

¿Un agente de revisión de PR basado en IA necesita acceso de escritura a mi repositorio?

No. Necesita pull-requests: write para publicar una revisión y contents: read para obtener el diff. Esa es toda la lista, y se establece en el bloque permissions: del workflow, que limita lo que puede hacer el GITHUB_TOKEN de cada trabajo. Con esas dos líneas, el agente puede comentar una pull request, pero no puede enviar un commit ni fusionar una rama. Publique las revisiones con event: COMMENT en lugar de REQUEST_CHANGES para que tampoco pueda bloquear una fusión.

¿Por qué falla mi comentario de revisión con HTTP 422?

GitHub sólo acepta un comentario de revisión insertado en una línea que forme parte del diff de la pull request. Si no es así, devuelve Pull request review thread line must be part of the diff. Compruebe que path sea relativo al repositorio y no incluya el prefijo b/ de la cabecera del diff, y que el número de línea aparezca dentro de un hunk de ese archivo. side debe ser RIGHT para una línea añadida o sin cambios, y LEFT para una línea eliminada. Anteponer a cada línea del diff su número de línea del archivo nuevo antes de enviarlo al modelo evita que el modelo invente números desde el principio.

¿Puedo ejecutarlo en un repositorio público con pull requests de forks?

No con este diseño. GitHub no entrega secretos a un workflow activado desde un fork, por lo que falta la clave del modelo y la ejecución falla. GitHub también indica que los self-hosted runners «casi nunca deberían usarse para repositorios públicos», porque cualquiera puede abrir una pull request que haga ejecutar código en su máquina. En un proyecto público, restrinja el revisor a las ramas enviadas al propio repositorio, que es lo que hace la protección if:, o traslade el paso de revisión a un runner alojado por GitHub y acepte que el diff salga de su propia infraestructura.

¿Qué modelo debo usar para revisar pull requests?

Empiece con Haiku 4.5. Leer un diff acotado comparándolo con una lista fija de tipos de defectos no requiere un razonamiento complejo, y el modelo más barato mantiene la factura mensual en un importe razonable. Pase a Sonnet 5 si observa que omite errores reales en su lenguaje o framework, y mídalo en lugar de darlo por supuesto. Opus 5 es, con mucha diferencia, el más caro de los tres por pull request. Es más fácil justificarlo en una rama de release que en cada push de cada rama de funcionalidades.