Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 253 additions & 5 deletions .github/workflows/noema-review.yml

Large diffs are not rendered by default.

81 changes: 70 additions & 11 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
name: validate-pr-metadata
if: github.event_name == 'repository_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 8
permissions:
contents: read
pull-requests: read
Expand Down Expand Up @@ -224,6 +225,7 @@ jobs:
needs.validate-pr-metadata.result == 'success'
&& github.event_name == 'repository_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 12
permissions:
contents: read
id-token: write
Expand Down Expand Up @@ -371,6 +373,7 @@ jobs:
&& needs.coverage-source-tree.result != 'cancelled'
&& github.event_name == 'repository_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 300
permissions:
# The PR tree arrives through a same-run artifact. No repository-content,
# identity, secret, or write token is available to untrusted tests.
Expand Down Expand Up @@ -2309,9 +2312,9 @@ jobs:
# 36-minute publication gate, the 18-minute Noema handoff, and setup/cleanup
# overhead without truncating a late current-head verdict, handoff, merge
# scheduler follow-up, or bounded failure reason.
timeout-minutes: 325
timeout-minutes: 305
permissions:
actions: read
actions: write
checks: read
id-token: write
contents: read
Expand Down Expand Up @@ -4003,9 +4006,9 @@ jobs:
OPENCODE_MODEL_ATTEMPTS: "1"
# Preserve reviews that legitimately need tens of minutes to inspect a
# large repository. Changed-file count is not a repository-complexity
# proxy, so every cadence class gets 90 minutes per candidate while the
# bounded provider-pool watchdog remains the outer guard.
OPENCODE_RUN_TIMEOUT_SECONDS: "5400"
# proxy. Let Contextual Orchestrator use the existing total review
# budget; the bounded provider-pool watchdog remains the outer guard.
OPENCODE_RUN_TIMEOUT_SECONDS: "11700"
OPENCODE_EXPORT_TIMEOUT_SECONDS: "180"
OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "11700"
OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "12000"
Expand All @@ -4017,22 +4020,22 @@ jobs:
OPENCODE_DYNAMIC_REVIEW_CADENCE: "true"
OPENCODE_SMALL_CHANGE_FILE_THRESHOLD: "3"
OPENCODE_MEDIUM_CHANGE_FILE_THRESHOLD: "20"
OPENCODE_SMALL_CHANGE_RUN_TIMEOUT_SECONDS: "5400"
OPENCODE_SMALL_CHANGE_RUN_TIMEOUT_SECONDS: "11700"
OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS: "11700"
OPENCODE_MEDIUM_CHANGE_RUN_TIMEOUT_SECONDS: "5400"
OPENCODE_MEDIUM_CHANGE_RUN_TIMEOUT_SECONDS: "11700"
OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS: "11700"
OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS: "5400"
OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS: "11700"
OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "11700"
OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "5400"
OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "11700"
OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "11700"
OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS: "5400"
OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS: "11700"
OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700"
OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "1"
OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600"
OPENCODE_DYNAMIC_MAX_CYCLES: "1"
CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }}
CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }}
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_RUN_TIMEOUT_SECONDS: "5400"
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_RUN_TIMEOUT_SECONDS: "11700"
Comment on lines 4020 to +4038

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Review timeout margins stay narrow

The model gets 195 minutes inside a 200-minute watchdog and 205-minute step. Future setup growth can exhaust the remaining publication margin.

(Refers to this code)

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_TOTAL_BUDGET_SECONDS: "11700"
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_MAX_CYCLES: "1"
OPENCODE_BACKOFF_INITIAL_SECONDS: "30"
Expand Down Expand Up @@ -7545,6 +7548,7 @@ jobs:
echo "::endgroup::"

- name: Enforce current-head formal OpenCode review receipt
id: formal_review_receipt
if: >-
always()
&& needs.validate-pr-metadata.result == 'success'
Expand All @@ -7568,6 +7572,61 @@ jobs:
--head-sha "$PR_HEAD_SHA" \
"${draft_args[@]}"

