From f79c6e2b7d719aae230c5ea3ad8c4320ad643cd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 18:17:23 +0900 Subject: [PATCH 1/6] fix: retry reviews after coverage blockers clear --- scripts/ci/pr_review_merge_scheduler.py | 54 ++++++++++++++++ tests/test_pr_review_merge_scheduler.py | 86 +++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 118d0d9031..f7c91e5914 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -158,6 +158,11 @@ "deterministic fallback approval", "did not emit a usable current-head control block", ) +COVERAGE_REVIEW_MARKERS = ( + "coverage evidence did not pass", + "coverage-evidence", + "test evidence: not proven passing", +) LAST_PUSH_APPROVAL_RESTAMP_MESSAGE = "chore: refresh head for last-push approval" @@ -1193,6 +1198,33 @@ def has_current_head_changes_requested(pr: dict[str, Any]) -> bool: return current_head_review_state(pr, "CHANGES_REQUESTED") +def current_head_coverage_change_request(pr: dict[str, Any]) -> bool: + """Return whether the latest current-head request is only a coverage gate.""" + for review in reversed((pr.get("reviews") or {}).get("nodes") or []): + if not is_opencode_review(review) or not review_matches_current_head(review, pr): + continue + if (review.get("state") or "").upper() != "CHANGES_REQUESTED": + return False + body = (review.get("body") or "").lower() + return all(marker in body for marker in COVERAGE_REVIEW_MARKERS) + return False + + +def coverage_evidence_state(pr: dict[str, Any]) -> str: + """Return missing, running, complete, or failed for the latest coverage gate.""" + for node in reversed(context_nodes(pr)): + name = (node.get("name") or node.get("context") or "").lower() + if name != "coverage-evidence": + continue + status = (node.get("status") or node.get("state") or "").upper() + if status in RUNNING_CHECK_STATES: + return "running" + if node.get("__typename") == "CheckRun": + return "complete" if (node.get("conclusion") or "").upper() == "SUCCESS" else "failed" + return "complete" if status == "SUCCESS" else "failed" + return "missing" + + def stale_opencode_change_request_ids(pr: dict[str, Any]) -> list[int]: """Return dismissible automated change requests tied to previous heads.""" review_ids: list[int] = [] @@ -2401,6 +2433,28 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio return request_branch_update( "current-head OpenCode review requested changes; branch is outdated before re-review" ) + coverage_ready = ( + trigger_reviews + and review_dispatch_allowed + and current_head_coverage_change_request(pr) + and coverage_evidence_state(pr) == "complete" + and strix_evidence_state(pr) == "complete" + and not failed_status_checks(pr) + ) + if coverage_ready: + wait_reason = repository_dispatch_wait_reason(repo, workflow) + if wait_reason: + return decide("wait", wait_reason) + dispatch_result = dispatch_opencode_review(repo, workflow, pr, dry_run=dry_run) + if dispatch_result == "already_running": + return decide( + "wait", + "current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active", + ) + return decide( + "review_dispatch", + "current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched", + ) if pr.get("autoMergeRequest"): return finish( disable_auto_merge_decision( diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index f2dd258136..489a542390 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1239,6 +1239,51 @@ def test_review_state_and_failed_checks(): assert sched.has_current_head_approval(superseded) assert not sched.has_current_head_changes_requested(superseded) + coverage_request = make_pr( + reviews={ + "nodes": [ + { + **opencode_review("CHANGES_REQUESTED", "head"), + "body": ( + "OpenCode cannot approve yet because required coverage evidence did not pass. " + "The coverage-evidence gate reported test evidence: not proven passing." + ), + } + ] + }, + statusCheckRollup={ + "contexts": { + "nodes": [ + strix_check(), + { + "__typename": "CheckRun", + "name": "coverage-evidence", + "status": "COMPLETED", + "conclusion": "SUCCESS", + }, + ] + } + }, + ) + assert sched.current_head_coverage_change_request(coverage_request) + assert sched.coverage_evidence_state(coverage_request) == "complete" + assert sched.coverage_evidence_state( + make_pr( + statusCheckRollup={ + "contexts": {"nodes": [{"name": "coverage-evidence", "state": "PENDING"}]} + } + ) + ) == "running" + assert sched.coverage_evidence_state(make_pr()) == "missing" + ordinary_request = make_pr( + reviews={ + "nodes": [ + {**opencode_review("CHANGES_REQUESTED", "head"), "body": "Fix the estimator."} + ] + } + ) + assert not sched.current_head_coverage_change_request(ordinary_request) + stale_gate_reviews = make_pr( reviews={ "nodes": [ @@ -3052,6 +3097,47 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): "current-head OpenCode review requested changes" ) assert update_calls == [] + coverage_request = make_pr( + reviews={ + "nodes": [ + { + **opencode_review("CHANGES_REQUESTED", "head"), + "body": ( + "OpenCode cannot approve yet because required coverage evidence did not pass. " + "The coverage-evidence gate reported test evidence: not proven passing." + ), + } + ] + }, + statusCheckRollup={ + "contexts": { + "nodes": [ + strix_check(), + { + "__typename": "CheckRun", + "name": "coverage-evidence", + "status": "COMPLETED", + "conclusion": "SUCCESS", + }, + ] + } + }, + ) + dispatched = [] + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: dispatched.append( + (repo, workflow, pr["headRefOid"], dry_run) + ) + or "dispatched", + ) + coverage_decision = inspect(coverage_request) + assert coverage_decision.action == "review_dispatch" + assert coverage_decision.reason == ( + "current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched" + ) + assert dispatched == [("owner/repo", "OpenCode Review", "head", True)] action_required_pr = make_pr( statusCheckRollup={ "contexts": { From adfe46d6d754bb0d5923290a99ec49c7ccac45b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 18:30:48 +0900 Subject: [PATCH 2/6] fix(scheduler): match coverage blocker review body --- scripts/ci/pr_review_merge_scheduler.py | 2 +- tests/test_pr_review_merge_scheduler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index f7c91e5914..ad8f763dc2 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -161,7 +161,7 @@ COVERAGE_REVIEW_MARKERS = ( "coverage evidence did not pass", "coverage-evidence", - "test evidence: not proven passing", + "required test/docstring evidence", ) LAST_PUSH_APPROVAL_RESTAMP_MESSAGE = "chore: refresh head for last-push approval" diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 489a542390..fa1c741b5c 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1246,7 +1246,7 @@ def test_review_state_and_failed_checks(): **opencode_review("CHANGES_REQUESTED", "head"), "body": ( "OpenCode cannot approve yet because required coverage evidence did not pass. " - "The coverage-evidence gate reported test evidence: not proven passing." + "The coverage-evidence gate reported that required test/docstring evidence was not proven." ), } ] @@ -3104,7 +3104,7 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): **opencode_review("CHANGES_REQUESTED", "head"), "body": ( "OpenCode cannot approve yet because required coverage evidence did not pass. " - "The coverage-evidence gate reported test evidence: not proven passing." + "The coverage-evidence gate reported that required test/docstring evidence was not proven." ), } ] From 27f3d8ac5a219855145e088e910f241fbf7a728d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:57:42 +0900 Subject: [PATCH 3/6] test(scheduler): cover coverage evidence gate branches --- .../organization_commercial_readiness_loop.py | 3 +- tests/test_pr_review_merge_scheduler.py | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0a..1ede18a00b 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Initialize the client with one non-empty GitHub credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index fa1c741b5c..399ad68fd3 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1274,7 +1274,38 @@ def test_review_state_and_failed_checks(): } ) ) == "running" + assert sched.coverage_evidence_state( + make_pr(statusCheckRollup={"contexts": {"nodes": [strix_check()]}}) + ) == "missing" + assert sched.coverage_evidence_state( + make_pr( + statusCheckRollup={ + "contexts": {"nodes": [{"name": "coverage-evidence", "state": "SUCCESS"}]} + } + ) + ) == "complete" + assert sched.coverage_evidence_state( + make_pr( + statusCheckRollup={ + "contexts": {"nodes": [{"name": "coverage-evidence", "state": "FAILURE"}]} + } + ) + ) == "failed" assert sched.coverage_evidence_state(make_pr()) == "missing" + human_coverage_request = make_pr( + reviews={ + "nodes": [ + { + **opencode_review("CHANGES_REQUESTED", "head", login="human"), + "body": "coverage evidence did not pass; coverage-evidence; required test/docstring evidence", + } + ] + } + ) + assert not sched.current_head_coverage_change_request(human_coverage_request) + assert not sched.current_head_coverage_change_request( + make_pr(reviews={"nodes": [opencode_review("APPROVED", "head")]}) + ) ordinary_request = make_pr( reviews={ "nodes": [ @@ -3132,6 +3163,33 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): ) or "dispatched", ) + monkeypatch.setattr( + sched, + "repository_dispatch_wait_reason", + lambda repo, workflow: "review dispatch waits", + ) + coverage_wait = inspect(coverage_request) + assert coverage_wait.action == "wait" + assert coverage_wait.reason == "review dispatch waits" + monkeypatch.setattr(sched, "repository_dispatch_wait_reason", lambda repo, workflow: None) + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: "already_running", + ) + coverage_active = inspect(coverage_request) + assert coverage_active.action == "wait" + assert coverage_active.reason == ( + "current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active" + ) + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: dispatched.append( + (repo, workflow, pr["headRefOid"], dry_run) + ) + or "dispatched", + ) coverage_decision = inspect(coverage_request) assert coverage_decision.action == "review_dispatch" assert coverage_decision.reason == ( From fb98a07618003aba35f969b71166a2564cf0c5c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:33:57 -0700 Subject: [PATCH 4/6] test(scheduler): reproduce coverage retry self-block --- tests/test_pr_review_merge_scheduler.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 399ad68fd3..d7ed27743d 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -3150,6 +3150,15 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): "status": "COMPLETED", "conclusion": "SUCCESS", }, + { + "__typename": "CheckRun", + "name": "opencode-review", + "status": "COMPLETED", + "conclusion": "FAILURE", + "checkSuite": { + "workflowRun": {"workflow": {"name": "OpenCode Review"}} + }, + }, ] } }, @@ -3196,6 +3205,19 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): "current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched" ) assert dispatched == [("owner/repo", "OpenCode Review", "head", True)] + + coverage_request["statusCheckRollup"]["contexts"]["nodes"].append( + { + "__typename": "CheckRun", + "name": "Security Scan", + "status": "COMPLETED", + "conclusion": "FAILURE", + } + ) + unrelated_failure = inspect(coverage_request) + assert unrelated_failure.action == "block" + assert unrelated_failure.reason == "current-head OpenCode review requested changes" + action_required_pr = make_pr( statusCheckRollup={ "contexts": { From 11491068712859e936e7ce4ed7f204f5c1157f0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:38:29 -0700 Subject: [PATCH 5/6] fix(scheduler): unblock coverage-only review retry --- scripts/ci/pr_review_merge_scheduler.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index ad8f763dc2..a1fc87ec67 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -1407,8 +1407,17 @@ def dismiss_stale_opencode_change_requests(repo: str, pr: dict[str, Any], *, dry return len(review_ids) -def failed_status_checks(pr: dict[str, Any]) -> list[str]: - """Return failing check or status context names from the PR rollup.""" +def failed_status_checks( + pr: dict[str, Any], + *, + ignore_opencode: bool = False, +) -> list[str]: + """Return failing check or status context names from the PR rollup. + + ``ignore_opencode`` is reserved for the authenticated coverage-only retry + path: the previous OpenCode run is expected to be failing there because it + published the current-head coverage change request being retried. + """ failed: list[str] = [] latest_check_runs: dict[ tuple[str, str], @@ -1449,6 +1458,8 @@ def failed_status_checks(pr: dict[str, Any]) -> list[str]: for _, _, node in sorted(latest_check_runs.values(), key=lambda item: item[1]): conclusion = (node.get("conclusion") or "").upper() if conclusion in FAILED_CHECK_CONCLUSIONS: + if ignore_opencode and is_opencode_context(node): + continue if is_strix_context(node) and "strix" in successful_status_contexts: continue if is_opencode_context(node) and "opencode-review" in successful_status_contexts: @@ -1457,6 +1468,8 @@ def failed_status_checks(pr: dict[str, Any]) -> list[str]: for node in status_contexts: state = (node.get("state") or "").upper() if state in {"FAILURE", "ERROR"}: + if ignore_opencode and is_opencode_context(node): + continue failed.append(node.get("context") or "status-context") return failed @@ -2439,7 +2452,7 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio and current_head_coverage_change_request(pr) and coverage_evidence_state(pr) == "complete" and strix_evidence_state(pr) == "complete" - and not failed_status_checks(pr) + and not failed_status_checks(pr, ignore_opencode=True) ) if coverage_ready: wait_reason = repository_dispatch_wait_reason(repo, workflow) From e19066db8797a334f8bb2d9bd2d202564ff1bc20 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:32:52 +0900 Subject: [PATCH 6/6] fix(ci): refresh audit lock and scheduler assertion --- requirements-pip-audit-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49a..0ae099d8fe 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \