diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 4b6a0abb..9c1f8f5b 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -823,7 +823,7 @@ jobs: needs: [coverage-evidence] if: always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target') runs-on: ubuntu-latest - timeout-minutes: 75 + timeout-minutes: 360 permissions: actions: write checks: read @@ -2239,7 +2239,7 @@ jobs: id: opencode_review_model_pool if: needs.coverage-evidence.result == 'success' continue-on-error: true - timeout-minutes: 20 + timeout-minutes: 300 env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} GITHUB_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -2249,9 +2249,9 @@ jobs: NO_COLOR: "1" OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano" OPENCODE_MODEL_ATTEMPTS: "1" - OPENCODE_RUN_TIMEOUT_SECONDS: "240" + OPENCODE_RUN_TIMEOUT_SECONDS: "18000" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" - OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "360" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000" OPENCODE_BACKOFF_INITIAL_SECONDS: "30" OPENCODE_BACKOFF_MAX_SECONDS: "30" OPENCODE_FIRST_ATTEMPT_AGENT: ci-review @@ -2602,7 +2602,7 @@ jobs: - name: Approve PR if OpenCode review passed if: always() - timeout-minutes: 75 + timeout-minutes: 300 env: GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || github.token }} CHECK_LOOKUP_GH_TOKEN: ${{ github.token }} @@ -3896,7 +3896,7 @@ jobs: } >"$prompt_file" cd "$OPENCODE_REVIEW_WORKDIR" - if ! timeout --kill-after=30s "${OPENCODE_RUN_TIMEOUT_SECONDS:-240}s" opencode run "$(cat "$prompt_file")" \ + if ! timeout --kill-after=30s "${OPENCODE_RUN_TIMEOUT_SECONDS:-18000}s" opencode run "$(cat "$prompt_file")" \ --pure \ --agent ci-review-fallback \ --model "$MODEL" \ diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index 6240ff32..a4b84e9e 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -372,7 +372,7 @@ jobs: } trap restore_workspace_config EXIT cd "$TARGET_WORKSPACE" - timeout 900 opencode run "$(cat "$prompt_file")" \ + timeout 18000 opencode run "$(cat "$prompt_file")" \ --pure \ --agent ci-autofix \ --model "$MODEL" \ diff --git a/scripts/ci/pr_review_fix_scheduler.py b/scripts/ci/pr_review_fix_scheduler.py index 97f1fd54..34a57826 100755 --- a/scripts/ci/pr_review_fix_scheduler.py +++ b/scripts/ci/pr_review_fix_scheduler.py @@ -114,8 +114,10 @@ def change_request_is_autofixable(pr: dict[str, Any]) -> bool: def needs_autofix(pr: dict[str, Any]) -> tuple[bool, tuple[str, ...]]: """Return whether current-head evidence justifies an autofix attempt.""" reasons: list[str] = [] - if has_current_head_changes_requested(pr) and change_request_is_autofixable(pr): - reasons.append("current-head OpenCode requested changes") + if not (has_current_head_changes_requested(pr) and change_request_is_autofixable(pr)): + return False, () + + reasons.append("current-head OpenCode requested changes") unresolved = unresolved_thread_count(pr) if unresolved: reasons.append(f"{unresolved} active unresolved review thread(s)") @@ -209,7 +211,7 @@ def inspect_pr( needs_fix, reasons = needs_autofix(pr) if not needs_fix: - return "skip", ("no current-head change request or active unresolved review thread",) + return "skip", ("no current-head autofixable OpenCode change request",) if comments is None: comments = issue_comments(repo, number) diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 9ce4aaae..063bb35a 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -94,7 +94,7 @@ run_one_model_attempt() { local opencode_export_file="$8" local run_timeout_seconds export_timeout_seconds opencode_status session_id - run_timeout_seconds="${OPENCODE_RUN_TIMEOUT_SECONDS:-180}" + run_timeout_seconds="${OPENCODE_RUN_TIMEOUT_SECONDS:-18000}" export_timeout_seconds="${OPENCODE_EXPORT_TIMEOUT_SECONDS:-60}" rm -f "$opencode_json_file" "$opencode_export_file" "$candidate_output_file" @@ -149,7 +149,7 @@ main() { local opencode_json_file opencode_export_file agent retry_sleep original_run_timeout run_status attempts="${OPENCODE_MODEL_ATTEMPTS:-3}" - original_run_timeout="${OPENCODE_RUN_TIMEOUT_SECONDS:-900}" + original_run_timeout="${OPENCODE_RUN_TIMEOUT_SECONDS:-18000}" deadline=$((SECONDS + ${OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000})) : >"$OPENCODE_OUTPUT_FILE" cd "$OPENCODE_REVIEW_WORKDIR" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ffea57cd..8c3d5aea 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -508,8 +508,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "Read and follow the complete review contract" "opencode review uses a compact launcher while keeping the full review contract on disk" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "tokens_limit_reached" "opencode review detects provider context-window overflow" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "skipping remaining attempts for this model" "opencode review skips same-model retries after context-window overflow" - assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode primary review has a bounded per-model timeout before trying fallback models" - assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' "opencode model pool has a one-hour total retry budget" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "18000"' "opencode primary review has a bounded per-model timeout before trying fallback models" + assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' "opencode model pool has a five-hour total retry budget" assert_file_contains "$workflow_file" "needs.coverage-evidence.result == 'success'" "opencode model pool only runs after coverage evidence passed" assert_file_contains "$workflow_file" "id: opencode_review_model_pool" "opencode DeepSeek V3 fallback still runs after a primary model timeout or step failure when coverage evidence passed" assert_file_contains "$workflow_file" "always()" "opencode fallback chain uses always() so failed model steps cannot skip every fallback" @@ -578,7 +578,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'load_selected_review_output()' "opencode approval step has a direct selected-output fallback when the overview comment is stale or invalid" assert_file_contains "$workflow_file" "gate result from Review Overview comment" "opencode approval step distinguishes overview-comment gate results" assert_file_contains "$workflow_file" "gate result from selected OpenCode output" "opencode approval step can recover from an invalid overview by validating the selected successful output" - assert_file_contains "$workflow_file" 'timeout-minutes: 75' "opencode approval step has a bounded wall-clock timeout" + assert_file_contains "$workflow_file" 'timeout-minutes: 300' "opencode approval step has a bounded wall-clock timeout" assert_file_contains "$workflow_file" 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' "opencode approval waits for bounded long-running peer checks before approving" assert_file_contains "$workflow_file" 'CHECK_LOOKUP_RETRY_ATTEMPTS: "5"' "opencode approval retries transient GitHub check lookup failures before changing review state" assert_file_contains "$workflow_file" 'GitHub Checks lookup failed; retrying' "opencode approval logs transient check lookup retries" @@ -618,7 +618,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "no model produced a valid review control block" "opencode model-failure path documents why approval is withheld" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode primary and fallback paths avoid multi-attempt stalls on one model" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode catalog fallback tries each model once before moving on" - assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode catalog fallback has a bounded model review timeout before step timeout" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "18000"' "opencode catalog fallback has a bounded model review timeout before step timeout" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode %s attempt %s/%s failed" "opencode catalog fallback records per-model retry failures" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps" assert_file_contains "$workflow_file" "github-models/openai/o3 github-models/openai/o3-mini github-models/openai/o4-mini" "opencode review includes additional OpenAI reasoning model fallbacks" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c5dd7a9b..515e5917 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -210,15 +210,15 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert '"## Review outcome"' in workflow assert '"## Check outcome"' not in workflow assert "publish REQUEST_CHANGES when coverage-evidence blocker states" in workflow - assert 'timeout-minutes: 75' in workflow - assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 20", workflow) + assert re.search(r"opencode-review-target:[\s\S]{0,240}timeout-minutes: 360", workflow) + assert 'timeout-minutes: 300' in workflow assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' in workflow assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30"' in workflow assert 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' in workflow assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow - assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "240"' in workflow + assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "18000"' in workflow assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "360"' in workflow + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow assert re.search(r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', workflow) diff --git a/tests/test_pr_review_fix_scheduler.py b/tests/test_pr_review_fix_scheduler.py index a838147a..8b22055f 100644 --- a/tests/test_pr_review_fix_scheduler.py +++ b/tests/test_pr_review_fix_scheduler.py @@ -38,7 +38,7 @@ def test_recent_fix_marker_is_head_scoped(): def test_needs_autofix_uses_current_head_evidence(): - """Autofix only starts from current-head review or thread evidence.""" + """Autofix starts from current-head OpenCode change requests.""" head = "a" * 40 pr = make_pr( headRefOid=head, @@ -62,6 +62,15 @@ def test_needs_autofix_uses_current_head_evidence(): ) +def test_needs_autofix_ignores_thread_only_feedback(): + """Thread-only feedback must not start an autonomous autofix run.""" + pr = make_pr( + reviewThreads={"nodes": [{"id": "thread", "isResolved": False, "isOutdated": False}]}, + ) + + assert fix.needs_autofix(pr) == (False, ()) + + @pytest.mark.parametrize( ("merge_state", "body"), [ @@ -381,7 +390,10 @@ def test_fix_inspect_skip_wait_and_error_paths(monkeypatch): ) monkeypatch.setattr(fix, "needs_autofix", lambda pr: (False, ())) - assert fix.inspect_pr("owner/repo", make_pr(), args) == ("skip", ("no current-head change request or active unresolved review thread",)) + assert fix.inspect_pr("owner/repo", make_pr(), args) == ( + "skip", + ("no current-head autofixable OpenCode change request",), + ) monkeypatch.setattr(fix, "needs_autofix", lambda pr: (True, ("reason",))) monkeypatch.setattr(fix, "issue_comments", lambda repo, number: [{"body": f"{fix.FIX_MARKER} head_sha={'a' * 40} epoch={int(time.time())} -->"}])