verdict_gate.py now names every incomplete review on a machine line the done template renders immediately after the verdict: `GAUNTLET_REVIEW_INCOMPLETE: <name>[, <name>]` (sorted). Names are the lanes retry_gate gave up on (`review_incomplete`) plus the `pipeline` pseudo-lane whenever `signals_error` carries a `PIPELINE-FAULT:` — parse death, builder / retry-gate / default-lanes crash guards — and the gate's own crash guard emits the `pipeline` line too. The report gains a `## Review incomplete` section (after the signals_error blockquote, immediately before `## Full lane reports`) and the three PIPELINE-FAULT lane rows carry ` ×N` when the lane was re-run (N = 1 texts unchanged). retry_gate.py stands in a synthetic `PIPELINE-FAULT: <lane> lane produced no report` marker for a lane that ran but collected nothing, so it is stashed, classified FAULTED and never renders SKIPPED downstream. MAX_RETRY_ELAPSED_SECS trimmed 45600 → 45000 for margin slack. graph.yaml: `gauntlet_incomplete_line` seeded in initial_state and spliced into the done output. README + mermaid document retry_gate, the dotted _next back-edge, the 3-attempt / wall-clock budget, that retry never narrows selection, the crash-guard append semantics, and the machine-line contract (present whenever any lane or the pipeline was incomplete; it can accompany real findings). Tests: ×N on all three branches (N>1, N=1 byte-identical, no leak onto non-fault rows), exact incomplete line / section placement, pipeline pseudo-lane alone and alongside real lanes (via default_lanes.py's real crash guard), mixed finding+fault, crash-guard chaining from build_items / retry_gate / parse_fault / verdict_gate, synthetic no-report marker, done-template wiring, README pins (positive + stale negative).
445 lines
16 KiB
YAML
445 lines
16 KiB
YAML
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:
|
||
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 — a ref
|
||
range ("main...HEAD", "HEAD~3..HEAD"), a single ref, or the literal
|
||
"worktree" for uncommitted changes against 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, verbatim (or a file path the caller
|
||
pointed at). 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.
|
||
- `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 override exactly. 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.
|
||
|
||
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}}
|