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)
This commit is contained in:
7 files changed
+603
-80
No files matched your search
@@ -8,20 +8,15 @@ 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
|
||||
|
||||
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", "{}"))
|
||||
|
||||
|
||||
state = load_state()
|
||||
|
||||
ALIASES = {
|
||||
"code-review": "code-review",
|
||||
"code_review": "code-review",
|
||||
@@ -39,80 +34,113 @@ ALIASES = {
|
||||
}
|
||||
|
||||
|
||||
def text(key):
|
||||
v = state.get(key)
|
||||
return v.strip() if isinstance(v, str) else ""
|
||||
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", "{}"))
|
||||
|
||||
|
||||
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)))
|
||||
def main():
|
||||
state = load_state()
|
||||
|
||||
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")
|
||||
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(
|
||||
"security: attack-surface signals (auth/deps/exec) or hardened posture"
|
||||
"code-review: always on — the gauntlet is only spawned for non-trivial work"
|
||||
)
|
||||
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"
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
||||
# 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}")
|
||||
|
||||
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),
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user