# Revisión de código con IA en CI usando un agente de código abierto: la configuración que de verdad aguantó

> Queríamos un segundo revisor que nunca se canse pero que tampoco invente un problema de la nada. Así integramos Octomind en GitHub Actions para revisión de código automatizada: los inputs reales de octomind-action, ejecutar los comandos de lint y test del equipo a través de .agents/tools/, mantener el CI no interactivo y seguro, y acotar el coste.

La primera versión de nuestro revisor con IA señaló 14 problemas "críticos" en un cambio de configuración de una línea. Doce eran imaginarios. Uno era una errata real. Y uno era un bug genuino que nadie más había detectado. Esa proporción —una señal enterrada bajo doce invenciones— es exactamente la razón por la que la mayoría de los equipos apaga el bot en una semana y no lo vuelve a encender.

No queríamos ese bot. Queríamos un segundo revisor que nunca se canse pero que tampoco invente un problema de la nada. Un revisor que ejecute nuestra suite de tests _real_ en lugar de adivinarla, que comente una sola vez con lo que encontró, y que cueste dinero predecible. Esta es la configuración que, tras unas cuantas iteraciones vergonzosas, de verdad aguantó en nuestro CI.

Todo aquí corre sobre código abierto: [Octomind](https://github.com/Muvon/octomind), el agente basado en sesiones, y [octomind-action](https://github.com/Muvon/octomind-action), la GitHub Action que lo envuelve. Sin revisor SaaS, sin precios por asiento, sin que el código salga de tu runner salvo hacia el proveedor de modelos que ya pagas.

---

## Por qué un "revisor de IA autoalojado" en vez de un bot SaaS

Hoy existe un mercado saludable de productos de revisión de código con IA alojados. Funcionan. Pero para la revisión de código con IA en CI, tres cosas nos empujaron hacia un revisor de IA autoalojado corriendo dentro de nuestras propias GitHub Actions:

1. **El revisor debe ejecutar nuestros comandos, no su idea de nuestros comandos.** Un bot alojado lee el diff. No ejecuta `cargo clippy --all-targets` con nuestra configuración exacta de lint, ni nuestra suite de integración tras los feature flags correctos. Un revisor que no puede ejecutar las comprobaciones reales del proyecto queda reducido a hacer pattern-matching sobre texto, que es precisamente de donde salen las alucinaciones.
2. **El agente corre en el runner que ya confiamos.** Mismo checkout, mismo límite de secretos, mismas reglas de egress de red. Nada del diff va a ningún sitio que no hayamos autorizado ya.
3. **Es un archivo de configuración en el repo, revisado como cualquier otro.** El comportamiento del revisor es un YAML de workflow y unos pocos scripts en `.agents/tools/` bajo control de versiones. Cuando se vuelve ruidoso, lo arreglas en un PR. Cuando alguien nuevo pregunta "¿qué comprueba el bot en realidad?", la respuesta es `git log`.

Si leíste nuestro artículo anterior sobre por qué [los MCP personalizados pertenecen a tu repo](/blog/custom-mcps-belong-in-your-repo), esta es la misma filosofía apuntada al CI: las herramientas que dirigen a tu agente viven junto al código sobre el que operan.

---

## Cómo se comporta `octomind run` de forma no interactiva

Todo empieza con un hecho sobre el agente: `octomind run` cambia a modo no interactivo en cuanto le das un `--format` o le pasas stdin por tubería. Según la definición de la CLI:

```
--format <FORMAT>   Output format: plain or jsonl. When set, runs
                    non-interactively (reads input from stdin).
```

Por dentro, el runtime resuelve un modo de salida a partir del flag y de si stdin es una terminal:

| Invocación                                 | Modo                                          |
| ------------------------------------------ | --------------------------------------------- |
| `octomind run` en una terminal             | `Interactive` (prompts, colores, animaciones) |
| `echo "..." \| octomind run` (por tubería) | `NonInteractive`                              |
| `octomind run --format plain`              | `NonInteractive`                              |
| `octomind run --format jsonl`              | `Jsonl` (un objeto JSON por línea)            |

El CI es el caso de la tubería, y `--format jsonl` es el que quieres ahí. Suprime el adorno interactivo y emite un flujo de eventos estructurados —mensajes del asistente, un evento final de coste— que un workflow puede parsear sin raspar prosa. Esa es la razón entera de que el agente sea automatizable: no hay ningún prompt oculto esperando a que un humano pulse `y`.

Puedes probarlo a mano para verlo:

```bash
echo "Review the staged diff and report only real defects." \
  | octomind run developer:rust --format jsonl
```

El último evento `assistant` lleva el texto de la revisión; un evento `cost` final lleva los totales de tokens y dólares. Ese es el mismo flujo que parsea la GitHub Action.

---

## La Action y sus inputs reales

`octomind-action` ofrece dos puntos de entrada. Usamos ambos para trabajos distintos:

- `muvon/octomind-action@v1` (alias de `muvon/octomind-action/run@v1`) ejecuta una única sesión de `octomind run` a partir de un prompt.
- `muvon/octomind-action/workflow@v1` ejecuta un `octomind workflow` de varios pasos desde un archivo TOML.

Para la revisión de código, la sesión única `run` es suficiente. Estos son sus inputs reales —copiados de `action.yml`, no inventados—:

| Input           | Por defecto           | Qué hace                                                          |
| --------------- | --------------------- | ----------------------------------------------------------------- |
| `prompt`        | _(requerido)_         | La tarea/mensaje enviado a octomind                               |
| `role`          | el del config         | Tag de rol/agente, p. ej. `developer:rust`                        |
| `model`         | —                     | Override de modelo, p. ej. `openrouter:anthropic/claude-sonnet-5` |
| `name`          | —                     | Sesión con nombre (crear o reanudar)                              |
| `resume`        | —                     | Reanudar una sesión con nombre                                    |
| `resume_recent` | `false`               | Reanudar la sesión más reciente del cwd                           |
| `sandbox`       | `false`               | Restringir las escrituras al directorio de trabajo                |
| `version`       | `latest`              | Versión de Octomind a instalar                                    |
| `tap`           | —                     | Añadir un tap antes de ejecutar                                   |
| `config`        | —                     | Ruta a un archivo de config de octomind                           |
| `comment`       | `none`                | Modo de comentario en PR: `full`, `compact` o `none`              |
| `github_token`  | `${{ github.token }}` | Token usado para publicar el comentario en el PR                  |

Y los outputs que puedes leer en pasos posteriores:

| Output       | Qué lleva                                        |
| ------------ | ------------------------------------------------ |
| `result`     | Último mensaje del asistente (la revisión en sí) |
| `session_id` | ID de sesión, para reanudar en un paso posterior |
| `cost`       | `{"tokens": N, "cost": N}`                       |
| `raw_output` | Flujo JSONL completo                             |
| `exit_code`  | Código de salida del proceso                     |

Dos de estos son decisivos para un revisor. `comment: compact` publica la revisión como un bloque plegable `<details>` en el PR —un comentario, plegable, con el coste en un pie `<sub>`— en vez de un muro de texto. Y `cost` te deja poner el precio de cada revisión en los logs, lo cual importa más de lo que parece.

---

## Conectar el `lint` y `test` reales del equipo mediante `.agents/tools/`

Esta es la parte que separó "de verdad aguantó" de "apagado en una semana". Un revisor que solo lee texto inventa problemas porque el texto es todo lo que tiene. Dale la capacidad de ejecutar tus comprobaciones reales y dejará de adivinar.

Octomind descubre herramientas locales del proyecto en `<workdir>/.agents/tools/<name>` —scripts con shebang y una cabecera de comentario que se convierte en el esquema de la herramienta—. Aparecen ante el modelo como un servidor MCP `local`, sin proceso de servidor aparte, sin registro. Cubrimos la mecánica en [los MCP personalizados pertenecen a tu repo](/blog/custom-mcps-belong-in-your-repo); aquí va el punto relevante para CI: los mismos scripts que usan localmente los agentes de tus desarrolladores son los que ejecuta el agente revisor en CI, porque están commiteados en el repo.

Así que pusimos los comandos _exactos_ del equipo detrás de herramientas con nombre.

`.agents/tools/lint`:

```bash
#!/usr/bin/env bash
# @description Run the project linter exactly as CI does. Returns warnings and errors.
set -euo pipefail
cd "$OCTOMIND_WORKDIR"
cargo clippy --all-targets --all-features -- -D warnings
```

`.agents/tools/test`:

```bash
#!/usr/bin/env bash
# @description Run the test suite. Defaults to the fast unit set.
# @param scope string One of: unit, all (default: unit)
set -euo pipefail
cd "$OCTOMIND_WORKDIR"
scope="${OCTOMIND_PARAM_SCOPE:-unit}"
case "$scope" in
  unit) cargo test --lib ;;
  all)  cargo test --all-targets ;;
  *) echo "Unknown scope: $scope" >&2; exit 2 ;;
esac
```

Ahora el prompt del revisor puede decir "ejecuta `lint` y `test`, y reporta solo defectos que puedas sustentar". El agente llama a la herramienta con nombre, obtiene un código de salida real y una salida real, y ancla su revisión en lo que el proyecto hace de verdad. Una afirmación como "esto fallará clippy" deja de ser una adivinanza: el agente ejecutó clippy. Si clippy pasó, la afirmación nunca se escribe.

Esta es la mayor palanca contra el modo de fallo del problema-alucinado. El modelo es malo prediciendo si tu código compila. Es perfectamente bueno _leyendo la salida del comando que lo compiló_.

---

## Acotar qué puede tocar el revisor

Un revisor debe leer, ejecutar comprobaciones y reportar. No debe editar archivos, hacer push ni abrir PRs. Los roles de Octomind controlan esto con `server_refs` (qué servidores de herramientas ve el rol) y `allowed_tools` (qué herramientas dentro de ellos). La forma, según las plantillas de rol que vienen incluidas:

```toml
[[roles]]
name = "reviewer:strict"
system = """
You are a code reviewer. Run the project's lint and test tools to ground
every claim. Report only defects you can substantiate with tool output or a
specific line. If you find nothing, say so in one sentence. Never edit files.
"""
temperature = 0.1

[roles.mcp]
server_refs = ["filesystem"]
allowed_tools = ["view", "lint", "test"]
```

Unas cuantas decisiones deliberadas ahí. `temperature = 0.1` porque un revisor debe ser aburrido y repetible, no creativo. `allowed_tools` lista solo herramientas de lectura y comprobación —ni escritura por shell, ni edición— para que aunque el modelo decidiera "arreglar" algo, no tenga herramienta para hacerlo. Y las últimas dos frases del system prompt son el control de ruido: _reporta solo lo que puedas sustentar; si no encuentras nada, dilo en una frase._ Esa única instrucción llevó nuestro comentario medio de catorce "problemas" a, en un PR limpio, "No se encontraron defectos".

Como cinturón y tirantes extra, el input `sandbox: true` de la Action restringe cualquier escritura al directorio de trabajo, de modo que una herramienta que sí necesite escribir un archivo temporal puede hacerlo, pero nada escapa del checkout.

---

## Permisos y seguridad en una ejecución no interactiva

Lo que la gente teme de un agente en CI son las aprobaciones. En una sesión interactiva se te pediría confirmación antes de ejecutar una herramienta. En CI no hay nadie a quien preguntar, y ese es el punto: en modo no interactivo no hay puerta `y/n` que cuelgue el job. La seguridad no viene de un humano en el bucle, viene de que el bucle sea estrecho:

- **El `allowed_tools` del rol es la lista blanca para las herramientas de servidores MCP.** El revisor no puede llamar a una herramienta de servidor que no esté listada — un límite estático, no un juicio en tiempo de ejecución del que se pudiera convencer al modelo. La excepción son los scripts locales de `.agents/tools/`, que siempre quedan expuestos: su límite es lo que confirmas en ese directorio, y una revisión de PR ve cada cambio allí.
- **`sandbox: true`** mantiene las escrituras dentro del checkout.
- **Los secretos quedan en el entorno del runner**, nunca en el repo. Los scripts de `.agents/tools/` leen `ACME_TOKEN` y compañía del entorno; el script está commiteado, el secreto es un secret de GitHub Actions. Si falta un secreto, la herramienta sale con código distinto de cero y un mensaje claro en lugar de inventar un resultado.
- **`github_token` está acotado a comentar.** El `${{ github.token }}` por defecto publica el comentario del PR; no se le entrega al modelo como herramienta.

El radio de impacto del revisor es: leer archivos, ejecutar dos comandos en lista blanca, escribir un comentario en el PR. Ese es un límite sobre el que puedes razonar, que es más de lo que puedes decir de "el modelo prometió que no tocaría nada".

---

## El workflow que aguantó

Este es el job de revisión real, anclado por completo en los inputs de arriba:

```yaml
name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write # needed to post the review comment

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # so the agent can diff against the base

      - uses: muvon/octomind-action@v1
        with:
          role: reviewer:strict
          model: openrouter:anthropic/claude-sonnet-5
          sandbox: true
          comment: compact
          prompt: |
            Review the changes on this branch against the base.
            Run the `lint` and `test` tools to ground your findings.
            Report ONLY defects you can substantiate with tool output or a
            specific changed line. Group by severity. If nothing is wrong,
            reply in one sentence. Do not restate the diff.
        env:
          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
```

Notas sobre las decisiones:

- **`role: reviewer:strict`** aporta la temperatura baja, la lista blanca de herramientas y el system prompt de "sustentar o callar". El input del prompt es la _tarea_; el rol es la _personalidad y las barandillas_. Mantenlos separados para ajustarlos por separado.
- **`model:` está fijado**, no se deja a la deriva. Fijar el modelo es la mitad de tu control de coste y casi toda tu reproducibilidad. Lo fijamos a un modelo de clase Sonnet y solo lo cambiamos a propósito.
- **`comment: compact`** da un comentario plegable por ejecución. En `synchronize` (cada push al PR) obtienes una revisión nueva sin que el hilo se convierta en un scroll.
- **`OPENROUTER_API_KEY`** es el único secreto que necesita el job. Octomind habla con OpenRouter, Anthropic, OpenAI, DeepSeek, Google Vertex, AWS Bedrock y Cloudflare; la variable de entorno que definas elige el proveedor.

Ese es el revisor entero. Un job, una Action, dos scripts de herramienta commiteados, una definición de rol.

---

## Mantener el coste acotado

Un agente que puede ejecutar herramientas puede, en principio, hacer bucles para siempre y facturar para siempre. En la práctica lo acotamos con unas cuantas palancas sosas y efectivas:

- **Fija el modelo.** Un modelo de clase Sonnet fijado tiene un coste por token predecible. "Lo último y lo mejor" es como consigues una factura sorpresa.
- **Dispara en los eventos correctos.** Solo `opened` y `synchronize`. No en cada comentario, no en PRs en borrador (filtra con `if: github.event.pull_request.draft == false` cuando quieras).
- **Lee el output `cost` y registralo.** Cada ejecución emite `cost` como `{"tokens": N, "cost": N}`. Vuélcalo al resumen del job. Una vez que el precio de cada revisión está en tus logs de Actions, el coste deja de ser una sensación y se convierte en un número al que puedes ponerle un umbral.

```yaml
      - run: echo "Review cost: ${{ steps.review.outputs.cost }}" >> "$GITHUB_STEP_SUMMARY"
```

El mayor motor de coste son las herramientas, no el prompt. Un revisor que ejecuta tu suite de integración completa en cada push es caro porque la _suite_ es cara, no porque el agente lo sea. Nuestra herramienta `test` usa por defecto el set rápido de unidad exactamente por esto; el agente solo recurre a `scope: all` cuando el diff toca algo que lo amerita, y aun así podemos limitarlo a nivel de rol.

---

## Los problemas, con honestidad

**No determinismo.** Dos ejecuciones sobre el mismo diff pueden redactar la revisión de forma distinta, y de vez en cuando una encuentra algo que la otra no. `temperature = 0.1` reduce esto mucho pero no lo elimina. El arreglo no es perseguir el determinismo, es tratar la revisión de IA como una comprobación _consultiva_ entre varias. Nunca bloquea el merge por sí sola. Eso lo hacen un humano y las comprobaciones reales de CI.

**El ruido es un problema de prompt, no de modelo.** Nuestro primer revisor era ruidoso porque le pedimos "revisa el código", que un modelo entusiasta lee como "encuentra catorce cosas". La cura fueron las dos frases del system prompt: _sustenta cada afirmación; si no encuentras nada, dilo en una frase._ Más las herramientas: la mitad del ruido era el modelo adivinando resultados que podría simplemente haber ejecutado. Si tu revisor es ruidoso, arregla el prompt y dale la capacidad de comprobar, antes de culpar al modelo.

**Secretos en la salida de las herramientas.** Una herramienta que vuelca el entorno o logs verbosos puede filtrar un secreto directo a un comentario del PR. Mantén la salida de las herramientas ajustada: devuelve el resultado de lint/test, no el mundo. Lo aprendimos cuando un runner de tests verboso hizo echo de un connection string al cuerpo de la revisión. Ahora las herramientas imprimen resultados, no el entorno.

**El diff contra la base.** Usa `fetch-depth: 0` en el checkout o el agente no puede comparar contra la rama base y acaba "revisando" el archivo entero. Error barato, síntoma molesto.

**Es un segundo revisor, no el revisor.** El día que empieces a confiar en que lo atrape todo es el día que se le escapa el que importa. Es una primera pasada incansable. Los humanos cansados siguen siendo dueños del merge.

---

## Preguntas frecuentes

**¿El agente necesita acceso de escritura a mi repo?**

No. El rol del revisor lista solo herramientas de lectura y comprobación en `allowed_tools`, `sandbox: true` confina cualquier escritura al checkout, y `github_token` se usa solo para publicar el comentario. Al modelo nunca se le entrega una herramienta de push o edición.

**¿Puedo ejecutar el lint/test real del equipo en vez de uno genérico?**

Ese es el punto entero. Pon tus comandos exactos en `.agents/tools/lint` y `.agents/tools/test`, commitéalos, y el agente los ejecuta en CI. Los mismos scripts que usan los agentes locales de tus desarrolladores. Ver [los MCP personalizados pertenecen a tu repo](/blog/custom-mcps-belong-in-your-repo).

**¿Cómo leo el coste de una revisión?**

La Action expone un output `cost` (`{"tokens": N, "cost": N}`) y el JSONL completo vía `raw_output`. Haz echo de `cost` a `$GITHUB_STEP_SUMMARY` y estará en el log de cada ejecución.

**¿Y si quiero un pipeline de varios pasos, no una sola revisión?**

Usa `muvon/octomind-action/workflow@v1` con un archivo TOML: reúne contexto en un paso, revisa en el siguiente, resume en un tercero. Mismo entorno de proveedor, mismo plumbing de comentario en PR, más un input `dry_run` para validar el pipeline sin gastar tokens.

**¿Qué proveedores de modelos funcionan?**

El que ya pagues: OpenRouter, Anthropic, OpenAI, DeepSeek, Google Vertex, AWS Bedrock, Cloudflare. Define la API key correspondiente en `env` y fija el modelo con el input `model`.

---

## El cambio

El instinto con un revisor de IA es hacerlo más listo —mejor modelo, prompt más largo, más ingenioso—. Lo que de verdad aguantó para nosotros fue hacerlo _más estrecho y más anclado_. Menos herramientas, todas nuestras. Una temperatura baja. Un system prompt cuyo trabajo entero es mantener al modelo callado salvo que pueda demostrar el punto. Y la capacidad de ejecutar las comprobaciones reales, para que razone desde la salida de `cargo clippy` en vez de desde una corazonada sobre lo que `cargo clippy` podría decir.

El resultado no es un revisor genio. Es uno fiable: nunca se cansa, ejecuta las mismas comprobaciones cada vez y —la parte que más costó ganar— dejó de alucinar problemas de la nada. Para el conjunto más amplio de cosas que salieron alrededor de esto, el [resumen de junio de 2026](/blog/release-round-june-2026) tiene el resto.

Cablearlo primero en un repo de bajo riesgo. Mira unas cuantas revisiones. Ajusta el prompt dos veces. Luego déjalo sentado tranquilo en tus PRs siendo el segundo par de ojos que nunca parpadea.

— Don

---

_[Octomind](https://github.com/Muvon/octomind) y [octomind-action](https://github.com/Muvon/octomind-action) son código abierto bajo Apache-2.0. Si tu revisor es ruidoso, el arreglo casi siempre es el prompt y las herramientas; y si no lo es, [abre un issue](https://github.com/Muvon/octomind/issues) con el JSONL y le echaremos un vistazo._