- name: Wake exact-head required OpenCode workflow
if: >-
always()
&& github.event_name == 'repository_dispatch'
&& steps.formal_review_receipt.outcome == 'success'
&& needs.validate-pr-metadata.outputs.target_repository != ''
&& needs.validate-pr-metadata.outputs.head_sha != ''
&& github.event.client_payload.required_run_id != ''
env:
GH_TOKEN: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }}
GH_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }}
PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }}
REQUIRED_RUN_ID: ${{ github.event.client_payload.required_run_id }}
WAKE_TOKEN_SOURCE: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && 'github-token' || secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || 'unavailable' }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ] || [ "$WAKE_TOKEN_SOURCE" = "unavailable" ]; then
echo "::error::Actions-capable wake credential is unavailable. Native runs use github.token; sibling runs require PR_REVIEW_MERGE_TOKEN or OPENCODE_APPROVE_TOKEN."
exit 1
fi
[[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]] || {
echo "::error::Required OpenCode run id is missing or non-canonical."
exit 1
}
# The immutable run id is scoped to GH_REPOSITORY. Revalidate its
# event, central workflow path, and live PR head before rerunning it;
# rendered titles and workflow_url differ between native and
# organization-required workflow contexts.
for attempt in $(seq 1 12); do
run="$(gh api "repos/${GH_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")"
required_run="$(printf '%s\n' "$run" | jq -r --arg head "$PR_HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" '
select(.id == $run_id)
| select(.event == "pull_request_target")
| select(.path == ".github/workflows/opencode-review.yml")
| select(.head_sha == $head)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Completed reviews cannot wake checks

pull_request_target records the trusted base commit in .head_sha, but the wake gate compares it to the pull-request head. Completed reviews cannot rerun the failed required check, and the scheduler fallback cannot discover that run.

Prompt for agents
The event-driven continuation validates a pull_request_target workflow run by comparing the REST run object's head_sha with the reviewed PR head. For pull_request_target, that field represents the trusted base-side workflow commit, so the selector always rejects the required run. scripts/ci/pr_review_merge_scheduler.py:discover_opencode_required_run_id also passes the PR head as the Actions runs head_sha filter and therefore cannot find the same run. Rework both paths around an identity that actually binds the required run to the PR head, such as the immutable run ID plus event/path and the run's pull_requests association, while retaining target-repository and exact-PR validation. Add fixtures that model a base SHA distinct from the PR head SHA for real pull_request_target run payloads.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

| [(.id // ""), (.status // ""), (.conclusion // "")]
| @tsv
')"
IFS=$'\t' read -r required_run_id required_status required_conclusion <<<"$required_run"
if [ "$required_status" = "completed" ] && [ "$required_conclusion" = "failure" ]; then
gh api -X POST "repos/${GH_REPOSITORY}/actions/runs/${required_run_id}/rerun-failed-jobs" >/dev/null
echo "Re-ran failed jobs for exact-head Required OpenCode Review run ${required_run_id}."
exit 0
fi
if [ "$required_status" = "completed" ] && [ "$required_conclusion" = "success" ]; then
echo "Exact-head Required OpenCode Review run ${required_run_id} already succeeded."
exit 0
fi
if [ "$attempt" -lt 12 ]; then
sleep 5
fi
done
echo "::error::Formal OpenCode receipt exists, but the exact-head required workflow did not reach a rerunnable failed state."
exit 1

- name: Publish repository_dispatch OpenCode status
if: >-
always()
Expand Down
132 changes: 82 additions & 50 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ jobs:
echo "Required OpenCode workflow materialized without checking out or
executing pull-request content."

- name: Reject untrusted fork review resource consumption
env:
PR_ACTION: ${{ github.event.action }}
BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }}
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
run: |
set -euo pipefail
if [ "$PR_ACTION" = "closed" ]; then
echo "PR closed; fork-resource-consumption check is not required."
exit 0
fi
if [ -z "$BASE_REPOSITORY" ] || [ -z "$HEAD_REPOSITORY" ] || [ "$HEAD_REPOSITORY" != "$BASE_REPOSITORY" ]; then
echo "::error::Long-running required review is restricted to branches in the base repository. A maintainer must materialize an external contribution on a trusted branch before review."
exit 1
fi

- name: Resolve immutable central policy source
id: trusted_source
env:
Expand Down Expand Up @@ -235,66 +251,35 @@ jobs:
name: opencode-review
needs: [coverage-evidence]
runs-on: ubuntu-latest
timeout-minutes: 100
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
id-token: write
steps:
- name: Request current-head OpenCode review execution
if: github.event.action != 'closed'
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "::error::OpenCode review dispatch requires GitHub OIDC."
exit 1
fi
separator='&'
[[ "$ACTIONS_ID_TOKEN_REQUEST_URL" == *\?* ]] || separator='?'
oidc_token="$(curl -fsS -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}${separator}audience=${OIDC_AUDIENCE}" | jq -r '.value // empty')"
if [ -z "$oidc_token" ]; then
echo "::error::OpenCode review dispatch could not obtain its OIDC token."
exit 1
fi
app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -r '.token // empty')"
if [ -z "$app_token" ]; then
echo "::error::OpenCode review dispatch could not obtain its repository-scoped app token."
exit 1
fi
echo "::add-mask::$app_token"
jq -cn \
--arg target_repository "$TARGET_REPOSITORY" \
--arg pr_number "$PR_NUMBER" \
--arg base_branch "$BASE_BRANCH" \
'{event_type:"merge-scheduler",client_payload:{target_repository:$target_repository,pr_number:$pr_number,base_branch:$base_branch,max_prs:"1",review_dispatch_limit:"1",trigger_reviews:true,enable_auto_merge:false,update_branches:false,dry_run:false}}' |
GH_TOKEN="$app_token" gh api -X POST repos/ContextualWisdomLab/.github/dispatches --input -

