@riddledc/riddle-proof 0.8.3 → 0.8.5

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 (49) hide show
  1. package/dist/adapters/openclaw.js +4 -4
  2. package/dist/advanced/engine-harness.cjs +34 -12
  3. package/dist/advanced/engine-harness.js +4 -4
  4. package/dist/advanced/index.cjs +34 -12
  5. package/dist/advanced/index.d.cts +1 -1
  6. package/dist/advanced/index.d.ts +1 -1
  7. package/dist/advanced/index.js +6 -6
  8. package/dist/advanced/proof-run-engine.cjs +29 -9
  9. package/dist/advanced/proof-run-engine.d.cts +1 -1
  10. package/dist/advanced/proof-run-engine.d.ts +1 -1
  11. package/dist/advanced/proof-run-engine.js +1 -1
  12. package/dist/advanced/runner.js +4 -4
  13. package/dist/checkpoint.cjs +3 -1
  14. package/dist/checkpoint.js +1 -1
  15. package/dist/{chunk-SKIAZTQ7.js → chunk-4FOHZ7JG.js} +3 -1
  16. package/dist/{chunk-YB5ACBZE.js → chunk-BDFSMWTI.js} +5 -5
  17. package/dist/{chunk-U4FUFBSH.js → chunk-FMOYUYH2.js} +1 -1
  18. package/dist/{chunk-TZ3YMCDM.js → chunk-GMZ57RRY.js} +29 -9
  19. package/dist/{chunk-OIFHYMHP.js → chunk-OD5UNE57.js} +2 -2
  20. package/dist/{chunk-SMBZT46I.js → chunk-RBWSCU6V.js} +1 -1
  21. package/dist/{chunk-ZX45XGDJ.js → chunk-UIJ7X63P.js} +1 -1
  22. package/dist/{chunk-TNCDVE5O.js → chunk-YZUVEJ5B.js} +1 -1
  23. package/dist/cli/index.js +5 -5
  24. package/dist/cli.cjs +34 -12
  25. package/dist/cli.js +5 -5
  26. package/dist/engine-harness.cjs +34 -12
  27. package/dist/engine-harness.js +4 -4
  28. package/dist/index.cjs +34 -12
  29. package/dist/index.js +5 -5
  30. package/dist/openclaw.js +4 -4
  31. package/dist/{proof-run-engine-Rkd_hXB-.d.cts → proof-run-engine-B7DCPzpK.d.cts} +3 -3
  32. package/dist/{proof-run-engine-DxWW1VX1.d.ts → proof-run-engine-BomAcXhA.d.ts} +3 -3
  33. package/dist/proof-run-engine.cjs +29 -9
  34. package/dist/proof-run-engine.d.cts +1 -1
  35. package/dist/proof-run-engine.d.ts +1 -1
  36. package/dist/proof-run-engine.js +1 -1
  37. package/dist/run-card.js +2 -2
  38. package/dist/runner.js +4 -4
  39. package/dist/spec/checkpoint.cjs +3 -1
  40. package/dist/spec/checkpoint.js +1 -1
  41. package/dist/spec/index.cjs +3 -1
  42. package/dist/spec/index.js +3 -3
  43. package/dist/spec/run-card.js +2 -2
  44. package/dist/spec/state.js +3 -3
  45. package/dist/state.js +3 -3
  46. package/package.json +1 -1
  47. package/runtime/lib/util.py +57 -0
  48. package/runtime/lib/verify.py +158 -14
  49. package/runtime/tests/recon_verify_smoke.py +211 -0
@@ -1925,9 +1925,9 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
1925
1925
  state = readState(config.statePath);
1926
1926
  const continuedStage = params.continue_from_checkpoint ? checkpointContinueStage(state) : null;
1927
1927
  if (params.continue_from_checkpoint && !params.advance_stage && !continuedStage) {
1928
- const recommended = recommendedAdvanceStage(state);
1928
+ const recommended2 = recommendedAdvanceStage(state);
1929
1929
  return checkpoint(
1930
- state?.active_checkpoint_stage || recommended || "recon",
1930
+ state?.active_checkpoint_stage || recommended2 || "recon",
1931
1931
  "continue_unavailable",
1932
1932
  "This run call asked to continue from a checkpoint, but the current state has no resumable checkpoint. Inspect status or set advance_stage explicitly.",
1933
1933
  {
@@ -1939,9 +1939,9 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
1939
1939
  details: {
1940
1940
  executed,
1941
1941
  activeCheckpoint: state?.active_checkpoint || null,
1942
- suggestedAdvanceStage: recommended || null
1942
+ suggestedAdvanceStage: recommended2 || null
1943
1943
  },
1944
- suggestedAdvanceStage: recommended || null,
1944
+ suggestedAdvanceStage: recommended2 || null,
1945
1945
  executed
1946
1946
  }
1947
1947
  );
@@ -2205,7 +2205,7 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
2205
2205
  }
