Skip to main content

adversarial-friends

Adversarial Friends

Hand your spec, plan, or review to agent CLIs — claude, codex, Antigravity (agy), opencode — under adversarial lenses, then merge their critiques into one ranked findings report.

Python Dependencies License Tests

It automates a workflow you may already do by hand: run a review, paste the findings into a different model, ask whether they hold up, carry the argument back. Doing that manually means holding a claim ledger in your head. This keeps the ledger on disk.


📋 Contents


🎯 Why more than one model

A single reviewer produces confident prose. Several reviewers produce claims that can be compared — and the disagreements are where the real problems are.

This tool's own design spec was built exactly this way:

Reviewer Result
codex 17 findings
claude 15 findings, plus one marked unproven"lens leaks attribution"
Antigravity (agy) independently reproduced two of claude's findings, and caught a shared-worktree race neither of the other two flagged

That unproven claim was later confirmed and fixed. No single reviewer's pass would have surfaced all of it — see the revision history in the design spec for the full account.


📦 Install

Requires Python 3.11+ and at least one agent CLI. Judging modes additionally require two independent non-host friends. The runner itself is stdlib-only — zero runtime dependencies.

uv tool install adversarial-friends
Other install methods
# From git, for a version that is not yet released
uv tool install git+https://github.com/livingstaccato/adversarial-friends

# From a local checkout
git clone https://github.com/livingstaccato/adversarial-friends
cd adversarial-friends
uv tool install .

# Without installing at all
python -m adversarial_friends doctor

Then confirm what's actually available:

afriend doctor

doctor reports shared readiness — including ready, reachable-unconfigured, unavailable, disabled, host-excluded, and policy-blocked — plus what each friend can genuinely enforce. For Ollama, reachability alone is insufficient because dispatch also requires a model.


🚀 Quickstart

In an agent host, select /afriend to route an explicit Adversarial Friends request, or select $adversarial-friends:afriend directly. It hands review, status, setup/configuration, and resolution requests to focused skills: review, status, configure, and resolve.

Conversational phrases such as afriend review and afriend status are routing language, not executable aliases: the stable CLI commands remain afriend run, afriend status, and afriend doctor. afriend resume <run-id> similarly routes to afriend run --resume <run-id>; it is not a claim resolution and needs no disposition or evidence.

afriend run docs/my-design.md

On the first review request in a host task, /afriend presents one compact preflight before dispatch:

About to start Adversarial Friends to review <artifact> in <mode> mode with <profile>. Scope: <repository snapshot|document only>. Friends: <name, provider, lens, role>; external tools: <denied|explicit grant>.

You can accept it, choose a task-only profile or mode, change the task-only roster, or stop. It is shown again only before a requested new loop iteration. The preflight describes authority; it never grants external tools, provider enablement, unsafe arguments, or sandbox exceptions. Direct CLI runs stay non-interactive.

The built-in profiles are quick (one report fan-out), balanced (crossexam), and thorough (loop). quick is the default. Use --profile NAME for one run; an explicit --mode wins over the profile's mode and other explicit safe run flags win over profile values.

The host is the orchestrator. In Codex, Codex remains the orchestrator and is included as a friend by default. The report labels it host-self-review (advisory), independent=false: it contributes findings and advisory verdicts, but cannot satisfy two-independent-friend admission, --require-friends participation, judging quorum, gate clearance, or loop convergence. Judging modes need two independent non-host friends in addition to any host; a report can be host-only. Non-Codex hosts are excluded by default. --include-self and --exclude-self are mutually exclusive overrides.

Manage persistent user defaults, including a default Ollama model, with:

afriend providers list
afriend providers enable claude
afriend providers disable opencode
afriend providers set-model ollama qwen3:8b
afriend providers clear-model ollama

Set up those defaults with an inspectable no-write preview, then apply only the exact choices you name:

