Skip to content
disensor.devv0.14.0

Versioned evidence for adversarial AI code review

Your AI wrote the code. Another AI reviewed it.

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 disensor
  • init writes five things into your repo. A config file, a CI workflow, a .gitignore line for the report and, for Claude Code, a skill and an instruction, so the record gets written as each round closes. Any other agent gets the same guide from disensor guide.
  • The gate runs no model and holds no API key. It validates what is already in the repository. The round does run one: disensor round executes the reviewer installed on your machine. Either way you need a human arbiter; without one the artifact does not validate.
  • Try it without touching your CI. 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

Controlled disagreement

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.

01family A

Generate

the plan or the diff

A model produces the plan or the diff to be merged.

02another family

Attack

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.

03family A

Verify

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.

04

Declare

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

Human arbiter

Required in every event. Without one the declaration does not comply with the protocol.

R0
fix_verification: pending_in_diff_gateIf what was reviewed was the plan, the fix does not exist yet and there is nowhere to verify it. It stays pending for the diff event, which has its own declaration.

Watch a round run, on an example

§ 03

The artifact

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.

The counts add up. The residue still has 4 items.
total_findings19
incorporated18
debt_recorded0
owner_decision0
refuted_verifiable1
refuted_interpretive0
escalated_open0
residue.items4
.residue/27089d19-ee91-4c6c-b517-63b9627969e0.jsonv0.14.0
{
  "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
    }
  }
}
Excerpt of a real event in the tool’s own repository, the declaration of its pull request #101: every value shown is the file’s own, in its order, and the build checks it. The keys and the enumerated values are the contract; the prose inside is in Spanish, the language its authors write in. See the whole artifact

§ 04

What the gate enforces

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.

It fails closed
If the gate cannot resolve the pull request range, it does not go green: it could not read the change, so it has nothing to approve.
Evidence is append-only
A pull request cannot modify, delete or rename declarations that were already there, nor reuse an existing event identifier.
A stale declaration covers nothing
If the path changed after the reviewed commit, the declaration stops covering it. Reviewing and then writing more does not count.
Everything comes from git
Scope is derived from the objects in the reviewed range, never from the working tree: reading from disk would classify one tree and validate another.

§ 05

What it does not catch

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.

Try it without touching your CI

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