feat(TASK-020): finding-verifier + step-runner fail-closed hardening

finding-verifier: parse gains max_attempts 2 + parse_failure capture +
fallback to a new parse_fault marker that rewrites verdicts to a single
UNVERIFIABLE PIPELINE-FAULT entry and continues to done (sentinel
preserved — the caller's verify lane degrades instead of dying);
verify_one gains a branch-local verify_fault marker normalizing the
engine failure text into a schema-conformant UNVERIFIABLE verdict.

step-runner: implement gains fallback end_failure (no retry — reruns
against a mutated tree are unsafe; STEP_FAILED renders the failure via
coder_result); independent_review gains fallback write_handoff with the
handoff instructions and route_review treating an "Agent node failed:"
review_report as review-did-not-run (fail-visible, guard ahead of the
🔴 grep); orient/edge_case_sweep/write_handoff capture their failure
text via state_updates, shrinking the bundled fallback-capture warning
baseline back to zero.

R5 tests: fault-marker normalization + crash paths (raw non-JSON
GRAPH_STATE), route_review.sh fault/critical/clean routing via bash+jq,
and structural wiring tests pinning fallbacks, sentinels, and
end_failure's initial_state-backed template keys.
This commit is contained in:
Dark-Alex-17 committed 2026-09-14 10:50:33 -06:00
1 parent a8496aa50e
commit 4263d684e9
6 files changed
+585 -24

No files matched your search

+25
View File
@@ -64,6 +64,7 @@ initial_state:
plan_summary: ''
implementation_brief: ''
staleness_report: ''
orient_failure: ''
has_major_deviation: false
deviation_summary: ''
user_feedback: ''
@@ -80,12 +81,14 @@ initial_state:
tests_output: ''
edge_case_report: ''
downstream_updates: ''
sweep_failure: ''
needs_independent_review: false
review_report: ''
review_attempts: 0
max_review_attempts: 1
handoff_attempts: 0
handoff_fix: ''
handoff_failure: ''
step_summary: ''
start: resolve_step
@@ -191,6 +194,8 @@ nodes:
has_major_deviation,
deviation_summary,
]
state_updates:
orient_failure: '{{output}}'
fallback: end_failure
next: route_staleness
@@ -270,6 +275,12 @@ nodes:
timeout: 3600
state_updates:
coder_result: '{{output}}'
# No max_attempts: rerunning a coder that died mid-edit against an
# already-mutated tree is unsafe. On failure the "Agent node failed:"
# text lands in coder_result (via state_updates above) and end_failure
# renders it; end_failure's other keys all have initial_state defaults,
# so the STEP_FAILED output degrades to empty build/tests sections.
fallback: end_failure
next: route_coder_result
route_coder_result:
@@ -376,6 +387,8 @@ nodes:
needs_independent_review:
type: boolean
required: [edge_case_report, downstream_updates, needs_independent_review]
state_updates:
sweep_failure: '{{output}}'
fallback: write_handoff
next: route_sweep
@@ -408,6 +421,7 @@ nodes:
max_attempts: 2
state_updates:
review_report: '{{output}}'
fallback: write_handoff
next: route_review
route_review:
@@ -451,6 +465,15 @@ nodes:
If "Gate feedback" below is non-empty, a previous handoff attempt
failed validation - fix exactly what it lists.
PIPELINE-FAULT VISIBILITY: an "Independent review report" below that
starts with "Agent node failed:" means the independent review DID NOT
RUN - the graph routed here through the reviewer's failure fallback.
Never present that text as review findings. Flag it prominently
instead: add a Deviations entry "PIPELINE-FAULT: independent review
did not run - <failure text>", repeat it under Notes for next step,
and carry it into `step_summary` so the user sees it at the approval
gate.
prompt: |
## Step
{{step_number}} ({{step_title}}) - plan at {{step_plan_path}}
@@ -502,6 +525,8 @@ nodes:
type: string
description: 3-6 sentence summary of the step for the user's approval decision - what was done, deviations, anything needing their attention
required: [step_summary]
state_updates:
handoff_failure: '{{output}}'
fallback: end_failure
next: check_handoff
@@ -13,6 +13,16 @@ review_report=$(echo "$state" | jq -r '.review_report // ""')
review_attempts=$(echo "$state" | jq -r '.review_attempts // 0')
max_review_attempts=$(echo "$state" | jq -r '.max_review_attempts // 1')
# Fail-visible guard: a review_report holding the engine's "Agent node
# failed:" text means the independent review DID NOT RUN (its fallback
# normally routes straight to write_handoff, bypassing this script). Never
# treat that text as findings or spend a fix-loop attempt on it -
# write_handoff flags the fault prominently in the handoff instead.
if [[ "$review_report" == "Agent node failed:"* ]]; then
jq -nc '{"_next": "write_handoff"}'
exit 0
fi
if ! grep -qF "🔴" <<< "$review_report"; then
jq -nc '{"_next": "write_handoff"}'
exit 0