afriend init --guided
afriend init --guided --default-profile balanced --enable-provider claude
afriend init --guided --apply --default-profile balanced --enable-provider claude

Guided setup reports discovered providers, the Codex advisory-host role when applicable, built-in profiles, and that external tools remain denied. Plain afriend init remains the direct roster-generation command.

Manage named user profiles separately:

afriend profiles list
afriend profiles create focused --base quick --timeout 300
afriend profiles set-default focused

Profiles can carry only review-safe mode, preset, lenses, friend/timeout ceilings, and round/iteration ceilings. They cannot select providers, a friend roster, models, credentials, forwarded environment, external tools, unsafe arguments, or sandbox exceptions.

For one automatic roster, --enable-provider NAME and --disable-provider NAME override those defaults. Disabled providers are not probed. An explicit --friend roster remains authoritative and may name the host or a disabled provider.

External-tool authority is separate from provider selection. The repeatable required-value form is --allow-external-tools=PROVIDER; the explicit global grant is --allow-external-tools=*. Unknown, duplicate, or mixed * plus provider grants are invalid, and the old valueless form is invalid. --unsafe-extra-args requires the global * grant plus --i-accept-unsandboxed. Grants do not change provider defaults and must be repeated as the same normalized set on resume.

Stdout carries one thing — the run directory. Read report.md inside it:

cat "$(afriend run docs/my-design.md --mode report)/report.md"

Pick your reviewers and lenses explicitly:

afriend run spec.md --friend codex:security --friend claude:ops

A third slot picks the model — required for ollama, which has no default:

afriend run spec.md --friend ollama:security:qwen3:0.6b

⚠️ --friend replaces discovery rather than adding to it. One --friend flag means a one-friend roster. A report with one friend is allowed as a recorded downgrade in run.json and report.md rather than being presented as a full review. crossexam, gate, and loop require at least two independent non-host friends; with fewer, they refuse with exit 3 before a run directory is created.

Runtime depends on the slowest selected friend, document size, and mode. Progress goes to stderr: a line per friend as it finishes, and a heartbeat naming whatever is still outstanding, so a quiet run is distinguishable from a hung one. --no-progress silences it for a caller that captures both streams together; reducing --timeout turns slow friends into failures rather than making them faster.

Each run also appends safe lifecycle records to events.jsonl: run_started, friend completion/failure, round completion, and run_finished. Records carry only identity, mode/profile, scope, status, round, duration, and next action; they never copy prompts, raw outputs, diagnostics, credentials, environment, or authority grants. After the run, inspect it without dispatching anything:

afriend status <run-id-or-path>
afriend status <run-id-or-path> --watch
afriend status <run-id-or-path> --json

--watch prints only new lifecycle events until the terminal event and treats an unterminated final event line as still being written. Runs without an event stream remain inspectable from run.json, claims.jsonl, and report.md.


⚙️ How it works

module architecture

Every friend gets its own prompt built from its own lens, runs in its own isolated directory, in its own process group:

Stage What happens
🔍 Resolve Discover agent CLIs on PATH, round-robin a lens to each
✍️ Prompt Build a per-friend prompt: shared contract header + that friend's lens prose + the artifact
🔒 Isolate Each friend's effective scope selects its isolation directory: repo scope gets a private git worktree from one shared snapshot, while doc scope gets an artifact-only directory. Adapter read-only flags and, where required, OS confinement (sandbox-exec / bwrap) are then applied separately as a second line of defense — or the friend is refused
🛂 Deny remote authority External tools are denied by default. An adapter that cannot neutralize provider-managed tools, plugins, apps, or MCP servers is policy-blocked unless --allow-external-tools=PROVIDER explicitly opts that provider in for this run
🧰 Stage harnesses Adapter-owned workspace assets are copied into each isolated run workspace. Antigravity receives the controlled afriend-reviewer agent selected with --agent, --disable-slash-commands, --mode plan, and --sandbox
Dispatch Parallel, one thread per friend, each in its own process group with a kill deadline of --timeout + 60s
🧩 Normalize Unwrap the CLI's own JSON envelope, strip ANSI, recover the payload, validate against the claim schema
🔗 Merge Exact-merge identical claims into aliases — accumulating origins so corroboration survives
📄 Report Rank findings, render report.md, write the append-only ledger

