@riddledc/riddle-proof 0.8.10 → 0.8.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/advanced/engine-harness.cjs +97 -12
  2. package/dist/advanced/engine-harness.js +2 -2
  3. package/dist/advanced/index.cjs +98 -12
  4. package/dist/advanced/index.d.cts +2 -2
  5. package/dist/advanced/index.d.ts +2 -2
  6. package/dist/advanced/index.js +4 -4
  7. package/dist/advanced/proof-run-core.cjs +31 -1
  8. package/dist/advanced/proof-run-core.d.cts +1 -1
  9. package/dist/advanced/proof-run-core.d.ts +1 -1
  10. package/dist/advanced/proof-run-core.js +3 -1
  11. package/dist/advanced/proof-run-engine.cjs +46 -12
  12. package/dist/advanced/proof-run-engine.d.cts +2 -2
  13. package/dist/advanced/proof-run-engine.d.ts +2 -2
  14. package/dist/advanced/proof-run-engine.js +2 -2
  15. package/dist/advanced/runner.js +2 -2
  16. package/dist/{chunk-5N5QFI2S.js → chunk-7GZY5PLT.js} +31 -1
  17. package/dist/{chunk-46DDSZJR.js → chunk-JBY2SU5U.js} +18 -12
  18. package/dist/{chunk-5N6MQCLC.js → chunk-NGX4SUQN.js} +1 -1
  19. package/dist/{chunk-BBUO7HM4.js → chunk-RTLA6CPP.js} +53 -1
  20. package/dist/{chunk-2PXL3RDB.js → chunk-SZUC4MDN.js} +1 -1
  21. package/dist/cli/index.js +3 -3
  22. package/dist/cli.cjs +97 -12
  23. package/dist/cli.js +3 -3
  24. package/dist/engine-harness.cjs +97 -12
  25. package/dist/engine-harness.js +2 -2
  26. package/dist/index.cjs +97 -12
  27. package/dist/index.js +3 -3
  28. package/dist/{proof-run-core-Ci9uFxMc.d.cts → proof-run-core-CrpYH-qH.d.cts} +5 -2
  29. package/dist/{proof-run-core-Ci9uFxMc.d.ts → proof-run-core-CrpYH-qH.d.ts} +5 -2
  30. package/dist/proof-run-core.cjs +31 -1
  31. package/dist/proof-run-core.d.cts +1 -1
  32. package/dist/proof-run-core.d.ts +1 -1
  33. package/dist/proof-run-core.js +3 -1
  34. package/dist/{proof-run-engine-Bd1T43Dy.d.cts → proof-run-engine-C6vYAZd8.d.cts} +1 -1
  35. package/dist/{proof-run-engine-CXyhB-io.d.ts → proof-run-engine-h9C1lC0w.d.ts} +1 -1
  36. package/dist/proof-run-engine.cjs +46 -12
  37. package/dist/proof-run-engine.d.cts +2 -2
  38. package/dist/proof-run-engine.d.ts +2 -2
  39. package/dist/proof-run-engine.js +2 -2
  40. package/dist/runner.js +2 -2
  41. package/package.json +1 -1
  42. package/runtime/lib/verify.py +119 -19
  43. package/runtime/tests/recon_verify_smoke.py +208 -4
  44. package/runtime/tests/trust_boundary_regression.py +18 -0
@@ -505,6 +505,25 @@ function visualDeltaShipGateReason(state = {}) {
505
505
  if (reason) return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof: ${reason}`;
506
506
  return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof`;
507
507
  }
