Files
coyote/assets/agents/review-gauntlet/scripts/build_items.py
T
Dark-Alex-17 8407729d5a feat(TASK-018): review-gauntlet lane degradation + verification passthrough
- lane_fault.py + fallback on all four run_* lane nodes: an agent-node
  failure is normalized into a PIPELINE-FAULT lane result instead of
  killing the graph
- verdict_gate.py: PIPELINE-FAULT lane results block distinctly from
  SKIPPED and missing-sentinel; a PIPELINE-FAULT in signals_error blocks
  at the gauntlet level
- build_items.py: restructured into main() + fail-closed guard (crash
  emits empty lanes + PIPELINE-FAULT) and folds lanes_degraded into
  lanes_summary
- parse: state_updates/fallback -> parse_fault.py, which records the
  fault in signals_error and jumps to the verdict gate
- select_lanes: state_updates/fallback -> default_lanes.py, a
  deterministic wider-never-narrower lane fallback
- verification_commands threaded through parse into run_adversary's
  prompt (declared-only, never invented)
- run_probe prompt gains the aborted-attempt reconcile line
- 8 gauntlet_* R5 tests in src/config/request_context.rs (script harness
  + graph interpolation)
2026-09-14 10:50:33 -06:00

147 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Deterministic lane builder for the review gauntlet.
Selection = hard rules (the floor) ∪ LLM additions; forced lanes override
everything exactly. Emits one 0-or-1-item list per lane: an empty list makes
that lane's map node run zero branches (map-over-nothing), which IS the
skip mechanism — no dynamic routing anywhere.
The LLM's add_* flags can only ADD lanes. A flag that is false never
removes a lane the hard rules selected.
Fail-closed: a builder crash emits all four lists empty plus a
PIPELINE-FAULT in signals_error — verdict_gate blocks on it and the crash
never kills the graph.
"""
import json
import os
ALIASES = {
"code-review": "code-review",
"code_review": "code-review",
"code-reviewer": "code-review",
"adversary": "adversary",
"adversarial": "adversary",
"adversarial-review": "adversary",
"security": "security",
"security-review": "security",
"security-reviewer": "security",
"probe": "probe",
"usage": "probe",
"usage-pattern": "probe",
"usage-pattern-testing": "probe",
}
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
def text(key):
v = state.get(key)
return v.strip() if isinstance(v, str) else ""
forced_raw = state.get("forced_lanes") or []
forced, unknown = [], []
for lane in forced_raw:
canon = ALIASES.get(str(lane).strip().lower())
(forced.append(canon) if canon else unknown.append(str(lane)))
reasons = []
if forced:
lanes = set(forced)
reasons.append(f"forced lanes honored exactly: {sorted(lanes)}")
else:
lanes = {"code-review"}
reasons.append(
"code-review: always on — the gauntlet is only spawned for non-trivial work"
)
if text("plan_context"):
lanes.add("adversary")
reasons.append("adversary: a spec/plan/acceptance-criteria context is present")
if (
state.get("touches_auth")
or state.get("touches_deps")
or state.get("touches_exec")
or state.get("security_posture") == "hardened"
):
lanes.add("security")
reasons.append(
"security: attack-surface signals (auth/deps/exec) or hardened posture"
)
if state.get("consumer_surface") and text("probe_context"):
lanes.add("probe")
reasons.append(
"probe: consumer-facing surface changed and a local-run recipe/suite pointer exists"
)
# LLM judgment is additive-only.
llm_why = text("lane_reasons")[:300]
for flag, lane in (
("add_code_review", "code-review"),
("add_adversary", "adversary"),
("add_security", "security"),
("add_probe", "probe"),
):
if state.get(flag) and lane not in lanes:
if lane == "probe" and not text("probe_context"):
reasons.append(
"probe: requested by lane judgment but skipped — no local-run "
"recipe/suite pointer (a probe without one is guaranteed INCONCLUSIVE)"
)
continue
if lane == "adversary" and not text("plan_context"):
reasons.append(
"adversary: requested by lane judgment but skipped — no spec/plan "
"to check conformance against"
)
continue
lanes.add(lane)
reasons.append(f"{lane}: added by lane-selection judgment — {llm_why}")
if unknown:
reasons.append(f"ignored unknown forced lane name(s): {unknown}")
# A degraded lane selection (default_lanes fallback) must survive into the
# final report; lanes_summary is the only channel this script doesn't
# overwrite silently.
degraded = text("lanes_degraded")
if degraded:
reasons.insert(0, degraded)
print(
json.dumps(
{
"code_review_items": [{"lane": "code-review"}] if "code-review" in lanes else [],
"adversary_items": [{"lane": "adversary"}] if "adversary" in lanes else [],
"security_items": [{"lane": "security"}] if "security" in lanes else [],
"probe_items": [{"lane": "probe"}] if "probe" in lanes else [],
"lanes_summary": "\n".join(f"- {r}" for r in reasons),
}
)
)
try:
main()
except Exception as e: # noqa: BLE001 — a builder crash must never kill the graph
print(
json.dumps(
{
"code_review_items": [],
"adversary_items": [],
"security_items": [],
"probe_items": [],
"lanes_summary": "",
"signals_error": f"PIPELINE-FAULT: lane builder crashed — {e}; no lanes were run",
}
)
)