The snapshot includes untracked files (git stash create omits them), and the working tree is never touched — a friend reviewing your repo can't see a half-staged index or scribble on your checkout.

There are two supported ways to select review context:

afriend run docs/plan.md --mode report
afriend run /tmp/reviews/plan.md --repo "$PWD" --mode report

The first form selects scope automatically only when the artifact's resolved final target is inside the invocation repository: it receives a repository snapshot. An in-repository symlink whose target resolves outside that repository is doc scope only, and no repository snapshot is minted; a path outside a Git repository likewise produces a visible doc-scope warning on stderr before friends start. The second form selects the named repository explicitly; --repo must name that repository's Git worktree root. It is the deliberate route for an independently frozen external artifact together with selected repository code. It does not grant new provider, external-tool, or write authority. Normal untracked, non-ignored files are included in an automatic snapshot.

Gitignored artifacts are intentionally excluded from automatic Git-blob binding and fail rather than falling back to a stale HEAD version. Use the explicit --repo form when an ignored or outside artifact needs the named repository's code context.

On Linux, a confined friend uses bwrap with the required system paths read-only. If /etc/resolv.conf resolves to a safe regular file elsewhere on the host, the sandbox exposes that resolver target read-only too. DNS therefore continues to work without granting general access to the host filesystem.

For a provider without a verified read-only/write-protection mode, make sandbox-exec (macOS) or bwrap (Linux) available, or choose a provider with a verified read-only/write-protection mode. That mode controls writes, not filesystem reads, and does not replace OS read confinement. A provider with a verified read-only/write-protection mode does not need --allow-unsandboxed-friend. That flag is explicit risk acceptance, not a normal fix: the affected provider runs without OS confinement and retains same-user filesystem read access.

Full run flow, step by step

run flow

Corroboration is the point

Two friends independently reaching the same conclusion is the strongest signal this tool produces, so deduplication is built to never destroy it:

claim lifecycle

Dedup is deliberately exact-match — whitespace and case only. Two friends describing one defect in different words produce two claims, which costs a round. Guessing at equivalence would corrupt the ledger, which is worse.

Claim states in cross-examination

Every claim --mode crossexam produces ends in one of eight states. Two of them — deadlocked and settled-upheld — deliberately need a human, and the report says so rather than quietly resolving them.

crossexam states

The gate loop

--mode gate is the one that fails a build, and clearing it is a back-and-forth rather than a single command:

gate workflow


🔬 Lenses

A lens is prose, not a config string. Its text is injected into that friend's prompt, so it shapes what the friend actually looks for.

Lens Default scope Requires a failure scenario
assumptions doc
security repo
ops repo
testability repo
spec-vs-reality repo
scope doc ❌ — advisory only

scope is the one lens that doesn't demand a concrete failure scenario; "this is more than you need" is a legitimate finding without one. Claims from it are marked (advisory) in the report so they never carry the same weight as a reproducible defect.


📂 What you get back

<run-dir>/
├── report.md          ← ranked findings, corroboration, downgrades
├── run.json           ← machine-readable: friends, statuses, downgrades
├── events.jsonl       ← append-only safe lifecycle events
├── claims.jsonl       ← append-only ledger: claims, aliases
├── artifact/          ← frozen copy of what was reviewed, hashed
└── round-1/
    ├── <friend>.prompt ← exactly what this friend was asked
    ├── <friend>.raw    ← its unmodified stdout
    ├── <friend>.err    ← its stderr (always present, even when empty)
    ├── <friend>.meta   ← argv, exit code, duration, timeout, orphan status
    └── <friend>.sandbox ← the OS policy it ran under, when one was applied