2206
2206
  const noImplementationMode = !implementationRequired(params, state);
2207
2207
  const authorNextStage = stageAfterAuthor(state, params);
2208
- const explicitAuthorDebug = params.advance_stage === "author";
2208
+ const explicitAuthorDebug = params.advance_stage === "author" && !params.continue_from_checkpoint;
2209
2209
  recordAttempt("author", "completed", "Author applied the supervising agent's proof packet to recon observations.", {
2210
2210
  autoApproved: authorRes.autoApproved || false,
2211
2211
  checkpoint: explicitAuthorDebug ? "author_review" : "author_auto_continue",
@@ -2254,16 +2254,16 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
2254
2254
  state = readState(config.statePath);
2255
2255
  }
2256
2256
  if (!effectiveAdvanceStage) {
2257
- const recommended = recommendedAdvanceStage(state);
2257
+ const recommended2 = recommendedAdvanceStage(state);
2258
2258
  const noImplementationMode = !implementationRequired(params, state);
2259
2259
  return checkpoint(
2260
- recommended || (noImplementationMode ? "verify" : "implement"),
2260
+ recommended2 || (noImplementationMode ? "verify" : "implement"),
2261
2261
  "awaiting_stage_advance",
2262
2262
  "Proof authoring is ready. The wrapper will not guess the next stage from here, explicitly choose whether to revisit recon/author, validate implementation, capture verify evidence, or ship.",
2263
2263
  {
2264
2264
  nextActions: ["inspect_state", "set_advance_stage", "resume_run"],
2265
2265
  advanceOptions: noImplementationMode ? ["recon", "author", "verify", "ship"] : ["recon", "author", "implement", "verify", "ship"],
2266
- recommendedAdvanceStage: recommended,
2266
+ recommendedAdvanceStage: recommended2,
2267
2267
  details: { executed },
2268
2268
  executed
2269
2269
  }
@@ -2463,7 +2463,7 @@ ${implementRes.stderr || ""}`;
2463
2463
  state = readState(config.statePath);
2464
2464
  }
2465
2465
  const checkpointName = "verify_capture_retry";
2466
- const summary = "Verify ran, but the proof packet still needs internal capture-plan work before it should ship.";
2466
+ const summary = stringValue(proofAssessment.summary) || "Verify ran, but the proof packet still needs internal capture-plan work before it should ship.";
2467
2467
  recordAttempt("verify", "checkpoint", summary, {
2468
2468
  autoApproved: verifyRes.autoApproved || false,
2469
2469
  checkpoint: checkpointName,
@@ -2819,6 +2819,26 @@ ${implementRes.stderr || ""}`;
2819
2819
  }
2820
2820
  );
2821
2821
  }
2822
+ state = readState(config.statePath);
2823
+ const recommended = recommendedAdvanceStage(state);
2824
+ const fallbackStage = recommended || (implementationRequired(params, state) ? "implement" : "verify");
2825
+ return checkpoint(
2826
+ fallbackStage,
2827
+ "awaiting_stage_advance",
2828
+ "The requested run continuation finished its immediate work but did not resolve to verify or ship. Choose the next workflow stage explicitly.",
2829
+ {
2830
+ nextActions: ["inspect_state", "set_advance_stage", "resume_run"],
2831
+ advanceOptions: implementationRequired(params, state) ? ["recon", "author", "implement", "verify", "ship"] : ["recon", "author", "verify"],
2832
+ recommendedAdvanceStage: fallbackStage,
2833
+ continueWithStage: fallbackStage,
2834
+ blocking: false,
2835
+ details: {
2836
+ executed,
2837
+ requestedAdvanceStage: effectiveAdvanceStage
2838
+ },
2839
+ executed
2840
+ }
2841
+ );
2822
2842
  }
2823
2843
  if (action === "ship") {
2824
2844
  const state = readState(config.statePath);
@@ -1,2 +1,2 @@
1
1
  import './proof-run-core-CE0jx7wL.cjs';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-Rkd_hXB-.cjs';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-B7DCPzpK.cjs';
@@ -1,2 +1,2 @@
1
1
  import './proof-run-core-CE0jx7wL.js';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-DxWW1VX1.js';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-BomAcXhA.js';
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createRiddleProofEngine,
3
3
  executeWorkflow
4
- } from "./chunk-TZ3YMCDM.js";
4
+ } from "./chunk-GMZ57RRY.js";
5
5
  import "./chunk-RV6LK7HU.js";
6
6
  import "./chunk-MLKGABMK.js";
