Metadata-Version: 2.4
Name: contrarian
Version: 0.5.0
Summary: MCP server adding an independent second LLM perspective (Judge B) with full project graph context
Author-email: Anthony Micho <anthony.micho76@gmail.com>
License-Expression: AGPL-3.0-or-later
Project-URL: Repository, https://github.com/anthonymicho/contrarian
Keywords: mcp,llm,code-review,claude,judge
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp
Requires-Dist: openai>=1.0
Requires-Dist: python-dotenv
Requires-Dist: networkx>=3.4
Requires-Dist: graphifyy[leiden]
Requires-Dist: json-repair>=0.6
Provides-Extra: all
Requires-Dist: graphifyy[all]; extra == "all"
Dynamic: license-file

# Contrarian

MCP server that adds a second LLM perspective (Judge B) directly inside Claude Code.

Claude Code is Judge A — it already has full project context. Contrarian adds Judge B, an independent model from a different lab, via a single tool call. Same rubric, different priors, no context switch.

---

## How it works

1. Claude Code invokes `contrarian_review()` — no args for auto-detect, or pass specific files.
2. Contrarian resolves the input: git diff, last commit, or full audit (in that order).
3. Judge B (external model) reviews against `JUDGE.md` in the project root.
4. Findings are returned inline and appended to `REPORT.md` for the dialogue log.

Auto-detect order:
- **diff** — staged/unstaged changes and untracked files. Two documented gaps. **Deleted** paths are *discovered* but never reviewed: `git diff HEAD --name-only` lists them, then `_resolve_input()` raises `FileNotFoundError` on the existence check and `_build_review_context()` swallows it with a silent `continue` — so a commit that only removes code is reviewed as if it changed nothing. In **detached HEAD** (rebase, bisect, CI checkout) the worktree diff is skipped entirely and you get the last commit instead.
- **last-commit** — clean tree → reviews the last commit
- **audit** — forced with `audit=true`, or nothing else found

---

## Installation

Requires Python 3.10+ and `git` (auto-detect mode uses `git diff`/`git log`; without a git repo, Contrarian falls back to a full audit). You'll need a free API key from [Google AI Studio](https://aistudio.google.com) (no credit card required).

```bash
pip install contrarian
contrarian setup
```

`contrarian setup` prompts for your API key, then writes the MCP server config to `~/.claude/.claude.json` automatically. Restart Claude Code after running it.

That's it.

**Updates:** `pip install --upgrade contrarian`. When a newer version is on PyPI, Contrarian prints a one-line notice on use (checked at most once/day, fail-silent). Opt out with `CONTRARIAN_NO_UPDATE_CHECK=1`.

---

### Manual config (alternative)

If you prefer to configure manually, add to `~/.claude/.claude.json`:

```json
{
  "mcpServers": {
    "contrarian": {
      "type": "stdio",
      "command": "contrarian",
      "args": [],
      "env": {
        "JUDGE_B_API_KEY": "AI...",
        "JUDGE_B_BASE_URL": "https://generativelanguage.googleapis.com/v1beta/openai/"
      }
    }
  }
}
```

Default provider is Gemini (`generativelanguage.googleapis.com`, model `gemini-2.5-pro`). Change `JUDGE_B_BASE_URL` and `JUDGE_B_MODEL` to use any OpenAI-compatible provider (DeepSeek, Groq, Ollama, OpenRouter, etc.).

Anthropic models are blocked at runtime — Judge B must come from a different lab than Judge A.

### Provider switching

Judge B has two kinds of provider:

**API providers** (per-token billing) — any OpenAI-compatible endpoint, selected by `JUDGE_B_BASE_URL` + `JUDGE_B_MODEL` (or `contrarian use <preset>`). `JUDGE_B_API_KEY` required.

**CLI-agent providers** (flat subscription cost) — drive a local agent CLI instead of an API. No API key of Contrarian's own, no per-token cost. Two providers:

- **`cli:codex`** — runs Judge B on your **Codex subscription** via `codex exec`, **agentic**: it reads files beyond the diff and catches cross-file ripple effects the diff-only API path misses. It is **write-denied** (`--sandbox read-only`) — but see the note below: that flag bounds writes, not read scope. Optional model pin: `cli:codex:<model>` (e.g. `cli:codex:gpt-5.2`).

