The advisory flock in run_checks.py uses one system-wide lock path (~/.cache/coyote/review-stack.lock). Concurrent runs of the timeout/deadline tests contend on it: the losers sleep in the bounded retry loop, the total deadline (computed before lock acquisition) expires, and commands record 'skipped: deadline' instead of the expected timeout/kill behavior. - run_checks.py: ADVERSARY_STACK_LOCK_PATH env override relocates the lock file (isolation seam, documented alongside the other env seams); every other lock semantic (default path, bounded wait, proceed-unlocked warning, fd held for process lifetime, skip when no commands) is unchanged. - run_checks.py: guard the POSIX-only fcntl import — windows-latest CI has python3 and runs these tests, so the unconditional import crashed the script there; on non-POSIX the lock is now skipped with a stderr WARN, matching the script's documented non-POSIX limitations. - tests: the shared adversary_script_command helper points every spawned script at its own throwaway lock path, isolating the run_checks tests from each other and from any real review running on the same machine. - tests: adversary_run_checks_stack_lock_path_is_env_overridable pins the override (lock file appears at the overridden path across two sequential runs; no lock file is created when no commands are declared).
Adversary
Important
v2: now a GRAPH agent. The plan's acceptance criteria are extracted into a list and the graph fans out ONE fresh-context verification branch per criterion — partial coverage ("checked 8 of 12") is structurally impossible. Each verdict passes a deterministic contract gate (MET requires evidence including a proving test; no test = at best PARTIAL), a holistic ADDITIVE-only pass hunts absence/scope-drift/substitution/gamed-tests, and the CONFORMS/DIVERGES sentinel is computed by a script (missing plan = DIVERGES, fail-closed). The conformance doctrine below is structurally enforced: CONFORMS requires ALL MET + zero holistic complaints + zero pipeline faults + zero red verification runs (exit≠0/timeout/skipped, or a runner error when commands were declared).
An adversarial plan-conformance reviewer. Where code-reviewer
asks "is this code good?", adversary asks a different, harder question:
"Is this the code the plan asked for — all of it, and only it?"
It hunts the gap between what a task/plan specified and what the implementer actually built: silently skipped acceptance criteria, scope creep, interface substitution, approach drift, and the requirements that never showed up in the diff at all ("the dog that didn't bark"). It assumes the implementer drifted until the diff proves otherwise — the independence is the value.
Why it's separate from code-reviewer
code-reviewer |
adversary |
|
|---|---|---|
| Question | Is the code correct/clean/safe? | Does the code match the plan? |
| Input | The diff | The diff + the plan's acceptance criteria |
| Blind spot it covers | slop, bugs, coupling, footguns | skipped criteria, scope drift, contract breakage |
| Output | severity-tagged findings (🔴🟡🟢) | a blocking verdict: CONFORMS / DIVERGES |
They are complementary passes, not substitutes. sisyphus runs both on non-trivial work: one
guards quality, the other guards fidelity to the plan.
Verdict (blocking)
The agent ends every review with one sentinel:
ADVERSARIAL_REVIEW: CONFORMS
Criteria: N/N met (all with tests).
CONFORMS is deterministic: ALL MET + zero holistic complaints + zero pipeline faults + zero red verification runs (exit≠0/timeout/skipped, or a runner error when commands were declared).
ADVERSARIAL_REVIEW: DIVERGES
Criteria: X/N met, Y partial, Z unmet/diverged.
Complaints:
1. Acceptance criterion "<quoted>" — <Unmet|Partial|Diverged> — <what the diff does/omits, file:line> — <fix>
2. ...
Every verdict — CONFORMS or DIVERGES — ends with a Verification runs: section: the recorded
PASS/FAIL results (command, exit code, duration, failure tail) of the caller-declared verification
commands, or an explicit marker when none were declared or the run never reached that stage. The
one exception is the verdict script's own crash guard, which emits a bare DIVERGES with a
"verdict computation error" line — and no section — when verdict computation itself dies.
A DIVERGES verdict blocks completion. The caller (sisyphus/architect) must reconcile it —
resume the SAME coder/sisyphus session with the complaints pasted verbatim — or escalate. It mirrors
the oracle + plan-review gate used before implementation, but applied after implementation.
Every complaint ties to a quoted acceptance criterion (or a named scope/interface/out-of-scope
violation) and cites file:line. Vague complaints are not emitted.
How it reviews
flowchart TD
A["parse (llm): extract acceptance criteria"] --> B["facts (diff_facts.py): resolve + bound the diff"]
V["verification_commands (declared graph variable — never parsed from the prompt)"] --> R
B --> R["run_checks (script): run the declared verification commands ONCE, record results"]
R --> C["map over criteria: one fresh-context branch each (parallel)"]
C --> D["check_criterion (llm + fs/ast tools)"]
D --> E["crit_gate (script): MET needs change+test evidence"]
E -. "reject-retry once; exhausted = PARTIAL" .-> D
E --> F["holistic (llm): ADDITIVE-only hunt - absence, scope drift, substitution, gamed tests"]
F --> G["verdict (script): CONFORMS iff ALL MET + zero extras + zero faults + zero red runs; no criteria = DIVERGES"]
G --> H(["ADVERSARIAL_REVIEW: CONFORMS | DIVERGES"])
A -. "fallback" .-> P["pipeline_fault (script): record PIPELINE-FAULT marker"]
B -. "fallback" .-> P
R -. "fallback" .-> P
D -. "fallback" .-> CF["criterion_fault (script): UNMET + PIPELINE-FAULT evidence for that ONE criterion"]
CF -. "map join" .-> F
F -. "fallback" .-> G
P --> G
The conformance doctrine is baked into the graph's own nodes:
- Every acceptance criterion gets its own fresh-context verification branch — MET / PARTIAL / UNMET / DIVERGED, machine-gated (MET requires the satisfying change AND the proving test, cited). No test ⇒ at best PARTIAL. Partial coverage of the criteria list is structurally impossible.
- Ground-truth with read-only tools (
fs_grep/fs_read/ast_grep): confirm required symbols exist as specified, changes land where they must, new behavior is actually reached, tests target behavior not implementation. - Execution evidence is recorded once, deterministically:
run_checksruns ONLY the caller-declared verification commands (never invented or auto-detected), sequentially under a 3300s total deadline — commands the deadline leaves no budget for are recorded as SKIPPED. Criterion branches have no execution tools by design: "tests pass"-style criteria are judged against the record — a recorded green run is MET citing it; no declared command ⇒ at best PARTIAL. Independently of the criterion judgments, the verdict script blocks CONFORMS on any red run (exit≠0/timeout/skipped) and on an ENVIRONMENT runner-error marker when commands were declared — the declared commands never ran, so the run is unproven. The commands arrive through the declaredverification_commandsgraph variable (see Declaring verification commands) —parsenever extracts them from the prompt, so the prompt is no longer an extraction source for the commands. - Hunt adversarially for the absent: skipped criteria, scope creep, interface/approach substitution, out-of-scope touches, downstream contract breakage.
- Fail closed on pipeline faults: if parse, diff resolution, or the verification runner dies, its fallback routes to
pipeline_fault, which records aPIPELINE-FAULT:marker and continues to the verdict (a failed holistic pass falls back straight to the verdict, which synthesizes the same marker). Any recorded fault forces DIVERGES with the fault as complaint #1 — degraded is never passing, and the sentinel is always emitted: the graph never dies without one. - Fail closed per criterion: a criterion check that dies (max_iterations exhausted, API failure) falls back to
criterion_fault, which records a schema-shaped UNMET verdict carrying aPIPELINE-FAULT: criterionevidence marker for that one criterion. The other criteria still get verdicts, the report says the check DIED rather than judged, and the run is DIVERGES. That exact marker prefix (the onecriterion_faultemits) forces the criterion into unmet regardless of the status it carries, so a verdict that echoes it withMETcan never count toward CONFORMS;check_criterionis told never to begin its own evidence withPIPELINE-FAULT:.
It is read-only — it produces a verdict, never a fix.
Usage
Typically spawned by sisyphus (or architect) alongside code-reviewer. The spawn prompt IS its
entire context, so it must include the diff (or a base ref to fetch) and the acceptance criteria.
Verification commands travel separately, as a declared variable:
agent__spawn --agent adversary --prompt "
## TASK
Adversarially review the recent changes for TASK-NNN against its plan. Return CONFORMS/DIVERGES.
## DIFF
Run get_diff (or --base main), or: <paste diff>
## PLAN — acceptance criteria to check against
<paste the task index.md body + the relevant PLAN-*.md section, verbatim>
" --variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
Direct invocation for ad-hoc use:
coyote -a adversary --agent-variable project_dir /path/to/repo \
--agent-variable verification_commands '["cargo test --all"]' \
"Review staged changes against these criteria: <paste criteria>"
Declaring verification commands
verification_commands is a declared graph variable (default '[]'): a JSON array of shell
commands, passed as a string because agent variables always arrive as strings. run_checks runs
exactly those commands with a shell — so the declaration is a trust boundary. The prompt is the
wrong side of it: it also carries plan and diff text pasted from the repo under review, and an LLM
parse node extracting commands from that prose would let anything in the repo's text run under
the reviewer's shell. Hence the commands are never in the prompt and never in parse's output
schema; review-gauntlet forwards its own verification_commands variable to the adversary lane
as a structured inputs: passthrough, not as prompt text.
'[]'(or omitted) ⇒ the "none declared" marker; execution criteria are at best PARTIAL.- Anything that is not a JSON array of strings (unparsable JSON, an object, non-string items) is
an invalid declaration:
run_checksexecutes nothing and records aPIPELINE-FAULT: verification_commands declaration invalid — …marker, which forces DIVERGES: it fails closed as a PIPELINE-FAULT (complaint #1) — it never degrades to an ENVIRONMENT marker. - The engine merges only the keys declared under
parse'soutput_schema.propertiesinto state, so an injected instruction that makes the parse LLM emit an extraverification_commandskey cannot reach state: the undeclared key is dropped and the declared variable stands.
Tools
- The graph's
diff_factsscript resolves the diff itself (staged → unstaged →HEAD~1, or an explicit ref/range named in the prompt) — there is noget_difftool anymore. - The
run_checksscript executes the commands declared via theverification_commandsvariable and records the results — criterion branches cite the record and never run anything themselves. - Criterion branches carry read-only
fs_read/fs_cat/fs_grep/ast_grepfor ground-truth checks.
Related
code-reviewer— the quality reviewer it runs alongside.plan-review— the pre-implementation plan gate;adversaryis its post-implementation counterpart.