Qué hace disensor, explicado despacio
Una IA escribe tu código. Otra IA lo revisa. Lo que pasó en esa conversación se pierde cuando cerrás la ventana. disensor es lo que hace que no se pierda, y lo que impide que el merge avance si el registro no está.
Un tilde verde no dice nada
Sección titulada «Un tilde verde no dice nada»Cuando un pull request tiene el tilde verde, lo único que sabés es que algo pasó y no falló. No sabés qué se miró, ni si alguien discutió algo, ni, sobre todo, qué quedó sin resolver.
Con revisión hecha por IA es peor, porque todo eso ocurre dentro de una ventana de chat. El modelo encontró siete cosas, vos incorporaste cuatro, discutiste dos y la séptima quedó abierta porque nadie podía probarla. Cerrás la ventana y esa información no existe más. Dentro de seis meses, cuando el bug aparezca, nadie va a poder decir si se sabía o no se sabía.
En criollo: disensor no revisa tu código. Lo que hace es obligar a que quede escrito qué pasó en la revisión, en un archivo que vive en tu repo, y frenar el merge si ese archivo no está o está mal.
Dos modelos que no son parientes, y un humano que responde
Sección titulada «Dos modelos que no son parientes, y un humano que responde»El método se llama desacuerdo controlado y tiene cuatro pasos. Lo importante de entrada: el gate no corre ese ciclo. El ciclo ocurre dentro de tu flujo con los modelos que uses, y desde la 0.9 disensor round puede ejecutar el paso de ataque por vos. Lo que disensor define y después revisa es el archivo en el que el ciclo termina.
Por qué la otra familia: pedirle a un modelo que revise su propia salida no es un control independiente. cross_family es la aproximación operativa que usa el método para buscar errores decorrelacionados, no una garantía de que dos familias distintas fallen en lugares distintos: esa comparación no se midió. Por eso R4 no exige una familia determinada. Exige que la independencia esté declarada, que coincida con las familias que declaraste, y que si fue menor se diga por qué.
El archivo que queda, por dentro
Sección titulada «El archivo que queda, por dentro»¿Esto lo tengo que escribir yo? No. disensor init deja en tu repo la instrucción y la skill que le enseñan a tu agente a cerrar la ronda. El mismo modelo que escribió el código es el que emite la declaración, y la valida antes de commitear. Si tu agente no es Claude Code, disensor guide le entrega las mismas instrucciones. Vos seguís trabajando como ya trabajás. Lo de abajo es lo que queda registrado, no un formulario para llenar.
Cada evento de revisión produce un JSON en .residue/. Tiene cinco bloques y ninguno es decorativo. Lo que sigue es una representación simplificada para leerlo de un vistazo, no un artefacto válido: los valores están recortados y las flechas son anotaciones. Hay uno completo y válido en spec/examples/example_2_diff_gate.json.
{ "schema": "residue/v0.4", // ← qué contrato cumple este archivo "profile": "full",
"event": { // ← qué se revisó, y sobre qué código "repository": "…", "pr": "…", "base_commit": "c0ffee1", "head_commit": "beef042", "gate": "diff", plan · diff · architecture "criticality_level": "B" },
"actors": { // ← quién revisó, y qué tan independiente era "generator": { "family": "anthropic", "model": "claude-code" }, "reviewers": [{ "family": "openai", "model": "gpt-5.4", "independence": "cross_family", "prompt_hash": "sha256:1c2d3e4f…", la consigna exacta que se usó "confinement": { "mode": "read_only_by_instruction" } }], "human_arbiter": { "present": true } },
"findings": [ … ], // ← cada hallazgo y en qué terminó "residue": { "items": [ … ] } // ← lo que el ciclo no cerró por sí solo}El bloque que importa es el último. Un informe de cobertura te dice qué se revisó. Este te dice dónde quedó residuo, incluso después de cerrar el ciclo: residuo es más ancho que «lo que quedó abierto». Un hallazgo que el generador refutó con evidencia queda cerrado y entra igual, porque la refutación es del principal sobre el revisor y alguien tiene que poder auditarla.
El prompt_hash merece una mención aparte: es el sha256 de la consigna adversarial que la declaración dice haber usado. Sirve para comprobar si una consigna conocida es exactamente esa, byte a byte. No prueba que esa consigna se le haya enviado al modelo: eso depende de que la declaración diga la verdad, igual que todo lo demás. Lo que aporta es que la consigna deja de ser una descripción y pasa a ser algo que se puede comparar.
Un hallazgo puede terminar en seis lugares, y tres obligan a declarar residuo
Sección titulada «Un hallazgo puede terminar en seis lugares, y tres obligan a declarar residuo»| estado | qué significa, en criollo |
|---|---|
| incorporated | El hallazgo era correcto y lo arreglaste. En el gate de diff, además tiene que venir con su arreglo verificado (R7). |
| debt_recorded | Es real, no lo arreglás ahora, y queda anotado como deuda con identificador. |
| owner_decision | No es un defecto técnico: es una decisión de producto y la tomó una persona. |
| refuted_verifiable | Le discutiste al revisor y tenés con qué: el código o la ejecución muestran que se equivocó. |
| refuted_interpretive | Le discutiste por criterio, sin prueba dura. Por eso R8 obliga a marcarlo como algo que un humano tiene que mirar. |
| escalated_open | Nadie lo cerró. Pasa a tu nombre, abierto y con dueño. |
Las cinco clases de residuo
Sección titulada «Las cinco clases de residuo»El residuo es la lista de aquello que, aun terminado el ciclo, sigue dependiendo de juicio, evidencia incompleta o una limitación del proceso. Hay cinco clases. Las tres primeras son las del método, tal como las define la sección 6 del protocolo. Las dos últimas las agregó residue/v0.4 para los modos degradados: existen porque el esquema admite correr con menos independencia de la ideal, y obliga a declararlo cuando pasa.
escalation_without_decision
Se escaló a un humano y el humano todavía no decidió. Queda abierto y con dueño.
principal_refutation
El que escribió el código le discutió al revisor. Que haya discutido es un dato y se guarda.
execution_gap
Había que ejecutar algo para saber y no se pudo. Exige declarar por qué: entorno no reproducible, sin ambiente de un tercero, u otro.
reviewer_correlation
El revisor no era de otra familia. Los errores que pueda compartir con el generador quedaron fuera del alcance de esta ronda, y eso se declara (R11).
reviewer_hardening_gap
El material revisado pudo haberle hablado al revisor antes que la consigna. Si el endurecimiento no está verificado, hay que declararlo (R12).
Ojo con execution_gap: en el corpus real del propio repo es, de lejos, la clase más común: 24 de los 31 ítems. O sea que lo que más queda sin cerrar no es que el modelo se equivoque, sino que un paso de verificación no se pudo correr y el pipeline siguió igual.
Las catorce reglas, en castellano
Sección titulada «Las catorce reglas, en castellano»disensor validate chequea dos cosas: que el JSON cumpla el esquema, y después estas catorce reglas, que son coherencias que un esquema no puede expresar.
| regla | lo que no te deja hacer |
|---|---|
| R0 | Cerrar un evento sin un árbitro humano presente. |
| R1 | Declarar un hallazgo escalado o refutado sin el ítem de residuo que lo nombre, o con la clase equivocada. |
| R2 | Dejar marcadores genéricos de plantilla en los campos de texto. Si no lo llenaste, no valida. |
| R3 | Usar el camino abreviado en un cambio que toca uno de los cinco casos protegidos. |
| R4 | Declarar cross_family cuando el revisor comparte familia con el generador. Y si bajaste de cross_family, hay que decir por qué. |
| R5 | En nivel A, dejar un gap de ejecución sin la aceptación escrita de un responsable. Bloquea el merge. |
| R6 | Declarar contadores que no coinciden con la lista de hallazgos. |
| R7 | En el gate de diff, marcar un hallazgo como incorporado sin verificación del arreglo. |
| R8 | Refutar por criterio y decir que no requiere atención humana. |
| R9 | Dejar texto libre en los campos que el perfil minimizado sí alcanza. Ojo: angosta el canal de filtración, no lo cierra. Los campos que la regla no toca siguen admitiendo prosa. |
| R10 | Omitir la lista de hallazgos en el perfil completo. Cero hallazgos es un resultado válido, pero hay que declararlo con la lista vacía. |
| R11 | Declarar independencia menor a cross_family sin un ítem de reviewer_correlation que lo nombre. |
| R12 | Declarar endurecimiento no verificado sin un ítem de reviewer_hardening_gap. |
| R13 | Repetir un identificador local, o dejar una referencia apuntando a algo que no existe. |
El patrón de todas: ninguna te pide que hagas mejor tu trabajo. Te piden que no afirmes más de lo que hiciste. Si revisaste con un modelo de la misma familia, podés, pero lo declarás y queda anotado como residuo.
Qué hace fallar el CI
Sección titulada «Qué hace fallar el CI»El validador mira un archivo. El gate mira el pull request completo: qué archivos toca, qué declaración agrega, y si esa declaración de verdad cubre este cambio. Son nueve chequeos.
| chequeo | cuándo falla |
|---|---|
| G1 | El PR toca rutas que piden revisión y no agrega ninguna declaración válida. |
| G2 | El nivel del artefacto no es el que declara el repositorio. |
| G3 | Se usa nivel A en un repo donde la gobernanza de datos no está validada. |
| G4 | El confinamiento del revisor no es de los que admite el nivel A. |
| G5 | El commit que la declaración dice haber revisado no existe en este repositorio. |
| G6 | Un archivo cambió después de la revisión que afirma cubrirlo. |
| G7 | Los archivos tocados no admiten un gate en común, así que ninguna declaración sola puede cubrirlos. |
| G8 | Se modificó, borró o renombró evidencia que ya estaba en la base del PR. |
| G9 | La declaración dice una versión de esquema distinta de la vigente. |
El más interesante es G8. Impide que un PR borre el registro de una revisión anterior. Sin eso, la forma más fácil de pasar el gate sería hacer desaparecer lo que decía que algo quedó abierto.
A, B y C: cuánta ceremonia pide cada cambio
Sección titulada «A, B y C: cuánta ceremonia pide cada cambio»No todo cambio merece lo mismo. El nivel se declara en la config del repositorio y viaja con el código, no con la intención original.
A el más estricto
Exige que el aislamiento del revisor esté garantizado por permisos o sandbox, no alcanza con pedírselo. No admite independencia menor a cross_family, y un gap de ejecución bloquea el merge hasta que un responsable lo acepte por escrito. Hay que habilitarlo explícitamente.
B · el que se usa
Admite read_only_by_instruction con advertencia. Es el nivel de las 35 declaraciones del corpus del propio repo.
C · el liviano
Para cambios de bajo impacto donde correr una ronda cuesta más de lo que aporta.
Un dato honesto: en el corpus real del repo de disensor, el nivel A no se usó ni una vez. No fue una decisión: el confinamiento real de esas corridas no llega al requisito. El gate deja esa brecha a la vista en lugar de esconderla, que es la parte que vale.
Los comandos, en el orden en que se usan
Sección titulada «Los comandos, en el orden en que se usan»pip install disensor
Una vez por máquina. Una sola dependencia: jsonschema.disensor init
Una vez por repositorio. Escribe la config, el workflow de CI y, si usás Claude Code, una skill y una instrucción, para que tu asistente escriba el registro al cerrar cada ronda. Con otro agente,disensor guideentrega la misma guía.disensor reviewer suggest→reviewer add
Registra qué revisores puede correr esta máquina. Sin esto, no hay ronda.disensor round --gate diff
Arma el paquete, corre el revisor, captura el informe y ancla el resultado. Es el camino automático.disensor validate .residue/*.json
Chequea el archivo contra el esquema y las catorce reglas antes de commitear.disensor gate
Lo que corre la GitHub Action. En tu máquina dice exactamente lo mismo que diría en CI, así que podés probarlo sin tocar tu pipeline.
El gate no corre ningún modelo y no pide claves de API. Solo lee lo que ya está en el repositorio. El que corre un modelo es round, y elegís cuál.
Lo que no hace, dicho de frente
Sección titulada «Lo que no hace, dicho de frente»No detecta una declaración falsa. El validador encuentra el campo vacío y la frase genérica, porque son verificables contra una estructura. Si alguien declara que revisó y no revisó, o inventa un hallazgo, el gate lo acepta. Detectarlo exigiría saber la respuesta correcta, que es justamente lo que estás delegando.
No mejora la revisión. No hace que el modelo encuentre más cosas ni mejores. Registra lo que la revisión que ya hacés dejó abierto.
No es una auditoría ni una certificación. No está certificado para nada y no sabe qué exige ninguna regulación. Lo que produce es un registro que sobrevive a la revisión.
Y no sirve solo. Si no hay una revisión con un modelo aparte, declarar es papeleo.
Por qué esto está en la documentación y no escondido: alguien tiene que abrir PR ya mergeados al azar y leerlos, y eso no lo reemplaza ninguna regla. Lo que el gate saca del camino es el chequeo mecánico de que los campos estén completos, para que esa lectura humana se gaste en lo que ninguna regla puede mirar.
Escrito sobre disensor 0.9.4 y el esquema residue/v0.4, verificando cada regla, cada chequeo del gate y cada enumeración contra el código de origin/main el 29/08/2026. Las cifras del corpus (35 declaraciones, 31 ítems de residuo, 24 de ellos execution_gap) salen de contar .residue/ del propio repositorio ese día, y suben con cada evento nuevo: son una foto con fecha, no un total. El método está publicado con DOI 10.5281/zenodo.21633495 y el código es MIT.