- **`cli:opencode:<provider>/<model>`** — runs Judge B through **opencode**. opencode is a *router, not a lab*, so the model is **mandatory**: `cli:opencode:openrouter/google/gemini-3.6-flash`. It needs whatever credential its chosen provider wants (Contrarian itself still needs no key). Write-class tools **and** out-of-directory reads are denied through an application-level permission list — `read`/`grep`/`glob` are kept, since a blind judge is worthless — and the child is launched in a per-run config sandbox (the whole `OPENCODE_*` namespace stripped, XDG roots redirected) against a version pinned to `[1.18.5, 1.19)`. Knob: `JUDGE_B_OPENCODE_TIMEOUT`.

> **Two limits on `cli:opencode`, stated because they are easy to over-read.** The out-of-directory read deny is **passed, not verified**: the symlink case that would settle it has never produced a measurement, so treat it as a configured intent rather than a proven boundary. And the version pin is **self-reported by the binary**, so a wrapper or a patched build can satisfy it.

> `cli:agy` (Antigravity/Gemini, isolated) was the earlier second CLI provider and was **removed on 2026-07-26** for failing the bar it was measured against. Its replacement did not clear that bar either — it was never held to it. `gemini-3.6-flash` **missed the reference traversal defect twice**, with read/grep/glob access. It occupies the second slot by explicit decision — Google next to OpenAI, so the convergence hint below is not one lab agreeing with itself — not because it was vetted.

```
JUDGE_B_MODEL=cli:codex                        # persistent: all reviews via Codex subscription
contrarian_review({ model: "<api-model>" })    # per-call override
```

**Fan out to several models at once** — one review, N independent opinions, never merged:

```
JUDGE_B_MODEL="cli:codex+<api-model>"                       # persistent multi-model
contrarian_review({ models: ["cli:codex", "<api-model>"] }) # per-call
```

Two constraints on that second slot, both real:

- **It must come from a different lab.** Two slots at one lab make the convergence hint below report one lab agreeing with itself, which is not corroboration. Either kind of provider can fill it: `cli:codex+cli:opencode:openrouter/google/gemini-3.6-flash` is the pairing this project runs by default (OpenAI + Google). Note the cost of a two-CLI fan-out — the calls are **sequential**, so the review takes codex's latency *plus* the router's.
- **An API slot must be served by your configured `JUDGE_B_BASE_URL`.** There is one global endpoint, not one per slot, so `cli:codex+deepseek-chat` fails against the default Gemini URL — the API half is sent to Gemini under a name it does not serve. Set the endpoint and key for the lab you want first (`contrarian use deepseek`, or set `JUDGE_B_BASE_URL`/`JUDGE_B_API_KEY` directly), then name a model that endpoint serves. Heterogeneous API labs in one fan-out work only through an aggregator endpoint (e.g. OpenRouter).

Each model's review is returned as a separate labelled block; you remain the supreme judge (no reconciliation, no auto-merge). One provider failing (timeout, outage, auth error — any reason) shows as a `FAILED` block and doesn't sink the others; if OpenAI is down, the API half of a `cli:codex+<api-model>` fan-out still returns its review. `JUDGE_B_CLI_TIMEOUT` (default 600s) bounds each CLI call. codex reviews at a bounded reasoning effort (`JUDGE_B_CODEX_EFFORT`, default `low`) so an agentic review completes instead of over-exploring; raise it (`medium`/`high`/`xhigh`) for more depth at the cost of latency.

Above the reviews sits a **convergence hint** — a code-only, lexical pass that flags when several judges appear to have raised the same finding independently (`⚠ 2/3 judges:`, every member printed and attributed so you can check the claim). It never merges, ranks, or decides: there is no Judge C.

**Read it asymmetrically, because that is how it measures.** On the archived rounds it caught **3 of 7** real agreements. So:

- **When it fires** — treat it as a pointer, not a verdict. It matched words, not meaning. Every member of a group is printed and attributed precisely so you can confirm by reading; this half is not measured, and there is a known way for it to be wrong (below).
- **When it is silent** — that means *nothing*. It is the absence of a detection, not the presence of disagreement. **4 of 7** real agreements looked exactly like silence. Always read every review.

Its *calibration* is still Python-shaped — the archived rounds it is tuned against are all this repo. The matcher is no longer *Python*-shaped: file-path words are stripped by shape, not by an extension list, so `src/router.ts`, `cmd/server.go`, `src/my-service/retry-policy.py` and `src/.env.local` strip the same as `.py` (INCIDENT-006: until 2026-07-26 they did not, and two unrelated findings about one file linked on the filename alone). It is still **Unix-path-shaped**, which is a weaker claim and the honest one — a path outside that grammar (`packages/@scope/core/index.ts`, a bare `.eslintrc`) leaks part of its filename into the score and can manufacture one of these hints. That residue is filed, not fixed (INCIDENT-007): closing it by widening the character set is the same mistake as the extension list. What remains repo-shaped is the **threshold**, fitted to those rounds: on a codebase with very different finding length or vocabulary, expect the recall figure above to move.

