From 3e0b1f7238fbd6050b0852213db4a316711f4320 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:55:46 +0800 Subject: [PATCH 1/7] fix: converge repeated PR review findings Co-Authored-By: Codex --- docs/ai_autonomy_architecture.md | 8 + scripts/run_codex_pr_review.py | 744 ++++++++++++++++++-- tests/fixtures/codex_pr_review_replays.json | 54 ++ tests/test_run_codex_pr_review.py | 234 +++++- 4 files changed, 966 insertions(+), 74 deletions(-) create mode 100644 tests/fixtures/codex_pr_review_replays.json diff --git a/docs/ai_autonomy_architecture.md b/docs/ai_autonomy_architecture.md index d04509d6..3e0ce9b2 100644 --- a/docs/ai_autonomy_architecture.md +++ b/docs/ai_autonomy_architecture.md @@ -207,6 +207,14 @@ Contract Oscillation Guard 是 `AIAuditBridge` 的中央 PR review gate 语义 trusted review comment 只保存最近固定轮数、固定字节上限且脱敏后的 blocking finding 摘要,包括 head SHA、file、category、severity、description 和 suggestion。历史只能由已验证的 review bot comment 恢复;legacy comment 没有 history marker 时保持兼容,但既有 blocker 会被迁移为 `invalid_history` 并继续 fail closed,不能因一次 clean review 自动清除。畸形或超限 history 同样 fail closed。 +新一版 review contract 还会为每个 finding 记录稳定的 `contract_key` 和 `behavior_digest`,并用 `fingerprint_v2` 避免只靠 file/category/severity 产生粗粒度碰撞。状态机按 `initial_review -> remediation(每个 contract 最多 2 轮) -> arbitration_required -> cleared / blocked_human_contract` 收敛,超过上限后禁止继续自动修复。 + +初次 review 审阅累计 diff;remediation 只审阅受信任上一 head 到当前 head 的 delta、未解决 contract 与必要影响上下文。达到上限时仅允许一次累计 diff sanity,再交给独立仲裁;同一 head 不会反复重新开启 remediation。`tests/fixtures/codex_pr_review_replays.json` 保存了脱敏的 dispatch 三态与 scored-date/cached-runner replay 输入,用于防止同义措辞、同文件不同 contract 和错误 clearance 回归。 + +当仲裁给出 `clear` 时,系统会写入受信任的 `clearance.v1`,绑定 repo、PR、head SHA、diff digest、contract_key、证据摘要和仲裁身份;同一 head 重新运行时可验证 clearance 并直接通过 required review check。若 head/diff 变化、clearance 过期、或被伪造,则继续 fail closed。 + +对大 PR,review prompt 会先要求纵向切片:超过 1500 行、12 个文件,或跨 adapter/gateway/runtime 边界时,不允许用删 tests/docs 的方式“缩小”变更面,而是先拆成更小的 contract PR。 + 若 `overflow` / `invalid_history` 状态中没有可供仲裁的 trusted prior finding,系统不得用空上下文自动 `clear`。此时需要人工确认 source-of-truth 后修复或删除损坏的 trusted bot state,再重新运行普通 required review check;这只恢复可审计状态,不直接放行 merge,也不绕过 branch protection。 当同一 file/category/severity 的前后 finding 可能要求相反行为时,独立仲裁必须同时读取上一轮 finding、当前 finding 和累计 PR diff,并优先以公共接口、schema、tests、docs 等 source-of-truth 判断: diff --git a/scripts/run_codex_pr_review.py b/scripts/run_codex_pr_review.py index d5adbea2..74bf9b82 100644 --- a/scripts/run_codex_pr_review.py +++ b/scripts/run_codex_pr_review.py @@ -66,6 +66,8 @@ HEAD_SHA_MARKER_SUFFIX = " -->" FINDING_HISTORY_MARKER_PREFIX = "" +CLEARANCE_MARKER_PREFIX = "" CONTRACT_CONFLICT_MARKER_PREFIX = "\n" + "\n" + f"{clearance}\n" + + run_codex_pr_review.build_finding_history_marker( + [], findings, "abc1234", status="cleared" + ) + ) + env = { + "GH_TOKEN": "token", + "GITHUB_REPOSITORY": "org/repo", + "GITHUB_EVENT_PATH": str(event_path), + "GITHUB_OUTPUT": str(Path(tmpdir) / "github-output.txt"), + } + previous_cwd = os.getcwd() + try: + os.chdir(tmpdir) + with ( + patch.dict(os.environ, env, clear=True), + patch( + "scripts.run_codex_pr_review.fetch_pr_files", + return_value=[{"filename": "service/review.py"}], + ), + patch("scripts.run_codex_pr_review.fetch_pr_diff", return_value=diff), + patch( + "scripts.run_codex_pr_review.load_policy", + return_value=run_codex_pr_review._default_policy(), + ), + patch( + "scripts.run_codex_pr_review.find_existing_review_comment", + return_value=(99, prior_comment), + ), + patch( + "scripts.run_codex_pr_review.run_codex_review_with_fallback", + return_value=json.dumps({"summary": "block", "findings": findings}), + ) as backend, + patch("scripts.run_codex_pr_review.upsert_pr_comment") as comment, + ): + self.assertEqual(run_codex_pr_review.main(), 0) + finally: + os.chdir(previous_cwd) + + backend.assert_called_once() + body = comment.call_args.args[3] + decision = json.loads( + (Path(tmpdir) / "data/output/codex_pr_review/decision.json").read_text( + encoding="utf-8" + ) + ) + + self.assertFalse(decision["blocked"]) + self.assertEqual(decision["next_action"], "none") + self.assertIn("trusted clearance matches this head and diff", body) + self.assertIn("codex-pr-review-clearance:v1:", body) + self.assertNotIn("Codex Review Arbitration", body) + def test_main_malformed_trusted_history_blocks_before_review(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: event_path = self._write_event(tmpdir, ["docs/guide.md"]) From 65d198d19f9704384ab9421abb55153698cc5e7a Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:05:37 +0800 Subject: [PATCH 2/7] fix: bind clearances to trusted review runs Co-Authored-By: Codex --- docs/ai_autonomy_architecture.md | 2 +- scripts/run_codex_pr_review.py | 75 ++++++++++++++++++++++++------- tests/test_run_codex_pr_review.py | 41 ++++++++++++++++- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/docs/ai_autonomy_architecture.md b/docs/ai_autonomy_architecture.md index 3e0ce9b2..ee610921 100644 --- a/docs/ai_autonomy_architecture.md +++ b/docs/ai_autonomy_architecture.md @@ -211,7 +211,7 @@ trusted review comment 只保存最近固定轮数、固定字节上限且脱敏 初次 review 审阅累计 diff;remediation 只审阅受信任上一 head 到当前 head 的 delta、未解决 contract 与必要影响上下文。达到上限时仅允许一次累计 diff sanity,再交给独立仲裁;同一 head 不会反复重新开启 remediation。`tests/fixtures/codex_pr_review_replays.json` 保存了脱敏的 dispatch 三态与 scored-date/cached-runner replay 输入,用于防止同义措辞、同文件不同 contract 和错误 clearance 回归。 -当仲裁给出 `clear` 时,系统会写入受信任的 `clearance.v1`,绑定 repo、PR、head SHA、diff digest、contract_key、证据摘要和仲裁身份;同一 head 重新运行时可验证 clearance 并直接通过 required review check。若 head/diff 变化、clearance 过期、或被伪造,则继续 fail closed。 +当仲裁给出 `clear` 时,系统会写入受信任的 `clearance.v1`,绑定 repo、PR、head SHA、diff digest、contract_key、证据摘要、仲裁身份和成功的 `pull_request_target` review workflow run;同一 head 重新运行时可验证 clearance 并直接通过 required review check。若 head/diff 变化、clearance 过期、workflow run 不匹配或被伪造,则继续 fail closed。 对大 PR,review prompt 会先要求纵向切片:超过 1500 行、12 个文件,或跨 adapter/gateway/runtime 边界时,不允许用删 tests/docs 的方式“缩小”变更面,而是先拆成更小的 contract PR。 diff --git a/scripts/run_codex_pr_review.py b/scripts/run_codex_pr_review.py index 74bf9b82..8ca5d829 100644 --- a/scripts/run_codex_pr_review.py +++ b/scripts/run_codex_pr_review.py @@ -383,8 +383,8 @@ def build_review_prompt( ## Review Scope: Remediation Review only the unresolved findings below, the remediation delta, and the minimal -affected context needed to validate their contracts. Do not restart a full PR -review or introduce unrelated findings. +affected context needed to validate their contracts. Do not reopen untouched +parts of the PR, but report any new blocking issue found in the remediation delta. ## Unresolved Findings {json.dumps(unresolved_findings or [], ensure_ascii=False, indent=2)} @@ -1114,6 +1114,7 @@ def build_clearance_marker( findings: list[dict[str, Any]], arbiter_identity: str, evidence: str, + workflow_run_id: int, issued_at: int | None = None, expires_at: int | None = None, ) -> str: @@ -1143,6 +1144,7 @@ def build_clearance_marker( "fingerprints": fingerprints, "evidence_digest": hashlib.sha256(_sanitize_history_text(evidence, 1000).encode("utf-8")).hexdigest()[:24], "arbiter_identity": _sanitize_history_text(arbiter_identity, 120), + "workflow_run_id": int(workflow_run_id), "issued_at": timestamp, "expires_at": expiry, } @@ -1178,6 +1180,7 @@ def parse_clearance_marker(body: str) -> tuple[dict[str, Any] | None, bool]: "fingerprints": list, "evidence_digest": str, "arbiter_identity": str, + "workflow_run_id": int, "issued_at": int, "expires_at": int, } @@ -1238,6 +1241,31 @@ def clearance_matches( return True +def clearance_workflow_run_is_trusted( + token: str, repo: str, clearance: dict[str, Any] +) -> bool: + """Verify that a clearance was emitted by a successful review workflow run.""" + run_id = int(clearance.get("workflow_run_id") or 0) + if run_id <= 0: + return False + try: + run = github_request(token, "GET", f"/repos/{repo}/actions/runs/{run_id}") + except ReviewError: + return False + if not isinstance(run, dict): + return False + workflow_path = str(run.get("path") or "") + run_repo = run.get("repository") if isinstance(run.get("repository"), dict) else {} + return bool( + run.get("event") == "pull_request_target" + and run.get("conclusion") == "success" + and str(run.get("head_sha") or "").strip().lower() + == str(clearance.get("head_sha") or "").strip().lower() + and str(run_repo.get("full_name") or "") == repo + and workflow_path.startswith(".github/workflows/codex_pr_review.yml@") + ) + + # --------------------------------------------------------------------------- # Findings evaluation # --------------------------------------------------------------------------- @@ -1594,28 +1622,40 @@ def conflicting_contract_findings( for finding in current_findings if isinstance(finding, dict) } - conflicts: list[dict[str, Any]] = [] + latest_by_contract: dict[str, dict[str, Any]] = {} + resolved_contracts: set[str] = set() for round_state in reversed(history): prior = round_state.get("findings") if not isinstance(prior, list): continue + status = round_state.get("status", "blocking") + if status in {"clear", "cleared"}: + if not prior: + break + resolved_contracts.update( + str(finding.get("contract_key") or _contract_key(finding)) + for finding in prior + if isinstance(finding, dict) + ) + continue for finding in prior: if not isinstance(finding, dict): continue contract_key = str(finding.get("contract_key") or _contract_key(finding)) - current = current_by_contract.get(contract_key) - if not current: - continue - current_fingerprint = str(current.get("fingerprint_v2") or _fingerprint_v2(current)) - prior_fingerprint = str(finding.get("fingerprint_v2") or _fingerprint_v2(finding)) - if current_fingerprint != prior_fingerprint: - conflicts.append( - { - **finding, - "current_fingerprint_v2": current_fingerprint, - "history_head_sha": str(round_state.get("head_sha") or ""), - } - ) + if contract_key not in resolved_contracts and contract_key not in latest_by_contract: + latest_by_contract[contract_key] = { + **finding, + "history_head_sha": str(round_state.get("head_sha") or ""), + } + conflicts: list[dict[str, Any]] = [] + for contract_key, prior in latest_by_contract.items(): + current = current_by_contract.get(contract_key) + if not current: + continue + current_fingerprint = str(current.get("fingerprint_v2") or _fingerprint_v2(current)) + prior_fingerprint = str(prior.get("fingerprint_v2") or _fingerprint_v2(prior)) + if current_fingerprint != prior_fingerprint: + conflicts.append({**prior, "current_fingerprint_v2": current_fingerprint}) return conflicts @@ -2429,7 +2469,7 @@ def main() -> int: head_sha=current_head_sha, diff_digest=diff_digest, findings=decision["blocking_findings"], - ): + ) and clearance_workflow_run_is_trusted(token, repo, clearance_marker): decision.update( { "blocked": False, @@ -2631,6 +2671,7 @@ def main() -> int: or previous_findings, arbiter_identity=env_value("GITHUB_ACTOR", "github-actions[bot]"), evidence=str(arbitration.get("reason") or decision.get("summary") or ""), + workflow_run_id=int(env_value("GITHUB_RUN_ID", "0") or 0), ) arbitration_cleared = bool(arbitration and arbitration.get("verdict") == "clear") if ( diff --git a/tests/test_run_codex_pr_review.py b/tests/test_run_codex_pr_review.py index 9acfb7e4..58734e06 100644 --- a/tests/test_run_codex_pr_review.py +++ b/tests/test_run_codex_pr_review.py @@ -68,7 +68,8 @@ def test_remediation_prompt_limits_review_to_delta_and_unresolved_contracts(self unresolved_findings=[{"file": "service/review.py", "contract_key": "abc"}], ) self.assertIn("Review Scope: Remediation", prompt) - self.assertIn("Do not restart a full PR\nreview", prompt) + self.assertIn("Do not reopen untouched\nparts of the PR", prompt) + self.assertIn("new blocking issue found in the remediation delta", prompt) self.assertIn('"contract_key": "abc"', prompt) def test_review_script_never_imports_from_the_pr_checkout(self) -> None: @@ -181,6 +182,16 @@ def test_sanitized_replays_keep_semantic_contracts_separate(self) -> None: [], ) self.assertEqual(len(run_codex_pr_review.conflicting_contract_findings(history, [opposite])), 1) + cleared_history, valid = run_codex_pr_review.parse_finding_history( + run_codex_pr_review.build_finding_history_marker( + history, [same], "feedface", status="cleared" + ) + ) + self.assertTrue(valid) + self.assertEqual( + run_codex_pr_review.conflicting_contract_findings(cleared_history, [opposite]), + [], + ) def test_parse_arbitration_output_requires_supported_verdict(self) -> None: self.assertEqual( @@ -285,6 +296,7 @@ def test_trusted_clearance_marker_round_trips_and_binds_head_diff(self) -> None: findings=findings, arbiter_identity="github-actions[bot]", evidence="public interface and regression tests prove the false positive", + workflow_run_id=123, issued_at=1, expires_at=2_000_000_000, ) @@ -312,6 +324,28 @@ def test_trusted_clearance_marker_round_trips_and_binds_head_diff(self) -> None: findings=findings, ) ) + trusted_run = { + "event": "pull_request_target", + "conclusion": "success", + "head_sha": "abc1234", + "path": ".github/workflows/codex_pr_review.yml@base-sha", + "repository": {"full_name": "org/repo"}, + } + with patch("scripts.run_codex_pr_review.github_request", return_value=trusted_run): + self.assertTrue( + run_codex_pr_review.clearance_workflow_run_is_trusted( + "token", "org/repo", parsed or {} + ) + ) + with patch( + "scripts.run_codex_pr_review.github_request", + return_value={**trusted_run, "event": "workflow_dispatch"}, + ): + self.assertFalse( + run_codex_pr_review.clearance_workflow_run_is_trusted( + "token", "org/repo", parsed or {} + ) + ) self.assertFalse( run_codex_pr_review.clearance_matches( parsed or {}, @@ -1347,6 +1381,7 @@ def test_main_same_head_reuses_trusted_clearance_without_arbitration(self) -> No findings=findings, arbiter_identity="github-actions[bot]", evidence="source-of-truth tests prove the false positive", + workflow_run_id=123, issued_at=1, expires_at=2_000_000_000, ) @@ -1386,6 +1421,10 @@ def test_main_same_head_reuses_trusted_clearance_without_arbitration(self) -> No "scripts.run_codex_pr_review.run_codex_review_with_fallback", return_value=json.dumps({"summary": "block", "findings": findings}), ) as backend, + patch( + "scripts.run_codex_pr_review.clearance_workflow_run_is_trusted", + return_value=True, + ), patch("scripts.run_codex_pr_review.upsert_pr_comment") as comment, ): self.assertEqual(run_codex_pr_review.main(), 0) From 71e3eecce6caf2498d6eceba8e1f3e128e00e9e9 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:12:14 +0800 Subject: [PATCH 3/7] fix: preserve bounded remediation terminal state Co-Authored-By: Codex --- scripts/run_codex_pr_review.py | 3 ++- tests/test_run_codex_pr_review.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/run_codex_pr_review.py b/scripts/run_codex_pr_review.py index 8ca5d829..a12bf70a 100644 --- a/scripts/run_codex_pr_review.py +++ b/scripts/run_codex_pr_review.py @@ -2711,8 +2711,9 @@ def main() -> int: serialized_history, serialized_history_valid = parse_finding_history( finding_history_marker ) - serialized_history_requires_confirmation = finding_history_requires_confirmation( + serialized_history_requires_confirmation = bool( serialized_history + and serialized_history[-1].get("status") in {"overflow", "invalid_history"} ) if not serialized_history_valid or serialized_history_requires_confirmation: decision = apply_arbitration_failure( diff --git a/tests/test_run_codex_pr_review.py b/tests/test_run_codex_pr_review.py index 58734e06..e2d14d3d 100644 --- a/tests/test_run_codex_pr_review.py +++ b/tests/test_run_codex_pr_review.py @@ -487,6 +487,25 @@ def test_invalid_history_sentinel_is_fail_closed_but_recoverable(self) -> None: self.assertTrue(valid) self.assertFalse(run_codex_pr_review.has_active_blocking_history(recovered)) + def test_blocked_human_contract_is_not_reclassified_as_invalid_history(self) -> None: + marker = run_codex_pr_review.build_finding_history_marker( + [], + [{ + "severity": "high", + "category": "contract", + "file": "service/review.py", + "description": "The bounded remediation limit was reached.", + "suggestion": "Resolve the contract before another review.", + }], + "deadbeef", + status="blocked_human_contract", + ) + history, valid = run_codex_pr_review.parse_finding_history(marker) + + self.assertTrue(valid) + self.assertTrue(run_codex_pr_review.finding_history_requires_confirmation(history)) + self.assertNotIn(history[-1]["status"], {"overflow", "invalid_history"}) + def test_matching_history_aggregates_keys_from_multiple_rounds(self) -> None: first = { "severity": "high", From a1198ec4f87844959cfba4207253fd5e02b4dbe0 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:21:00 +0800 Subject: [PATCH 4/7] fix: preserve terminal review convergence state Co-Authored-By: Codex --- scripts/run_codex_pr_review.py | 40 ++++++++++++++++++++++++++++--- tests/test_run_codex_pr_review.py | 17 ++++--------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/scripts/run_codex_pr_review.py b/scripts/run_codex_pr_review.py index a12bf70a..4b910e32 100644 --- a/scripts/run_codex_pr_review.py +++ b/scripts/run_codex_pr_review.py @@ -908,7 +908,10 @@ def _contract_subject(finding: dict[str, Any]) -> str: anchors = re.findall(r"`([^`]{1,120})`", text) anchors.extend(re.findall(r"\b[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+\b", text)) normalized = [_normalize_contract_text(anchor) for anchor in anchors] - return "|".join(sorted({anchor for anchor in normalized if anchor})) + anchored_subject = "|".join(sorted({anchor for anchor in normalized if anchor})) + return anchored_subject or _normalize_contract_text( + finding.get("suggestion") or text + ) def _contract_surface(finding: dict[str, Any]) -> tuple[str, str, str, str, str]: @@ -2090,6 +2093,7 @@ def main() -> int: previous_fingerprints = parse_blocking_fingerprints(previous_comment) previous_head_sha = parse_reviewed_head_sha(previous_comment) clearance_marker_text = extract_clearance_marker(previous_comment) + clearance_marker, clearance_valid = parse_clearance_marker(previous_comment) finding_history, history_valid = parse_finding_history(previous_comment) history_valid = history_source_valid and history_valid if history_valid and finding_history: @@ -2227,7 +2231,8 @@ def main() -> int: latest_status = str((finding_history[-1] if finding_history else {}).get("status") or "") review_scope = ( "final_sanity" - if previous_streak >= MAX_REMEDIATION_ROUNDS and latest_status == "remediation" + if latest_status == "blocked_human_contract" + or (previous_streak >= MAX_REMEDIATION_ROUNDS and latest_status == "remediation") else "remediation" ) @@ -2235,6 +2240,36 @@ def main() -> int: # only the trusted previous-head delta, unless this is its one final sanity pass. diff = fetch_pr_diff(token, repo, pr_number) print(f"Fetched diff: {len(diff)} chars, {len(diff.splitlines())} lines") + if ( + clearance_marker + and clearance_matches( + clearance_marker, + repo=repo, + pr_number=pr_number, + head_sha=current_head_sha, + diff_digest=_diff_digest(diff), + findings=[], + ) + and clearance_workflow_run_is_trusted(token, repo, clearance_marker) + ): + decision = { + "blocked": False, + "blocking_findings": [], + "non_blocking_findings": [], + "total_findings": 0, + "summary": "✅ **Merge allowed**: trusted clearance matches this head and diff", + "contract_conflict": False, + "auto_fix_allowed": False, + "next_action": "none", + } + return publish_review_decision( + token, repo, pr_number, pr_url, decision, exit_code=0, + current_head_sha=current_head_sha, reviewed_head_sha=current_head_sha, + finding_history_marker=build_finding_history_marker( + finding_history, [], current_head_sha + ), + clearance_marker=clearance_marker_text, + ) review_diff = diff if review_scope == "remediation": try: @@ -2440,7 +2475,6 @@ def main() -> int: } ) diff_digest = _diff_digest(diff) - clearance_marker, clearance_valid = parse_clearance_marker(previous_comment) if CLEARANCE_MARKER_PREFIX in previous_comment and not clearance_valid: decision = apply_arbitration_failure( decision, ReviewError("trusted clearance marker is malformed") diff --git a/tests/test_run_codex_pr_review.py b/tests/test_run_codex_pr_review.py index e2d14d3d..336c52f5 100644 --- a/tests/test_run_codex_pr_review.py +++ b/tests/test_run_codex_pr_review.py @@ -1289,12 +1289,12 @@ def test_main_contract_conflict_is_consistent_across_comment_artifact_and_output "severity": "high", "category": "contract", "file": "service/review.py", - "description": "Missing panel must return a structured blocked result.", + "description": "`MissingPanel` must return a structured blocked result.", "suggestion": "Return ReviewResult(blocked=True).", } current = dict( prior, - description="Missing panel must fail fast.", + description="`MissingPanel` must fail fast.", suggestion="Raise ReviewError instead of returning a result.", ) fingerprint = run_codex_pr_review.blocking_finding_fingerprint([prior]) @@ -1450,17 +1450,10 @@ def test_main_same_head_reuses_trusted_clearance_without_arbitration(self) -> No finally: os.chdir(previous_cwd) - backend.assert_called_once() - body = comment.call_args.args[3] - decision = json.loads( - (Path(tmpdir) / "data/output/codex_pr_review/decision.json").read_text( - encoding="utf-8" - ) - ) - - self.assertFalse(decision["blocked"]) - self.assertEqual(decision["next_action"], "none") + backend.assert_not_called() + body = comment.call_args.args[3] self.assertIn("trusted clearance matches this head and diff", body) + self.assertIn("codex-pr-review-next-action:none", body) self.assertIn("codex-pr-review-clearance:v1:", body) self.assertNotIn("Codex Review Arbitration", body) From a18d82205cdc4eeef83de24d6eaefa0093afb7b4 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:34:22 +0800 Subject: [PATCH 5/7] fix: retain remediation state across retries Co-Authored-By: Codex --- scripts/run_codex_pr_review.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/run_codex_pr_review.py b/scripts/run_codex_pr_review.py index 4b910e32..b79a49d9 100644 --- a/scripts/run_codex_pr_review.py +++ b/scripts/run_codex_pr_review.py @@ -287,6 +287,11 @@ def fetch_pr_diff(token: str, repo: str, pr_number: int) -> str: def fetch_compare_diff(token: str, repo: str, base_sha: str, head_sha: str) -> str: """Fetch only the remediation delta between two trusted reviewed heads.""" + comparison = github_request( + token, "GET", f"/repos/{repo}/compare/{base_sha}...{head_sha}" + ) + if not isinstance(comparison, dict) or comparison.get("status") != "ahead": + raise ReviewError("remediation head is not a strict descendant of the trusted reviewed head") compare_url = ( f"{API_BASE}/repos/{repo}/compare/" f"{urllib.parse.quote(base_sha, safe='')}...{urllib.parse.quote(head_sha, safe='')}" @@ -2185,7 +2190,7 @@ def main() -> int: current_head_sha=current_head_sha, reviewed_head_sha=current_head_sha, finding_history_marker=build_finding_history_marker( - finding_history, [], current_head_sha + finding_history, [], "" ), ) @@ -2301,7 +2306,7 @@ def main() -> int: reviewed_head_sha=previous_head_sha, new_head=True, finding_history_marker=build_finding_history_marker( - finding_history, [], current_head_sha + finding_history, [], "" ), ) print( From a6b74e63bdd96bda84b351db21f59f2b2ebe88c5 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:22:06 +0800 Subject: [PATCH 6/7] fix: separate remediation convergence state Co-Authored-By: Codex --- scripts/run_codex_pr_review.py | 45 +++++++++++-------------------- tests/test_run_codex_pr_review.py | 19 +++++++++++++ 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/scripts/run_codex_pr_review.py b/scripts/run_codex_pr_review.py index b79a49d9..c44d53af 100644 --- a/scripts/run_codex_pr_review.py +++ b/scripts/run_codex_pr_review.py @@ -940,12 +940,11 @@ def _contract_key(finding: dict[str, Any]) -> str: def _behavior_digest(finding: dict[str, Any]) -> str: - file_path, category, severity, subject, behavior_text = _contract_surface(finding) + file_path, category, _severity, subject, behavior_text = _contract_surface(finding) payload = json.dumps( { "file": file_path, "category": category, - "severity": severity, "subject": subject, "behavior": behavior_text, }, @@ -1660,10 +1659,10 @@ def conflicting_contract_findings( current = current_by_contract.get(contract_key) if not current: continue - current_fingerprint = str(current.get("fingerprint_v2") or _fingerprint_v2(current)) - prior_fingerprint = str(prior.get("fingerprint_v2") or _fingerprint_v2(prior)) - if current_fingerprint != prior_fingerprint: - conflicts.append({**prior, "current_fingerprint_v2": current_fingerprint}) + current_behavior = str(current.get("behavior_digest") or _behavior_digest(current)) + prior_behavior = str(prior.get("behavior_digest") or _behavior_digest(prior)) + if current_behavior != prior_behavior: + conflicts.append({**prior, "current_behavior_digest": current_behavior}) return conflicts @@ -1689,6 +1688,10 @@ def unresolved_history_findings(history: list[dict[str, Any]]) -> list[dict[str, return previous_matching_findings(history, all_findings) +def remediation_round_count(history: list[dict[str, Any]]) -> int: + return sum(1 for round_state in history if round_state.get("status") == "remediation") + + def has_active_blocking_history(history: list[dict[str, Any]]) -> bool: if not history: return False @@ -2234,10 +2237,11 @@ def main() -> int: ) if active_blocking_history and new_head_for_scope: latest_status = str((finding_history[-1] if finding_history else {}).get("status") or "") + completed_remediation_rounds = remediation_round_count(finding_history) review_scope = ( "final_sanity" if latest_status == "blocked_human_contract" - or (previous_streak >= MAX_REMEDIATION_ROUNDS and latest_status == "remediation") + or completed_remediation_rounds >= MAX_REMEDIATION_ROUNDS else "remediation" ) @@ -2520,27 +2524,7 @@ def main() -> int: "next_action": "none", } ) - else: - decision = apply_arbitration_failure( - decision, ReviewError("trusted clearance does not match this head or diff") - ) - return publish_review_decision( - token, - repo, - pr_number, - pr_url, - decision, - exit_code=1, - blocking_streak=previous_streak, - previous_head_sha=previous_head_sha, - current_head_sha=current_head_sha, - reviewed_head_sha=current_head_sha, - new_head=bool(previous_head_sha and current_head_sha != previous_head_sha), - finding_history_marker=build_invalid_finding_history_marker(current_head_sha), - history_valid=False, - ) - - clearance_marker_out = clearance_marker_text if clearance_marker and not decision["blocked"] else "" + clearance_marker_out = "" history_requires_confirmation = finding_history_requires_confirmation( finding_history ) or legacy_blocking_state @@ -2571,6 +2555,7 @@ def main() -> int: repeated_finding = bool(decision["blocked"] and repeated_fingerprints) contract_conflicts = conflicting_contract_findings(finding_history, decision["blocking_findings"]) new_head = bool(previous_head_sha and current_head_sha and previous_head_sha != current_head_sha) + completed_remediation_rounds = remediation_round_count(finding_history) blocking_streak = next_blocking_streak( previous_streak, blocked=bool(decision["blocked"]), @@ -2587,7 +2572,7 @@ def main() -> int: "next_action": "contract_arbitration", } ) - elif decision["blocked"] and blocking_streak <= MAX_REMEDIATION_ROUNDS: + elif decision["blocked"] and completed_remediation_rounds < MAX_REMEDIATION_ROUNDS: decision["next_action"] = "auto_remediation" elif decision["blocked"]: decision.update( @@ -2729,7 +2714,7 @@ def main() -> int: history_status = "blocked_human_contract" elif decision.get("next_action") == "human_contract_resolution": history_status = "blocked_human_contract" - elif blocking_streak > MAX_REMEDIATION_ROUNDS or contract_conflicts or decision.get("next_action") == "contract_arbitration": + elif completed_remediation_rounds >= MAX_REMEDIATION_ROUNDS or contract_conflicts or decision.get("next_action") == "contract_arbitration": history_status = "arbitration_required" elif blocking_streak > 1: history_status = "remediation" diff --git a/tests/test_run_codex_pr_review.py b/tests/test_run_codex_pr_review.py index 336c52f5..8e292ec6 100644 --- a/tests/test_run_codex_pr_review.py +++ b/tests/test_run_codex_pr_review.py @@ -192,6 +192,25 @@ def test_sanitized_replays_keep_semantic_contracts_separate(self) -> None: run_codex_pr_review.conflicting_contract_findings(cleared_history, [opposite]), [], ) + severity_only = [dict(same, severity="critical")] + self.assertEqual( + run_codex_pr_review.conflicting_contract_findings(history, severity_only), + [], + ) + + def test_remediation_round_count_is_independent_of_blocking_streak(self) -> None: + finding = { + "severity": "high", "category": "logic", "file": "service/review.py", + "description": "`Review.run()` keeps the contract.", "suggestion": "Keep `Review.run()` stable.", + } + first, _ = run_codex_pr_review.parse_finding_history( + run_codex_pr_review.build_finding_history_marker([], [finding], "deadbeef", status="initial_review") + ) + second, _ = run_codex_pr_review.parse_finding_history( + run_codex_pr_review.build_finding_history_marker(first, [finding], "feedface", status="remediation") + ) + self.assertEqual(run_codex_pr_review.remediation_round_count(second), 1) + self.assertLess(run_codex_pr_review.remediation_round_count(second), run_codex_pr_review.MAX_REMEDIATION_ROUNDS) def test_parse_arbitration_output_requires_supported_verdict(self) -> None: self.assertEqual( From de8cd1151f38697cf4f7b3d19338da2170211ec5 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:48:45 +0800 Subject: [PATCH 7/7] fix: bind clearance to reviewer implementation Co-Authored-By: Codex --- scripts/run_codex_pr_review.py | 44 ++++++++++++++++++++++++------- tests/test_run_codex_pr_review.py | 11 ++++++++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/scripts/run_codex_pr_review.py b/scripts/run_codex_pr_review.py index c44d53af..381009bd 100644 --- a/scripts/run_codex_pr_review.py +++ b/scripts/run_codex_pr_review.py @@ -80,6 +80,13 @@ ARBITRATION_REPEAT_THRESHOLD = MAX_REMEDIATION_ROUNDS + 1 CLEARANCE_TTL_SECONDS = 7 * 24 * 60 * 60 CONTRACT_HISTORY_VERSION = 2 +REVIEW_TRANSITION_MATRIX = { + "initial_review": "remediation", + "remediation": "remediation", + "final_sanity": "arbitration_required", + "arbitration_required": "arbitration_required", + "blocked_human_contract": "final_sanity", +} class ReviewError(RuntimeError): @@ -339,6 +346,14 @@ def _diff_digest(diff: str) -> str: return hashlib.sha256(diff.encode("utf-8")).hexdigest()[:24] +def review_implementation_digest() -> str: + """Identify the trusted bridge implementation that issued a clearance.""" + digest = hashlib.sha256() + for path in (Path(__file__), PROMPT_TEMPLATE_PATH): + digest.update(path.read_bytes()) + return digest.hexdigest()[:24] + + # --------------------------------------------------------------------------- # Review prompt # --------------------------------------------------------------------------- @@ -1122,6 +1137,7 @@ def build_clearance_marker( arbiter_identity: str, evidence: str, workflow_run_id: int, + implementation_digest: str | None = None, issued_at: int | None = None, expires_at: int | None = None, ) -> str: @@ -1152,6 +1168,7 @@ def build_clearance_marker( "evidence_digest": hashlib.sha256(_sanitize_history_text(evidence, 1000).encode("utf-8")).hexdigest()[:24], "arbiter_identity": _sanitize_history_text(arbiter_identity, 120), "workflow_run_id": int(workflow_run_id), + "implementation_digest": implementation_digest or review_implementation_digest(), "issued_at": timestamp, "expires_at": expiry, } @@ -1188,6 +1205,7 @@ def parse_clearance_marker(body: str) -> tuple[dict[str, Any] | None, bool]: "evidence_digest": str, "arbiter_identity": str, "workflow_run_id": int, + "implementation_digest": str, "issued_at": int, "expires_at": int, } @@ -1198,6 +1216,8 @@ def parse_clearance_marker(body: str) -> tuple[dict[str, Any] | None, bool]: return None, False if not all(isinstance(item, str) and re.fullmatch(r"[0-9a-f]{20}", item) for item in payload["fingerprints"]): return None, False + if not re.fullmatch(r"[0-9a-f]{24}", payload["implementation_digest"]): + return None, False return payload, True @@ -1231,6 +1251,8 @@ def clearance_matches( return False if str(clearance.get("diff_digest") or "").strip().lower() != str(diff_digest or "").strip().lower(): return False + if str(clearance.get("implementation_digest") or "") != review_implementation_digest(): + return False current_contract_keys = { _contract_key(finding) for finding in findings @@ -1692,13 +1714,24 @@ def remediation_round_count(history: list[dict[str, Any]]) -> int: return sum(1 for round_state in history if round_state.get("status") == "remediation") +def review_scope_for_history(history: list[dict[str, Any]], *, new_head: bool) -> str: + if not history or not new_head: + return "initial_review" + status = str(history[-1].get("status") or "") + if status == "blocked_human_contract": + return "final_sanity" + if status in {"initial_review", "remediation"}: + return "final_sanity" if remediation_round_count(history) >= MAX_REMEDIATION_ROUNDS else "remediation" + return "initial_review" + + def has_active_blocking_history(history: list[dict[str, Any]]) -> bool: if not history: return False latest = history[-1] status = latest.get("status", "blocking") return finding_history_requires_confirmation(history) or ( - status in {"blocking", "remediation", "arbitration_required", "blocked_human_contract"} + status in {"initial_review", "blocking", "remediation", "arbitration_required", "blocked_human_contract"} and bool(latest.get("findings")) ) @@ -2236,14 +2269,7 @@ def main() -> int: previous_head_sha and current_head_sha and previous_head_sha != current_head_sha ) if active_blocking_history and new_head_for_scope: - latest_status = str((finding_history[-1] if finding_history else {}).get("status") or "") - completed_remediation_rounds = remediation_round_count(finding_history) - review_scope = ( - "final_sanity" - if latest_status == "blocked_human_contract" - or completed_remediation_rounds >= MAX_REMEDIATION_ROUNDS - else "remediation" - ) + review_scope = review_scope_for_history(finding_history, new_head=True) # Keep the cumulative diff for clearance binding. Remediation review uses # only the trusted previous-head delta, unless this is its one final sanity pass. diff --git a/tests/test_run_codex_pr_review.py b/tests/test_run_codex_pr_review.py index 8e292ec6..a325cddf 100644 --- a/tests/test_run_codex_pr_review.py +++ b/tests/test_run_codex_pr_review.py @@ -212,6 +212,17 @@ def test_remediation_round_count_is_independent_of_blocking_streak(self) -> None self.assertEqual(run_codex_pr_review.remediation_round_count(second), 1) self.assertLess(run_codex_pr_review.remediation_round_count(second), run_codex_pr_review.MAX_REMEDIATION_ROUNDS) + def test_review_transition_matrix_keeps_initial_active_for_two_remediations(self) -> None: + finding = {"severity": "high", "category": "logic", "file": "service/review.py", "description": "`Review.run()` blocks.", "suggestion": "Keep `Review.run()` blocked."} + history, _ = run_codex_pr_review.parse_finding_history(run_codex_pr_review.build_finding_history_marker([], [finding], "deadbeef", status="initial_review")) + self.assertTrue(run_codex_pr_review.has_active_blocking_history(history)) + self.assertEqual(run_codex_pr_review.review_scope_for_history(history, new_head=True), "remediation") + for head in ("feedface", "c0ffee0"): + marker = run_codex_pr_review.build_finding_history_marker(history, [finding], head, status="remediation") + history, _ = run_codex_pr_review.parse_finding_history(marker) + self.assertEqual(run_codex_pr_review.review_scope_for_history(history, new_head=True), "final_sanity") + self.assertEqual(run_codex_pr_review.review_scope_for_history(history, new_head=False), "initial_review") + def test_parse_arbitration_output_requires_supported_verdict(self) -> None: self.assertEqual( run_codex_pr_review.parse_arbitration_output('{"verdict":"clear","reason":"covered by the regression test"}'),