Skip to content

What disensor does, explained slowly

Your AI wrote the code. Another AI reviewed it. What happened in that conversation is lost when you close the window. disensor is what keeps it from being lost, and what stops the merge if the record is not there.

01 · the problem

When a pull request has the green check, all you know is that something ran and did not fail. You do not know what was looked at, whether anyone argued about anything, or, above all, what was left unresolved.

With review done by an AI it is worse, because all of it happens inside a chat window. The model found seven things, you incorporated four, argued about two, and the seventh stayed open because nobody could prove it either way. You close the window and that information stops existing. Six months later, when the bug shows up, nobody can say whether it was known.

In plain terms: disensor does not review your code. What it does is force a written record of what happened in the review, in a file that lives in your repo, and stop the merge if that file is missing or wrong.

02 · the idea

Two models that are not relatives, and a human who answers for it

Section titled “Two models that are not relatives, and a human who answers for it”

The method is called controlled disagreement and it has four steps. The important part up front: the gate does not run that cycle. The cycle happens inside your workflow with whatever models you use, and since 0.9 disensor round can execute the attack step for you. What disensor defines and later checks is the file the cycle ends with.

THE REVIEW EVENT · happens in your workflowGeneratefamily AAttackanother familydisensor round executes itVerifyfamily ADeclarea JSON filethe plan or the diffthe findingswhat happened to each oneHuman arbiter: present in the whole event, or the artifact does not validate (R0)The cycle closes when every finding reached a terminal state.Never by consensus between the two models..residue/<id>.jsonversioned in the repothe gatevalidates the fileand blocks the merge
The gate is outside the dashed box: it does not generate, does not attack and does not verify. It defines the format of the file the cycle ends with, validates it against a schema and a set of rules, and fails CI if it is missing or if it lies about something checkable. The disensor round command does enter the cycle: since 0.9 it can run the reviewer of step 02 and anchor its report to the commits reviewed, though it neither reads nor judges it.

Why another family: asking a model to review its own output is not an independent control. cross_family is the operational approximation the method uses to look for decorrelated errors, not a guarantee that two different families fail in different places: that comparison was never measured. Which is why R4 does not demand a particular family. It demands that the independence be declared, that it match the families you declared, and that if it was lower you say why.

03 · the artifact

Do I have to write this myself? No. disensor init leaves in your repo the instruction and the skill that teach your agent to close the round. The same model that wrote the code is the one that emits the declaration, and validates it before committing. If your agent is not Claude Code, disensor guide hands it the same instructions. You keep working the way you already work. Everything below is what gets recorded, not a form to fill in.

Every review event produces a JSON file in .residue/. It has five blocks and none of them is decorative. What follows is a simplified representation for reading at a glance, not a valid artifact: the values are trimmed and the arrows are annotations. There is a complete, valid one in spec/examples/example_2_diff_gate.json.

{
"schema": "residue/v0.4", // ← which contract this file meets
"profile": "full",
"event": { // ← what was reviewed, and over which code
"repository": "", "pr": "",
"base_commit": "c0ffee1", "head_commit": "beef042",
"gate": "diff", plan · diff · architecture
"criticality_level": "B"
},
"actors": { // ← who reviewed, and how independent they were
"generator": { "family": "anthropic", "model": "claude-code" },
"reviewers": [{
"family": "openai", "model": "gpt-5.4",
"independence": "cross_family",
"prompt_hash": "sha256:1c2d3e4f…", the exact brief that was used
"confinement": { "mode": "read_only_by_instruction" }
}],
"human_arbiter": { "present": true }
},
"findings": [ ], // ← each finding and how it ended
"residue": { "items": [ ] } // ← what the cycle did not close by itself
}

The block that counts is the last one. A coverage report tells you what was reviewed. This one tells you where residue was left, even after the cycle closed: residue is wider than “what stayed open”. A finding the generator refuted with evidence is closed and enters anyway, because the refutation is the principal’s over the reviewer’s and someone has to be able to audit it.