- name: Fail closed without a current-head OpenCode verdict
- name: Resolve current-head formal OpenCode verdict
id: verdict
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_ACTION: ${{ github.event.action }}
run: |
set -euo pipefail
if [ "${{ github.event.action }}" = "closed" ]; then
if [ "$PR_ACTION" = "closed" ]; then
echo "PR closed; a current-head OpenCode verdict is not required."
echo "verdict=CLOSED" >>"$GITHUB_OUTPUT"
exit 0
fi
if [ -z "${PR_NUMBER:-}" ] || [ -z "${HEAD_SHA:-}" ]; then
echo "::error::Missing PR number or head SHA; cannot verify a current-head OpenCode verdict."
exit 1
fi
verdict=""
for attempt in $(seq 1 180); do
reviews="$(gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews")"
verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$HEAD_SHA" '
if ! reviews="$(timeout 25 gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews")"; then
reviews="[]"
fi
verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$HEAD_SHA" '
(add // [])
| [
.[]
Expand Down Expand Up @@ -322,15 +307,62 @@ jobs:
empty
end
')"
if [ -n "$verdict" ]; then
break
fi
if [ "$attempt" -lt 180 ]; then
sleep 30
fi
done
if [ -z "$verdict" ]; then
echo "verdict=${verdict}" >>"$GITHUB_OUTPUT"
if [ -n "$verdict" ]; then
echo "Current-head OpenCode verdict: ${verdict}."
fi

- name: Request current-head OpenCode review execution
if: github.event.action != 'closed' && steps.verdict.outputs.verdict == ''
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "::error::OpenCode review dispatch requires GitHub OIDC."
exit 1
fi
separator='&'
[[ "$ACTIONS_ID_TOKEN_REQUEST_URL" == *\?* ]] || separator='?'
oidc_token="$(curl -fsS -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}${separator}audience=${OIDC_AUDIENCE}" | jq -r '.value // empty')"
if [ -z "$oidc_token" ]; then
echo "::error::OpenCode review dispatch could not obtain its OIDC token."
exit 1
fi
app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -r '.token // empty')"
if [ -z "$app_token" ]; then
echo "::error::OpenCode review dispatch could not obtain its repository-scoped app token."
exit 1
fi
echo "::add-mask::$app_token"
jq -cn \
--arg target_repository "$TARGET_REPOSITORY" \
--argjson pr_number "$PR_NUMBER" \
--arg pr_base_ref "$BASE_BRANCH" \
--arg pr_base_sha "$BASE_SHA" \
--arg pr_head_ref "$HEAD_BRANCH" \
--arg pr_head_sha "$HEAD_SHA" \
--argjson required_run_id "$GITHUB_RUN_ID" \
'{event_type:"opencode-review",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head_ref:$pr_head_ref,pr_head_sha:$pr_head_sha,required_run_id:$required_run_id}}' |
GH_TOKEN="$app_token" gh api -X POST repos/ContextualWisdomLab/.github/dispatches --input -

