Ir al contenido
Documenta la v0.12.1, la versión que se instala. Es el README.es.md del repositorio, con los enlaces reescritos para que apunten a ese tag. Se omitieron el puntero a la versión inglesa que abre el README y «Migración de v0.3 a v0.4 (versiones del paquete)»; están en el original.

Referencia

Tu IA escribió el código. Otra IA lo revisó. Lo que pasó en esa revisión termina como un archivo JSON en tu repo, al lado del código que juzga.

PyPI CI Python License: MIT DOI

Ni un badge verde ni un puntaje de cobertura. Un registro versionado de lo que la revisión no pudo establecer: qué hallazgos se verificaron y contra qué, cuáles se refutaron, qué no se pudo ejecutar y qué sigue descansando sobre el juicio de alguien. El registro dice quién generó el cambio, quién lo revisó y de qué familia de modelos, sobre qué commits, con qué consigna y quién aceptó el riesgo residual. Lista residuo, no cobertura: dirige el escrutinio del revisor humano en lugar de leerse como sello de calidad. Un CLI lo valida, y un gate de CI frena el pull request cuando el cambio exige un registro y ese registro falta, es genérico o no coincide con el cambio que el PR hace.

El método detrás es el desacuerdo controlado: un modelo genera, un modelo de otra familia ataca, el generador verifica cada hallazgo, y el ciclo termina cuando todo hallazgo quedó resuelto, refutado con evidencia o escalado a un humano. Este repositorio es la implementación de referencia del artefacto que ese método define, la declaración de residuo: lo que la ronda deja para que lo mire una persona.

Paper del método: Rocchia, N. (2026), Desacuerdo controlado: revisión adversarial automatizada con un segundo asistente de código en el desarrollo de software, DOI 10.5281/zenodo.21633495.

  • spec/residue.schema.json: el esquema del artefacto (JSON Schema 2020-12), versión residue/v0.4. Las versiones superadas conservan su propio recurso congelado al lado.
  • spec/examples/: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
  • src/disensor/: paquete Python con el validador (reglas R0 a R13), el gate de CI (chequeos G1 a G9), el render del comentario de PR, el informe HTML de residuo (report), el scaffolding de artefactos y el de repositorios (init), y la guía de llenado empaquetada (GUIDE.md).
  • action.yml: GitHub Action compuesta, lista para usar.
  • docs/integracion-claude-code.md: cómo el flujo real (Claude Code más un revisor de otra familia) emite el artefacto al cierre de cada evento.
  • docs/antecedentes.md: dónde se ubica el método respecto de la literatura (residual doubt y defeaters, design rationale y su capture bottleneck, revisión adversarial multi-agente, governance runtimes, provenance de cadena de suministro), con el estado de verificación de cada referencia.

El paquete se instala una vez (global); cada repositorio se inicializa una vez:

Terminal window
pip install disensor # o pipx install disensor, recomendado para CLIs
disensor init # en la raíz del repo: config, CLAUDE.md, skill de llenado y workflow de CI
disensor pin # la Action del workflow, congelada al SHA de commit del tag de la release
disensor reviewer suggest # qué revisores tiene esta máquina, sin red
disensor round --gate diff --generator-family anthropic --base main --head HEAD --result ../result.json
disensor new --gate diff --level B --round ../result.json # la declaración de esa ronda
disensor prompt --gate diff # la consigna adversarial, para pegarle al revisor de otra familia
disensor pack --gate diff --base main --head HEAD # el paquete completo, si manejás la ronda vos
disensor new --gate diff --level B # plantilla prellenada en .residue/
disensor validate .residue/<id>.json # schema + reglas R0 a R13
disensor gate --no-comment --base main --head HEAD # lo que va a correr CI, en local; en verde escribe el informe
disensor report --open # el informe de residuo, en el navegador
disensor guide # la guía de llenado, para cualquier agente o humano
disensor guide --lang es # la misma guía, en castellano
disensor prompt --gate diff --hash # el sha256: de la consigna empaquetada, que es lo que pide prompt_hash
disensor hash consigna.md # o el de la tuya, si la escribiste vos

La consigna viaja adentro del paquete, así que su hash es reproducible: cualquiera puede recomputarlo desde la misma versión y ver qué se le pidió realmente al revisor. Si la editás, el hash cambia y el artefacto declara que se usó otra consigna, que es justamente para lo que sirve el campo.

Hay dos modos y conviene no mezclarlos. Para probarlo, no hace falta workflow, ni required checks, ni permisos de organización: el gate corre igual en tu máquina y dice exactamente lo mismo que diría en CI.

Terminal window
disensor init --no-workflow # config, CLAUDE.md y skill; sin tocar .github/
disensor prompt --gate diff # la consigna, al revisor de otra familia
disensor new --gate diff --level B # y llenás la declaración con lo que pasó
disensor validate .residue/<id>.json
disensor gate --no-comment --base <base-sha> --head HEAD