The prompt_hash deserves its own mention: it is the sha256 of the adversarial brief the declaration says it used. It lets you check whether a brief you have is exactly that one, byte for byte. It does not prove that brief was the one sent to the model: that rests on the declaration telling the truth, like everything else. What it adds is that the brief stops being a description and becomes something you can compare.

04 · the findings

A finding can end in six places, and three of them force a residue item

Section titled “A finding can end in six places, and three of them force a residue item”
a findingfinal_stateCLOSES WITHOUT RESIDUEREQUIRES A RESIDUE ITEM (R1)incorporateddebt_recordedowner_decisionescalated_openrefuted_verifiablerefuted_interpretiveescalation_without_decisionprincipal_refutationand the class has to match the state, or R1 fails
The three states at the bottom cannot be declared on their own: they require a residue item that names them, and of the right class. That is what stops you from closing an event saying “it stayed open” without recording what stayed open.
statewhat it means, in plain terms
incorporatedThe finding was right and you fixed it. In the diff gate it also has to come with its fix verified (R7).
debt_recordedIt is real, you are not fixing it now, and it is recorded as debt with an identifier.
owner_decisionIt is not a technical defect. It is a product decision and a person made it.
refuted_verifiableYou pushed back on the reviewer and you have the evidence: the code or the execution shows it was wrong.
refuted_interpretiveYou pushed back on judgement, with no hard proof. That is why R8 forces you to mark it as something a human has to look at.
escalated_openNobody closed it. It goes on your name, open and owned.
05 · the residue

The residue is the list of what, even after the cycle has closed, still depends on judgement, incomplete evidence or a limitation of the process. There are five classes. The first three are the method’s, as section 6 of the protocol defines them. The last two were added by residue/v0.4 for the degraded modes: they exist because the schema allows running with less independence than the ideal, and forces you to declare it when that happens.

escalation_without_decision

It was escalated to a human and the human has not decided yet. It stays open and owned.

principal_refutation

Whoever wrote the code pushed back on the reviewer. That there was pushback is itself data, and it is kept.

execution_gap

Something had to be executed to know, and it could not be. It requires declaring why: environment not reproducible, no third-party test environment, or other.

reviewer_correlation

The reviewer was not from another family. Whatever errors it may share with the generator fell outside the reach of this round, and that gets declared (R11).

reviewer_hardening_gap

The material under review may have addressed the reviewer before your brief did. If the hardening is not verified, it has to be declared (R12).

Watch execution_gap: in the real corpus of the repo itself it is by far the most common class: 24 of the 31 items. What most often stays unclosed is not the model being wrong. It is that a verification step could not be run and the pipeline moved on anyway.

06 · the validator

disensor validate checks two things: that the JSON meets the schema, and then these fourteen rules, which are coherences a schema cannot express.

rulewhat it stops you from doing
R0Closing an event with no human arbiter present.
R1Declaring an escalated or refuted finding without the residue item that names it, or with the wrong class.
R2Leaving generic template markers in the text fields. If you did not fill it in, it does not validate.
R3Using the abbreviated path on a change that touches one of the five protected cases.
R4Declaring cross_family when the reviewer shares a family with the generator. And if you went below cross_family, you have to say why.
R5At level A, leaving an execution gap without a lead accepting it in writing. It blocks the merge.
R6Declaring counts that do not match the findings list.
R7In the diff gate, marking a finding as incorporated without verification of the fix.
R8Refuting on judgement and saying it needs no human attention.
R9Leaving free text in the fields the minimized profile does reach. Note: it narrows the leak channel, it does not close it. The fields the rule does not reach still admit prose.
R10Omitting the findings list in the full profile. Zero findings is a valid result, but you declare it with the empty list.
R11Declaring independence below cross_family without a reviewer_correlation item that names it.
R12Declaring unverified hardening without a reviewer_hardening_gap item.
R13Repeating a local identifier, or leaving a reference pointing at something that does not exist.

The pattern in all of them: none asks you to do your job better. They ask you not to claim more than you did. If you reviewed with a model of the same family, you can, but you declare it and it is recorded as residue.

07 · the gate

