diff --git a/bin/fm-crosscheck-pi-reviewer.py b/bin/fm-crosscheck-pi-reviewer.py index e72a1a27cec..4d0a782f9c1 100755 --- a/bin/fm-crosscheck-pi-reviewer.py +++ b/bin/fm-crosscheck-pi-reviewer.py @@ -12,6 +12,9 @@ from typing import Any +VERDICT_REPAIR_EFFORT = "low" + + class ReviewError(RuntimeError): """A fail-closed Pi launch or verdict-protocol failure.""" @@ -331,7 +334,7 @@ def run(argv: list[str]) -> int: "--model", model, "--thinking", - effort if attempt == 0 else "minimal", + effort if attempt == 0 else VERDICT_REPAIR_EFFORT, "--tools", "submit_crosscheck_verdict", "--extension", @@ -370,7 +373,7 @@ def run(argv: list[str]) -> int: repair_prompt.write_text( "VERDICT PROTOCOL REPAIR (trusted controller instruction):\n" "Perform the exact independent review packet below in this fresh " - "minimal-reasoning attempt. Do not end with prose and do not call " + f"{VERDICT_REPAIR_EFFORT}-reasoning attempt. Do not end with prose and do not call " "the tool more than once. Submit the complete schema-valid verdict " "through submit_crosscheck_verdict exactly once.\n\n" + prompt.read_text(encoding="utf-8"), diff --git a/docs/crosscheck.md b/docs/crosscheck.md index d445ee769b5..1ba13a9ea5e 100644 --- a/docs/crosscheck.md +++ b/docs/crosscheck.md @@ -87,7 +87,7 @@ The regular-lane reviewer record binds `review_depth_passes: "2"`, `review_depth Successful current-contract `clear` and `blocking` records, including reusable records, fail validation when any of those fields is missing or contradictory. Failed `tool-failure`, `unreviewed`, and `cannot-certify` attempts may omit terminal and depth evidence they never earned, so their ledgers remain reloadable for a later retry; they are never reusable. Crosscheck accepts exactly one verdict tool call from each successful pass and preserves usage across Pi auto-retries. -If an attempt reaches the model but ends without exactly one well-formed verdict call, including an output-limit or provider terminal error, one fresh ephemeral minimal-reasoning attempt receives a fixed repair instruction plus the identical exact-head review packet. +If an attempt reaches the model but ends without exactly one well-formed verdict call, including an output-limit or provider terminal error, one fresh ephemeral low-reasoning attempt receives a fixed repair instruction plus the identical exact-head review packet. The repair is attempted once per pass, its usage is included in the run economics, and a second protocol miss fails closed instead of selecting a convenient call or rotating to another reviewer. Provider terminal-error diagnostics have credential-shaped values redacted, are whitespace-normalized and stripped of non-printable characters, and are limited to 512 characters before they reach operator-visible failure output. The model decides the provider slot through an explicit mapping derived from the lane registry that maps each registered model to its own slot, maps `gpt-5.6-sol` to `openai-codex`, and refuses an unmapped model rather than guessing. diff --git a/tests/fm-crosscheck-azure.test.sh b/tests/fm-crosscheck-azure.test.sh index 8ee4097e516..af404d467ef 100755 --- a/tests/fm-crosscheck-azure.test.sh +++ b/tests/fm-crosscheck-azure.test.sh @@ -2939,6 +2939,12 @@ scenario = os.environ["SCENARIO"] effective = scenario if scenario.endswith("-then-valid"): effective = scenario.removesuffix("-then-valid") if len(captures) == 1 else "valid" +reasoning_effort = sys.argv[sys.argv.index("--thinking") + 1] +provider_reasoning_efforts = { + "low", "medium", "high", "xhigh", "max", "none", "adaptive", +} +if reasoning_effort not in provider_reasoning_efforts: + effective = "invalid-reasoning-effort" if effective == "nonzero": print("bounded fake provider failure", file=sys.stderr) raise SystemExit(17) @@ -3006,6 +3012,13 @@ elif effective == "terminal-error-secrets": "secret='secret-visible'; access_token=access-visible; opaque=" + "Z" * 48 ) +elif effective == "invalid-reasoning-effort": + message["content"] = [] + message["stopReason"] = "error" + message["errorMessage"] = ( + "reasoning_effort must be low, medium, high, xhigh, max, none, or adaptive; " + f"received {reasoning_effort!r}" + ) print(json.dumps({"type": "turn_end", "message": message})) print(json.dumps({"type": "agent_end"})) if effective == "internal-retry": @@ -3066,7 +3079,7 @@ def assert_launch(captured, account, prompt, schema, attempts=1): active_prompt = prompt if index == 0 else prompt.parent / "repair-prompt.txt" assert launch["argv"] == [ "--mode", "json", "--offline", "--provider", provider, - "--model", model, "--thinking", "xhigh" if index == 0 else "minimal", + "--model", model, "--thinking", "xhigh" if index == 0 else "low", "--tools", "submit_crosscheck_verdict", "--extension", str(extension), "--system-prompt", system_prompt, @@ -3074,13 +3087,14 @@ def assert_launch(captured, account, prompt, schema, attempts=1): "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "--no-approve", f"@{active_prompt}", ], launch["argv"] + assert "minimal" not in launch["argv"], launch["argv"] assert launch["account"] == str(account), launch assert launch["schema"] == str(schema), launch if attempts == 2: repair = captured[1]["prompt_text"] assert repair.startswith("VERDICT PROTOCOL REPAIR"), repair assert repair.endswith("PROMPT BY FILE"), repair - assert "fresh minimal-reasoning attempt" in repair, repair + assert "fresh low-reasoning attempt" in repair, repair assert "submit_crosscheck_verdict exactly once" in repair, repair