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.
A green check says nothing
Section titled “A green check says nothing”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.
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.
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.
The file that stays, from the inside
Section titled “The file that stays, from the inside”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.
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”| state | what it means, in plain terms |
|---|---|
| incorporated | The finding was right and you fixed it. In the diff gate it also has to come with its fix verified (R7). |
| debt_recorded | It is real, you are not fixing it now, and it is recorded as debt with an identifier. |
| owner_decision | It is not a technical defect. It is a product decision and a person made it. |
| refuted_verifiable | You pushed back on the reviewer and you have the evidence: the code or the execution shows it was wrong. |
| refuted_interpretive | You 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_open | Nobody closed it. It goes on your name, open and owned. |
The five classes of residue
Section titled “The five classes of 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.
The fourteen rules, in plain English
Section titled “The fourteen rules, in plain English”disensor validate checks two things: that the JSON meets the schema, and then these fourteen rules, which are coherences a schema cannot express.
| rule | what it stops you from doing |
|---|---|
| R0 | Closing an event with no human arbiter present. |
| R1 | Declaring an escalated or refuted finding without the residue item that names it, or with the wrong class. |
| R2 | Leaving generic template markers in the text fields. If you did not fill it in, it does not validate. |
| R3 | Using the abbreviated path on a change that touches one of the five protected cases. |
| R4 | Declaring cross_family when the reviewer shares a family with the generator. And if you went below cross_family, you have to say why. |
| R5 | At level A, leaving an execution gap without a lead accepting it in writing. It blocks the merge. |
| R6 | Declaring counts that do not match the findings list. |
| R7 | In the diff gate, marking a finding as incorporated without verification of the fix. |
| R8 | Refuting on judgement and saying it needs no human attention. |
| R9 | Leaving 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. |
| R10 | Omitting the findings list in the full profile. Zero findings is a valid result, but you declare it with the empty list. |
| R11 | Declaring independence below cross_family without a reviewer_correlation item that names it. |
| R12 | Declaring unverified hardening without a reviewer_hardening_gap item. |
| R13 | Repeating 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.
What makes CI fail
Section titled “What makes CI fail”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.
| check | when it fails |
|---|---|
| G1 | The PR touches paths that need review and adds no valid declaration. |
| G2 | The artifact level is not the one the repository declares. |
| G3 | Level A is used in a repo where data governance is not validated. |
| G4 | The reviewer confinement is not one that level A admits. |
| G5 | The commit the declaration claims to have reviewed does not exist in this repository. |
| G6 | A file changed after the review that claims to cover it. |
| G7 | The touched files admit no gate in common, so no single declaration can cover them. |
| G8 | Evidence that was already in the PR base was modified, deleted or renamed. |
| G9 | The 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.
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.
The commands, in the order you use them
Section titled “The commands, in the order you use them”pip install disensor
Once per machine. A single dependency: jsonschema.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 guidehands over the same guide.disensor reviewer suggest→reviewer add
Registers which reviewers this machine can run. Without this, there is no round.disensor round --gate diff
Builds the pack, runs the reviewer, captures the report and anchors the result. This is the automatic path.disensor validate .residue/*.json
Checks the file against the schema and the fourteen rules before you commit.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.
What it does not do, said up front
Section titled “What it does not do, said up front”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.