Skip to content

Adversarial Code Review: Workflow, Evidence and Limits

Adversarial code review is the practice of having one party produce a change and a different, deliberately independent party attack it: look for what is wrong, missing or unproven, and say so with evidence. With AI assistants the independent party is usually a model from another family than the one that wrote the code. People also call it cross-model review, second-opinion review or maker-checker; the practice is the same.

This page explains the practice on its own terms. It is useful whether or not you ever install anything. Disensor appears at the end, where the practice leaves a problem it does not solve by itself.

A model that reviews its own output shares that output’s blind spots. The assumptions it did not question while writing are the ones it does not question while re-reading, so asking it to self-critique produces self-critique, not review. The fix is not a smarter reviewer. It is a reviewer that fails somewhere else: a different training, different habits, different mistakes. That is why the family of the reviewing model matters more than its raw capability.

The separation also changes the incentive. The reviewer is not asked whether the change is acceptable. It is asked to try to break it, to cite where, and to rank what it found. It has no stake in the change going through, and the brief should say so. Adversarial does not mean hostile; it means the burden of proof sits on the change.

The example below is a real, anonymised review event; it ships with disensor as a sample file, but the practice is the same with any tooling.

  1. Freeze what is under review. A plan, a diff or a design decision. In the example, a diff that adds an export flow to a service.
  2. Hand it to the reviewer with a brief. The brief says what to attack (correctness, error paths, missing tests, undefined behaviour, security) and what evidence to bring back: file and line, not impressions. Keep the brief versioned, so that later you can tell what was actually asked.
  3. The reviewer returns findings. Three, in the example: the write stream is not released if the query fails midway; there is no defined limit for exports that exceed the allowed range; the export does not validate the date range it receives.
  4. The generator verifies each one. Against the repository, by running something, or against a specification. Not on the reviewer’s word: a reviewer from another family is decorrelated, not right.
  5. Each finding ends in a terminal state. Fixed and verified; recorded as debt with an owner; an explicit owner decision; refuted with evidence; refuted by judgement; or escalated to a human. In the example, the stream leak was fixed and covered by a specific test; the date validation was refuted with a link to the shared query object that already validates the range; the export limit was escalated to product, because it is not a technical decision.
  6. The cycle closes when every finding has a state. Agreement between the two models is not the closing condition, and neither is silence.

Treat every finding as a claim to be checked, whichever model made it.

  • Locate it. Read the code path the finding names. If it names none, ask for one before doing anything else.
  • Reproduce it when you can. A failing test or a run that shows the behaviour beats an argument. When you cannot run it, say so: that is part of the outcome, not a detail to skip.
  • Name what you verified against. The repository, an execution, or an external source such as a specification or an advisory. The distinction matters later, when someone asks how much to trust the closure.
  • Fixing is not closing. A finding is closed when the fix has been verified, with a test or a second pass, not when the patch has been applied.
  • Do not confuse agreement with evidence. Two models converging on a verdict is not confirmation. Recent work on multi-agent code review reports exactly that failure, agents agreeing without sufficient evidence, and treats it as structural (arXiv:2608.18167). The round is built on disagreement grounded in code; consensus is not its output.

A reviewer from another family produces false positives. That is the price of decorrelation, and it is fine as long as they are handled in the open.

  • Refute with evidence. Cite the code, test or document that contradicts the finding, and keep the citation. A refutation that says only “this is not a problem” is an opinion against an opinion.
  • Separate verifiable from interpretive. Some refutations rest on judgement rather than proof: a risk considered acceptable, a convention the reviewer did not know. Those need a human to look at them, precisely because no evidence settles them.
  • Do not drop it silently. A dropped finding is invisible. A refuted finding, with its evidence, is something a third person can audit later and disagree with.
  • Fix the finding, not the remedy. Reviewers propose patches, and the patch can be wrong while the finding is right. Verify the problem first, then choose the fix.

Adversarial review is a control, and every control has an edge.

  • It does not find what neither model knows. Two decorrelated blind spots are fewer than one, not zero.
  • The material under review can address the reviewer. A repository can carry instructions aimed at whatever model reads it; unless the reviewer runs through something that neutralises that, assume it may have been steered.
  • It cannot run what it cannot run. In practice the most common leftover is not the reviewer being wrong; it is a verification that could not be executed, and the pipeline moving on anyway.
  • Some decisions are not technical. A row limit, an accepted risk, a scope change: those belong to a person, and the round should hand them over rather than resolve them by fiat.
  • It costs a round. For a trivial change the attack is wasted computation; for a change that touches what cannot be undone, it is the cheapest moment to be wrong.

Everything above happens inside a conversation. The model found seven things; you incorporated four, refuted two and escalated one that nobody could test. Then the window closes and that information no longer exists. Six months later, when the bug shows up, nobody can say whether it was known.

A green check does not fill that gap. It says something ran and did not fail. It does not say what was attacked, what was refuted and with what, or what is still resting on someone’s judgement.

A useful record of a round contains: who reviewed and how independent they were; the brief that was used; each finding with its terminal state and the evidence behind it; and, above all, the residue: what the round could not close by itself, listed by name. Residue rather than coverage, because a list of what was covered reads as a seal of quality, and a seal of quality is an invitation to stop looking.

That record is what disensor defines and enforces. It is a JSON file versioned in your repository, next to the code it judges, validated against a schema and a set of rules, and checked as a gate on the pull request. The gate runs no model and holds no API key: it reads what is already in the repository. The optional disensor round command can run the reviewer step for you, with a reviewer installed on your machine, and never judges what it returns. If the practice on this page is what you do, or want to do, the next pages explain what the file looks like and what CI blocks, the method behind it, and the exact contract.