508
+ function proofAssessmentHardBlockersForState(state = {}) {
509
+ const request = objectValue(state?.proof_assessment_request);
510
+ const blockers = [];
511
+ const add = (value) => {
512
+ if (typeof value !== "string") return;
513
+ const trimmed = value.trim();
514
+ if (trimmed && !blockers.includes(trimmed)) blockers.push(trimmed);
515
+ };
516
+ if (Array.isArray(request.hard_blockers)) {
517
+ for (const blocker of request.hard_blockers) add(blocker);
518
+ }
519
+ add(state?.structured_interaction_capture_failure_summary);
520
+ add(state?.structured_interaction_failure_summary);
521
+ const mergeRecommendation = String(state?.merge_recommendation || "").trim();
522
+ if (mergeRecommendation === "do-not-merge" && blockers.length) {
523
+ add("merge_recommendation=do-not-merge because the proof bundle contains hard blockers.");
524
+ }
525
+ return blockers;
526
+ }
508
527
  function visualDeltaEvidenceIssueCode(state = {}, blocker = "") {
509
528
  const visualDelta = visualDeltaForState(state || {});
510
529
  const status = String(visualDelta.status || "").trim();
@@ -539,6 +558,7 @@ function validateShipGate(state = {}) {
539
558
  const visualDelta = visualDeltaForState(state);
540
559
  const visualDeltaRequired = visualDeltaRequiredForState(state);
541
560
  const visualDeltaBlocker = visualDeltaShipGateReason(state);
561
+ const hardBlockers = proofAssessmentHardBlockersForState(state);
542
562
  const reasons = [];
543
563
  if (!["before", "prod", "both"].includes(reference)) {
544
564
  reasons.push(`reference must be before, prod, or both; got ${reference}`);
@@ -570,6 +590,9 @@ function validateShipGate(state = {}) {
570
590
  if (visualDeltaBlocker) {
571
591
  reasons.push(visualDeltaBlocker);
572
592
  }
593
+ for (const blocker of hardBlockers) {
594
+ reasons.push(`proof hard blocker prevents ready_to_ship: ${blocker}`);
595
+ }
573
596
  return {
574
597
  ok: reasons.length === 0,
575
598
  reasons,
@@ -586,7 +609,8 @@ function validateShipGate(state = {}) {
586
609
  proof_assessment_source: proofAssessment.source,
587
610
  visual_delta_required: visualDeltaRequired,
588
611
  visual_delta_status: typeof visualDelta.status === "string" ? visualDelta.status : null,
589
- visual_delta_passed: typeof visualDelta.passed === "boolean" ? visualDelta.passed : null
612
+ visual_delta_passed: typeof visualDelta.passed === "boolean" ? visualDelta.passed : null,
613
+ hard_blockers: hardBlockers
590
614
  }
591
615
  };
592
616
  }
@@ -700,6 +724,10 @@ var CHECKPOINT_CONTRACT_SPECS = {
700
724
  }],
701
725
  required_state: ["verify_decision_request"]
702
726
  },
727
+ verify_capture_blocked: {
728
+ purpose: "Verify capture produced conclusive failed browser evidence and should stop instead of retrying proof authoring.",
729
+ required_state: ["verify_decision_request"]
730
+ },
703
731
  verify_supervisor_judgment: {
704
732
  purpose: "Supervising agent judges whether captured evidence proves the change is ready to ship.",
705
733
  accepted_inputs: [{
@@ -1284,14 +1312,18 @@ function verifyAssessment(state) {
1284
1312
  };
1285
1313
  }
1286
1314
  if (state?.verify_status === "capture_incomplete") {
1315
+ const captureQuality = verifyDecision?.capture_quality || {};
1316
+ const terminalBlocker = captureQuality?.terminal_blocker === true || captureQuality?.blocking === true;
1317
+ const recommendedStage = terminalBlocker ? null : verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author";
1318
+ const continueWithStage = terminalBlocker ? null : verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author";
1287
1319
  return {
1288
- decision: verifyDecision?.capture_quality?.decision || "revise_capture",
1320
+ decision: captureQuality?.decision || "revise_capture",
1289
1321
  summary: verifyDecision?.summary || "Verify needs another internal capture iteration before the evidence can be judged.",
1290
- recommendedStage: verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author",
1291
- continueWithStage: verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author",
1322
+ recommendedStage,
1323
+ continueWithStage,
1292
1324
  escalationTarget: "agent",
1293
- reasons: Array.isArray(verifyDecision?.capture_quality?.reasons) ? verifyDecision.capture_quality.reasons : [],
1294
- raw: verifyDecision?.capture_quality || verifyDecision,
1325
+ reasons: Array.isArray(captureQuality?.reasons) ? captureQuality.reasons : [],
1326
+ raw: captureQuality || verifyDecision,
1295
1327
  source: "workflow_capture"
1296
1328
  };
1297
1329
  }
@@ -2511,7 +2543,9 @@ ${implementRes.stderr || ""}`;
2511
2543
  convergenceSignals
2512
2544
  };
2513
2545
  if (verifyStatus !== "evidence_captured") {
2514
- if ((verifyContinueWithStage || verifyRecommendedStage || "author") === "author") {
2546
+ const captureQuality = verifyDecisionRequest?.capture_quality || {};
2547
+ const captureTerminalBlocker = captureQuality?.terminal_blocker === true || captureQuality?.blocking === true;
2548
+ if (!captureTerminalBlocker && (verifyContinueWithStage || verifyRecommendedStage || "author") === "author") {
2515
2549
  updateState(config.statePath, (currentState) => {
2516
2550
  currentState.author_status = "needs_authoring";
2517
2551
  currentState.proof_plan_status = "needs_authoring";
@@ -2519,7 +2553,7 @@ ${implementRes.stderr || ""}`;
2519
2553
  });
2520
2554
  state = readState(config.statePath);
2521
2555
  }