Recién cuando quieras que haga cumplir, corré disensor init completo (que escribe el workflow) y aplicá los requisitos de despliegue de más abajo. Antes de eso es una herramienta que te dice cómo te iría; después es un control que bloquea.

Los subcomandos y flags de la v0.1 en español (nuevo, validar, --compuerta, --nivel, --directorio, --sin-comentario) siguen funcionando como alias.

disensor init escribe, en forma idempotente, el disensor.config.json (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en CLAUDE.md, la skill de Claude Code con el runbook del evento (.claude/skills/disensor/SKILL.md, cargada a demanda al cerrar cada ronda) y el workflow del gate; lo que ya existe se respeta y se informa. El principio es que después de pip install disensor y disensor init el usuario no toque nada a mano: Claude sabe cuándo (CLAUDE.md) y cómo (la skill), cualquier otro agente recibe lo mismo de disensor guide, que imprime ese runbook y la guía de llenado, y el CI hace cumplir el resultado. Config resultante:

{
"criticality_level": "B",
"level_A_enabled": false
}

Y el workflow (ver docs/ejemplo-workflow.yml):

on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: NicolasRocchia/disensor@v0.12.1

El gate valida las declaraciones que el PR agrega, aplica la política y publica el resultado como comentario (se actualiza en el lugar en cada push). Todo lo que decide sale de objetos de git en el rango merge-base..head, nunca del working tree: en un evento pull_request el checkout deja el merge commit sintético mientras head.sha apunta al head real, así que leer del disco clasificaría un árbol y validaría otro.

La ronda era la parte que se hacía a mano: empaquetar el material, entregárselo a otro asistente, traer el informe, acordarse de mirar el árbol después. disensor round hace la mitad mecánica, así que nunca se copia y pega material entre modelos.

Terminal window
disensor reviewer suggest # qué tiene esta máquina, sin red
disensor reviewer add codex --model <model> --yes # una vez por máquina, con un modelo que corra tu cuenta
disensor round --gate diff --generator-family anthropic \
--base main --head HEAD --result ../result.json
disensor new --gate diff --level B --round ../result.json

Cualquier asistente puede ser el revisor. Nosotros corremos Claude Code con Codex atacando porque es lo que tenemos; la herramienta no está atada a ninguno de los dos. Sirve cualquier línea de comandos que reciba un texto y devuelva un texto: el CLI de otro proveedor, un modelo local por Ollama, lo que ya estés pagando. El catálogo empaquetado es un atajo para los casos que ya probamos, no la lista de lo que está permitido. Si el tuyo no está, tu asistente lee su --help, propone la entrada y vos la aprobás una vez.

Dos cosas que conviene saber sobre esa aprobación. Los revisores viven en tu máquina (~/.disensor/reviewers.json) y nunca en el repositorio: una entrada es código ejecutable, y un PR que pudiera agregar una correría comandos en la máquina de quien lo revise. Y lo que propone tu asistente no se registra hasta que digas que sí, porque un repositorio puede traer instrucciones dirigidas a tu asistente, y registrar un ejecutable que después va a recibir tu código privado es una decisión tuya, no suya.

Qué corre solo y dónde aparecés vos. El runner le pregunta a la política si hace falta una ronda (un cambio que solo toca rutas exentas no gasta un token), se niega a correr con el árbol sucio, elige el revisor más independiente disponible, lo ejecuta, captura el informe y emite un resultado anclado a los commits exactos que se revisaron. Nunca lee el informe: juzgar lo que dijo el revisor es trabajo de tu asistente. Vos aparecés cuando hay que consentir que el material salga de tu máquina, cuando un riesgo necesita dueño, cuando algo se escala sin resolver, y en el PR.

Cuando no hay una segunda familia. El método quiere un revisor de otra familia de modelo, y eso es lo que la política sigue exigiendo en nivel A. Por debajo, una ronda con el mismo modelo y sin contexto es un modo degradado declarable: la declaración registra la independencia que de hecho tuvo, por qué se conformó con menos, y un ítem de residuo que dice que los errores que el modelo comparte consigo mismo no los cubrió esa ronda. Peor que lo real, e infinitamente mejor que no poder declarar lo que pasó.

disensor report lee las declaraciones y escribe un solo archivo HTML autocontenido: CSS y JS inline, sin red, con una Content-Security-Policy que prohíbe cargar cualquier cosa, tipografía del sistema. Se abre con doble clic, viaja por mail y funciona en una máquina sin internet. Lee, no valida: un archivo que no tiene la forma de una declaración se lista al final y el resto sigue. El residuo va primero y la cobertura en gris: los hallazgos incorporados describen el registro, no la calidad del código, y nada en la página se puede leer como un sello de aprobación. La primera vista, Abierto, abre con un digest de lo que todo el repositorio dejó abierto (cuántas filas esperan una decisión y desde cuándo, cuántas deudas, cuántos huecos de ejecución que piden atención humana), sigue con el bloque de la última declaración, también cuando no dejó nada abierto, y después viene la cola, agrupada por lo que cada fila le pide a quien lee y no por el campo del esquema del que salió: espera una decisión, deuda anotada, no se pudo ejecutar (por el motivo declarado), sobre el revisor, descansa en un juicio y, plegado porque no pide nada, riesgo aceptado por el dueño. Dentro de cada grupo, lo más viejo arriba. Un hallazgo al que apunta un ítem de residuo de la misma declaración es una fila, no dos. El artefacto no tiene un campo para decir que algo se cerró, así que la vista no dice “abierto”: dice “declarado abierto el , sin evidencia posterior de cierre”, y lo explica arriba (issue #6). Una quinta vista, Tablero, es para quien coordina y no para quien revisa: qué espera una decisión y desde cuándo, los riesgos que el dueño aceptó, las deudas anotadas, lo que las declaraciones dicen de cómo se hizo cada revisión, declaraciones por semana y huecos de ejecución por motivo, abriendo con el mismo límite, porque nada en el artefacto registra un cierre. No puntúa el código ni a las personas. También recorre la rama: de los merges desde el commit que trajo la primera declaración válida (en el orden de la rama, diga lo que diga la fecha de una declaración) que exigían revisión según la política de la punta (decidida por la misma función del gate, así que gate.required y las exenciones significan lo mismo), lista aquellos cuyo PR no trajo una declaración que el gate admita, juzgados con las mismas funciones del gate: las comprobaciones sobre la declaración (identidad y unicidad, G8; nivel, G2 y G3; confinamiento, G4; pertenencia, G5) y la cobertura (cada ruta exigente en su estado final, G6; el árbol integrado visto, G7), y cuenta aparte los commits directos. La única comprobación que no se aplica es G9, la versión vigente del esquema: una versión anterior era la vigente cuando un merge viejo ocurrió. Son las compuertas de hoy: un merge anterior a una de ellas puede figurar sin cobertura aunque el gate de su momento lo haya aprobado. Eso mide declaraciones, no corridas del gate: el repositorio no guarda el veredicto de ninguna corrida. El gate recorre la base del PR; el comando recorre --branch, HEAD por defecto, con la configuración que nombra --config (el mismo archivo con el que corre el gate), y lee las declaraciones de la punta, nunca del árbol de trabajo. El ancla #tablero la abre; Abierto sigue siendo la primera vista. El informe es una función pura de las declaraciones: sin fecha de generación, el pie nombra el commit del que se leyó, y dos corridas sobre el mismo commit dan bytes idénticos.

Nadie tiene que tipear el comando para que el informe exista. Cuando disensor gate llega a un veredicto verde escribe informe-residuo.html en la raíz del repositorio, leído de los mismos objetos git que juzgó, y su salida termina con el mismo digest con el que abre la página, lo que dejó abierto la última declaración y la ruta en la última línea. Lo escribe sólo si git ignora el archivo (disensor init e init --upgrade agregan la línea al .gitignore), porque disensor round exige árbol limpio. Mejor esfuerzo y nunca en silencio: un fallo del informe no cambia el veredicto y termina en una línea [gate] report: FAILED. --report-out elige otro destino y --no-report lo saltea. El comando queda para lo demás: el directorio tal como está en disco, un rango de fechas, otro archivo.

Terminal window
disensor report --open # todas las declaraciones, en el navegador
disensor report --since 2026-09-01 --out /tmp/residuo.html # sólo las recientes, en otro lado

Por artefacto (reglas R0 a R13): coherencia entre hallazgos y residuo, conteos que cierran, decorrelación de familias entre generador y revisor, evidencia material obligatoria en refutaciones verificables (text, link o hash) contra un blanco verificable (verification.against distinto de none), atención humana obligatoria en refutaciones interpretativas, corrección verificada antes de cerrar un hallazgo en compuerta de diff, rechazo de marcadores genéricos (en inglés y en español), y perfil minimizado con el texto libre que R9 cubre removido. Desde residue/v0.4 se suman la coherencia entre la independencia declarada y las familias y modelos que la declaración nombra (R4), los mínimos del modo degradado por nivel (R11) y su residuo de correlación por revisor (R12), y desde esta versión la integridad de los identificadores locales: únicos, y toda referencia a un revisor resuelve contra los declarados (R13).

Por artefacto, contra el PR: nivel igual al declarado del repositorio (G2), Nivel A bloqueado mientras la gobernanza no esté validada (G3), política de confinamiento del revisor por nivel (G4), y pertenencia al PR del commit revisado (G5), que para la compuerta de diff exige además base_commit, porque una revisión de diff identifica el par (base revisada, head revisada) y no un head suelto.

Por PR:

  • G1: si el PR toca rutas que requieren revisión, agrega al menos una declaración válida.
  • G6, cobertura: cada ruta cambiada está cubierta por una declaración cuya compuerta la política de alcance acepta para esa ruta, y que califica para ella, o sea que la ruta no cambió entre el commit revisado y el head. Una declaración rancia no cubre nada.
  • G7, testigo de integración: alguna declaración vio el árbol final completo. La cobertura ruta por ruta no alcanza: dos ramas laterales revisadas por separado y después fusionadas cubren entre las dos todas las rutas mientras nadie revisó la integración.
  • G8, la evidencia es de solo agregar: un PR no puede modificar, borrar ni renombrar declaraciones que ya estaban, ni reutilizar un event_id existente.
  • G9, lo nuevo declara la versión vigente: una declaración que el PR agrega tiene que declarar residue/v0.4. Las versiones superadas se siguen leyendo para que la historia no se reescriba; esa legibilidad no es un permiso para seguir emitiendo bajo las reglas más débiles. El plano de evidencia aplica el mismo criterio en la ingesta.

El gate falla cerrado: si no puede resolver el rango del PR, no da verde. Un control de cumplimiento que no puede decidir, no aprueba.

Límite honesto, heredado del protocolo: la máquina detecta el campo vacío y el marcador genérico, no la declaración falsa. El muestreo humano de PR cerrados sigue siendo la única defensa real contra el cumplimiento cosmético.

Qué compuerta se acepta para cada ruta se declara en el config, y se lee siempre de la punta actual de la rama destino, nunca del checkout del PR. Del destino y no del merge-base, que es otra pregunta: el merge-base es tan viejo como la rama, así que una rama creada antes de que el repositorio endureciera su política arrastraría la vieja. El alcance del PR se mide contra el merge-base; la política que rige es la que el destino tiene hoy. Por eso un PR que cambia la política se juzga con la política anterior, que es lo correcto y además evita el bloqueo mutuo del diseño ingenuo, donde el PR que afloja la configuración queda rechazado por la regla que quiere cambiar y no hay transición posible.

{
"criticality_level": "B",
"level_A_enabled": false,
"gate": {
"required": true,
"scope": [
{ "paths": ["docs/adr/**"], "accepts": ["architecture", "diff"] },
{ "paths": ["CHANGELOG.md"], "accepts": [] },
{ "paths": ["**"], "accepts": ["diff"] }
]
}
}

Gana la primera entrada que matchea. accepts: [] es una exención explícita, que es la salida gobernada para changelogs o PRs automatizados. Los patrones están anclados a la raíz, * no cruza /, ** matchea cero o más segmentos completos, y el match es sensible a mayúsculas byte a byte para que la misma política signifique lo mismo en cualquier runner. Una ruta que no matchea nada exige diff: la ausencia de política no es un permiso.

Piso no relajable: la ruta efectiva de configuración, .github/workflows/** y el directorio de evidencia siempre exigen diff, diga lo que diga scope. Sin ese piso, una política de aspecto inocente como **/*.yml con architecture rebajaría los workflows, que son la fuente del propio control.

Esto es requisito, no sugerencia. El gate corre dentro del workflow que audita, así que hay una frontera que ningún código suyo puede cruzar y que resuelve la plataforma:

  • Required check estricto (o merge queue) sobre pull_request, para que el check tenga que corresponder al último head.
  • CODEOWNERS sobre la ruta efectiva de configuración (puede no llamarse disensor.config.json si se usa --config) y sobre .github/workflows/.
  • Ruleset o required workflow de organización, definido fuera del repositorio auditado.
  • Pin de la Action por SHA, no por tag: un tag es movible y no es raíz de confianza. disensor init resuelve el tag de la versión instalada al commit al que apunta y escribe el workflow ya congelado; sin red al momento del init el tag queda y disensor pin termina el trabajo. El comando resuelve los tags anotados al commit que envuelven, nunca al objeto tag, que es la trampa clásica de hacerlo a mano. La documentación de este repo sigue usando el tag, porque documenta qué versión corresponde; el SHA congelado lo produce quien despliega.
  • Secret scanning con push protection sobre el repositorio que aloja .residue/: la evidencia cita material real y vive en git para siempre, y el validador mira forma, no significado, así que nada propio de disensor va a cazar una credencial pegada. La detección es de la plataforma; el remedio para un secreto filtrado es la rotación, no el borrado (#14).
  • Bootstrap: el primer PR que agrega el config y el workflow no puede convertirse a sí mismo en raíz de confianza. La activación inicial es un paso administrativo, previo a que el gate signifique algo.
  • Cuándo sube el pin: el workflow está en el piso no relajable, así que subir el pin por PR cuesta una ronda adversarial por un cambio cuya corrección un git rev-parse verifica mejor que cualquier modelo. La convención de este repositorio es que el pin nuevo viaje en el próximo PR de trabajo real, con su declaración, en vez de ir en un PR propio. Excepción: si la release corrige la seguridad del gate o cambia la versión del esquema, el pin se sube de inmediato, porque la ventana en que el repositorio se juzga con la versión anterior deja de ser inocua: un gate viejo no conoce el contrato nuevo y rechaza lo que el CLI recién publicado emite. Es una convención, no un control: mientras la rama no exija pull request con bypass deshabilitado también para administradores, nada impide empujar el pin directo (#17).

Límite explícito: leer la política de la base convierte un bypass de un paso en uno de dos, no lo elimina. Quien pueda mergear una relajación la usa en el PR siguiente. Y nada de esto protege contra un workflow modificado, salteado o sustituido. Eso solo lo resuelve la plataforma.

El veredicto no depende de GitHub; el comentario sí. Con --base, --head y --no-comment, el gate no necesita ninguna variable GITHUB_* ni hace llamadas de red: decide desde los objetos git del rango, así que cualquier CI que pueda correr Python sobre un checkout del repositorio lo aloja. Hacen falta cuatro cosas:

  • El rango, desde las variables del propio CI. --base es la punta de la rama de destino, que es de donde se lee la política, y --head es el último commit del cambio. En un pipeline de merge request de GitLab, la rama de destino se trae por nombre (git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME") y --base es FETCH_HEAD; --head es CI_COMMIT_SHA, o CI_MERGE_REQUEST_SOURCE_BRANCH_SHA en un pipeline de resultados combinados, donde CI_COMMIT_SHA es el commit de merge temporal. No CI_MERGE_REQUEST_DIFF_BASE_SHA: ese es el merge base, y la política se leería de un commit más viejo que la punta del destino.
  • La historia completa, el equivalente de fetch-depth: 0: sin ella no hay merge base y el gate falla cerrado. En GitLab, GIT_DEPTH: "0".
  • --no-comment, y el veredicto por código de salida: 0 es verde, y 1 es rojo o un gate que no pudo decidir. El cuerpo que llevaría el comentario sale por stdout, y con veredicto verde --report-out <archivo> escribe el informe HTML donde el CI lo pueda archivar.
  • Los mismos requisitos de despliegue, con el nombre que les da cada plataforma: un check obligatorio que corra sobre el head de cada cambio, la configuración y la definición del pipeline con dueño fuera del repositorio auditado, y el ejecutable fijado a una versión exacta del paquete, corrido como lo corre action.yml (python -I -m), porque el directorio de trabajo es el checkout que se está juzgando.

Lo que no viaja es el comentario del PR y el resumen del job, que escriben en GitHub; quien los quiera en otra plataforma los arma desde stdout. Un job mínimo de GitLab CI está en docs/ejemplo-gitlab-ci.yml.

El gate de CI no corre modelos, no pide claves de API y no manda código a ningún servicio: valida un JSON que ya está versionado en el repo. Correr la ronda es opcional y no sale de tu máquina: disensor round maneja un CLI de revisor que registraste vos, y un revisor en la nube necesita consentimiento con alcance antes de que salga material. El perfil minimized del artefacto está pensado para ambientes donde el texto de los hallazgos no puede salir del entorno.

En el perfil minimized, R9 remueve los campos del hallazgo que el protocolo define, el text y el link de toda evidencia, la description del ítem de residuo y un repository que empiece con http. El esquema exige además que todo valor bajo extensions sea opaco (un hash sha256:, un número, un booleano, null o contenedores de esos) y que toda clave tenga forma de identificador: un nombre, no un mensaje.

El perfil angosta el canal de fuga; no lo cierra. R9 no alcanza a todo string del artefacto. residue.declaration, event.pr, verification.detail, human_arbiter.id y lead_acceptance son algunos de los campos que siguen admitiendo prosa libre, y la lista no pretende ser exhaustiva: la superficie vigente está en el esquema. Ojo con que un repository hasheado no sirve de nada si event.pr lleva la URL. El propio esquema lo dice del espacio de extensión: una clave con forma de identificador todavía puede llevar un mensaje. minimized es una reducción de superficie, no la garantía de que no sale nada.

Sobre un checkout que no es de confianza, corré disensor con la ruta completa de un intérprete de confianza y -I: /ruta/al/venv/bin/python -I -m disensor ... (en Windows, C:\ruta\al\venv\Scripts\python.exe -I -m disensor ...). El comando disensor que instala pip arranca Python sin aislamiento, así que PYTHONPATH y PYTHONUSERBASE deciden qué se importa antes de que corra disensor, y un valor relativo apunta al directorio en el que estás; -I lo apaga, como en la Action. La ruta completa importa en Windows: cmd.exe busca un python o un disensor sin ruta en el directorio actual antes que en el PATH. -I también ignora el site del usuario, así que disensor tiene que estar instalado en ese entorno, no con pip install --user. Y cloná el repositorio vos: un directorio que llega con su .git adentro trae una configuración de git que todo comando de git obedece ahí, los de disensor incluidos.

Casi todo el vocabulario con el que se busca en este espacio describe la revisión: quién revisa, con cuántos modelos, en qué orden. disensor está un paso después. Define y valida el registro con el que la revisión termina, y lo hace cumplir en el pull request. Así que comparte el lado de la revisión con cada uno de estos términos y se diferencia en lo que agrega.

  • Revisión de código entre modelos (cross-model, multi-model code review). El método la exige: el revisor tiene que venir de una familia de modelo distinta de la del generador (R4), y desde residue/v0.4 la declaración dice qué independencia tuvo realmente la ronda. Lo que disensor agrega es que el resultado queda escrito, versionado y con gate, participen los modelos que participen.
  • Maker-checker. La misma separación entre quien construye y quien certifica, con una diferencia en lo que firma el que certifica: no “aprobado” sino la lista de lo que no cerró. El árbitro humano (R0) es el último checker, y sin él el artefacto no valida.
  • Segunda opinión (second-opinion review). Un revisor de otra familia es una segunda opinión por construcción. disensor no se queda en la opinión: cada hallazgo tiene que llegar a un estado terminal, y una refutación necesita evidencia, no una réplica.
  • IA que revisa código generado por IA. El caso para el que se escribió el método. Su límite conocido está dicho más arriba: el gate detecta el campo vacío y el marcador genérico, no la declaración falsa, así que el muestreo humano de PR mergeados sigue siendo parte del diseño.
  • Diversidad de modelos, revisor decorrelacionado. La razón detrás de R4 es la decorrelación: dos modelos del mismo linaje tienden a fallar en los mismos lugares. La magnitud de ese efecto no está medida, y docs/antecedentes.md lo dice; la regla es un diseño plausible, no un resultado demostrado.

Las herramientas que ya existen cubren bien el lado de la revisión: la guía de revisión adversarial de código de Augment Code, el loop alecnielsen/adversarial-review entre Claude y Codex, y las funciones de revisión de asistentes como GitHub Copilot. Cualquiera puede alimentar una declaración de residuo; ninguna la reemplaza, porque ninguna deja un registro versionado y con gate de lo que la revisión dejó para que lo mire una persona.

Relación con Adversarial Review (arXiv 2608.18167)

Sección titulada «Relación con Adversarial Review (arXiv 2608.18167)»

Qiu, E. S. y Gill, J. (2026), Adversarial Review: Structured Disagreement for Grounded Agentic Code Review, arXiv:2608.18167. Los nombres se superponen y las preocupaciones son vecinas, así que conviene decir la diferencia.

AR es un protocolo de orquestación: un agente principal trabaja con un revisor y un crítico, el crítico audita la revisión mediante desacuerdo estructurado antes de que el agente principal edite, y el resultado se mide por pass rate y F1 en benchmarks. El gate no orquesta ni corre modelos: disensor define el artefacto con el que termina cualquier ciclo de revisión, lo valida y lo hace cumplir en CI. El disensor round opcional sí corre el paso del revisor, con uno instalado en tu máquina, y nunca juzga lo que devuelve; el diálogo entre revisor y crítico que AR orquesta no es algo que disensor haga.

AR reporta un modo de falla de falso consenso, agentes que convergen en un acuerdo sin evidencia suficiente, y lo ataca dentro del protocolo obligando al crítico a fundar su desacuerdo en evidencia. disensor ataca el mismo problema desde el otro lado: la declaración lista residuo, no cobertura, el árbitro humano es obligatorio (R0) y generador y revisor tienen que ser de familias distintas (R4). Acá el desacuerdo no es un paso del protocolo: es lo que queda registrado, aunque el ciclo haya cerrado.

Son complementarios: un ciclo AR puede terminar en una declaración de residuo, y lo que el crítico no pudo zanjar con evidencia es exactamente lo que la declaración le lleva a un humano. Notas de lectura, con el abstract y la entrada BibTeX, en docs/notes/arxiv-2608.18167.md.

El paquete y el esquema se numeran por separado, y prometen cosas distintas.

Estable dentro de una serie mayor del paquete. Los códigos de salida de disensor round (0 revisado, 1 error, 3 no hacía falta ronda, 4 cadena agotada, 5 árbol modificado durante la ronda, 6 no se pudo decidir si hacía falta); los subcomandos y banderas documentados en este archivo; los cuatro inputs de la Action (github-token, directory, config, python-version); y las claves de disensor.config.json, que ya fallan cerrado ante cualquier cosa desconocida. Romperlos exige una mayor.

Versionado por su cuenta. El esquema de la declaración (residue/vX.Y, declarado dentro de cada artefacto) y el resultado de disensor round (result_version). Ninguno sigue la numeración del paquete, y un cambio en ellos no es una mayor del paquete.

Sin promesa. El registro de revisores de tu máquina, el catálogo de recetas y la API de ingesta del plano de evidencia. El catálogo es la única superficie que depende de CLIs de terceros: la receta de Codex cambió el día que una cuenta dejó de ofrecer el modelo que nombraba.

Una asimetría que conviene saber antes de pinear. El gate exige que una declaración que el PR agrega traiga la versión de esquema vigente, y rechaza tanto las superadas como las más nuevas. La Action instala el CLI desde su propio checkout, así que el SHA con el que la pines fija también qué versión de esquema acepta tu gate. Un cambio de esquema exige entonces actualizar ese pin antes de poder mergear declaraciones bajo la versión nueva.

spec/vectors/ contiene los vectores de conformidad, una suite por versión del esquema: 11 artefactos para v0.2, 35 para v0.3 y 43 para v0.4, cada uno con su veredicto esperado (válido o no, y las etiquetas de regla que deben dispararse). Toda implementación del validador tiene que pasarlos idénticos: la referencia en Python los corre en la suite (tests/test_vectors.py) y el port TypeScript del plano de evidencia los corre con npm run conformidad. Se comparan etiquetas, no mensajes. Los vectores se regeneran con python -m disensor.vectors <directorio>. El generador produce la versión de esquema vigente y se niega a escribir sobre una suite que declara otra: pisar una suite histórica borraría la única cobertura negativa que tienen esas reglas. El runner falla si una versión conocida no tiene vectores que la declaren, porque si no una versión puede figurar como soportada sin que nada lo verifique, que es exactamente como v0.2 llegó hasta acá. spec/version_ordinality.json lleva los otros vectores compartidos: la forma de un identificador de esquema y qué reglas alcanzan a qué declaración, verificados por las dos implementaciones. En total, la conformidad corre 89 vectores en tres suites más 28 casos compartidos.

Cada vector se valida con el schema de la versión que declara. El port TypeScript implementa las reglas de v0.2, v0.3 y v0.4, así que el claim de dos implementaciones independientes cubre la versión que el CLI emite. Ante una versión que no implementa lo dice y se niega, en vez de devolver un veredicto sin haber corrido las reglas que esa versión agregó.

plano-evidencia/ contiene el Worker de ingesta (Cloudflare Workers más D1) con el port TypeScript del validador y el recibo de integridad de solo agregado. Ver su README para el estado de verificación y el despliegue.

La terminología del paper es en español; el contrato (claves y enums del esquema, CLI) es en inglés desde v0.2. Equivalencias principales:

Paper (ES) Esquema/CLI (EN)
residuo residue
hallazgo finding
compuerta (plan, diff, arquitectura) gate (plan, diff, architecture)
nivel de criticidad criticality_level
perfil completo / minimizado profile full / minimized
actores: generador, revisores, árbitro humano actors: generator, reviewers, human_arbiter
familia (de modelo) family
confinamiento (permisos, sandbox, solo lectura por instrucción, sin confinamiento) confinement (permissions, sandbox, read_only_by_instruction, no_confinement)
consigna (hash de la consigna adversarial) prompt_hash
estado final: incorporado, deuda registrada, decisión del dueño, refutado verificable, refutado interpretativo, escalado abierto final_state: incorporated, debt_recorded, owner_decision, refuted_verifiable, refuted_interpretive, escalated_open
clases de residuo: escalado sin decisión, refutación del principal, gap de ejecución residue classes: escalation_without_decision, principal_refutation, execution_gap
ruta abreviada / casos protegidos abbreviated_path / protected_cases_touched
verificación de la corrección fix_verification
aceptación de referente lead_acceptance
ausencia declarada / declaración declared_absence / declaration
métricas: conteos, válidos, falsos positivos metrics: counts, valid, false_positives

Migración desde v0.1: renombrar .residuo/ a .residue/, las claves del config (nivel_criticidad a criticality_level, nivel_A_habilitado a level_A_enabled) y las claves de los artefactos según el glosario. El validador reconoce artefactos v0.1 y lo dice explícitamente; el gate rechaza en voz alta un config con claves viejas en lugar de aplicar defaults en silencio.

Migración del esquema: residue/v0.2 a residue/v0.3

Sección titulada «Migración del esquema: residue/v0.2 a residue/v0.3»

Cuidado con la ambigüedad: esta sección habla de la versión del esquema; la siguiente habla de versiones del paquete. Son dos numeraciones distintas.

La v0.3 no renombra ni agrega claves. Endurece los puntos donde la garantía declarada era más fuerte que la implementada (tres detectados antes de la ronda y dos que la propia ronda adversarial de v0.3 agregó), y suma un valor a un enum:

Antes valía Ahora se rechaza Por qué
refuted_verifiable con evidence: {} El objeto de evidencia tiene que traer text, link o hash v0.2 exigía la presencia del objeto, no su contenido: se podía cerrar un hallazgo sin tocar el código declarando evidencia vacía (#5)
refuted_verifiable con verification.against: "none" against tiene que ser repository, execution o external_source Refutar sin haber verificado nada es una contradicción, no una refutación (#5)
Perfil minimized con texto libre en extensions Todo valor bajo extensions tiene que ser opaco: hash sha256:, número, booleano, null, o contenedores de esos El espacio de extensión no lo interpretan las reglas, así que el texto estacionado ahí salía del entorno mientras el perfil afirmaba que nada salía (#8)
refuted_verifiable con evidencia presente pero en blanco (link: "", text de puros espacios) text y link tienen que traer al menos un carácter no blanco La presencia sin contenido reabría el hueco del #5 por la pata más débil del anyOf; lo cazó la propia ronda adversarial de v0.3
Perfil minimized con texto libre en las claves de extensions Toda clave bajo un objeto opaco tiene forma de identificador ([A-Za-z0-9._:-], máximo 128) El valor opaco no alcanza si el mensaje viaja en el nombre: el #8 cerraba los valores y dejaba las claves

Y verification.against acepta ahora external_source: literatura, especificaciones de terceros, advisories o documentación externa. En v0.2 una verificación contra una fuente externa no tenía categoría verdadera disponible y había que declararla como repository (#7).

Cómo migrar: poner el campo schema en residue/v0.3 (la clave se conserva; cambia su valor). Si el artefacto ya satisface los invariantes de la tabla, no hay nada más que hacer: ningún fixture de este repositorio que fuera válido bajo v0.2 necesitó corrección. Los vectores de conformidad sí incluyen artefactos que los violan, a propósito, como casos negativos. El validador reconoce un artefacto v0.2 y explica qué endureció la v0.3 en lugar de limitarse a decir que el const falló.

Por qué se subió el identificador en vez de endurecer v0.2 en el lugar: no fue por compatibilidad, que no había ninguna que proteger. Fue porque el producto entero se apoya en que un identificador de esquema signifique una cosa; si v0.2 significara distinto según cuándo se lo lea, la herramienta se contradiría en su propio repositorio.

El contrato v0.2 original queda congelado, byte a byte como se publicó, en spec/residue.schema.v0.2.json: el esquema vigente sigue leyendo v0.2, pero el documento al que ese identificador apunta ya no depende de una reconstrucción.

Migración del esquema: residue/v0.3 a residue/v0.4

Sección titulada «Migración del esquema: residue/v0.3 a residue/v0.4»

Las declaraciones históricas no cambian. Cada versión tiene ahora su propio recurso congelado y se valida con sus propias reglas, así que una declaración v0.3 sigue validando igual que antes: leer registros viejos nunca fue un permiso para seguir emitiendo bajo reglas más débiles, y tampoco es un motivo para reescribirlos. Lo que cambia es lo que tiene que decir una declaración NUEVA.

Qué agrega v0.4 Por qué
reviewers[].independence (obligatorio) R4 exigía familia distinta y punto, así que una ronda sin segundo modelo no se podía declarar de ninguna manera, ni diciendo la verdad. Ahora la independencia se declara y la regla verifica que coincida con las familias declaradas: cross_family con dos revisores de la misma familia se rechaza, y declararse degradado teniendo otra familia también.
reviewers[].fallback_reason Obligatorio por debajo de cross_family. Un código enumerado, no prosa: el texto libre se vuelve boilerplate en el segundo evento, y ahí la cadena pasa a ser una excusa para ir siempre por el camino barato.
reviewers[].hardening verified cuando el revisor corrió por un adaptador cuya neutralización de las instrucciones del proyecto se probó contra un repositorio hostil. Se deriva, no se elige.
Clases de residuo reviewer_correlation y reviewer_hardening_gap Una por revisor degradado, nombrándolo. La correlación es lo que el revisor no podía ver; el endurecimiento es lo que el material revisado podía decirle. Riesgos distintos, ítems distintos.

Cómo migrar: nada, para lo que ya está escrito. Para lo que escribas de ahora en más, disensor new emite v0.4 y prellena estos campos desde la ronda; disensor validate te dice exactamente qué falta si escribís una a mano. El nivel A no admite independencia por debajo de cross_family: declarable no es lo mismo que admisible en el nivel que el protocolo reserva para lo que no se puede deshacer.

Un detalle del despliegue: un CLI 0.9 emite v0.4, y un gate todavía pineado a una release anterior no conoce esa versión. disensor init --upgrade mueve el pin, o lo avisa antes de que generes una declaración que tu propio CI rechazaría.

v0.12.1, sobre residue/v0.4. Qué cambió en cada versión, de la más nueva a la más vieja, está en CHANGELOG.es.md.

El esquema puede cambiar; cada versión desde residue/v0.2 en adelante se congela con su propio identificador, y una declaración se sigue validando con las reglas que la juzgaron cuando se emitió. La residuo/v0.1 de claves en castellano se reconoce y se rechaza con instrucciones de migración, no se valida. No hay una versión comprometida como el punto donde el esquema se estabiliza: cuando haya un contrato ratificado como estable, se dice acá. Decisión abierta: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes).

MIT.