The validator looks at one file. The gate looks at the whole pull request: which files it touches, which declaration it adds, and whether that declaration really covers this change. There are nine checks.

a PRdoes it touch pathsthat need review?per your policynopassesyesis there a validdeclaration coveringthis code?nothe merge is blockedyes“covering” is literal: if the file changed after the review that claims to cover it, check G6 rejects it,and if the PR deletes evidence that was already in the base, G8 rejects it.
The gate does not judge whether the code is good. It rejects the change that arrives with no record, or with a record that does not correspond to that code.
checkwhen it fails
G1The PR touches paths that need review and adds no valid declaration.
G2The artifact level is not the one the repository declares.
G3Level A is used in a repo where data governance is not validated.
G4The reviewer confinement is not one that level A admits.
G5The commit the declaration claims to have reviewed does not exist in this repository.
G6A file changed after the review that claims to cover it.
G7The touched files admit no gate in common, so no single declaration can cover them.
G8Evidence that was already in the PR base was modified, deleted or renamed.
G9The declaration states a schema version other than the current one.

The most interesting one is G8. It stops a PR from deleting the record of an earlier review. Without it, the easiest way to pass the gate would be to make the thing that said something stayed open disappear.

08 · the levels

A, B and C: how much ceremony each change asks for

Section titled “A, B and C: how much ceremony each change asks for”

Not every change deserves the same. The level is declared in the repository config and travels with the code, not with the original intent.

A the strictest

It requires the reviewer isolation to be guaranteed by permissions or a sandbox; asking for it is not enough. It admits no independence below cross_family, and an execution gap blocks the merge until a lead accepts it in writing. It has to be enabled explicitly.

B · the one in use

It admits read_only_by_instruction with a warning. It is the level of all 35 declarations in the repo’s own corpus.

C · the light one

For low-impact changes where running a round costs more than it gives back.

An honest data point: in the real corpus of the disensor repo, level A was never used, not once. It was not a decision: the actual confinement of those runs does not meet the requirement. The gate leaves that gap in plain sight instead of hiding it, which is the part that counts.

09 · in practice
  1. pip install disensor
    Once per machine. A single dependency: jsonschema.
  2. disensor init
    Once per repository. It writes the config, the CI workflow and, if you use Claude Code, a skill and an instruction, so your assistant writes the record as each round closes. With another agent, disensor guide hands over the same guide.
  3. disensor reviewer suggestreviewer add
    Registers which reviewers this machine can run. Without this, there is no round.
  4. disensor round --gate diff
    Builds the pack, runs the reviewer, captures the report and anchors the result. This is the automatic path.
  5. disensor validate .residue/*.json
    Checks the file against the schema and the fourteen rules before you commit.
  6. disensor gate
    What the GitHub Action runs. On your machine it says exactly what it would say in CI, so you can try it without touching your pipeline.

The gate runs no model and asks for no API keys. It only reads what is already in the repository. The one that runs a model is round, and you pick which.

10 · the limits

It does not detect a false declaration. The validator finds the empty field and the generic phrase, because those are checkable against a structure. If someone declares they reviewed and did not, or invents a finding, the gate accepts it. Detecting that would require knowing the right answer, which is exactly what you are delegating.

It does not improve the review. It does not make the model find more things or better ones. It records what the review you already do left open.

It is not an audit or a certification. It is not certified for anything and it does not know what any regulation requires. What it produces is a record that outlives the review.

And it is no use on its own. With no review by a separate model, declaring is paperwork.

Why this is in the documentation and not hidden: someone has to open merged pull requests at random and read them, and no rule replaces that. What the gate takes off the table is the mechanical check that the fields are complete, so that human reading is spent on what no rule can look at.

Written against disensor 0.9.4 and the residue/v0.4 schema, checking every rule, every gate check and every enumeration against the code in origin/main on 2026-08-29. The corpus figures (35 declarations, 31 residue items, 24 of them execution_gap) come from counting .residue/ in the repository itself that day, and they grow with every new event: a dated snapshot, not a total. The method is published with DOI 10.5281/zenodo.21633495 and the code is MIT.