**Security invariant:** a Judge never mutates the code it judges. codex is forced `--sandbox read-only` (non-configurable, runtime-guarded), and any CLI provider added later must enforce the same with a live runtime guard, not a comment.

> **What `read-only` does and does not mean.** It bounds *writes*, not read *scope*. Contrarian confines its own review input to the project root, but an **agentic** judge reads through its own tool calls, outside that check — verified 2026-07-26: `codex exec --sandbox read-only`, launched with the repo as cwd, read an absolute path outside the repo on request. So `cli:codex` can read whatever your user account can read and can quote it into a review that is sent to the provider. Fine for your own repo on your own machine; if that is not your threat model, use the API path — it sends only what Contrarian itself assembles, and each part is bounded for its own reason. **`cli:opencode` is not the contained alternative to this.** It denies out-of-directory reads through its permission list where codex has no equivalent knob at all, which is a real difference — but that deny has never been exercised against the symlink case that would prove it, so on this route the boundary is configured, not demonstrated. Treat both CLI providers as "reads what your account can read" until that measurement exists; the API path is the one where the read scope is bounded by construction. The path-derived input (the diff, or whole files under `full`, or repo chunks under `audit`) is confined to the project root by Business Rule 14 — `_resolve_within_project()` for explicit paths, `Path.is_relative_to()` for audit symlink targets. **That rule covers what Contrarian itself reads, on every route; it does not reach what an agentic judge reads through its own tools.** The graph summary and prior `REPORT.md` history are not covered by Rule 14 at all; they are repo-internal by construction, read from `contrarian-out/graph.json` and the project's own `REPORT.md`. Bounded and inspectable, but *not* "just the diff". Independence is preserved — Judge B is always a different lab than Judge A (Claude), and an explicit adversarial preamble overrides the agent CLI's own collaboration framing (verified by a differential: same diff, project vs neutral cwd, no softening of verdict or core findings).

---

## Usage

Inside any Claude Code session, the tool is available as `contrarian_review`.

```
contrarian_review()                                    # auto-detect mode
contrarian_review({ path: "src/auth.py" })             # single file, diff
contrarian_review({ path: "src/auth.py", full: true }) # full file
contrarian_review({ paths: ["src/a.py", "src/b.py"] }) # multi-file, reviewed as a unit
contrarian_review({ audit: true })                     # force full repo walk
contrarian_review({ model: "google/gemini-3.1-pro-preview" }) # one-off model override
```

Same engine is also available as a **CLI command** (prints to stdout — full visibility, always the latest installed code, no `/mcp` reconnect; Claude can invoke it via its Bash tool and stay in-conversation):

```
contrarian review                                      # auto-detect
contrarian review src/auth.py --full
contrarian review --models cli:codex <api-model>       # fan out, two independent reviews
```

---

## Rubric

Place a `JUDGE.md` at the project root to customize what Judge B looks for. Without it, a built-in fallback rubric applies.

The built-in rubric covers four dimensions:

| Dimension | What to find |
|---|---|
| **Exactitude** | Logic errors, wrong assumptions, incorrect implementation |
| **Missing** | Unhandled cases, absent validation, unconsidered implications |
| **Premises** | Assumptions that could be wrong |
| **Alternatives** | Fundamentally different approaches |

Output is always JSON:

```json
{
  "exactitude": { "verdict": "ok|warn|fail", "findings": [] },
  "missing": [],
  "premises": [],
  "alternatives": []
}
```

---

## Report log

Findings are appended to `REPORT.md` at the project root under `[Judge B]` blocks. Claude Code annotates findings under `[Judge A]` blocks. Judge B reads previous exchanges before each review — closed findings (`addressed`, `won't fix`, `disagree`) are not re-raised.

Run `contrarian stats` from the project root for a summary: review count, model breakdown, exactitude verdicts, and finding totals parsed from `REPORT.md`.

---

## Phase roadmap

- **Phase 2:** MCP server + project graph context. Auto-detect mode. Python rewrite.
- **Phase 3:** Operational hardening — token budget guard, graph latency, detached HEAD handling.
- **Phase 4 (current):** PyPI distribution, CLI provider switching (`setup`/`model`/`use`), usage stats (`contrarian stats`). Next: agent watchdog — autonomous background review on commit.

---

## License

Dual-licensed: [AGPL-3.0-or-later](LICENSE), with a commercial license available — see [LICENSING.md](LICENSING.md). Versions ≤ 0.2.4 remain MIT.