- name: Fail closed without a current-head OpenCode verdict
env:
VERDICT: ${{ steps.verdict.outputs.verdict }}
run: |
set -euo pipefail
if [ "$VERDICT" = "CLOSED" ]; then
exit 0
fi
if [ -z "$VERDICT" ]; then
echo "::error::No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head. This required check is not a review and must not succeed until the authenticated dispatch posts a current-head verdict."
exit 1
fi
echo "Current-head OpenCode verdict: ${verdict}."
echo "Current-head OpenCode verdict: ${VERDICT}."
4 changes: 4 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# The live tests now construct these token-like strings at runtime; new findings remain blocking.
# Historical synthetic OpenAI-key fixture from the provider-diagnostic regression test.
b8a18e9bba3c0afb9eeea51d33bc0d5307b733d8:tests/test_opencode_model_pool_runner.py:generic-api-key:87
# Historical synthetic UUID-shaped fixture (unrecognized-secret-shape regression test) that
# predates commit 3f3bb47, which now constructs it at runtime via fake_secret(*parts); this
# entry only silences the superseded historical commit still reachable in this PR's range.
6657eb76f0e2cf6dab9197cfa861a1f584653aba:tests/test_noema_review_gate.py:generic-api-key:187
995d6b3606e0effe6722c422b369da9ef0171824:tests/test_pr_review_merge_scheduler.py:github-pat:2900
995d6b3606e0effe6722c422b369da9ef0171824:tests/test_pr_review_merge_scheduler.py:github-pat:2908
995d6b3606e0effe6722c422b369da9ef0171824:tests/test_pr_review_merge_scheduler.py:github-pat:2923
Expand Down
6 changes: 6 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@
## 2026-08-29 - [대용량 텍스트 스캔 시 정규표현식 대신 네이티브 메서드 활용]
**Learning:** `scripts/ci/opencode_review_normalize_output.py`의 라벨 스캐닝 루프에서 긴 LLM 리뷰 텍스트를 대상으로 `pattern.finditer()`를 호출하는 패턴이 있었습니다. 마이크로 벤치마크 결과, 단순 문자열 매칭에서는 네이티브 `str.find()`와 `while` 루프를 조합하는 것이 정규표현식 실행 오버헤드 없이 훨씬 빠르다는 것을 확인했습니다.
**Action:** 내부 탐색 루프에서 정확히 일치하는 리터럴 문자열(라벨 접두사 등)을 검색할 때는 `re.compile(re.escape(string)).finditer()` 대신 고도로 최적화된 Python 네이티브 `text.find(candidate, index)` 메서드를 사용하십시오. 단, 무한 루프를 방지하기 위해 루프의 모든 분기에서 인덱스가 올바르게 진행되도록 보장해야 합니다.
## 2026-08-29 - [문자열 검색 윈도우 동적 축소를 통한 라벨 스캐닝 최적화]
**Learning:** `scripts/ci/opencode_review_normalize_output.py`의 `label_section` 함수에서 다음 라벨을 찾을 때, 전체 텍스트에 대해 모든 후보 라벨의 위치를 파악하는 방식은 O(N * L) (N=텍스트 길이, L=라벨 개수)의 심각한 오버헤드를 발생시킵니다.
**Action:** 긴 텍스트에서 다음 마커(라벨 등)를 찾을 때, 현재까지 발견된 가장 가까운 다음 마커의 위치(`index`)로 검색 윈도우의 끝(`end = min(end, index)`)을 동적으로 축소하면서 네이티브 `text.find(candidate, start, end)`를 호출하십시오. 이는 중복 스캐닝을 크게 줄입니다.
## 2026-08-31 - [LLM 재시도 루프 시 오차 위치 명시]
**Learning:** `noema_review_gate.py` 등 LLM 응답을 파싱하고 검증하는 로직에서 오류 발생 시 해당 오류 메시지(`str(exc)`)를 프롬프트에 포함하여 다시 LLM을 호출(`repair_error`)하는 구조가 존재합니다. 이때 에러 메시지에 LLM이 잘못 생성한 데이터(예: 존재하지 않는 파일 경로, 일치하지 않는 라인 번호 등)를 구체적으로 포함시키지 않으면, LLM은 무엇이 틀렸는지 알지 못해 동일한 실수를 반복하여 CI 실패를 초래합니다.
**Action:** LLM의 형식 오류나 검증 실패로 인해 `RuntimeError` 등을 발생시킬 때, 단순히 `is not an exact changed-side line`과 같이 이유만 명시하지 말고 `It cited: {location}`처럼 구체적으로 오류를 일으킨 잘못된 LLM 출력을 함께 포함하십시오.
Loading
Loading