name: review-gauntlet description: | Graph-based post-implementation review gate — "the done gate". Given a diff spec and the work's context (plan/acceptance criteria, rigor, security posture), it deterministically computes diff signals, selects the applicable independent review lanes — code-reviewer (always), adversary (when a spec/plan exists), security-reviewer (attack-surface signals), probe (consumer-facing surface with a local-run recipe) — runs the selected lanes IN PARALLEL as isolated sub-agents, and machine-parses their verdict sentinels into one deterministic verdict: GAUNTLET: PASS or GAUNTLET: BLOCKED. An orchestrator that must not claim "done" without green reviews spawns this once instead of judging trigger rules and parsing verdicts itself — the graph structure makes skipping a lane or misreading a verdict impossible. Invariants: lane selection can only be WIDENED by LLM judgment, never narrowed (deterministic rules are the floor); a missing verdict sentinel is a lane failure, never a pass; probe INCONCLUSIVE blocks with an environment note (it is never PASS or FAIL); reviewer lanes run verdict-independent — no cross-lane messaging. version: '1.0' # The limit is consulted by the SPAWNER against the child's absolute depth # (root = 0). Deepest documented chain: architect(0) → sisyphus(1) → # review-gauntlet(2) → code-reviewer(3) → # {domain-reviewer(4) → file-reviewer(5) | finding-verifier(4)}. # This agent spawns depth-3 children; 4 = one level of headroom. max_agent_depth: 4 max_concurrent_agents: 12 variables: - name: project_dir description: Repository the change lives in; signals and every review lane run against it. default: '.' - name: verification_commands description: | JSON array of shell commands the CALLER declares for the deterministic run_checks stage — e.g. '["cargo test --all"]'. Never inferred from the prompt; empty array = none declared. default: '[]' settings: max_loop_iterations: 10 log_state_snapshots: true # A faulted lane can be re-run by retry_gate for up to 3 passes, and each # pass's agent node retries up to max_attempts = 2 with a fresh per-attempt # budget (src/graph/agent.rs retry_transient/bounded_attempt): # 3 passes × 2 attempts × 11400 (max lane timeout) = 68400, + 1200 margin # for the script stages (signals/select/build/retry gate/verdict gate) # = 69600; the parse and select_lanes llm nodes carry no `timeout:` and # are bounded by max_iterations. timeout: 69600 initial_state: retry_notes: [] diff_spec: worktree plan_context: '' rigor: production security_posture: standard probe_context: '' extra_context: '' forced_lanes: [] changed_files: [] file_count: 0 signals_summary: '' signals_error: '' touches_auth: false touches_deps: false touches_exec: false touches_proto: false touches_migrations: false consumer_surface: false docs_only: false add_code_review: false add_adversary: false add_security: false add_probe: false lane_reasons: '' parse_failure: '' select_lanes_failure: '' lanes_degraded: '' code_review_items: [] adversary_items: [] security_items: [] probe_items: [] code_review_results: [] adversary_results: [] security_results: [] probe_results: [] lane_attempts: {} kept_results: {} retry_lanes: [] review_incomplete: [] gauntlet_started_at: 0 lanes_summary: '' gauntlet_verdict: '' gauntlet_report: '' gauntlet_incomplete_line: '' start: parse nodes: parse: id: parse type: llm description: Extract the review request's facts from the spawn prompt. Extraction only — no judging, no lane selection. instructions: | You extract the facts of a review request into structured fields. EXTRACTION ONLY — do not decide which review lanes should run. - `diff_spec`: the git diff specification the caller named, as a BARE revision token usable verbatim in `git diff ` — a ref range ("main...HEAD", "HEAD~3..HEAD"), a single ref, or the literal "worktree" for uncommitted changes against HEAD. NEVER prose, a command, or a path: from "run get_diff --base origin/main" extract "origin/main...HEAD"; from "diff origin/main...HEAD in ~/code/x" extract "origin/main...HEAD". When the caller did not name one, use "worktree". - `project_dir_in`: the repository directory the caller named as holding the change (absolute path preferred); empty string when the caller named none. - `plan_context`: the plan / spec / acceptance-criteria text the implementation must satisfy, copied through VERBATIM — never summarize it and never replace pasted text with file paths or a sentence saying it was included; the adversary lane fails closed on whatever you drop. Empty string when the caller supplied none. - `rigor`: poc | prototype | production. Default "production" when not stated. - `security_posture`: prototype | standard | hardened. Default "standard" when not stated. - `probe_context`: the local-run recipe and/or existing usage-suite pointers for black-box probing (how to boot the service, where the suites live). Empty string when the caller supplied none. - `forced_lanes`: lanes the caller explicitly demanded (verbatim names). Empty array = automatic selection. Forcing is ADDITIVE downstream: it widens the computed selection, never replaces it. - `extra_context`: any remaining caller context reviewers should see, condensed. prompt: | Extract the review-request facts from this request: {{initial_prompt}} tools: [] output_schema: type: object properties: diff_spec: type: string plan_context: type: string rigor: type: string security_posture: type: string probe_context: type: string forced_lanes: type: array items: type: string extra_context: type: string project_dir_in: type: string required: [diff_spec, plan_context, rigor, security_posture, probe_context, forced_lanes, extra_context, project_dir_in] max_iterations: 3 max_attempts: 2 state_updates: parse_failure: '{{output}}' fallback: parse_fault next: signals parse_fault: id: parse_fault type: script description: Fail-closed marker for a dead parse stage (reached only via parse's fallback) — records a PIPELINE-FAULT in signals_error and jumps straight to the verdict gate, which blocks on it. Lane item lists stay their initial [] so every lane records SKIPPED. script: scripts/parse_fault.py timeout: 30 next: verdict_gate signals: id: signals type: script description: Deterministic diff signals — file list, attack-surface and consumer-surface indicators. Pure git; no LLM judgment. script: scripts/signals.py timeout: 120 next: select_lanes select_lanes: id: select_lanes type: llm description: Additive-only lane judgment on top of the deterministic rules. It can WIDEN the gauntlet, never narrow it. instructions: | You look at a change's deterministic diff signals and decide whether any review lane the hard rules might have missed should ALSO run. You can only ADD lanes — the deterministic floor (enforced downstream, not by you) is: code-review always runs; adversary runs when a plan/spec is present; security runs on attack-surface signals or hardened posture; probe runs when consumer-facing surface changed and a local-run recipe exists. Setting a flag to false NEVER removes a lane the rules selected. Reasons to add: - `add_security`: the diff handles external/untrusted input, touches crypto/secrets handling, or changes trust boundaries in ways the path-based signals missed. - `add_adversary`: the request references behavior it must preserve or criteria scattered in prose that amount to a spec, even without a formal plan. - `add_probe`: the change alters observable API/CLI behavior even though the path signals missed it (only useful when a local-run recipe exists — the builder skips it otherwise, noting why). - `add_code_review`: always selected by the rules; flag exists only for forced-lane configurations. Put one line per added lane into `lane_reasons`. When nothing needs adding, all flags false and empty reasons is the correct answer. prompt: | Change signals (deterministic): - diff spec: {{diff_spec}} - files changed: {{file_count}} - changed files (first 200): {{changed_files}} - touches auth paths/content: {{touches_auth}} - touches dependency manifests: {{touches_deps}} - adds exec/shell/subprocess calls: {{touches_exec}} - touches protobuf/contracts: {{touches_proto}} - touches migrations: {{touches_migrations}} - consumer-facing surface paths: {{consumer_surface}} - docs-only diff: {{docs_only}} - {{signals_summary}} {{signals_error}} Caller context: - rigor: {{rigor}}; security posture: {{security_posture}} - plan/spec present: {{plan_context}} - probe recipe present: {{probe_context}} - extra: {{extra_context}} tools: [] output_schema: type: object properties: add_code_review: type: boolean add_adversary: type: boolean add_security: type: boolean add_probe: type: boolean lane_reasons: type: string required: [add_code_review, add_adversary, add_security, add_probe, lane_reasons] max_iterations: 3 max_attempts: 2 state_updates: select_lanes_failure: '{{output}}' fallback: default_lanes next: build_items default_lanes: id: default_lanes type: script description: Deterministic lane-selection fallback (reached only via select_lanes' fallback) — when the additive LLM judgment dies, forces code-review + adversary, plus probe when consumer_surface AND a probe_context (local-run recipe) is present, plus security on attack-surface signals (auth/deps/exec) or hardened posture, unioned with any caller-forced lanes. Selection degrades WIDER, never narrower; the degradation note flows into lanes_summary via build_items. script: scripts/default_lanes.py timeout: 30 next: build_items build_items: id: build_items type: script description: Deterministic lane builder — hard rules are the floor, LLM flags may only add, forced lanes are ADDITIVE (selection only ever widens; unavailable signals widen it too). Emits one 0-or-1-item list per lane; an empty list skips that lane's map. On a re-run pass (retry_lanes non-empty) it skips selection and emits items only for those lanes, each carrying its attempt number. script: scripts/build_items.py timeout: 30 next: [map_code_review, map_adversary, map_security, map_probe] map_code_review: id: map_code_review type: map over: '{{code_review_items}}' as: lane_ctx branch: run_code_review output_key: lane_out collect_into: code_review_results max_concurrency: 1 next: retry_gate map_adversary: id: map_adversary type: map over: '{{adversary_items}}' as: lane_ctx branch: run_adversary output_key: lane_out collect_into: adversary_results max_concurrency: 1 next: retry_gate map_security: id: map_security type: map over: '{{security_items}}' as: lane_ctx branch: run_security output_key: lane_out collect_into: security_results max_concurrency: 1 next: retry_gate map_probe: id: map_probe type: map over: '{{probe_items}}' as: lane_ctx branch: run_probe output_key: lane_out collect_into: probe_results max_concurrency: 1 next: retry_gate run_code_review: id: run_code_review type: agent description: Independent code-quality review lane. agent: code-reviewer # Exceeds the code-reviewer graph's own settings.timeout (10800) by 600 so # the child's graph timeout, not this lane timeout, is the binding bound. # NOTE: the child's own internal retry envelope (its nodes' max_attempts × # timeouts) is not derived here — deliberately out of scope for this # change; tracked as a follow-up. timeout: 11400 prompt: | Review the code changes in {{project_dir}} (diff: {{diff_spec}}) as an independent post-implementation review. Quality bar: rigor={{rigor}}. Plan / acceptance-criteria context (may be empty): {{plan_context}} {{extra_context}} Close your report with your Review Verdict line. max_attempts: 2 state_updates: lane_out: '{{output}}' fallback: lane_fault run_adversary: id: run_adversary type: agent description: Plan-conformance review lane. agent: adversary # Exceeds the adversary graph's own settings.timeout (9000) by 600 so the # child's graph timeout, not this lane timeout, is the binding bound. # NOTE: the child's own internal retry envelope (its nodes' max_attempts × # timeouts) is not derived here — deliberately out of scope for this # change; tracked as a follow-up. timeout: 9600 inputs: verification_commands: '{{verification_commands}}' prompt: | Adversarial plan-conformance review in {{project_dir}}. The implementation to check: diff {{diff_spec}}. The spec / plan / acceptance criteria it must satisfy: {{plan_context}} Verification commands arrive as a structured input (the verification_commands variable), not in this prompt. {{extra_context}} Return your ADVERSARIAL_REVIEW verdict. max_attempts: 2 state_updates: lane_out: '{{output}}' fallback: lane_fault run_security: id: run_security type: agent description: Security review lane. agent: security-reviewer timeout: 7200 prompt: | Security review of the code changes in {{project_dir}} (diff: {{diff_spec}}). Security posture: {{security_posture}}. {{extra_context}} Return your SECURITY_REVIEW verdict. max_attempts: 2 state_updates: lane_out: '{{output}}' fallback: lane_fault run_probe: id: run_probe type: agent description: Black-box usage-pattern verification lane. agent: probe timeout: 7200 prompt: | Usage-pattern verification (black-box probe) of the changes in {{project_dir}} (diff: {{diff_spec}}). Local-run recipe / existing usage suites: {{probe_context}} Plan / acceptance-criteria context: {{plan_context}} The workspace may contain test files from an aborted earlier attempt of this lane; reconcile rather than duplicate. Shared-stack rule: wrap any stack boot/teardown in `flock "$HOME/.cache/coyote/review-stack.lock" ` (the adversary's verification stage holds the same lock), and NEVER remove or stop containers you did not create this run — a held port means wait, re-port, or INCONCLUSIVE. Derive expected behaviors from the plan/contract BEFORE reading implementation code, run existing suites first, then author spec-first tests for the uncovered delta. Return your USAGE_PROBE verdict. max_attempts: 2 state_updates: lane_out: '{{output}}' fallback: lane_fault lane_fault: id: lane_fault type: script description: Fail-closed lane fault marker (reached only via the run_* lanes' fallback routes; shared across the four map branches). Normalizes the engine's "Agent node failed" text in lane_out into a PIPELINE-FAULT lane result the map collects; verdict_gate turns it into BLOCKED — a degraded lane is never a pass. script: scripts/lane_fault.py timeout: 30 retry_gate: id: retry_gate type: script description: Deterministic fault-only re-run gate. Stashes every lane report that ran this pass, restores reports a re-run pass overwrote with [] (a lane not being re-run maps over nothing), classifies each lane that ran as completed / faulted / skipped, counts attempts, and routes back into build_items for faulted lanes that still have attempts left inside the wall-clock budget; otherwise it records them in review_incomplete and falls through to verdict_gate. A real verdict is never re-run, and a gate crash never retries — it records a PIPELINE-FAULT and falls through. script: scripts/retry_gate.py timeout: 30 next: verdict_gate verdict_gate: id: verdict_gate type: script description: Deterministic sentinel parser and gate. A missing sentinel is a lane failure, never a pass; probe INCONCLUSIVE blocks with an environment note; the code-review critical count is read from render.py's summary line (raw 🔴 count if absent) and blocks regardless of the lane's own verdict line; a code-review verdict line that records the nested graph's own pipeline fault blocks as a degraded lane. Lanes retry_gate gave up on, and any pipeline-level fault, are named on a GAUNTLET_REVIEW_INCOMPLETE machine line so a caller can tell an incomplete review from a failed one. script: scripts/verdict_gate.py timeout: 60 next: done done: id: done type: end output: | GAUNTLET: {{gauntlet_verdict}}{{gauntlet_incomplete_line}} {{gauntlet_report}}