Runs land under ${XDG_STATE_HOME:-~/.local/state}/adversarial-friends/runs/, or wherever --out points.

Everything a friend was asked and everything it said is on disk. When a run comes back thin, that's what you read — not a guess.


✅ What's implemented

All four modes run.

Mode What it does
report The default: one critique fan-out. Every friend critiques in parallel; claims merge into one ranked report.
crossexam Then friends judge the claims they did not write, blind, for three total rounds by default or until each settles or deadlocks.
gate Then every non-advisory claim that did not clear needs an explicit resolution — this is the one that fails a build.
loop Repeats for a maximum of five iterations by default, until two consecutive dry rounds surface nothing new.
afriend run docs/design.md --mode crossexam
afriend run docs/design.md --mode gate       # exit 1 while anything blocks
afriend resolve <run-id> --list
afriend resolve <run-id> --next
afriend resolve <run-id> --claim c-0001@1 \
    --disposition fixed --evidence src/auth.py:38

Disagreement is the output rather than a problem: two judges who still disagree at --max-rounds leave the claim deadlocked, and the report quotes both sides verbatim instead of resolving it by majority.

A resolution is an attestation, and the tool says so. It cannot know a defect is gone — only whether the location you named actually changed since the run started. A fix that landed outside the reviewed artifact is fine. --disposition fixed requires a verifiably changed location; unchanged or unverifiable evidence is refused. Use accepted-risk when verification is intentionally unavailable.

Deduplication is judgment the runner declines to fake. --merge exact (the default) merges only identical claims and always finishes unaided; --merge orchestrator stops with exit 10, writes the claims to REQUEST.json, and waits for you to say which are duplicates:

afriend run docs/design.md --merge orchestrator   # exit 10, writes REQUEST.json
# ...fill in the merges, save as RESPONSE.json...
afriend run --resume <run-id>                     # round 1 is not re-run

Resume verifies the original frozen artifact hash and saved Git snapshot before dispatch; it never substitutes current files. It uses the saved repository scope and rejects --repo, so a resume cannot replace the original automatic or explicit selection. Security grants are also never restored from run.json: options such as --allow-external-tools=PROVIDER grants (or the global =* grant) must be repeated as the same normalized set.

Tired of --friend flags? afriend init writes a roster from what is actually installed, and ~/.config/adversarial-friends/roster.toml is picked up automatically. A repo-local roster never is — a cloned repo does not get to choose who reviews it (§13).

The same halt serves unparseable output (§14.2): repair is a pure transformation with no model call, so when it fails the runner asks you to read the raw text rather than discarding whatever the friend found.

There is no --max-spend-usd. A dollar cap needs per-CLI cost reporting nobody has captured, and a flag that silently never fires is worse than none — you would set it and believe you were protected. Use --max-calls, which is derived from your roster and actually enforced.

Friend Status
claude ✅ ships
codex ✅ ships
agy ✅ ships
opencode ✅ ships — no read-only mode, reported honestly
ollama ✅ ships — local models over HTTP, no schema/read-only to enforce; needs an explicit model

Antigravity's controlled reviewer is staged into the run's isolated workspace; it does not edit global Antigravity configuration. The staged agent and --sandbox are defense in depth, but sandbox does not mean external tools were denied. Antigravity remains external_tools=uncontrolled because its CLI cannot prove every plugin/MCP integration disabled invocation-locally. That is an accepted best-effort limitation: Antigravity is policy-blocked by default, while --allow-external-tools=agy records it as explicitly-allowed.

Antigravity is the shipped Google harness, identified as agy in CLI and provider configuration.

Exit codes