7
7
  export {
package/dist/run-card.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  RIDDLE_PROOF_RUN_CARD_VERSION,
3
3
  createRiddleProofRunCard
4
- } from "./chunk-U4FUFBSH.js";
5
- import "./chunk-SKIAZTQ7.js";
4
+ } from "./chunk-FMOYUYH2.js";
5
+ import "./chunk-4FOHZ7JG.js";
6
6
  import "./chunk-VY4Y5U57.js";
7
7
  import "./chunk-MLKGABMK.js";
8
8
  export {
package/dist/runner.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runRiddleProof
3
- } from "./chunk-ZX45XGDJ.js";
4
- import "./chunk-TNCDVE5O.js";
5
- import "./chunk-U4FUFBSH.js";
3
+ } from "./chunk-UIJ7X63P.js";
4
+ import "./chunk-YZUVEJ5B.js";
5
+ import "./chunk-FMOYUYH2.js";
6
6
  import "./chunk-RV6LK7HU.js";
7
- import "./chunk-SKIAZTQ7.js";
7
+ import "./chunk-4FOHZ7JG.js";
8
8
  import "./chunk-VY4Y5U57.js";
9
9
  import "./chunk-MLKGABMK.js";
10
10
  export {
@@ -121,7 +121,7 @@ function responseSchemaForAuthorPacket() {
121
121
  summary: { type: "string" },
122
122
  payload: {
123
123
  type: "object",
124
- description: "For decision=author_packet, provide the proof packet itself or {author_packet:{...}} with proof_plan and capture_script."
124
+ description: "For decision=author_packet, provide the proof packet itself or {author_packet:{...}} with proof_plan, capture_script, and refined_inputs.expected_terminal_path when the proof changes route, query, or hash."
125
125
  },
126
126
  reasons: { type: "array", items: { type: "string" } },
127
127
  continue_with_stage: { type: "string", enum: ["author", "recon"] },
@@ -428,9 +428,11 @@ function buildAuthorCheckpointPacket(input) {
428
428
  repo: input.request.repo || fullState.repo,
429
429
  branch: input.request.branch || fullState.branch,
430
430
  verification_mode: input.request.verification_mode || fullState.verification_mode,
431
+ success_criteria: fullState.success_criteria,
431
432
  reference: input.request.reference || fullState.reference,
432
433
  server_path: fullState.server_path,
433
434
  wait_for_selector: fullState.wait_for_selector,
435
+ route_expectation: jsonCloneRecord(fullState.route_expectation),
434
436
  author_summary: fullState.author_summary,
435
437
  author_request: jsonCloneRecord(authorRequest),
436
438
  recon_baseline_understanding: jsonCloneRecord(fullState.recon_baseline_understanding),
@@ -13,7 +13,7 @@ import {
13
13
  normalizeCheckpointResponse,
14
14
  proofContractFromAuthorCheckpointResponse,
15
15
  statePathsForRunState
16
- } from "../chunk-SKIAZTQ7.js";
16
+ } from "../chunk-4FOHZ7JG.js";
17
17
  import "../chunk-VY4Y5U57.js";
18
18
  import "../chunk-MLKGABMK.js";
19
19
  export {
@@ -344,7 +344,7 @@ function responseSchemaForAuthorPacket() {
344
344
  summary: { type: "string" },
345
345
  payload: {
346
346
  type: "object",
347
- description: "For decision=author_packet, provide the proof packet itself or {author_packet:{...}} with proof_plan and capture_script."
347
+ description: "For decision=author_packet, provide the proof packet itself or {author_packet:{...}} with proof_plan, capture_script, and refined_inputs.expected_terminal_path when the proof changes route, query, or hash."
348
348
  },
349
349
  reasons: { type: "array", items: { type: "string" } },
350
350
  continue_with_stage: { type: "string", enum: ["author", "recon"] },
@@ -651,9 +651,11 @@ function buildAuthorCheckpointPacket(input) {
651
651
  repo: input.request.repo || fullState.repo,
652
652
  branch: input.request.branch || fullState.branch,
653
653
  verification_mode: input.request.verification_mode || fullState.verification_mode,
654
+ success_criteria: fullState.success_criteria,
654
655
  reference: input.request.reference || fullState.reference,
655
656
  server_path: fullState.server_path,
656
657
  wait_for_selector: fullState.wait_for_selector,
658
+ route_expectation: jsonCloneRecord(fullState.route_expectation),
657
659
  author_summary: fullState.author_summary,
658
660
  author_request: jsonCloneRecord(authorRequest),
659
661
  recon_baseline_understanding: jsonCloneRecord(fullState.recon_baseline_understanding),
@@ -10,11 +10,11 @@ import {
10
10
  normalizePrLifecycleState,
11
11
  normalizeRunParams,
12
12
  setRunStatus
13
- } from "../chunk-TNCDVE5O.js";
13
+ } from "../chunk-YZUVEJ5B.js";
14
14
  import {
15
15
  RIDDLE_PROOF_RUN_CARD_VERSION,
16
16
  createRiddleProofRunCard
17
- } from "../chunk-U4FUFBSH.js";
17
+ } from "../chunk-FMOYUYH2.js";
18
18
  import {
19
19
  RIDDLE_PROOF_CHECKPOINT_PACKET_VERSION,
20
20
  RIDDLE_PROOF_CHECKPOINT_RESPONSE_VERSION,
@@ -30,7 +30,7 @@ import {
30
30
  normalizeCheckpointResponse,
31
31
  proofContractFromAuthorCheckpointResponse,
32
32
  statePathsForRunState
33
- } from "../chunk-SKIAZTQ7.js";
33
+ } from "../chunk-4FOHZ7JG.js";
34
34
  import {
35
35
  applyTerminalMetadata,
36
36
  compactRecord,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  RIDDLE_PROOF_RUN_CARD_VERSION,
3
3
  createRiddleProofRunCard
4
- } from "../chunk-U4FUFBSH.js";
5
- import "../chunk-SKIAZTQ7.js";
4
+ } from "../chunk-FMOYUYH2.js";
5
+ import "../chunk-4FOHZ7JG.js";
6
6
  import "../chunk-VY4Y5U57.js";
7
7
  import "../chunk-MLKGABMK.js";
8
8
  export {
@@ -9,9 +9,9 @@ import {
9
9
  normalizePrLifecycleState,
10
10
  normalizeRunParams,
11
11
  setRunStatus
12
- } from "../chunk-TNCDVE5O.js";
13
- import "../chunk-U4FUFBSH.js";
14
- import "../chunk-SKIAZTQ7.js";
12
+ } from "../chunk-YZUVEJ5B.js";
13
+ import "../chunk-FMOYUYH2.js";
14
+ import "../chunk-4FOHZ7JG.js";
15
15
  import "../chunk-VY4Y5U57.js";
16
16
  import "../chunk-MLKGABMK.js";
17
17
  export {
package/dist/state.js CHANGED
@@ -9,9 +9,9 @@ import {
9
9
  normalizePrLifecycleState,
10
10
  normalizeRunParams,
11
11
  setRunStatus
12
- } from "./chunk-TNCDVE5O.js";
13
- import "./chunk-U4FUFBSH.js";
14
- import "./chunk-SKIAZTQ7.js";
12
+ } from "./chunk-YZUVEJ5B.js";
13
+ import "./chunk-FMOYUYH2.js";
14
+ import "./chunk-4FOHZ7JG.js";
15
15
  import "./chunk-VY4Y5U57.js";
16
16
  import "./chunk-MLKGABMK.js";
17
17
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -710,6 +710,60 @@ def invoke(tool, args, timeout=180):
710
710
  return {'ok': False, 'error': r.stdout[:300], 'stderr': r.stderr[:300]}
711
711
 
712
712
 
713
+ def compact_error_text(value, depth=0):
714
+ if depth > 4 or value is None:
715
+ return ''
716
+ if isinstance(value, str):
717
+ return value
718
+ if isinstance(value, (int, float, bool)):
719
+ return str(value)
720
+ if isinstance(value, list):
721
+ return '\n'.join(compact_error_text(item, depth + 1) for item in value[:12])
722
+ if isinstance(value, dict):
723
+ parts = []
724
+ priority_keys = (
725
+ 'error', 'message', 'script_error', 'stderr', 'stdout', 'raw',
726
+ 'details', 'result', 'capture', 'proof', '_proof_json',
727
+ )
728
+ for key in priority_keys:
729
+ if key in value:
730
+ text = compact_error_text(value.get(key), depth + 1)
731
+ if text:
732
+ parts.append(text)
733
+ if not parts:
734
+ for item in list(value.values())[:12]:
735
+ text = compact_error_text(item, depth + 1)
736
+ if text:
737
+ parts.append(text)
738
+ return '\n'.join(parts)
739
+ return ''
740
+
741
+
742
+ def non_retryable_riddle_script_error(result):
743
+ text = compact_error_text(result).lower()
744
+ if not text:
745
+ return False
746
+ playwright_timeout = (
747
+ 'timeout' in text
748
+ and (
749
+ 'locator.' in text
750
+ or 'page.waitforurl' in text
751
+ or 'page.waitforselector' in text
752
+ or 'waiting for selector' in text
753
+ or 'waiting for navigation' in text
754
+ or 'getbyrole' in text
755
+ or 'getbytext' in text
756
+ or 'scrollintoviewifneeded' in text
757
+ )
758
+ )
759
+ deterministic_playwright_error = (
760
+ 'strict mode violation' in text
761
+ or 'selector_not_found' in text
762
+ or 'index_out_of_range' in text
763
+ )
764
+ return playwright_timeout or deterministic_playwright_error
765
+
766
+
713
767
  def invoke_retry(tool, args, retries=3, timeout=180):
714
768
  """Call an OpenClaw tool with automatic retries on failure."""
715
769
  last_result = None
@@ -720,6 +774,9 @@ def invoke_retry(tool, args, retries=3, timeout=180):
720
774
  if result.get('ok') or result.get('outputs') or result.get('screenshots'):
721
775
  return result
722
776
  print(f'invoke_retry({tool}) attempt {attempt}/{retries} failed: {str(result.get("error", "no output"))[:200]}')
777
+ if tool == 'riddle_script' and non_retryable_riddle_script_error(result):
778
+ print('invoke_retry(riddle_script) stopping early for deterministic Playwright script error')
779
+ return result
723
780
  if attempt < retries:
724
781
  import time
725
782
  time.sleep(5)
@@ -1845,16 +1845,17 @@ def normalize_observed_path(value):
1845
1845
  raw = str(value or '').strip()
1846
1846
  if not raw:
1847
1847
  return ''
1848
- parsed = urlparse(raw.split('#', 1)[0])
1848
+ parsed = urlparse(raw)
1849
1849
  path = parsed.path or ''
1850
1850
  query = parsed.query or ''
1851
+ fragment = parsed.fragment or ''
1851
1852
  if not path.startswith('/'):
1852
1853
  path = '/' + path.lstrip('/')
1853
1854
  parts = path.split('/')
1854
1855
  if len(parts) >= 4 and parts[1] == 's':
1855
1856
  path = '/' + '/'.join(parts[3:])
1856
1857
  path = path.rstrip('/') or '/'
1857
- return path + (('?' + query) if query else '')
1858
+ return path + (('?' + query) if query else '') + (('#' + fragment) if fragment else '')
1858
1859
 
1859
1860
 
1860
1861
  def observed_location_from_page_state(page_state):
@@ -1862,10 +1863,13 @@ def observed_location_from_page_state(page_state):
1862
1863
  return ''
1863
1864
  pathname = str(page_state.get('pathname') or '').strip()
1864
1865
  search = str(page_state.get('search') or '').strip()
1866
+ hash_value = str(page_state.get('hash') or page_state.get('fragment') or '').strip()
1865
1867
  if search and not search.startswith('?'):
1866
1868
  search = '?' + search
1869
+ if hash_value and not hash_value.startswith('#'):
1870
+ hash_value = '#' + hash_value
1867
1871
  if pathname:
1868
- return pathname + search
1872
+ return pathname + search + hash_value
1869
1873
  return str(page_state.get('href') or '').strip()
1870
1874
 
1871
1875
 
@@ -1889,9 +1893,29 @@ def route_matches_expected(expected_path, observed_path):
1889
1893
  if pair not in remaining:
1890
1894
  return False
1891
1895
  remaining.remove(pair)
1896
+ if expected_parsed.fragment and observed_parsed.fragment != expected_parsed.fragment:
1897
+ return False
1892
1898
  return True
1893
1899
 
1894
1900
 
1901
+ def route_parts(value):
1902
+ normalized = normalize_observed_path(value)
1903
+ if not normalized:
1904
+ return {
1905
+ 'href': '',
1906
+ 'pathname': '',
1907
+ 'query': '',
1908
+ 'hash': '',
1909
+ }
1910
+ parsed = urlparse(normalized)
1911
+ return {
1912
+ 'href': normalized,
1913
+ 'pathname': parsed.path.rstrip('/') or '/',
1914
+ 'query': parsed.query or '',
1915
+ 'hash': ('#' + parsed.fragment) if parsed.fragment else '',
1916
+ }
1917
+
1918
+
1895
1919
  EXPLICIT_TERMINAL_PATH_KEYS = (
1896
1920
  'expected_terminal_path', 'expectedTerminalPath',
1897
1921
  'expected_terminal_route', 'expectedTerminalRoute',
@@ -1980,6 +2004,16 @@ def terminal_path_from_record(record, depth=0):
1980
2004
  return ''
1981
2005
 
1982
2006
 
2007
+ def terminal_path_from_text(value):
2008
+ if not isinstance(value, str):
2009
+ return ''
2010
+ for match in re.findall(r"""['"`](/[^'"`\s]+[?#][^'"`\s]*)['"`]""", value):
2011
+ candidate = path_candidate(match)
2012
+ if candidate:
2013
+ return candidate
2014
+ return ''
2015
+
2016
+
1983
2017
  def interaction_assertions_pass(value):
1984
2018
  for record in proof_evidence_records(value):
1985
2019
  if any(record.get(key) is False for key in (
@@ -2039,29 +2073,58 @@ def interaction_terminal_path_from_state(state):
2039
2073
  candidate = terminal_path_from_record(state.get(key))
2040
2074
  if candidate:
2041
2075
  return candidate, key
2076
+ for key in (
2077
+ 'expected_terminal_path',
2078
+ 'expected_after_path',
2079
+ 'capture_script',
2080
+ 'proof_plan',
2081
+ 'success_criteria',
2082
+ 'change_request',
2083
+ ):
2084
+ candidate = path_candidate(state.get(key)) or terminal_path_from_text(state.get(key))
2085
+ if candidate:
2086
+ return candidate, key
2042
2087
  return '', ''
2043
2088
 
2044
2089
 
2045
2090
  def expected_path_for_verify(state, start_path, proof_evidence):
2046
2091
  mode = normalized_verification_mode(state.get('verification_mode'))
2047
2092
  normalized_start = normalize_observed_path(start_path) or '/'
2093
+ start_parts = route_parts(normalized_start)
2048
2094
  if mode not in INTERACTION_MODES:
2049
2095
  return normalized_start, {
2050
2096
  'mode': mode,
2051
2097
  'source': 'recon_start_path',
2052
2098
  'start_path': normalized_start,
2053
2099
  'expected_path': normalized_start,
2100
+ 'start_pathname': start_parts['pathname'],
2101
+ 'start_query': start_parts['query'],
2102
+ 'start_hash': start_parts['hash'],
2103
+ 'expected_pathname': start_parts['pathname'],
2104
+ 'expected_query': start_parts['query'],
2105
+ 'expected_hash': start_parts['hash'],
2054
2106
  }
2055
2107
  candidate, source = interaction_terminal_path_from_state(state)
2056
2108
  if not candidate:
2057
2109
  candidate, source = interaction_terminal_path_from_evidence(proof_evidence)
2058
2110
  expected = candidate or normalized_start
2111
+ expected_parts = route_parts(expected)
2059
2112
  return expected, {
2060
2113
  'mode': mode,
2061
2114
  'source': source or 'recon_start_path',
2062
2115
  'start_path': normalized_start,
2063
2116
  'expected_path': expected,
2064
2117
  'terminal_path': expected if expected != normalized_start else '',
2118
+ 'start_pathname': start_parts['pathname'],
2119
+ 'start_query': start_parts['query'],
2120
+ 'start_hash': start_parts['hash'],
2121
+ 'expected_href': expected_parts['href'],
2122
+ 'expected_pathname': expected_parts['pathname'],
2123
+ 'expected_query': expected_parts['query'],
2124
+ 'expected_hash': expected_parts['hash'],
2125
+ 'terminal_pathname': expected_parts['pathname'] if expected != normalized_start else '',
2126
+ 'terminal_query': expected_parts['query'] if expected != normalized_start else '',
2127
+ 'terminal_hash': expected_parts['hash'] if expected != normalized_start else '',
2065
2128
  }
2066
2129
 
2067
2130
 
@@ -2379,7 +2442,7 @@ def evaluate_capture_quality(payload, expected_path, verification_mode='proof'):
2379
2442
  }
2380
2443
 
2381
2444
 
2382
- def build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker=''):
2445
+ def build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker='', route_expectation=None):
2383
2446
  reasons = []
2384
2447
  if not required_baseline_present:
2385
2448
  reasons.append('Recon baseline is missing, so verify should return to recon instead of guessing a new reference context.')
@@ -2391,6 +2454,25 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2391
2454
  'reasons': reasons,
2392
2455
  }
2393
2456
 
2457
+ details = after_observation.get('details') if isinstance(after_observation.get('details'), dict) else {}
2458
+ route_expectation = route_expectation if isinstance(route_expectation, dict) else {}
2459
+ route_mismatch = None
2460
+ reason_text = str(after_observation.get('reason') or '')
2461
+ if 'wrong route' in reason_text:
2462
+ expected = details.get('expected_path') or ''
2463
+ observed = details.get('observed_path_raw') or details.get('observed_path') or ''
2464
+ if expected or observed:
2465
+ route_mismatch = {
2466
+ 'field': 'route',
2467
+ 'expected_path': expected,
2468
+ 'observed_after_path': observed,
2469
+ }
2470
+ error_messages = [
2471
+ str(item).strip()
2472
+ for item in (details.get('capture_error_messages') or [])
2473
+ if str(item).strip()
2474
+ ]
2475
+
2394
2476
  if proof_evidence_blocker:
2395
2477
  reasons.append(proof_evidence_blocker)
2396
2478
  decision = 'missing_proof_evidence'
@@ -2399,24 +2481,46 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2399
2481
  reasons.append('The capture reached usable page context, but the proof evidence explicitly failed its own required audio gate.')
2400
2482
  else:
2401
2483
  reasons.append('The capture reached usable page context, but the proof script did not emit the structured evidence required for this verification mode.')
2484
+ if route_mismatch:
2485
+ reasons.append(
2486
+ 'Route mismatch also present: expected after capture path ' +
2487
+ (route_mismatch.get('expected_path') or '(unknown)') +
2488
+ ', observed ' +
2489
+ (route_mismatch.get('observed_after_path') or '(unknown)') +
2490
+ '.'
2491
+ )
2402
2492
  reasons.append('Return to author so the capture script can expose passing proof evidence before verify asks for a supervising-agent judgment.')
2493
+ summary = proof_evidence_blocker
2494
+ if route_mismatch:
2495
+ summary += (
2496
+ ' Route mismatch: expected ' +
2497
+ (route_mismatch.get('expected_path') or '(unknown)') +
2498
+ ', got ' +
2499
+ (route_mismatch.get('observed_after_path') or '(unknown)') +
2500
+ '.'
2501
+ )
2502
+ if error_messages:
2503
+ reasons.append('Capture script error: ' + error_messages[0][:500])
2504
+ summary += ' Capture script error: ' + error_messages[0][:300]
2403
2505
  return {
2404
2506
  'decision': decision,
2405
- 'summary': proof_evidence_blocker,
2507
+ 'summary': summary,
2406
2508
  'recommended_stage': 'author',
2407
2509
  'continue_with_stage': 'author',
2408
2510
  'reasons': reasons,
2511
+ 'mismatch': route_mismatch,
2409
2512
  }
2410
2513
 
2411
2514
  reason = after_observation.get('reason') or 'after capture is not usable yet'
2412
2515
  reasons.append('The after evidence is not usable yet: ' + reason)
2413
- recommended_stage = 'recon' if 'wrong route' in reason else 'author'
2414
- details = after_observation.get('details') if isinstance(after_observation.get('details'), dict) else {}
2415
- error_messages = [
2416
- str(item).strip()
2417
- for item in (details.get('capture_error_messages') or [])
2418
- if str(item).strip()
2419
- ]
2516
+ route_expectation_source = str(route_expectation.get('source') or '')
2517
+ authored_terminal_route = bool(
2518
+ route_mismatch
2519
+ and route_expectation.get('mode') in INTERACTION_MODES
2520
+ and route_expectation.get('terminal_path')
2521
+ and route_expectation_source != 'recon_start_path'
2522
+ )
2523
+ recommended_stage = 'recon' if 'wrong route' in reason and not authored_terminal_route else 'author'
2420
2524
  mismatch = None
2421
2525
  if recommended_stage == 'recon':
2422
2526
  expected = details.get('expected_path') or ''
@@ -2433,7 +2537,17 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2433
2537
  summary = 'Verify capture route mismatch needs recon to refresh the reference path.'
2434
2538
  reasons.append('The capture appears to be on the wrong route or baseline context, so recon should refresh the reference path.')
2435
2539
  else:
2436
- if error_messages:
2540
+ if route_mismatch:
2541
+ expected = route_mismatch.get('expected_path') or ''
2542
+ observed = route_mismatch.get('observed_after_path') or ''
2543
+ mismatch = route_mismatch
2544
+ reasons.append('Route mismatch: expected after capture path ' + (expected or '(unknown)') + ', observed ' + (observed or '(unknown)') + '.')
2545
+ reasons.append('The terminal route came from the authored interaction proof packet, so the capture plan should be revised instead of refreshing recon.')
2546
+ summary = 'Verify capture route mismatch after authored interaction: expected ' + (expected or '(unknown)') + ', got ' + (observed or '(unknown)') + '.'
2547
+ if error_messages:
2548
+ reasons.append('Capture script error: ' + error_messages[0][:500])
2549
+ summary += ' Capture script error: ' + error_messages[0][:300]
2550
+ elif error_messages:
2437
2551
  reasons.append('Capture script error: ' + error_messages[0][:500])
2438
2552
  summary = 'Verify capture script failed: ' + error_messages[0][:300]
2439
2553
  else:
@@ -2528,6 +2642,9 @@ def build_semantic_context(state, results, after_observation, expected_path):
2528
2642
  after_semantic = semantic_observation('after', after_observation)
2529
2643
  expected_start_path = state.get('expected_start_path') or expected_path
2530
2644
  route_expectation = state.get('route_expectation') if isinstance(state.get('route_expectation'), dict) else {}
2645
+ expected_parts = route_parts(expected_path)
2646
+ start_parts = route_parts(expected_start_path)
2647
+ after_parts = route_parts(after_semantic.get('observed_path_raw') or after_semantic.get('observed_path') or '')
2531
2648
  return {
2532
2649
  'expected_path': expected_path,
2533
2650
  'expected_start_path': expected_start_path,
@@ -2540,10 +2657,20 @@ def build_semantic_context(state, results, after_observation, expected_path):
2540
2657
  'expected_after_path': expected_path,
2541
2658
  'expected_start_path': expected_start_path,
2542
2659
  'expected_terminal_path': route_expectation.get('terminal_path') or '',
2660
+ 'expected_pathname': expected_parts['pathname'],
2661
+ 'expected_query': expected_parts['query'],
2662
+ 'expected_hash': expected_parts['hash'],
2663
+ 'expected_start_pathname': start_parts['pathname'],
2664
+ 'expected_start_query': start_parts['query'],
2665
+ 'expected_start_hash': start_parts['hash'],
2666
+ 'expected_terminal_query': route_expectation.get('terminal_query') or route_expectation.get('expected_query') or '',
2667
+ 'expected_terminal_hash': route_expectation.get('terminal_hash') or route_expectation.get('expected_hash') or '',
2543
2668
  'expectation_source': route_expectation.get('source') or '',
2544
2669
  'before_observed_path': before_semantic.get('observed_path') or before.get('path') or '',
2545
2670
  'prod_observed_path': prod_semantic.get('observed_path') or prod.get('path') or '',
2546
2671
  'after_observed_path': after_semantic.get('observed_path') or '',
2672
+ 'after_observed_query': after_parts['query'],
2673
+ 'after_observed_hash': after_parts['hash'],
2547
2674
  },
2548
2675
  'before': before_semantic,
2549
2676
  'prod': prod_semantic,
@@ -3051,6 +3178,15 @@ if expected_start_path and expected_start_path != expected_path:
3051
3178
  summary_lines.append('Expected terminal proof path: ' + expected_path)
3052
3179
  else:
3053
3180
  summary_lines.append('Expected proof path from recon: ' + expected_path)
3181
+ route_expected_query = (s.get('route_expectation') or {}).get('expected_query') or ''
3182
+ route_expected_hash = (s.get('route_expectation') or {}).get('expected_hash') or ''
3183
+ if route_expected_query or route_expected_hash:
3184
+ summary_lines.append(
3185
+ 'Expected terminal query/hash: ' +
3186
+ ('?' + route_expected_query if route_expected_query else '(none)') +
3187
+ ' ' +
3188
+ (route_expected_hash if route_expected_hash else '(none)')
3189
+ )
3054
3190
  summary_lines.append('After observation: ' + after_observation['reason'])
3055
3191
  supporting = results['after'].get('supporting_artifacts') or {}
3056
3192
  if supporting.get('has_structured_payload'):
@@ -3064,6 +3200,14 @@ if supporting.get('has_structured_payload'):
3064
3200
  summary_lines.append('Structured after evidence: ' + ('; '.join(basis) if basis else 'present'))
3065
3201
  observed_path = (after_observation.get('details') or {}).get('observed_path') or expected_path
3066
3202
  summary_lines.append('Observed after path: ' + observed_path)
3203
+ observed_parts = route_parts((after_observation.get('details') or {}).get('observed_path_raw') or observed_path)
3204
+ if observed_parts.get('query') or observed_parts.get('hash') or route_expected_query or route_expected_hash:
3205
+ summary_lines.append(
3206
+ 'Observed after query/hash: ' +
3207
+ ('?' + observed_parts.get('query') if observed_parts.get('query') else '(none)') +
3208
+ ' ' +
3209
+ (observed_parts.get('hash') if observed_parts.get('hash') else '(none)')
3210
+ )
3067
3211
  details = after_observation.get('details') or {}
3068
3212
  if details.get('headings'):
3069
3213
  summary_lines.append('Visible headings: ' + '; '.join(str(item) for item in details.get('headings', [])[:6]))
@@ -3175,7 +3319,7 @@ if has_good_evidence:
3175
3319
  summary_lines.append('Proof assessment: awaiting supervising agent judgment')
3176
3320
  summary_lines.append('Proof next stage: supervising agent decides after reviewing the evidence packet')
3177
3321
  else:
3178
- capture_retry = visual_delta_recovery or build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker)
3322
+ capture_retry = visual_delta_recovery or build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker, s.get('route_expectation') or {})
3179
3323
  next_stage_options = ['author', 'verify', 'recon'] if no_implementation_mode else ['author', 'verify', 'implement', 'recon']
3180
3324
  s['verify_status'] = 'capture_incomplete'
3181
3325
  s['merge_recommendation'] = 'do-not-merge'