2522
- const checkpointName = "verify_capture_retry";
2556
+ const checkpointName = captureTerminalBlocker ? "verify_capture_blocked" : "verify_capture_retry";
2523
2557
  const summary = stringValue(proofAssessment.summary) || "Verify ran, but the proof packet still needs internal capture-plan work before it should ship.";
2524
2558
  recordAttempt("verify", "checkpoint", summary, {
2525
2559
  autoApproved: verifyRes.autoApproved || false,
@@ -2532,11 +2566,11 @@ ${implementRes.stderr || ""}`;
2532
2566
  summary,
2533
2567
  {
2534
2568
  ok: true,
2535
- nextActions: ["inspect_after_capture", "continue_internal_loop_with_checkpoint", "return_to_recon_if_baseline_is_wrong"],
2569
+ nextActions: captureTerminalBlocker ? ["inspect_after_capture", "report_specific_browser_evidence_blocker", "start_a_new_run_after_the_product_or_script_is_fixed"] : ["inspect_after_capture", "continue_internal_loop_with_checkpoint", "return_to_recon_if_baseline_is_wrong"],
2536
2570
  advanceOptions: verifyLoopAdvanceOptions,
2537
- recommendedAdvanceStage: verifyRecommendedStage || "author",
2538
- continueWithStage: verifyContinueWithStage || "author",
2539
- blocking: false,
2571
+ recommendedAdvanceStage: captureTerminalBlocker ? null : verifyRecommendedStage || "author",
2572
+ continueWithStage: captureTerminalBlocker ? null : verifyContinueWithStage || "author",
2573
+ blocking: captureTerminalBlocker,
2540
2574
  details: verifyDetails,
2541
2575
  verifyStatus,
2542
2576
  verifySummary,
@@ -1,2 +1,2 @@
1
- import './proof-run-core-Ci9uFxMc.cjs';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-Bd1T43Dy.cjs';
1
+ import './proof-run-core-CrpYH-qH.cjs';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-C6vYAZd8.cjs';
@@ -1,2 +1,2 @@
1
- import './proof-run-core-Ci9uFxMc.js';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-CXyhB-io.js';
1
+ import './proof-run-core-CrpYH-qH.js';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-h9C1lC0w.js';
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createRiddleProofEngine,
3
3
  executeWorkflow
4
- } from "./chunk-46DDSZJR.js";
5
- import "./chunk-5N5QFI2S.js";
4
+ } from "./chunk-JBY2SU5U.js";
5
+ import "./chunk-7GZY5PLT.js";
6
6
  import "./chunk-MLKGABMK.js";
7
7
  export {
8
8
  createRiddleProofEngine,
package/dist/runner.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  runRiddleProof
3
- } from "./chunk-5N6MQCLC.js";
3
+ } from "./chunk-NGX4SUQN.js";
4
4
  import "./chunk-YZUVEJ5B.js";
5
5
  import "./chunk-FMOYUYH2.js";
6
- import "./chunk-5N5QFI2S.js";
6
+ import "./chunk-7GZY5PLT.js";
7
7
  import "./chunk-4FOHZ7JG.js";
8
8
  import "./chunk-VY4Y5U57.js";
9
9
  import "./chunk-MLKGABMK.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.8.10",
3
+ "version": "0.8.12",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -2061,7 +2061,24 @@ EXPLICIT_TERMINAL_PATH_KEYS = (
2061
2061
  'final_url', 'finalUrl',
2062
2062
  'final_route', 'finalRoute',
2063
2063
  )
2064
- LOCATION_PATH_KEYS = ('path', 'pathname', 'route', 'url', 'href')
2064
+ FULL_LOCATION_PATH_KEYS = (
2065
+ 'url', 'href',
2066
+ 'hrefNormalized', 'href_normalized',
2067
+ 'terminalUrl', 'terminal_url',
2068
+ 'afterUrl', 'after_url',
2069
+ 'finalUrl', 'final_url',
2070
+ 'currentUrl', 'current_url',
2071
+ 'pathWithSearchAndHash', 'path_with_search_and_hash',
2072
+ 'fullPath', 'full_path',
2073
+ )
2074
+ PARTIAL_LOCATION_PATH_KEYS = (
2075
+ 'route',
2076
+ 'path',
2077
+ 'pathname',
2078
+ 'normalizedPath', 'normalized_path',
2079
+ 'rawPath', 'raw_path',
2080
+ )
2081
+ LOCATION_PATH_KEYS = FULL_LOCATION_PATH_KEYS + PARTIAL_LOCATION_PATH_KEYS
2065
2082
  AFTER_STATE_KEYS = (
2066
2083
  'after', 'after_state', 'afterState',
2067
2084
  'expected_after', 'expectedAfter',
@@ -2158,18 +2175,29 @@ def text_path_candidate(value):
2158
2175
  return path_candidate(raw)
2159
2176
 
2160
2177
 
2178
+ def text_route_candidate(value):
2179
+ candidate = text_path_candidate(value)
2180
+ if not candidate:
2181
+ return ''
2182
+ parsed = urlparse(candidate)
2183
+ first_segment = next((part for part in (parsed.path or '').split('/') if part), '')
2184
+ if first_segment and first_segment[:1].isupper():
2185
+ return ''
2186
+ return candidate
2187
+
2188
+
2161
2189
  def terminal_path_from_text(value):
2162
2190
  if not isinstance(value, str):
2163
2191
  return ''
2164
2192
  for match in re.findall(r"""['"`](/[^'"`\s]+[?#][^'"`\s]*)['"`]""", value):
2165
- candidate = text_path_candidate(match)
2193
+ candidate = text_route_candidate(match)
2166
2194
  if candidate:
2167
2195
  return candidate
2168
2196
  context_pattern = re.compile(
2169
- r"""(?is)\b(?:expected\s+(?:terminal|after|final)|terminal|after|final)\b[^/\r\n]{0,120}['"`]?(/[^'"`\s,;)]*)"""
2197
+ r"""(?is)\b(?:expected\s+(?:terminal|after|final)(?:\s+(?:route|path|url))?|terminal(?:\s+(?:route|path|url))?|after(?:\s+(?:route|path|url))?|final(?:\s+(?:route|path|url))?)\s*(?:should\s+(?:be|equal|match)|must\s+(?:be|equal|match)|is|as|to|=|:)?\s*['"`]?(/[^'"`\s,;)]*)"""
2170
2198
  )
2171
2199
  for match in context_pattern.findall(value):
2172
- candidate = text_path_candidate(match)
2200
+ candidate = text_route_candidate(match)
2173
2201
  if candidate:
2174
2202
  return candidate
2175
2203
  return ''
@@ -2372,6 +2400,25 @@ def failed_interaction_evidence_summary(proof_evidence):
2372
2400
  return summary
2373
2401
 
2374
2402
 
2403
+ def interaction_capture_failure_evidence_summary(proof_evidence):
2404
+ for record in proof_evidence_records_deep(proof_evidence):
2405
+ if not isinstance(record, dict):
2406
+ continue
2407
+ version = str(record.get('version') or '').strip()
2408
+ source = str(record.get('source') or '').strip()
2409
+ if version != 'riddle-proof.interaction.capture-failure.v1' and source != 'verify_capture_failure':
2410
+ continue
2411
+ summary = str(record.get('evidence_summary') or '').strip() or 'Interaction capture failed before usable authored proof evidence was emitted.'
2412
+ failures = collect_interaction_failed_assertions(record)
2413
+ if failures:
2414
+ summary += ' Failed checks: ' + ', '.join(failures[:8]) + '.'
2415
+ error = str(record.get('capture_error') or record.get('error') or '').strip()
2416
+ if error:
2417
+ summary += ' Capture script error: ' + error[:300]
2418
+ return summary
2419
+ return ''
2420
+
2421
+
2375
2422
  def interaction_terminal_path_from_evidence(proof_evidence):
2376
2423
  for record in proof_evidence_records(proof_evidence):
2377
2424
  candidate = terminal_path_from_record(record)
@@ -2389,6 +2436,13 @@ def interaction_terminal_path_from_evidence(proof_evidence):
2389
2436
 
2390
2437
 
2391
2438
  def interaction_terminal_path_from_state(state):
2439
+ for key in (
2440
+ 'expected_terminal_path',
2441
+ 'expected_after_path',
2442
+ ):
2443
+ candidate = path_candidate(state.get(key))
2444
+ if candidate:
2445
+ return candidate, key
2392
2446
  for key in (
2393
2447
  'interaction_contract',
2394
2448
  'proof_contract',
@@ -2401,19 +2455,32 @@ def interaction_terminal_path_from_state(state):
2401
2455
  if candidate:
2402
2456
  return candidate, key
2403
2457
  for key in (
2404
- 'expected_terminal_path',
2405
- 'expected_after_path',
2406
2458
  'capture_script',
2407
2459
  'proof_plan',
2408
- 'success_criteria',
2409
- 'change_request',
2410
2460
  ):
2411
- candidate = path_candidate(state.get(key)) or terminal_path_from_text(state.get(key))
2461
+ candidate = terminal_path_from_text(state.get(key))
2412
2462
  if candidate:
2413
2463
  return candidate, key
2414
2464
  return '', ''
2415
2465
 
2416
2466
 
2467
+ def proof_evidence_should_override_state_terminal_path(state_candidate, evidence_candidate, proof_evidence):
2468
+ if not evidence_candidate:
2469
+ return False
2470
+ if not state_candidate:
2471
+ return True
2472
+ if route_matches_expected(state_candidate, evidence_candidate):
2473
+ return False
2474
+ if interaction_assertions_pass(proof_evidence):
2475
+ return True
2476
+ for record in proof_evidence_records_deep(proof_evidence):
2477
+ if interaction_assertions_pass(record):
2478
+ return True
2479
+ if explicit_route_match_flag(record) is True:
2480
+ return True
2481
+ return False
2482
+
2483
+
2417
2484
  def expected_path_for_verify(state, start_path, proof_evidence):
2418
2485
  mode = normalized_verification_mode(state.get('verification_mode'))
2419
2486
  normalized_start = normalize_observed_path(start_path) or '/'
@@ -2431,9 +2498,14 @@ def expected_path_for_verify(state, start_path, proof_evidence):
2431
2498
  'expected_query': start_parts['query'],
2432
2499
  'expected_hash': start_parts['hash'],
2433
2500
  }
2434
- candidate, source = interaction_terminal_path_from_state(state)
2501
+ state_candidate, state_source = interaction_terminal_path_from_state(state)
2502
+ evidence_candidate, evidence_source = interaction_terminal_path_from_evidence(proof_evidence)
2503
+ if proof_evidence_should_override_state_terminal_path(state_candidate, evidence_candidate, proof_evidence):
2504
+ candidate, source = evidence_candidate, evidence_source
2505
+ else:
2506
+ candidate, source = state_candidate, state_source
2435
2507
  if not candidate:
2436
- candidate, source = interaction_terminal_path_from_evidence(proof_evidence)
2508
+ candidate, source = evidence_candidate, evidence_source
2437
2509
  expected = candidate or normalized_start
2438
2510
  expected_parts = route_parts(expected)
2439
2511
  return expected, {
@@ -2824,10 +2896,17 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2824
2896
 
2825
2897
  if proof_evidence_blocker:
2826
2898
  reasons.append(proof_evidence_blocker)
2827
- decision = 'missing_proof_evidence'
2899
+ interaction_capture_blocker = (
2900
+ proof_evidence_blocker.startswith('Interaction capture ')
2901
+ or 'Interaction capture failed before usable authored proof evidence was emitted' in proof_evidence_blocker
2902
+ or 'Interaction capture reached a different terminal route' in proof_evidence_blocker
2903
+ )
2904
+ decision = 'failed_interaction_capture' if interaction_capture_blocker else 'missing_proof_evidence'
2828
2905
  if 'proof_evidence_present=false' in proof_evidence_blocker:
2829
2906
  decision = 'failed_proof_evidence'
2830
2907
  reasons.append('The capture reached usable page context, but the proof evidence explicitly failed its own required audio gate.')
2908
+ elif interaction_capture_blocker:
2909
+ reasons.append('The capture produced conclusive structured interaction-failure evidence, so this run should block with that specific browser evidence instead of re-authoring in a loop.')
2831
2910
  else:
2832
2911
  reasons.append('The capture reached usable page context, but the proof script did not emit the structured evidence required for this verification mode.')
2833
2912
  if route_mismatch:
@@ -2838,7 +2917,10 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2838
2917
  (route_mismatch.get('observed_after_path') or '(unknown)') +
2839
2918
  '.'
2840
2919
  )
2841
- reasons.append('Return to author so the capture script can expose passing proof evidence before verify asks for a supervising-agent judgment.')
2920
+ if interaction_capture_blocker:
2921
+ reasons.append('Do not ask the authoring loop to infer a new route; the captured browser evidence is the terminal blocker.')
2922
+ else:
2923
+ reasons.append('Return to author so the capture script can expose passing proof evidence before verify asks for a supervising-agent judgment.')
2842
2924
  summary = proof_evidence_blocker
2843
2925
  if route_mismatch:
2844
2926
  summary += (
@@ -2854,8 +2936,10 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2854
2936
  return {
2855
2937
  'decision': decision,
2856
2938
  'summary': summary,
2857
- 'recommended_stage': 'author',
2858
- 'continue_with_stage': 'author',
2939
+ 'recommended_stage': None if interaction_capture_blocker else 'author',
2940
+ 'continue_with_stage': None if interaction_capture_blocker else 'author',
2941
+ 'blocking': bool(interaction_capture_blocker),
2942
+ 'terminal_blocker': bool(interaction_capture_blocker),
2859
2943
  'reasons': reasons,
2860
2944
  'mismatch': route_mismatch,
2861
2945
  }
@@ -3631,12 +3715,17 @@ if proof_evidence_required_for_mode(s.get('verification_mode')):
3631
3715
  summary_lines.append('Structured proof evidence gate: ' + proof_evidence_blocker)
3632
3716
 
3633
3717
  structured_interaction_failure_summary = ''
3718
+ structured_interaction_capture_failure_summary = ''
3634
3719
  proof_evidence = evidence_bundle.get('proof_evidence')
3635
3720
  if verification_mode in INTERACTION_MODES and proof_evidence is not None:
3636
3721
  structured_interaction_failure_summary = failed_interaction_evidence_summary(proof_evidence)
3722
+ structured_interaction_capture_failure_summary = interaction_capture_failure_evidence_summary(proof_evidence)
3637
3723
  if structured_interaction_failure_summary:
3638
3724
  summary_lines.append('Structured interaction evidence gate: ' + structured_interaction_failure_summary)
3725
+ if structured_interaction_capture_failure_summary:
3726
+ summary_lines.append('Structured interaction capture blocker: ' + structured_interaction_capture_failure_summary)
3639
3727
  s['structured_interaction_failure_summary'] = structured_interaction_failure_summary
3728
+ s['structured_interaction_capture_failure_summary'] = structured_interaction_capture_failure_summary
3640
3729
 
3641
3730
  visual_delta_recovery = build_visual_delta_recovery_decision(
3642
3731
  s.get('verification_mode'),
@@ -3648,6 +3737,7 @@ if visual_delta_recovery:
3648
3737
 
3649
3738
  has_judgable_failed_interaction_evidence = (
3650
3739
  bool(structured_interaction_failure_summary)
3740
+ and not structured_interaction_capture_failure_summary
3651
3741
  and required_baseline_present
3652
3742
  and not proof_evidence_blocker
3653
3743
  and not visual_delta_recovery
@@ -3656,6 +3746,7 @@ has_good_evidence = (
3656
3746
  required_baseline_present
3657
3747
  and (after_observation.get('valid') or has_judgable_failed_interaction_evidence)
3658
3748
  and not proof_evidence_blocker
3749
+ and not structured_interaction_capture_failure_summary
3659
3750
  and not visual_delta_recovery
3660
3751
  )
3661
3752
 
@@ -3712,7 +3803,12 @@ if has_good_evidence:
3712
3803
  summary_lines.append('Proof assessment: awaiting supervising agent judgment')
3713
3804
  summary_lines.append('Proof next stage: supervising agent decides after reviewing the evidence packet')
3714
3805
  else:
3715
- capture_retry = build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker, s.get('route_expectation') or {})
3806
+ capture_retry = build_capture_retry_decision(
3807
+ after_observation,
3808
+ required_baseline_present,
3809
+ proof_evidence_blocker or structured_interaction_capture_failure_summary,
3810
+ s.get('route_expectation') or {},
3811
+ )
3716
3812
  if visual_delta_recovery:
3717
3813
  observation_reason = str(after_observation.get('reason') or '')
3718
3814
  observation_details = after_observation.get('details') if isinstance(after_observation.get('details'), dict) else {}
@@ -3721,6 +3817,7 @@ else:
3721
3817
  or 'console/runtime errors' in observation_reason
3722
3818
  or (observation_details.get('capture_error_messages') or [])
3723
3819
  or proof_evidence_blocker
3820
+ or structured_interaction_capture_failure_summary
3724
3821
  )
3725
3822
  if has_primary_capture_failure:
3726
3823
  capture_retry['visual_delta_recovery'] = visual_delta_recovery
@@ -3728,6 +3825,9 @@ else:
3728
3825
  else:
3729
3826
  capture_retry = visual_delta_recovery
3730
3827
  next_stage_options = ['author', 'verify', 'recon'] if no_implementation_mode else ['author', 'verify', 'implement', 'recon']
3828
+ capture_terminal_blocker = bool(capture_retry.get('terminal_blocker') or capture_retry.get('blocking'))
3829
+ recommended_stage = None if capture_terminal_blocker else (capture_retry.get('recommended_stage') or 'author')
3830
+ continue_with_stage = None if capture_terminal_blocker else (capture_retry.get('continue_with_stage') or 'author')
3731
3831
  s['verify_status'] = 'capture_incomplete'
3732
3832
  s['merge_recommendation'] = 'do-not-merge'
3733
3833
  s['proof_assessment'] = {}
@@ -3742,8 +3842,8 @@ else:
3742
3842
  'latest_observation': after_observation,
3743
3843
  'capture_quality': capture_retry,
3744
3844
  'next_stage_options': next_stage_options,
3745
- 'recommended_stage': capture_retry.get('recommended_stage') or 'author',
3746
- 'continue_with_stage': capture_retry.get('continue_with_stage') or 'author',
3845
+ 'recommended_stage': recommended_stage,
3846
+ 'continue_with_stage': continue_with_stage,
3747
3847
  'fields_agent_may_update': ['capture_script', 'server_path', 'wait_for_selector', 'proof_plan'],
3748
3848
  'instructions': [
3749
3849
  'The after-proof evidence packet is incomplete, so use the recommended stage before proof review.',
@@ -3753,7 +3853,7 @@ else:
3753
3853
  ],
3754
3854
  }
3755
3855
  summary_lines.append('Proof assessment: not yet possible because the after capture is still incomplete')
3756
- summary_lines.append('Proof next stage: ' + str(capture_retry.get('recommended_stage') or 'author'))
3856
+ summary_lines.append('Proof next stage: blocked' if capture_terminal_blocker else 'Proof next stage: ' + str(recommended_stage or 'author'))
3757
3857
 
3758
3858
  s['verify_summary'] = '\n'.join(summary_lines)
3759
3859
  s['proof_summary'] = s['verify_summary']