Generate
the plan or the diff
A model produces the plan or the diff to be merged.
Versioned evidence for adversarial AI code review
What happened in that review ends up as a JSON file in your repo, next to the code it judges. Not a green badge, not a coverage score: which findings were verified, which were refuted, what could not be executed, and what still rests on someone's judgement. disensor is the CLI and the GitHub Action that check it against the schema, its rules, and the change the pull request actually makes.
First time here? Watch a round from start to finish, without installing anything
Install
pip install disensordisensor guide.disensor round executes the reviewer installed on your machine. Either way you need a human arbiter; without one the artifact does not validate.init --no-workflow writes everything but the workflow, and the gate runs the same on your machine. If you have never seen it, how it works, explained slowly.§ 01
That file is called a residue declaration. Residue is what the review writes down so a person knows where to look: the findings left undecided, the ones the generator refuted, even with evidence, what running the code could not settle, and the limits of the reviewer itself. The declaration lists that instead of coverage, and the method page explains why it reads nothing like a green badge.
Guide to adversarial code review What controlled disagreement is
§ 02
Within one event the cycle is linear: the reviewer attacks once. disensor stays out of that cycle. It defines the artifact the cycle ends with, and checks it.
the plan or the diff
A model produces the plan or the diff to be merged.
one or more reviewers (R4)
A model from another family reviews it with an adversarial brief. Two models of the same lineage fail in the same places, which is why the family has to differ.
against the repository or the execution
The generator checks every finding against the repository or the execution and drives it to a terminal state: incorporated, debt recorded, owner decision, refuted or escalated.
closes the cycle, not the problem
The cycle closes when every finding reached a terminal state. Whatever did not close stays written, versioned with the commit. Your assistant writes that file, not you.
escalated_open, refuted_interpretive
Required in every event. Without one the declaration does not comply with the protocol.
§ 03
A JSON file versioned in the repository, next to the code it judges. It records the actors, their family and how independent the reviewer actually was, every finding with its terminal state, and the event’s metrics. A round that could not reach another model family is recorded as the degraded mode it is, and carries its own residue item.
Read final_state first: the terminal state of each finding, one of six. In this real event, the 19 findings all reached one, and the round still left 4 residue items. The excerpt shows two findings from one reviewer run: one incorporated, with the test that verified the fix, and one refuted with evidence, which closes it and still goes into the residue so the arbiter can refuse to take it at face value.
The gate runs no model and holds no model API key: it validates a file that is already in the repository. Running the review is a separate step, and that one does hand your diff to a model from another family; since 0.9 disensor round orchestrates it with the reviewer installed on your machine.
| total_findings | 19 |
|---|---|
| incorporated | 18 |
| debt_recorded | 0 |
| owner_decision | 0 |
| refuted_verifiable | 1 |
| refuted_interpretive | 0 |
| escalated_open | 0 |
| residue.items | 4 |
{
"schema": "residue/v0.4",
"event": {
"event_id": "27089d19-ee91-4c6c-b517-63b9627969e0",
"created_at": "2026-09-29T10:21:11-03:00",
"repository": "github.com/NicolasRocchia/disensor",
"gate": "diff",
"criticality_level": "B",
"base_commit": "b340cfc30fc418957f2dfafc82c1e14bf8e87884",
"head_commit": "c2970915df2501b5eb214e3272174bb03638f610"
},
"actors": {
"generator": {
"family": "anthropic",
"model": "claude-fable-5-1"
},
"reviewers": [
{
"reviewer_id": "r8",
"family": "openai",
"model": "gpt-5.6-sol",
"independence": "cross_family",
"confinement": {
"mode": "read_only_by_instruction",
"verified": true,
"verification_method": "clean_git_status"
},
"hardening": "verified",
"prompt_hash": "sha256:3494df5ebd20cc793ff9c5a214a2e715758e69c41bbc9e09b70f7d3064ee6a1c"
}
],
"human_arbiter": {
"present": true
}
},
"findings": [
{
"id": "h15",
"origin": "r8",
"severity": "major",
"title": "Una declaracion que el gate rechaza por sus propias comprobaciones daba por cubierto el merge",
"description": "El recorrido validaba el esquema y aplicaba G5, G6 y G7, pero no las comprobaciones por artefacto del gate: una declaracion de nivel C en un repositorio de nivel B (G2), un id no canonico o un archivo con otro nombre (G8), o un nivel A sin confinamiento admitido (G4) cubrian igual. Las comprobaciones del gate se extrajeron a gate.judge_artifact, que _run_gate y el recorrido comparten; el recorrido no aplica G9 porque una version anterior del esquema era la vigente cuando un merge viejo ocurrio, y lo dice. Las declaraciones que el PR agrego se juzgan como en el gate, y una mutacion de evidencia o un historico ilegible tambien lo dejan sin cobertura, como al PR.",
"location": "src/disensor/gate.py (judge_artifact); src/disensor/branch.py (_judge_merge, _historical_ids)",
"final_state": "incorporated",
"verification": {
"against": "repository",
"detail": "Comparadas las comprobaciones inline de _run_gate (G9, G8, G2, G3, G4, G5) con lo que aplicaba _judge_merge; la extraccion conserva el orden y los textos, y la suite del gate sigue en verde."
},
"fix_verification": {
"type": "specific_test",
"reference": "tests/test_branch.py::test_a_declaration_the_gate_rejects_on_its_own_checks_covers_nothing, que corre run_gate sobre el mismo PR y afirma que devuelve 1"
}
},
{
"id": "h16",
"origin": "r8",
"severity": "info",
"title": "La resolucion de un merge con conflictos no entra en la cobertura",
"description": "Hipotesis del revisor: el recorrido evalua el rango desde el merge base hasta el segundo padre y no el arbol del commit de merge, asi que codigo introducido al resolver un conflicto puede diferir del head revisado y el merge figurar cubierto.",
"location": "src/disensor/branch.py (_judge_merge); src/disensor/gate.py (resolve_context, evaluate_coverage)",
"final_state": "refuted_verifiable",
"verification": {
"against": "repository",
"detail": "Es el contrato del gate, no del panel: resolve_context toma head_oid como head del PR y evaluate_coverage y check_range_membership juzgan ese head; el gate nunca ve el arbol del merge que la plataforma crea al integrar. El panel replica exactamente lo que el gate juzgo. Lo que el revisor senala es un limite del gate que existe desde G6, y no cambia con este PR."
},
"evidence": {
"text": "gate.py, resolve_context: head_oid = gitctx.resolve_commit(head, repo_dir); status = gitctx.changed_status(mb, head_oid, repo_dir). El rango juzgado es merge base a head del PR."
}
}
],
"residue": {
"items": [
{
"id": "r4",
"class": "principal_refutation",
"finding_ref": "h16",
"refutation_type": "verifiable",
"requires_human_attention": false,
"description": "El revisor sugirio que la cobertura mirara el arbol del commit de merge y no el head del PR; se refuto con el codigo del gate, que juzga el head. El limite que senala (lo que se escribe al resolver un conflicto no pasa por ninguna revision) es del gate, no de este panel, y queda anotado."
}
]
},
"metrics": {
"counts": {
"total_findings": 19,
"valid": {
"incorporated": 18,
"debt_recorded": 0,
"owner_decision": 0
},
"false_positives": {
"refuted_verifiable": 1,
"refuted_interpretive": 0
},
"escalated_open": 0
}
}
}§ 04
A GitHub Action that validates the declarations the pull request adds, applies the repository’s scope policy and posts the result as a comment. When it goes green it also writes one HTML report of every declaration in the repository, what stayed open first, with no network: the document is the last thing of an event.
§ 05
The validator detects the empty field and the generic marker. It does not detect the false declaration, and no tool can. Someone has to open merged pull requests at random and read them.
It also does not protect against a workflow that was modified, skipped or replaced. That part is on the platform: strict required checks, CODEOWNERS, and pinning the Action by SHA.
What the artifact demands and what it cannot yet check changes with each schema version. The known limits are declared in the reference.
The gate runs the same on your machine and says exactly what it would say in CI. Only when you want it to enforce do you write the workflow.
pip install disensor && disensor init --no-workflow