From bba540b4776cede02a4dfb0562f094928cbc5163 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 23 Jun 2026 21:56:38 +0900 Subject: [PATCH 01/17] Add centralized PR review merge scheduler --- .../workflows/pr-review-merge-scheduler.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/pr-review-merge-scheduler.yml diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml new file mode 100644 index 000000000..4c40b0220 --- /dev/null +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -0,0 +1,88 @@ +name: PR Review Merge Scheduler + +on: + schedule: + - cron: "17 */2 * * *" + workflow_dispatch: + inputs: + dry_run: + description: Print planned actions without mutating PRs + required: false + default: false + type: boolean + max_prs: + description: Maximum open PRs to inspect + required: false + default: "100" + trigger_reviews: + description: Dispatch OpenCode Review for PR heads without current approval + required: false + default: true + type: boolean + enable_auto_merge: + description: Enable auto-merge for current-head approved PRs + required: false + default: true + type: boolean + update_branches: + description: Update outdated PR branches after OpenCode approval + required: false + default: true + type: boolean + +concurrency: + group: pr-review-merge-scheduler + cancel-in-progress: false + +jobs: + scan-pr-queue: + runs-on: ubuntu-latest + permissions: + actions: write + checks: read + contents: write + pull-requests: write + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }} + MAX_PRS: ${{ inputs.max_prs || '100' }} + PROJECT_FLOW: ${{ vars.PROJECT_FLOW }} + TRIGGER_REVIEWS: ${{ github.event_name != 'workflow_dispatch' || inputs.trigger_reviews == true }} + ENABLE_AUTO_MERGE: ${{ github.event_name != 'workflow_dispatch' || inputs.enable_auto_merge == true }} + UPDATE_BRANCHES: ${{ github.event_name != 'workflow_dispatch' || inputs.update_branches == true }} + steps: + - name: Download central scheduler + env: + CENTRAL_REPOSITORY: ContextualWisdomLab/.github + CENTRAL_REF: main + run: | + set -euo pipefail + scheduler_dir="$RUNNER_TEMP/pr-governance" + mkdir -p "$scheduler_dir" + gh api "repos/${CENTRAL_REPOSITORY}/contents/scripts/ci/pr_review_merge_scheduler.py?ref=${CENTRAL_REF}" --jq .content | base64 -d > "$scheduler_dir/pr_review_merge_scheduler.py" + python3 "$scheduler_dir/pr_review_merge_scheduler.py" --self-test + + - name: Inspect PR review and merge queue + run: | + set -euo pipefail + project_flow="$PROJECT_FLOW" + if [ -z "$project_flow" ]; then + project_flow=github-flow + if [ "$DEFAULT_BRANCH" = develop ]; then + project_flow=git-flow + fi + fi + args=( + --repo "$GITHUB_REPOSITORY" + --base-branch "$DEFAULT_BRANCH" + --max-prs "$MAX_PRS" + --project-flow "$project_flow" + --review-workflow "OpenCode Review" + ) + if [ "$DRY_RUN" = true ]; then args+=(--dry-run); fi + if [ "$TRIGGER_REVIEWS" = true ]; then args+=(--trigger-reviews); else args+=(--no-trigger-reviews); fi + if [ "$ENABLE_AUTO_MERGE" = true ]; then args+=(--enable-auto-merge); else args+=(--no-enable-auto-merge); fi + if [ "$UPDATE_BRANCHES" = true ]; then args+=(--update-branches); else args+=(--no-update-branches); fi + python3 "$RUNNER_TEMP/pr-governance/pr_review_merge_scheduler.py" "${args[@]}" From 38e9a8207b37a90f4ee03bcb6d8537fab03f37e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 23 Jun 2026 22:02:02 +0900 Subject: [PATCH 02/17] Add OpenCode CI configuration --- opencode.jsonc | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 opencode.jsonc diff --git a/opencode.jsonc b/opencode.jsonc new file mode 100644 index 000000000..06e68e7c0 --- /dev/null +++ b/opencode.jsonc @@ -0,0 +1,148 @@ +{ + "$schema": "https://opencode.ai/config.json", + "model": "github-models/openai/gpt-5", + "small_model": "github-models/deepseek/deepseek-v3-0324", + "enabled_providers": ["github-models"], + "lsp": true, + "mcp": { + "codegraph": { + "type": "local", + "command": ["npx", "-y", "@colbymchenry/codegraph@0.9.9", "serve", "--mcp"], + "enabled": true + }, + "deepwiki": { + "type": "remote", + "url": "https://mcp.deepwiki.com/mcp", + "enabled": true, + "timeout": 10000 + }, + "context7": { + "type": "local", + "command": ["npx", "-y", "@upstash/context7-mcp@3.1.0", "--transport", "stdio"], + "enabled": true, + "timeout": 10000, + "environment": { + "NPM_CONFIG_IGNORE_SCRIPTS": "true", + "NPM_CONFIG_LOGLEVEL": "error" + } + }, + "web_search": { + "type": "local", + "command": ["npx", "-y", "@guhcostan/web-search-mcp@1.0.5"], + "enabled": true, + "timeout": 10000, + "environment": { + "NPM_CONFIG_IGNORE_SCRIPTS": "true", + "NPM_CONFIG_LOGLEVEL": "error" + } + } + }, + "permission": { + "edit": "deny", + "bash": "allow", + "read": "allow", + "grep": "allow", + "glob": "allow", + "list": "allow", + "task": "allow", + "webfetch": "allow", + "websearch": "allow", + "lsp": "allow", + "external_directory": "allow" + }, + "agent": { + "ci-review": { + "description": "Compact read-only CI pull request reviewer", + "mode": "primary", + "prompt": "{file:./ci-review-prompt.md}", + "steps": 4, + "permission": { + "edit": "deny", + "bash": "allow", + "read": "allow", + "grep": "allow", + "glob": "allow", + "list": "allow", + "task": "allow", + "webfetch": "allow", + "websearch": "allow", + "lsp": "allow", + "external_directory": "allow" + } + }, + "ci-review-fallback": { + "description": "Expanded read-only CI pull request reviewer fallback", + "mode": "primary", + "prompt": "{file:./ci-review-prompt.md}", + "steps": 12, + "permission": { + "edit": "deny", + "bash": "allow", + "read": "allow", + "grep": "allow", + "glob": "allow", + "list": "allow", + "task": "allow", + "webfetch": "allow", + "websearch": "allow", + "lsp": "allow", + "external_directory": "allow" + } + } + }, + "provider": { + "github-models": { + "npm": "@ai-sdk/openai-compatible", + "name": "GitHub Models", + "options": { + "baseURL": "https://models.github.ai/inference", + "apiKey": "{env:STRIX_GITHUB_MODELS_TOKEN}" + }, + "models": { + "openai/gpt-5": { + "name": "OpenAI GPT-5", + "tool_call": true, + "limit": { + "context": 200000, + "output": 100000 + } + }, + "deepseek/deepseek-r1-0528": { + "name": "DeepSeek R1 0528", + "tool_call": true, + "reasoning": true, + "limit": { + "context": 128000, + "output": 4096 + } + }, + "deepseek/deepseek-v3-0324": { + "name": "DeepSeek V3 0324", + "tool_call": true, + "limit": { + "context": 128000, + "output": 4096 + } + }, + "openai/o3": { + "name": "OpenAI o3", + "tool_call": true, + "reasoning": true, + "limit": { + "context": 200000, + "output": 100000 + } + }, + "openai/o4-mini": { + "name": "OpenAI o4-mini", + "tool_call": true, + "reasoning": true, + "limit": { + "context": 200000, + "output": 100000 + } + } + } + } + } +} From e40793ba7d471974b413d7b8e79eb9d1f562e7aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 23 Jun 2026 22:33:43 +0900 Subject: [PATCH 03/17] Pin central PR scheduler download --- .github/workflows/pr-review-merge-scheduler.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 4c40b0220..dbf44656a 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -56,12 +56,19 @@ jobs: - name: Download central scheduler env: CENTRAL_REPOSITORY: ContextualWisdomLab/.github - CENTRAL_REF: main + CENTRAL_REF: 6cb3250efdbc27e57e63ed09b14fb6e8ddf18384 + CENTRAL_SCHEDULER_SHA256: 1024bd751cdcf4f114d831ce45b08bd666b3eb5033be0039ceed657f103d8ff7 run: | set -euo pipefail scheduler_dir="$RUNNER_TEMP/pr-governance" mkdir -p "$scheduler_dir" - gh api "repos/${CENTRAL_REPOSITORY}/contents/scripts/ci/pr_review_merge_scheduler.py?ref=${CENTRAL_REF}" --jq .content | base64 -d > "$scheduler_dir/pr_review_merge_scheduler.py" + gh api "repos/${CENTRAL_REPOSITORY}/contents/scripts/ci/pr_review_merge_scheduler.py?ref=${CENTRAL_REF}" \ + --jq .content | base64 -d > "$scheduler_dir/pr_review_merge_scheduler.py" + actual_sha256="$(sha256sum "$scheduler_dir/pr_review_merge_scheduler.py" | awk '{print $1}')" + if [ "$actual_sha256" != "$CENTRAL_SCHEDULER_SHA256" ]; then + echo "::error::Central scheduler checksum mismatch: expected $CENTRAL_SCHEDULER_SHA256, got $actual_sha256" + exit 1 + fi python3 "$scheduler_dir/pr_review_merge_scheduler.py" --self-test - name: Inspect PR review and merge queue From e647a3f50307d3f0e0aeac9a922fc4d3b6ee822e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 23 Jun 2026 22:38:37 +0900 Subject: [PATCH 04/17] Update central scheduler pin --- .github/workflows/pr-review-merge-scheduler.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index dbf44656a..21628ac58 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -56,8 +56,8 @@ jobs: - name: Download central scheduler env: CENTRAL_REPOSITORY: ContextualWisdomLab/.github - CENTRAL_REF: 6cb3250efdbc27e57e63ed09b14fb6e8ddf18384 - CENTRAL_SCHEDULER_SHA256: 1024bd751cdcf4f114d831ce45b08bd666b3eb5033be0039ceed657f103d8ff7 + CENTRAL_REF: 7be2d9949831d94cfb96cd7eaadae9513e7d325d + CENTRAL_SCHEDULER_SHA256: f954b62efa4ad60964a65501d777cb4ba26f1ac5746c2d11406d610a5ab695f6 run: | set -euo pipefail scheduler_dir="$RUNNER_TEMP/pr-governance" From 2618c41938774c31aee15c82e1d060b5392355fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 23 Jun 2026 22:45:17 +0900 Subject: [PATCH 05/17] Materialize PR-head review policy for Strix --- .github/workflows/strix.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 0ea8fa862..79c75a032 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -110,6 +110,12 @@ jobs: # refs/pull//head has already advanced before this queued run starts. if git -C "$TRUSTED_WORKSPACE" fetch --no-tags --depth=1 origin "$PR_HEAD_SHA"; then git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA^{commit}" + if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:opencode.jsonc" 2>/dev/null; then + git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:opencode.jsonc" > "$TRUSTED_WORKSPACE/opencode.jsonc" + fi + if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" 2>/dev/null; then + git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" > "$TRUSTED_WORKSPACE/scripts/ci/pr_review_merge_scheduler.py" + fi git -C "$TRUSTED_WORKSPACE" update-ref "refs/remotes/pull/${PR_NUMBER}/head" "$PR_HEAD_SHA" exit 0 fi @@ -118,6 +124,12 @@ jobs: fetched_head_sha="$(git -C "$TRUSTED_WORKSPACE" rev-parse "refs/remotes/pull/${PR_NUMBER}/head")" if [ "$fetched_head_sha" = "$PR_HEAD_SHA" ]; then git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA^{commit}" + if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:opencode.jsonc" 2>/dev/null; then + git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:opencode.jsonc" > "$TRUSTED_WORKSPACE/opencode.jsonc" + fi + if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" 2>/dev/null; then + git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" > "$TRUSTED_WORKSPACE/scripts/ci/pr_review_merge_scheduler.py" + fi exit 0 fi if [ "$pr_head_fetch_attempt" -lt 6 ]; then From 35501fb3b379727b409e5b453a104866e50f7f04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 23 Jun 2026 22:56:30 +0900 Subject: [PATCH 06/17] Use scheduler-dispatched OpenCode reviews --- .github/workflows/opencode-review.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index c72bcba6e..b8f6665d9 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1,10 +1,6 @@ name: OpenCode Review on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - pull_request_target: - types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: inputs: pr_number: @@ -19,6 +15,10 @@ on: description: Pull request base SHA required: true type: string + pr_head_ref: + description: Pull request head branch + required: false + type: string pr_head_sha: description: Pull request head SHA required: true From 701e1869494fb56ecf8dd7c49f8c52aed832e40a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 23 Jun 2026 23:21:55 +0900 Subject: [PATCH 07/17] Align OpenCode scheduler self-test contract --- scripts/ci/test_strix_quick_gate.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 7abc27715..d1ca1ba79 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -329,15 +329,16 @@ assert_strix_child_target_uses_constant_argument() { assert_opencode_review_uses_codegraph_and_gpt5_fallback() { local workflow_file="$REPO_ROOT/.github/workflows/opencode-review.yml" + local scheduler_workflow_file="$REPO_ROOT/.github/workflows/pr-review-merge-scheduler.yml" local opencode_config="$REPO_ROOT/opencode.jsonc" - assert_file_contains "$workflow_file" "pull_request_target:" "opencode review workflow runs on the trusted PR trigger so merge-conflict PRs still get the standard review surface" - assert_file_contains "$workflow_file" "pull_request:" "opencode review workflow publishes a PR-associated required check while trusted review side effects stay on pull_request_target" - assert_file_contains "$workflow_file" "Wait for trusted OpenCode approval review" "opencode pull_request bridge only waits for a trusted same-head OpenCode approval" - assert_file_contains "$workflow_file" "Trusted OpenCode requested changes for head" "opencode pull_request bridge fails immediately when the trusted same-head review requested changes" - assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target'" "opencode review side effects are limited to pull_request_target or manual workflow dispatch" + assert_file_contains "$workflow_file" "workflow_dispatch:" "opencode review workflow is triggered by the central PR review scheduler" + assert_file_contains "$workflow_file" "pr_head_ref:" "opencode review workflow accepts the explicit PR head ref from the scheduler" + assert_file_contains "$workflow_file" "pr_head_sha:" "opencode review workflow accepts the explicit PR head SHA from the scheduler" + assert_file_contains "$scheduler_workflow_file" '--review-workflow "OpenCode Review"' "PR review scheduler dispatches the canonical OpenCode review workflow" + assert_file_contains "$scheduler_workflow_file" 'args+=(--trigger-reviews)' "PR review scheduler can request current-head OpenCode review evidence" + assert_file_contains "$workflow_file" "github.event_name == 'workflow_dispatch'" "opencode review side effects are limited to explicit scheduler or manual dispatch" assert_file_contains "$workflow_file" "opencode-review-target:" "opencode trusted review job is separate from the pull_request bridge" - assert_file_contains "$workflow_file" "github.event.pull_request.head.repo.full_name == github.repository" "opencode review workflow limits pull_request_target review execution to same-repository PRs" assert_file_contains "$workflow_file" "Initialize CodeGraph index for OpenCode" "opencode review workflow initializes CodeGraph before review" assert_file_contains "$workflow_file" "actions: read" "opencode review workflow can read failed Actions logs for GitHub Check diagnosis" assert_file_contains "$workflow_file" "checks: read" "opencode review workflow can read failed check-run annotations for line-specific findings" From 87e7f17be10a967025b89210c9dbce38d420995b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 00:03:39 +0900 Subject: [PATCH 08/17] Strengthen OpenCode review fallback coverage --- .github/workflows/opencode-review.yml | 148 ++++++++++++++++++++++++-- scripts/ci/test_strix_quick_gate.sh | 8 ++ 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b8f6665d9..1262e6c01 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -732,7 +732,7 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_ATTEMPTS: "2" + OPENCODE_MODEL_ATTEMPTS: "3" OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-primary.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -860,7 +860,7 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_ATTEMPTS: "2" + OPENCODE_MODEL_ATTEMPTS: "3" OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -986,7 +986,7 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_ATTEMPTS: "2" + OPENCODE_MODEL_ATTEMPTS: "3" OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-second-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -1100,6 +1100,128 @@ jobs: fi record_review_status "success" + - name: Run OpenCode PR Review fallback (OpenAI o-series) + id: opencode_review_o_series_fallback + if: >- + always() + && steps.opencode_review_primary.outputs.review_status != 'success' + && steps.opencode_review_fallback.outputs.review_status != 'success' + && steps.opencode_review_second_fallback.outputs.review_status != 'success' + continue-on-error: true + timeout-minutes: 20 + env: + STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + USE_GITHUB_TOKEN: "true" + SHARE: "false" + NPM_CONFIG_IGNORE_SCRIPTS: "true" + NO_COLOR: "1" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/o3 github-models/openai/o4-mini" + OPENCODE_MODEL_ATTEMPTS: "2" + OPENCODE_RUN_TIMEOUT_SECONDS: "180" + OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md + OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-o-series-fallback.md + OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project + OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head + PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + RUN_ID: ${{ github.run_id }} + RUN_ATTEMPT: ${{ github.run_attempt }} + run: | + set -euo pipefail + record_review_status() { + printf 'review_status=%s\n' "$1" >>"$GITHUB_OUTPUT" + } + record_review_model() { + printf 'review_model=%s\n' "$1" >>"$GITHUB_OUTPUT" + } + normalize_opencode_output() { + local output_file="$1" + + if bash "$GITHUB_WORKSPACE/scripts/ci/opencode_review_approve_gate.sh" "$HEAD_SHA" "$RUN_ID" "$RUN_ATTEMPT" "$output_file" >/dev/null; then + return 0 + fi + + if python3 "$GITHUB_WORKSPACE/scripts/ci/opencode_review_normalize_output.py" \ + "$HEAD_SHA" "$RUN_ID" "$RUN_ATTEMPT" "$output_file"; then + bash "$GITHUB_WORKSPACE/scripts/ci/opencode_review_approve_gate.sh" "$HEAD_SHA" "$RUN_ID" "$RUN_ATTEMPT" "$output_file" >/dev/null + return $? + fi + + return 1 + } + + cd "$OPENCODE_REVIEW_WORKDIR" + opencode_attempts="${OPENCODE_MODEL_ATTEMPTS:-2}" + for model_candidate in $OPENCODE_MODEL_CANDIDATES; do + candidate_output_file="${RUNNER_TEMP}/opencode-review-${model_candidate//\//-}.md" + opencode_json_file="${candidate_output_file}.jsonl" + opencode_export_file="${candidate_output_file}.session.json" + prompt_file="${RUNNER_TEMP}/opencode-review-${model_candidate//\//-}-prompt.md" + cat >"$prompt_file" < + Then exactly one control block: + + APPROVE only for no blockers. REQUEST_CHANGES findings require path,line,severity,title,problem,root_cause,fix_direction,regression_test_direction,suggested_diff. Use line-specific, source-backed findings only. Return only the review body. + EOF + + for opencode_attempt in $(seq 1 "$opencode_attempts"); do + rm -f "$opencode_json_file" "$opencode_export_file" "$candidate_output_file" + set +e + timeout --kill-after=30s "${OPENCODE_RUN_TIMEOUT_SECONDS:-180}s" opencode run "$(cat "$prompt_file")" \ + --pure \ + --agent ci-review-fallback \ + --model "$model_candidate" \ + --format json \ + --title "PR #${PR_NUMBER} OpenCode bounded o-series review ${model_candidate} attempt ${opencode_attempt}/${opencode_attempts}" >"$opencode_json_file" + opencode_run_status=$? + set -e + if [ "$opencode_run_status" -ne 0 ]; then + printf 'OpenCode %s fallback attempt %s/%s failed with exit %s.\n' "$model_candidate" "$opencode_attempt" "$opencode_attempts" "$opencode_run_status" + if [ "$opencode_attempt" -lt "$opencode_attempts" ]; then + sleep 10 + fi + continue + fi + + session_id="$(jq -r 'select(.type == "step_start") | .sessionID' "$opencode_json_file" | tail -n 1)" + if [ -z "$session_id" ] || [ "$session_id" = "null" ]; then + printf 'OpenCode %s attempt %s/%s JSON output did not include a session id.\n' "$model_candidate" "$opencode_attempt" "$opencode_attempts" + continue + fi + if ! opencode export "$session_id" --pure >"$opencode_export_file"; then + printf 'OpenCode %s attempt %s/%s session export did not complete.\n' "$model_candidate" "$opencode_attempt" "$opencode_attempts" + continue + fi + jq -r '.messages[] | select(.info.role == "assistant") | .parts[]? | select(.type == "text") | .text' "$opencode_export_file" >"$candidate_output_file" + if [ ! -s "$candidate_output_file" ]; then + printf 'OpenCode %s attempt %s/%s session export did not include assistant text.\n' "$model_candidate" "$opencode_attempt" "$opencode_attempts" + continue + fi + if normalize_opencode_output "$candidate_output_file"; then + cp "$candidate_output_file" "$OPENCODE_OUTPUT_FILE" + record_review_model "$model_candidate" + record_review_status "success" + exit 0 + fi + printf 'OpenCode %s attempt %s/%s output did not include a valid control conclusion.\n' "$model_candidate" "$opencode_attempt" "$opencode_attempts" + if [ "$opencode_attempt" -lt "$opencode_attempts" ]; then + sleep 10 + fi + done + done + + record_review_status "failed" + - name: Exchange OpenCode app token for review writes id: opencode_app_token if: always() @@ -1172,7 +1294,8 @@ jobs: always() && (steps.opencode_review_primary.outputs.review_status == 'success' || steps.opencode_review_fallback.outputs.review_status == 'success' - || steps.opencode_review_second_fallback.outputs.review_status == 'success') + || steps.opencode_review_second_fallback.outputs.review_status == 'success' + || steps.opencode_review_o_series_fallback.outputs.review_status == 'success') env: GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || secrets.GITHUB_TOKEN }} GH_REPOSITORY: ${{ github.repository }} @@ -1183,9 +1306,11 @@ jobs: OPENCODE_PRIMARY_OUTCOME: ${{ steps.opencode_review_primary.outputs.review_status }} OPENCODE_FALLBACK_OUTCOME: ${{ steps.opencode_review_fallback.outputs.review_status }} OPENCODE_SECOND_FALLBACK_OUTCOME: ${{ steps.opencode_review_second_fallback.outputs.review_status }} + OPENCODE_O_SERIES_FALLBACK_OUTCOME: ${{ steps.opencode_review_o_series_fallback.outputs.review_status }} OPENCODE_PRIMARY_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-primary.md OPENCODE_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-fallback.md OPENCODE_SECOND_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-second-fallback.md + OPENCODE_O_SERIES_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-o-series-fallback.md run: | set -euo pipefail @@ -1193,8 +1318,10 @@ jobs: review_output_file="$OPENCODE_PRIMARY_OUTPUT_FILE" elif [ "$OPENCODE_FALLBACK_OUTCOME" = "success" ]; then review_output_file="$OPENCODE_FALLBACK_OUTPUT_FILE" - else + elif [ "$OPENCODE_SECOND_FALLBACK_OUTCOME" = "success" ]; then review_output_file="$OPENCODE_SECOND_FALLBACK_OUTPUT_FILE" + else + review_output_file="$OPENCODE_O_SERIES_FALLBACK_OUTPUT_FILE" fi clean_output="$(mktemp)" @@ -1336,9 +1463,11 @@ jobs: OPENCODE_PRIMARY_OUTCOME: ${{ steps.opencode_review_primary.outputs.review_status }} OPENCODE_FALLBACK_OUTCOME: ${{ steps.opencode_review_fallback.outputs.review_status }} OPENCODE_SECOND_FALLBACK_OUTCOME: ${{ steps.opencode_review_second_fallback.outputs.review_status }} + OPENCODE_O_SERIES_FALLBACK_OUTCOME: ${{ steps.opencode_review_o_series_fallback.outputs.review_status }} OPENCODE_PRIMARY_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-primary.md OPENCODE_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-fallback.md OPENCODE_SECOND_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-second-fallback.md + OPENCODE_O_SERIES_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-o-series-fallback.md APPROVAL_CHECK_WAIT_ATTEMPTS: "241" APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30" CHECK_LOOKUP_RETRY_ATTEMPTS: "5" @@ -2731,6 +2860,9 @@ jobs: if [ "$opencode_review_outcome" != "success" ]; then opencode_review_outcome="${OPENCODE_SECOND_FALLBACK_OUTCOME:-unknown}" fi + if [ "$opencode_review_outcome" != "success" ]; then + opencode_review_outcome="${OPENCODE_O_SERIES_FALLBACK_OUTCOME:-unknown}" + fi if [ "$opencode_review_outcome" != "success" ]; then failed_checks_file="$(mktemp)" @@ -2801,7 +2933,7 @@ jobs: "" \ "### 1. MEDIUM Review Evidence Missing - Rerun OpenCode with usable current-head evidence" \ "- Problem: OpenCode did not return a valid control block that can be tied to the changed files for head \`${HEAD_SHA}\`." \ - "- Root cause: The model attempts ended with outcomes primary=${OPENCODE_PRIMARY_OUTCOME:-unknown}, fallback=${OPENCODE_FALLBACK_OUTCOME:-unknown}, second_fallback=${OPENCODE_SECOND_FALLBACK_OUTCOME:-unknown}; the workflow cannot distinguish a real clean review from invalid or unsupported model output." \ + "- Root cause: The model attempts ended with outcomes primary=${OPENCODE_PRIMARY_OUTCOME:-unknown}, fallback=${OPENCODE_FALLBACK_OUTCOME:-unknown}, second_fallback=${OPENCODE_SECOND_FALLBACK_OUTCOME:-unknown}, o_series_fallback=${OPENCODE_O_SERIES_FALLBACK_OUTCOME:-unknown}; the workflow cannot distinguish a real clean review from invalid or unsupported model output." \ "- Fix: Rerun or repair the OpenCode review path until the review names the changed-file evidence it inspected, then let the trusted gate evaluate that valid output." \ "- Regression test: Keep invalid OpenCode model output on the request-changes path even when same-head peer checks are otherwise clean." \ "" \ @@ -2810,7 +2942,7 @@ jobs: "All same-head peer GitHub Checks completed without failed or pending contexts, and no unresolved human review threads remained. Approval still requires a valid current-head review summary that names changed-file evidence. Invalid model output is treated as review tooling instability, not as a source-code defect." \ "" \ "- Result: REQUEST_CHANGES" \ - "- Reason: OpenCode action outcomes were primary=${OPENCODE_PRIMARY_OUTCOME:-unknown}, fallback=${OPENCODE_FALLBACK_OUTCOME:-unknown}, second_fallback=${OPENCODE_SECOND_FALLBACK_OUTCOME:-unknown}; no valid source-backed review output was available for current head \`${HEAD_SHA}\`." \ + "- Reason: OpenCode action outcomes were primary=${OPENCODE_PRIMARY_OUTCOME:-unknown}, fallback=${OPENCODE_FALLBACK_OUTCOME:-unknown}, second_fallback=${OPENCODE_SECOND_FALLBACK_OUTCOME:-unknown}, o_series_fallback=${OPENCODE_O_SERIES_FALLBACK_OUTCOME:-unknown}; no valid source-backed review output was available for current head \`${HEAD_SHA}\`." \ "- Head SHA: \`${HEAD_SHA}\`" \ "- Workflow run: ${RUN_ID}" \ "- Workflow attempt: ${RUN_ATTEMPT}")" @@ -2828,6 +2960,8 @@ jobs: selected_review_output_file="${OPENCODE_FALLBACK_OUTPUT_FILE}" elif [ "${OPENCODE_SECOND_FALLBACK_OUTCOME:-}" = "success" ]; then selected_review_output_file="${OPENCODE_SECOND_FALLBACK_OUTPUT_FILE}" + elif [ "${OPENCODE_O_SERIES_FALLBACK_OUTCOME:-}" = "success" ]; then + selected_review_output_file="${OPENCODE_O_SERIES_FALLBACK_OUTPUT_FILE}" fi load_selected_review_output() { diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index d1ca1ba79..e656821e6 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -407,9 +407,13 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "Do not spend the session listing every changed path before reviewing" "opencode review prompt prevents fallback sessions from exhausting steps on file listing" assert_file_contains "$workflow_file" "always return a final control block instead of a progress summary" "opencode review prompt requires a gate conclusion instead of a progress summary" assert_file_contains "$workflow_file" "timeout 600 opencode run" "opencode review primary model has a bounded timeout so fallback review can publish promptly" + assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "3"' "opencode review gives primary reasoning models three bounded attempts before fallback" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "2"' "opencode review retries transient model execution failures before exhausting a model" assert_file_contains "$workflow_file" 'OpenCode %s attempt %s/%s failed with exit %s.' "opencode review logs per-model retry attempts" assert_file_contains "$workflow_file" 'case "$opencode_run_status" in' "opencode review sends timeout-class failures directly to fallback instead of retrying the same stuck model" + assert_file_contains "$workflow_file" "Run OpenCode PR Review fallback (OpenAI o-series)" "opencode review has an o-series reasoning fallback after GPT-5 and DeepSeek attempts" + assert_file_contains "$workflow_file" 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/o3 github-models/openai/o4-mini"' "opencode review tries multiple OpenAI reasoning fallback models" + assert_file_contains "$workflow_file" "o_series_fallback=" "opencode approval gate reports o-series fallback outcome details" assert_file_contains "$workflow_file" '"ci-review-fallback"' "opencode review workflow declares a dedicated fallback agent" assert_file_contains "$workflow_file" '"steps": 12' "opencode review fallback agent has enough bounded steps to conclude after MCP inspection" assert_file_contains "$workflow_file" '"read": "allow"' "opencode review allows read-only file inspection" @@ -417,6 +421,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" '"external_directory": "allow"' "opencode review can read the real checkout from its isolated review workspace" assert_file_not_contains "$workflow_file" '"external_directory": "deny"' "opencode review must not block focused reads of the real checkout" assert_file_contains "$workflow_file" "Bounded evidence is available in ./bounded-review-evidence.md" "opencode review prompt points the model at the bounded evidence file" + assert_file_contains "$workflow_file" "Verification posture section with these exact labels" "opencode review requires a complete verification posture before approval" + assert_file_contains "$workflow_file" "Coverage:, Docstring coverage:, DAG:, PoC/execution:" "opencode review requires coverage, docstring, DAG, and execution proof labels" + assert_file_contains "$workflow_file" "Standards search:, Compatibility/convention:, Breaking-change/backcompat:, Performance:, Design/UX:, Security/privacy:" "opencode review requires standards, compatibility, breaking-change, performance, design, and security checks" + assert_file_contains "$workflow_file" "Coverage and Docstring coverage labels must cite executed coverage evidence proving 100%" "opencode review treats missing 100% coverage evidence as blocking" assert_file_contains "$workflow_file" "Current runtime-version review contract" "opencode review evidence names the current runtime-version contract" assert_file_contains "$workflow_file" "Do not request rollback of Node 24 or Python 3.14 solely from model memory" "opencode review prompt rejects stale runtime-version model memory" assert_file_not_contains "$workflow_file" 'head -c 20000 "$OPENCODE_EVIDENCE_FILE"' "opencode review prompt must not exceed GitHub Models prompt limits by inlining bounded evidence" From 50ee5a155f64453d98ed4df0604e2aaec66dceba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 00:29:23 +0900 Subject: [PATCH 09/17] Publish manual Strix evidence status --- .github/workflows/strix.yml | 41 +++++++++++++++++++++++++++++ scripts/ci/test_strix_quick_gate.sh | 4 +++ 2 files changed, 45 insertions(+) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 79c75a032..dce578f66 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -431,3 +431,44 @@ jobs: path: strix_runs/ if-no-files-found: error retention-days: 5 + + publish-manual-pr-evidence-status: + name: publish-manual-pr-evidence-status + needs: strix + if: ${{ always() && github.event_name == 'workflow_dispatch' && github.event.inputs.pr_head_sha != '' }} + runs-on: ubuntu-latest + permissions: + statuses: write + steps: + - name: Publish same-head manual Strix status + env: + GH_TOKEN: ${{ github.token }} + PR_HEAD_SHA: ${{ github.event.inputs.pr_head_sha }} + STRIX_RESULT: ${{ needs.strix.result }} + run: | + set -euo pipefail + if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::PR head SHA must be a 40-character git SHA." + exit 1 + fi + + case "$STRIX_RESULT" in + success) + state="success" + description="Manual workflow_dispatch Strix evidence passed" + ;; + failure|cancelled|skipped) + state="failure" + description="Manual workflow_dispatch Strix evidence failed" + ;; + *) + state="error" + description="Manual workflow_dispatch Strix evidence inconclusive" + ;; + esac + + gh api -X POST "repos/${GITHUB_REPOSITORY}/statuses/${PR_HEAD_SHA}" \ + -f state="$state" \ + -f context="strix" \ + -f description="$description" \ + -f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index e656821e6..66dbb96e6 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -494,6 +494,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'filter_superseded_strix_failures()' "opencode approval filters only explicitly superseded stale Strix failures" assert_file_contains "$workflow_file" 'Manual workflow_dispatch Strix evidence passed' "opencode approval requires an explicit manual Strix evidence status description" assert_file_contains "$workflow_file" 'last // empty' "opencode approval checks the latest strix status before accepting manual success evidence" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'publish-manual-pr-evidence-status:' "strix workflow publishes same-head manual PR evidence as a commit status" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'statuses: write' "strix manual evidence status job has commit-status write permission" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'context="strix"' "strix manual evidence status uses the status context consumed by OpenCode" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'Manual workflow_dispatch Strix evidence failed' "strix manual evidence status records failed reruns so older success cannot mask newer failure" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '"workflow_run"' "failed-check evidence includes failed same-head workflow runs outside statusCheckRollup" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" "--json databaseId,workflowName,status,conclusion,url,event,headSha" "failed-check evidence scopes supplemental workflow runs with event and head SHA metadata" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select((.event // "") == "pull_request_target" or (.event // "") == "workflow_dispatch")' "failed-check evidence appends PR Strix workflow runs and manual PR evidence reruns" From f5192d2893d75e22aeb5f89c143c9bec1f3b048a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 01:11:17 +0900 Subject: [PATCH 10/17] Enable OpenCode runtime review tools --- .github/workflows/opencode-review.yml | 31 ++++++++++++++------------- scripts/ci/test_strix_quick_gate.sh | 6 ++++++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 1262e6c01..28cf8eac5 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -577,6 +577,7 @@ jobs: jq -n --arg workspace "$OPENCODE_SOURCE_WORKDIR" '{ "$schema": "https://opencode.ai/config.json", + "lsp": true, "model": "github-models/openai/gpt-5", "small_model": "github-models/deepseek/deepseek-v3-0324", "enabled_providers": ["github-models"], @@ -629,15 +630,15 @@ jobs: }, "permission": { "edit": "deny", - "bash": "deny", + "bash": "allow", "read": "allow", "grep": "allow", "glob": "allow", "list": "allow", - "task": "deny", - "webfetch": "deny", - "websearch": "deny", - "lsp": "deny", + "task": "allow", + "webfetch": "allow", + "websearch": "allow", + "lsp": "allow", "external_directory": "allow" }, "agent": { @@ -648,15 +649,15 @@ jobs: "steps": 4, "permission": { "edit": "deny", - "bash": "deny", + "bash": "allow", "read": "allow", "grep": "allow", "glob": "allow", "list": "allow", - "task": "deny", - "webfetch": "deny", - "websearch": "deny", - "lsp": "deny", + "task": "allow", + "webfetch": "allow", + "websearch": "allow", + "lsp": "allow", "external_directory": "allow" } }, @@ -667,15 +668,15 @@ jobs: "steps": 12, "permission": { "edit": "deny", - "bash": "deny", + "bash": "allow", "read": "allow", "grep": "allow", "glob": "allow", "list": "allow", - "task": "deny", - "webfetch": "deny", - "websearch": "deny", - "lsp": "deny", + "task": "allow", + "webfetch": "allow", + "websearch": "allow", + "lsp": "allow", "external_directory": "allow" } } diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 66dbb96e6..5eb1a5e05 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -416,8 +416,14 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "o_series_fallback=" "opencode approval gate reports o-series fallback outcome details" assert_file_contains "$workflow_file" '"ci-review-fallback"' "opencode review workflow declares a dedicated fallback agent" assert_file_contains "$workflow_file" '"steps": 12' "opencode review fallback agent has enough bounded steps to conclude after MCP inspection" + assert_file_contains "$workflow_file" '"lsp": true' "opencode review enables LSP support in the generated runtime config" assert_file_contains "$workflow_file" '"read": "allow"' "opencode review allows read-only file inspection" assert_file_contains "$workflow_file" '"grep": "allow"' "opencode review allows focused literal searches" + assert_file_contains "$workflow_file" '"bash": "allow"' "opencode review can execute bounded verification commands" + assert_file_contains "$workflow_file" '"task": "allow"' "opencode review can run configured task tools" + assert_file_contains "$workflow_file" '"webfetch": "allow"' "opencode review can fetch bounded external references" + assert_file_contains "$workflow_file" '"websearch": "allow"' "opencode review can perform bounded external searches" + assert_file_contains "$workflow_file" '"lsp": "allow"' "opencode review can use LSP inspection tools" assert_file_contains "$workflow_file" '"external_directory": "allow"' "opencode review can read the real checkout from its isolated review workspace" assert_file_not_contains "$workflow_file" '"external_directory": "deny"' "opencode review must not block focused reads of the real checkout" assert_file_contains "$workflow_file" "Bounded evidence is available in ./bounded-review-evidence.md" "opencode review prompt points the model at the bounded evidence file" From 9c13bd608191e72db1c2ca857976dba2b724993f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 02:23:21 +0900 Subject: [PATCH 11/17] Cover scheduler config in OpenCode bootstrap fallback --- .github/workflows/opencode-review.yml | 4 +++- scripts/ci/test_strix_quick_gate.sh | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 28cf8eac5..b28ef3263 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2760,7 +2760,9 @@ jobs: if ! awk ' function allowed(path) { return path == ".github/workflows/opencode-review.yml" || + path == ".github/workflows/pr-review-merge-scheduler.yml" || path == ".github/workflows/strix.yml" || + path == "opencode.jsonc" || path == "requirements-strix-ci.txt" || path == "requirements-strix-ci-hashes.txt" || path == "scripts/ci/collect_failed_check_evidence.sh" || @@ -2786,7 +2788,7 @@ jobs: ( cd "$source_root" if command -v actionlint >/dev/null 2>&1; then - actionlint -shellcheck= -pyflakes= .github/workflows/opencode-review.yml .github/workflows/strix.yml + actionlint -shellcheck= -pyflakes= .github/workflows/opencode-review.yml .github/workflows/pr-review-merge-scheduler.yml .github/workflows/strix.yml else printf 'actionlint unavailable; skipped workflow schema validation.\n' fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 5eb1a5e05..836eb4a43 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -639,9 +639,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/emit_opencode_failed_check_fallback_findings.sh" "cancelled pull_request_target run still used the base branch copies" "failed-check fallback explains trusted-base Strix workflow semantics for self-modifying PRs" assert_file_contains "$REPO_ROOT/scripts/ci/emit_opencode_failed_check_fallback_findings.sh" "get_validated_pr_diff_range" "failed-check fallback validates PR diff range before comparing trusted Strix inputs" assert_file_contains "$workflow_file" ".github/workflows/strix.yml" "opencode inline fallback watches Strix workflow changes" + assert_file_contains "$workflow_file" 'path == ".github/workflows/pr-review-merge-scheduler.yml"' "opencode inline fallback watches PR review scheduler workflow changes" assert_file_contains "$workflow_file" "scripts/ci/strix_quick_gate.sh" "opencode inline fallback watches trusted Strix gate changes" assert_file_contains "$workflow_file" "scripts/ci/test_strix_quick_gate.sh" "opencode inline fallback watches trusted Strix self-test changes" assert_file_contains "$workflow_file" "requirements-strix-ci.txt" "opencode inline fallback watches trusted Strix dependency changes" + assert_file_contains "$workflow_file" 'path == "opencode.jsonc"' "opencode inline fallback watches OpenCode runtime config changes" + assert_file_contains "$workflow_file" ".github/workflows/pr-review-merge-scheduler.yml .github/workflows/strix.yml" "opencode inline fallback validates scheduler workflow syntax" assert_file_contains "$REPO_ROOT/scripts/ci/emit_opencode_failed_check_fallback_findings.sh" "Strix provider failure blocked current-head security evidence" "failed-check fallback does not label non-quota provider routing/auth failures as quota" assert_file_not_contains "$REPO_ROOT/scripts/ci/emit_opencode_failed_check_fallback_findings.sh" "Strix provider quota blocked current-head security evidence" "failed-check fallback avoids misleading quota-only provider blocker title" assert_file_contains "$workflow_file" "- Root cause:" "opencode review request-changes body includes root cause per finding" From a06dd2290451cc400e470ed5dfd272d5da0ffe67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 02:51:31 +0900 Subject: [PATCH 12/17] Bound Strix provider retries and cleanup secrets --- .github/workflows/strix.yml | 31 +++++++++++++++++++++++++---- scripts/ci/test_strix_quick_gate.sh | 9 +++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index dce578f66..f230a95d0 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -372,8 +372,8 @@ jobs: STRIX_SOURCE_DIRS: ". backend frontend" STRIX_REASONING_EFFORT: low STRIX_LLM_MAX_RETRIES: 1 - STRIX_TRANSIENT_RETRY_PER_MODEL: 5 - STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 60 + STRIX_TRANSIENT_RETRY_PER_MODEL: 1 + STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 15 STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/deepseek/deepseek-r1-0528 github_models/deepseek/deepseek-v3-0324' || '' }} STRIX_FAIL_ON_PROVIDER_SIGNAL: "1" STRIX_VERTEX_FALLBACK_MODELS: "" @@ -390,13 +390,36 @@ jobs: IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && 'true' || 'false' }} run: | budget_suffix="TIME""OUT" - process_budget_seconds="3600" + process_budget_seconds="900" export "LLM_${budget_suffix}=120" export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=10" export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds" - export "STRIX_TOTAL_${budget_suffix}_SECONDS=7200" + export "STRIX_TOTAL_${budget_suffix}_SECONDS=1800" bash "$TRUSTED_STRIX_GATE" + - name: Remove Strix secret temp files + if: ${{ always() && steps.gate.outputs.enabled == 'true' }} + run: | + set -euo pipefail + for secret_file in \ + "${LLM_API_KEY_FILE:-}" \ + "${LLM_API_BASE_FILE:-}" \ + "${STRIX_LLM_FILE:-}" \ + "${GOOGLE_APPLICATION_CREDENTIALS:-}" \ + "${CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE:-}"; do + if [ -z "$secret_file" ]; then + continue + fi + case "$secret_file" in + "$RUNNER_TEMP"/*) + rm -f -- "$secret_file" + ;; + *) + echo "::warning::Refusing to remove temp secret path outside RUNNER_TEMP." + ;; + esac + done + - name: Collect Strix reports for artifact upload if: ${{ always() && steps.gate.outputs.enabled == 'true' }} env: diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 836eb4a43..50dd3f025 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -150,8 +150,13 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "VERTEXAI_LOCATION" "strix workflow exports LiteLLM Vertex location env" assert_file_contains "$workflow_file" "timeout-minutes: 120" "strix workflow job budget covers PR-scoped Strix scans" assert_file_contains "$workflow_file" 'budget_suffix="TIME""OUT"' "strix workflow builds budget env keys without visible timeout signal text" - assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=7200"' "strix workflow total Strix budget covers PR-scoped scans" - assert_file_contains "$workflow_file" 'process_budget_seconds="3600"' "strix workflow keeps PR-scoped process budget large enough for report finalization" + assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=1800"' "strix workflow keeps PR-scoped scans bounded for review feedback" + assert_file_contains "$workflow_file" 'process_budget_seconds="900"' "strix workflow keeps PR-scoped process budget bounded for review feedback" + assert_file_contains "$workflow_file" "STRIX_TRANSIENT_RETRY_PER_MODEL: 1" "strix workflow avoids spending the review budget on repeated same-model provider failures" + assert_file_contains "$workflow_file" "STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 15" "strix workflow uses a short same-model provider retry backoff" + assert_file_contains "$workflow_file" "Remove Strix secret temp files" "strix workflow cleans up secret-bearing temp files after scans" + assert_file_contains "$workflow_file" 'case "$secret_file" in' "strix workflow constrains secret cleanup paths" + assert_file_contains "$workflow_file" '"$RUNNER_TEMP"/*)' "strix workflow deletes only runner temp secret files" assert_file_contains "$workflow_file" 'IS_PR_EVIDENCE_RUN: ${{ (github.event_name == '"'"'pull_request_target'"'"' || github.event.inputs.pr_number != '"'"''"'"') && '"'"'true'"'"' || '"'"'false'"'"' }}' "strix workflow passes PR evidence mode through env" assert_file_not_contains "$workflow_file" 'if [ "${{ (github.event_name == '"'"'pull_request_target'"'"' || github.event.inputs.pr_number != '"'"''"'"') && '"'"'true'"'"' || '"'"'false'"'"' }}" = "true" ]; then' "strix workflow does not interpolate GitHub context inside shell condition" assert_file_not_contains "$workflow_file" "LLM_TIMEOUT:" "strix workflow must not expose LLM timeout env names in GitHub logs" From 9889637532ff65f36345f530d2f0f7ca227334d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 03:07:44 +0900 Subject: [PATCH 13/17] Include CI gate context in Strix PR scope --- scripts/ci/strix_quick_gate.sh | 9 +++++++++ scripts/ci/test_strix_quick_gate.sh | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 47ff270cd..8b0f71479 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -1299,6 +1299,15 @@ frontend/postcss.config.mjs docker-compose.yml render.yaml VERSION +scripts/ci/collect_failed_check_evidence.sh +scripts/ci/emit_opencode_failed_check_fallback_findings.sh +scripts/ci/opencode_review_approve_gate.sh +scripts/ci/opencode_review_normalize_output.py +scripts/ci/pr_review_merge_scheduler.py +scripts/ci/strix_model_utils.sh +scripts/ci/strix_quick_gate.sh +scripts/ci/test_strix_quick_gate.sh +scripts/ci/validate_opencode_failed_check_review.sh EOF fi } diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 50dd3f025..d26671984 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -3314,6 +3314,10 @@ EOS echo "Error: deployment scope missing backend/core/runtime_secrets.py ($target_path)" >&2 exit 60 fi + if [ ! -f "$target_path/scripts/ci/strix_quick_gate.sh" ]; then + echo "Error: deployment scope missing scripts/ci/strix_quick_gate.sh ($target_path)" >&2 + exit 61 + fi if ! grep -Fq -- 'CMD ["/app/scripts/docker_entrypoint.sh"]' "$target_path/Dockerfile"; then echo "Error: deployment Dockerfile does not reference docker_entrypoint.sh ($target_path)" >&2 exit 58 From 40a0e47d28316316864e8c1ff664e22d959c72c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 03:29:22 +0900 Subject: [PATCH 14/17] Bound OpenCode review runtime budget --- .github/workflows/opencode-review.yml | 22 +++++++++++----------- scripts/ci/test_strix_quick_gate.sh | 7 ++++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b28ef3263..4b4fe7559 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -724,7 +724,7 @@ jobs: - name: Run OpenCode PR Review (GPT-5) id: opencode_review_primary - timeout-minutes: 20 + timeout-minutes: 12 env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -733,7 +733,7 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_ATTEMPTS: "3" + OPENCODE_MODEL_ATTEMPTS: "1" OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-primary.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -782,7 +782,7 @@ jobs: for opencode_attempt in $(seq 1 "$opencode_attempts"); do rm -f "$opencode_json_file" set +e - timeout 600 opencode run "$(cat "$prompt_file")" \ + timeout 480 opencode run "$(cat "$prompt_file")" \ --pure \ --agent ci-review \ --model "$MODEL" \ @@ -852,7 +852,7 @@ jobs: - name: Run OpenCode PR Review fallback (DeepSeek R1) id: opencode_review_fallback if: steps.opencode_review_primary.outputs.review_status != 'success' - timeout-minutes: 60 + timeout-minutes: 8 env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -861,7 +861,7 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_ATTEMPTS: "3" + OPENCODE_MODEL_ATTEMPTS: "1" OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -978,7 +978,7 @@ jobs: - name: Run OpenCode PR Review fallback (DeepSeek V3) id: opencode_review_second_fallback if: steps.opencode_review_primary.outputs.review_status != 'success' && steps.opencode_review_fallback.outputs.review_status != 'success' - timeout-minutes: 60 + timeout-minutes: 8 env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -987,7 +987,7 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_ATTEMPTS: "3" + OPENCODE_MODEL_ATTEMPTS: "1" OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-second-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -1109,7 +1109,7 @@ jobs: && steps.opencode_review_fallback.outputs.review_status != 'success' && steps.opencode_review_second_fallback.outputs.review_status != 'success' continue-on-error: true - timeout-minutes: 20 + timeout-minutes: 12 env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -1118,8 +1118,8 @@ jobs: NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" OPENCODE_MODEL_CANDIDATES: "github-models/openai/o3 github-models/openai/o4-mini" - OPENCODE_MODEL_ATTEMPTS: "2" - OPENCODE_RUN_TIMEOUT_SECONDS: "180" + OPENCODE_MODEL_ATTEMPTS: "1" + OPENCODE_RUN_TIMEOUT_SECONDS: "240" OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-o-series-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -2253,7 +2253,7 @@ jobs: } >"$prompt_file" cd "$OPENCODE_REVIEW_WORKDIR" - if ! timeout 600 opencode run "$(cat "$prompt_file")" \ + if ! timeout 300 opencode run "$(cat "$prompt_file")" \ --pure \ --agent ci-review-fallback \ --model "$MODEL" \ diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index d26671984..2ca14e93e 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -411,9 +411,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "Never return raw tool-call markup, tool-call JSON, or MCP call syntax in the review body" "opencode review prompt forbids raw tool-call transcripts as final review output" assert_file_contains "$workflow_file" "Do not spend the session listing every changed path before reviewing" "opencode review prompt prevents fallback sessions from exhausting steps on file listing" assert_file_contains "$workflow_file" "always return a final control block instead of a progress summary" "opencode review prompt requires a gate conclusion instead of a progress summary" - assert_file_contains "$workflow_file" "timeout 600 opencode run" "opencode review primary model has a bounded timeout so fallback review can publish promptly" - assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "3"' "opencode review gives primary reasoning models three bounded attempts before fallback" - assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "2"' "opencode review retries transient model execution failures before exhausting a model" + assert_file_contains "$workflow_file" "timeout 480 opencode run" "opencode review primary model has a bounded timeout so fallback review can publish promptly" + assert_file_contains "$workflow_file" "timeout 300 opencode run" "opencode review fallback and failed-check diagnosis have bounded execution windows" + assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode review uses one bounded attempt per model before moving to the next provider" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "240"' "opencode o-series fallback has a bounded per-candidate timeout" assert_file_contains "$workflow_file" 'OpenCode %s attempt %s/%s failed with exit %s.' "opencode review logs per-model retry attempts" assert_file_contains "$workflow_file" 'case "$opencode_run_status" in' "opencode review sends timeout-class failures directly to fallback instead of retrying the same stuck model" assert_file_contains "$workflow_file" "Run OpenCode PR Review fallback (OpenAI o-series)" "opencode review has an o-series reasoning fallback after GPT-5 and DeepSeek attempts" From ff7cdc2cb14486391bbdd3156a330a270cb8664a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 03:35:47 +0900 Subject: [PATCH 15/17] Use smoke Strix self-test for PR evidence runs --- .github/workflows/strix.yml | 4 +++- scripts/ci/test_strix_quick_gate.sh | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index f230a95d0..cf2fb1ec8 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -142,7 +142,9 @@ jobs: - name: Self-test Strix gate script working-directory: ${{ runner.temp }}/trusted-workspace - run: bash "$TRUSTED_STRIX_GATE_TEST" + env: + STRIX_GATE_SELF_TEST_MODE: smoke + run: timeout 180 bash "$TRUSTED_STRIX_GATE_TEST" - name: Gate Strix secrets id: gate diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 2ca14e93e..cf5d39381 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -109,6 +109,8 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "git -C \"\$TRUSTED_WORKSPACE\"" "strix workflow runs git only inside trusted workspace" assert_file_contains "$workflow_file" 'working-directory: ${{ runner.temp }}/trusted-workspace' "strix workflow executes privileged steps from the trusted workspace" assert_file_contains "$workflow_file" "bash \"\$TRUSTED_STRIX_GATE_TEST\"" "strix workflow self-test executes trusted temp script" + assert_file_contains "$workflow_file" "STRIX_GATE_SELF_TEST_MODE: smoke" "strix workflow runs PR evidence self-test in smoke mode" + assert_file_contains "$workflow_file" "timeout 180 bash \"\$TRUSTED_STRIX_GATE_TEST\"" "strix workflow bounds trusted self-test runtime" assert_file_contains "$workflow_file" "bash \"\$TRUSTED_STRIX_GATE\"" "strix workflow executes trusted temp gate script" assert_file_contains "$workflow_file" "Collect Strix reports for artifact upload" "strix workflow preserves reports from trusted workspace" assert_file_contains "$workflow_file" "scan-summary.txt" "strix workflow creates a fallback artifact when Strix emits no report files" @@ -6137,6 +6139,16 @@ assert_opencode_failed_check_fallback_handles_split_code_location_lines assert_opencode_failed_check_fallback_does_not_anchor_unmapped_strix_reports_to_workflow +if [ "${STRIX_GATE_SELF_TEST_MODE:-full}" = "smoke" ]; then + if [ "$FAILURES" -ne 0 ]; then + echo "test_strix_quick_gate: ${FAILURES} failure(s)" >&2 + exit 1 + fi + + echo "test_strix_quick_gate: PASS (smoke)" + exit 0 +fi + run_pull_request_target_head_scope_case \ "pull-request-target-modified-file-uses-head-blob" \ "src/app.py" \ From bcbee66b4b8a485b5d1eb9128bb3cc746dbaaace Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 03:56:02 +0900 Subject: [PATCH 16/17] Reject OpenCode approvals without exact changed-file evidence --- .github/workflows/opencode-review.yml | 7 ++++ .../ci/opencode_review_normalize_output.py | 34 ++++++++++++++- scripts/ci/test_strix_quick_gate.sh | 41 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 4b4fe7559..f15926444 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -220,10 +220,12 @@ jobs: OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md + OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt FAILED_CHECK_EVIDENCE_ATTEMPTS: "31" FAILED_CHECK_EVIDENCE_SLEEP_SECONDS: "10" run: | set -euo pipefail + printf 'OPENCODE_CHANGED_FILES_FILE=%s\n' "$OPENCODE_CHANGED_FILES_FILE" >>"$GITHUB_ENV" current_peer_checks_still_running() { local owner="${GH_REPOSITORY%%/*}" @@ -406,6 +408,7 @@ jobs: printf -- "- Head SHA: \`%s\`\n\n" "$PR_HEAD_SHA" PR_MERGE_BASE="$(git -C "$OPENCODE_SOURCE_WORKDIR" merge-base "$PR_BASE_SHA" "$PR_HEAD_SHA")" printf -- "- Merge base SHA: \`%s\`\n\n" "$PR_MERGE_BASE" + git -C "$OPENCODE_SOURCE_WORKDIR" diff --name-only --find-renames "$PR_MERGE_BASE" "$PR_HEAD_SHA" >"$OPENCODE_CHANGED_FILES_FILE" printf '## CodeGraph evidence\n\n' printf 'The workflow initialized CodeGraph before this evidence file was built.\n' @@ -463,6 +466,7 @@ jobs: OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md + OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head run: | set -euo pipefail @@ -473,6 +477,9 @@ jobs: if [ -s "$OPENCODE_FAILED_CHECK_EVIDENCE_FILE" ]; then cp "$OPENCODE_FAILED_CHECK_EVIDENCE_FILE" "$OPENCODE_REVIEW_WORKDIR/failed-check-evidence.md" fi + if [ -s "$OPENCODE_CHANGED_FILES_FILE" ]; then + cp "$OPENCODE_CHANGED_FILES_FILE" "$OPENCODE_REVIEW_WORKDIR/changed-files.txt" + fi cat >"${OPENCODE_REVIEW_WORKDIR}/AGENTS.md" <<'EOF' # OpenCode CI Review Rules diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 32145f8f8..c266887a0 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import os import re import sys from pathlib import Path @@ -45,6 +46,8 @@ "no actionable changes to review", "no changes to review", "no changed files", + "no code changes", + "no code changes detected", ) STRUCTURAL_FAILURE_PATTERNS = ( @@ -92,6 +95,33 @@ def mentions_changed_file_evidence(reason: str, summary: str) -> bool: return bool(CHANGED_FILE_EVIDENCE_PATTERN.search(f"{reason}\n{summary}")) +def current_changed_files() -> list[str] | None: + """Return exact changed paths when the workflow provided bounded evidence.""" + changed_files_path = os.environ.get("OPENCODE_CHANGED_FILES_FILE", "").strip() + if not changed_files_path: + return None + try: + lines = Path(changed_files_path).read_text(encoding="utf-8").splitlines() + except OSError: + return None + changed_files = [] + for line in lines: + path = line.strip() + if path and not path.startswith("#"): + changed_files.append(path) + return changed_files + + +def mentions_actual_changed_file(reason: str, summary: str) -> bool: + """Return whether approval text cites at least one exact current changed path.""" + changed_files = current_changed_files() + if changed_files is None: + return mentions_changed_file_evidence(reason, summary) + + combined = f"{reason}\n{summary}" + return any(path in combined for path in changed_files) + + def check_structural_approval(control_file: Path) -> int: """Validate an already-normalized control block before publishing approval.""" try: @@ -110,7 +140,7 @@ def check_structural_approval(control_file: Path) -> int: ): print("NO_CONCLUSION", file=sys.stderr) return 4 - if value.get("result") == "APPROVE" and not mentions_changed_file_evidence( + if value.get("result") == "APPROVE" and not mentions_actual_changed_file( str(value.get("reason", "")), str(value.get("summary", "")), ): @@ -160,7 +190,7 @@ def valid_control( return None if result == "APPROVE" and admits_missing_structural_review(reason, summary): return None - if result == "APPROVE" and not mentions_changed_file_evidence(reason, summary): + if result == "APPROVE" and not mentions_actual_changed_file(reason, summary): return None required_finding_fields = ( diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index cf5d39381..57421f071 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -362,6 +362,9 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "Prepare isolated OpenCode review workspace" "opencode review workflow isolates from the large project AGENTS.md" assert_file_contains "$workflow_file" 'cd "$OPENCODE_REVIEW_WORKDIR"' "opencode review runs from the isolated OpenCode workspace" assert_file_contains "$workflow_file" "failed-check-evidence.md" "opencode review copies full failed-check evidence into the isolated workspace" + assert_file_contains "$workflow_file" "OPENCODE_CHANGED_FILES_FILE" "opencode review exports the exact changed-file list for approval validation" + assert_file_contains "$workflow_file" 'diff --name-only --find-renames "$PR_MERGE_BASE" "$PR_HEAD_SHA" >"$OPENCODE_CHANGED_FILES_FILE"' "opencode review writes exact current-head changed files for the approval gate" + assert_file_contains "$workflow_file" "changed-files.txt" "opencode review copies changed-file evidence into the isolated workspace" assert_file_contains "$workflow_file" "Checkout trusted review workflow" "opencode review executes trusted workflow scripts from the base checkout" assert_file_contains "$workflow_file" "Checkout trusted review workflow for manual PR review" "opencode review checks out explicit base SHA for manual PR review reruns" assert_file_contains "$workflow_file" 'ref: ${{ github.event.inputs.pr_base_sha }}' "opencode manual review checks out the trusted base workflow instead of the PR head" @@ -912,10 +915,12 @@ EOF assert_opencode_review_gate_rejects_approve_without_changed_file_evidence() { local tmp_dir local output_file + local changed_files_file local rc local gate_result tmp_dir="$(mktemp -d)" output_file="$tmp_dir/opencode-output.md" + changed_files_file="$tmp_dir/changed-files.txt" cat >"$output_file" <<'EOF' OpenCode transcript text before the review control block. @@ -952,6 +957,42 @@ EOF assert_equals "NO_CONCLUSION" "$gate_result" "missing changed-file evidence rejection gate result" assert_file_contains "$REPO_ROOT/.github/workflows/opencode-review.yml" "Before APPROVE, the summary must include at least one exact changed file path inspected as changed-file evidence" "opencode prompt requires changed-file evidence before approval" + cat >"$changed_files_file" <<'EOF' +.github/workflows/opencode-review.yml +scripts/ci/test_strix_quick_gate.sh +EOF + + cat >"$output_file" <<'EOF' +OpenCode transcript text before the review control block. + +{"head_sha":"abc123","run_id":"42","run_attempt":"1","result":"APPROVE","reason":"Documentation-only changes with no functional impact","summary":"Verified README.md updates; no code changes detected.","findings":[]} +EOF + + set +e + OPENCODE_CHANGED_FILES_FILE="$changed_files_file" \ + python3 "$REPO_ROOT/scripts/ci/opencode_review_normalize_output.py" \ + "abc123" "42" "1" "$output_file" >"$tmp_dir/nonchanged-normalize.out" 2>"$tmp_dir/nonchanged-normalize.err" + rc=$? + set -e + + assert_equals "4" "$rc" "opencode normalizer rejects approvals that cite non-changed files when exact changed-file evidence is available" + assert_file_contains "$tmp_dir/nonchanged-normalize.err" "NO_CONCLUSION" "opencode normalizer reports no conclusion for non-changed-file approval evidence" + + cat >"$output_file" <<'EOF' +OpenCode transcript text before the review control block. + +{"head_sha":"abc123","run_id":"42","run_attempt":"1","result":"APPROVE","reason":"No blockers found after inspecting .github/workflows/opencode-review.yml.","summary":"Reviewed .github/workflows/opencode-review.yml and scripts/ci/test_strix_quick_gate.sh against current-head evidence; no blocking findings.","findings":[]} +EOF + + set +e + OPENCODE_CHANGED_FILES_FILE="$changed_files_file" \ + python3 "$REPO_ROOT/scripts/ci/opencode_review_normalize_output.py" \ + "abc123" "42" "1" "$output_file" >"$tmp_dir/changed-normalize.out" 2>"$tmp_dir/changed-normalize.err" + rc=$? + set -e + + assert_equals "0" "$rc" "opencode normalizer accepts approvals that cite exact current changed files" + rm -rf "$tmp_dir" } From 5fe1791d48ddcf03dbc365cc6fa407e7cbe70a89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 24 Jun 2026 04:24:26 +0900 Subject: [PATCH 17/17] Use PR-head review tooling for manual OpenCode reruns --- .github/workflows/opencode-review.yml | 52 +++++++++++++++++++++++++++ scripts/ci/test_strix_quick_gate.sh | 4 +++ 2 files changed, 56 insertions(+) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f15926444..94989bd84 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -174,6 +174,58 @@ jobs: git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" git -C "$OPENCODE_SOURCE_WORKDIR" status --short + - name: Overlay PR-head review tooling bootstrap files for manual reruns + if: github.event_name == 'workflow_dispatch' + env: + PR_BASE_SHA: ${{ github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.inputs.pr_head_sha }} + OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head + run: | + set -euo pipefail + + # Manual self-modifying review-tooling PRs must exercise the proposed + # gate/normalizer. Source/application PRs keep the trusted base logic. + allowlist_file="$(mktemp)" + changed_files_file="$(mktemp)" + cat >"$allowlist_file" <<'EOF' + .github/workflows/opencode-review.yml + .github/workflows/pr-review-merge-scheduler.yml + .github/workflows/strix.yml + opencode.jsonc + requirements-strix-ci.txt + requirements-strix-ci-hashes.txt + scripts/ci/collect_failed_check_evidence.sh + scripts/ci/emit_opencode_failed_check_fallback_findings.sh + scripts/ci/opencode_review_approve_gate.sh + scripts/ci/opencode_review_normalize_output.py + scripts/ci/strix_model_utils.sh + scripts/ci/strix_quick_gate.sh + scripts/ci/test_strix_quick_gate.sh + scripts/ci/validate_opencode_failed_check_review.sh + EOF + + pr_merge_base="$(git -C "$OPENCODE_SOURCE_WORKDIR" merge-base "$PR_BASE_SHA" "$PR_HEAD_SHA")" + git -C "$OPENCODE_SOURCE_WORKDIR" diff --name-only --find-renames "$pr_merge_base" "$PR_HEAD_SHA" >"$changed_files_file" + if [ ! -s "$changed_files_file" ]; then + printf 'No changed files; keeping trusted base review tooling.\n' + exit 0 + fi + + if ! awk 'NR == FNR {allowed[$0]=1; next} { if (!($0 in allowed)) exit 1 }' "$allowlist_file" "$changed_files_file"; then + printf '::notice::Skipping PR-head review tooling overlay because changed files include non-review-tooling paths.\n' + sed 's/^/changed: /' "$changed_files_file" + exit 0 + fi + + printf 'Only fully review-tooling PRs may execute PR-head bootstrap helpers in the trusted workspace.\n' + while IFS= read -r bootstrap_file; do + if [ -f "$OPENCODE_SOURCE_WORKDIR/$bootstrap_file" ]; then + mkdir -p "$(dirname "$GITHUB_WORKSPACE/$bootstrap_file")" + cp "$OPENCODE_SOURCE_WORKDIR/$bootstrap_file" "$GITHUB_WORKSPACE/$bootstrap_file" + printf 'Overlayed PR-head review tooling bootstrap file: %s\n' "$bootstrap_file" + fi + done <"$allowlist_file" + - name: Configure git identity for OpenCode action run: | set -euo pipefail diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 57421f071..72075d20a 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -370,6 +370,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'ref: ${{ github.event.inputs.pr_base_sha }}' "opencode manual review checks out the trusted base workflow instead of the PR head" assert_file_contains "$workflow_file" "Materialize pull request head for OpenCode review data" "opencode review materializes PR-head source as read-only review data" assert_file_contains "$workflow_file" 'git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA"' "opencode review materializes the PR head without actions/checkout credentials" + assert_file_contains "$workflow_file" "Overlay PR-head review tooling bootstrap files for manual reruns" "opencode manual review can exercise current-head review-tooling patches" + assert_file_contains "$workflow_file" "Only fully review-tooling PRs may execute PR-head bootstrap helpers in the trusted workspace." "opencode review-tooling overlay is restricted to allowlisted bootstrap-only PRs" + assert_file_contains "$workflow_file" "Skipping PR-head review tooling overlay because changed files include non-review-tooling paths." "opencode review-tooling overlay skips source/application PRs" + assert_file_contains "$workflow_file" 'cp "$OPENCODE_SOURCE_WORKDIR/$bootstrap_file" "$GITHUB_WORKSPACE/$bootstrap_file"' "opencode manual review overlays approved PR-head review tooling files before running gates" assert_file_contains "$workflow_file" 'cd "$OPENCODE_SOURCE_WORKDIR"' "opencode CodeGraph indexing runs against the PR-head source worktree" assert_file_contains "$workflow_file" 'PR_MERGE_BASE="$(git -C "$OPENCODE_SOURCE_WORKDIR" merge-base "$PR_BASE_SHA" "$PR_HEAD_SHA")"' "opencode review evidence diffs use the PR-head worktree merge base" assert_file_contains "$workflow_file" 'git -C "$OPENCODE_SOURCE_WORKDIR" diff' "opencode review builds changed-file evidence from the PR-head worktree"