Code Meaning
0 the run reached terminal states with nothing blocked
1 a gate still has claims needing a resolution, or every dispatched friend failed
2 usage error — bad flag, unknown CLI, missing artifact
3 no usable friends found, or a judging mode resolved fewer than two independent non-host friends; refused before a run directory
10 --merge orchestrator is waiting for you to adjudicate merges
11 a ceiling was hit — including natural --max-loop-iterations exhaustion without convergence; the run was truncated, not decided
12 --require-friends N was set and fewer than N friends answered
128+N aborted by signal N — isolation torn down, friends killed

A ceiling outranks everything below it, so a CI wrapper can read 11 as "retry" and 1 as "block" without ambiguity.


📚 Documentation

Where What
docs/ Documentation index
/afriend router Explicit product router and review workflow
review Start and interpret a review run
status Read-only provider and named-run status
configure Explicit provider-default changes
resolve Named-run claim resolutions
modes report, crossexam, gate, and loop
architecture/ Diagrams and their sources
design spec The full design, including the adversarial review that produced it

Using it as a skill or plugin

The skill payload ships inside the wheel as package data, and is mirrored under plugins/ for loaders that can't install a Python package:

# Claude Code
/plugin marketplace add /path/to/adversarial-friends/plugins

Plugins package capabilities; the Adversarial Friends plugin provides exactly five skills: /afriend, review, status, configure, and resolve. /afriend is the only router and short slash selector; direct qualified selection is $adversarial-friends:afriend. The CLI never runs automatically by itself. Generic “review this,” “poke holes,” “second opinion,” and architectural decision requests stay ordinary Codex work.

The package must therefore be installed for the skill to work — afriend doctor is the check. Conversational forms route to stable commands; they are not executable aliases.


🛠 Development

make install    # uv sync
make test       # pytest
make quality    # every portable CI gate, wheel checks, and tests
make diagrams   # re-render docs/architecture/*.puml

make quality runs every portable CI gate, including wheel construction and isolated installation. Linux CI additionally installs bubblewrap and requires the real OS-confinement tests to execute; macOS cannot reproduce that Linux- specific assertion locally. Use make act-ci for the closest local Linux run.

Two gates catch drift that is otherwise silent:

  • plugin-syncsrc/adversarial_friends/assets/ is canonical; its entrypoints project directly to plugin skills and runtime assets project beneath skills/afriend/. Edit assets, then make plugin-sync-copy.
  • version-syncVERSION must match the version field in every plugin manifest.

See AGENTS.md for repository layout and conventions.


📄 License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adversarial_friends-0.5.0.tar.gz (598.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

adversarial_friends-0.5.0-py3-none-any.whl (364.7 kB view details)

Uploaded Python 3

File details

Details for the file adversarial_friends-0.5.0.tar.gz.

File metadata

  • Download URL: adversarial_friends-0.5.0.tar.gz
  • Upload date:
  • Size: 598.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for adversarial_friends-0.5.0.tar.gz
Algorithm Hash digest
SHA256 914600ff68774155717009579beb21b2617d9d6e96d4c287ca1c3c8952426650
MD5 243b5f0126712729a19e09ad8cf68324
BLAKE2b-256 242c86fc9ecf9481d2c9cc7db2be01c82adc20f06e0a962e96d1815ee6d3b7dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for adversarial_friends-0.5.0.tar.gz:

Publisher: release.yml on livingstaccato/adversarial-friends

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file adversarial_friends-0.5.0-py3-none-any.whl.

File metadata

File hashes

Hashes for adversarial_friends-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f5334421cde7d65e97648e83a547d25f0167898baebae6658599785175879ca9
MD5 b94182d7a35b8960b528992fe0331e1b
BLAKE2b-256 ea1a529fdb922509d15c73b3fb07225fc5a902d1813bd1b503f2467ad1f4abf6

See more details on using hashes here.

Provenance

The following attestation bundles were made for adversarial_friends-0.5.0-py3-none-any.whl:

Publisher: release.yml on livingstaccato/adversarial-friends

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.7.0

2 files

0.6.2

2 files

0.6.1

2 files

This release

0.5.0 This release

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.3

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page