Skip to content
Open
75 changes: 68 additions & 7 deletions .github/actions/noema-review/two_phase.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,23 +145,81 @@ def _read_envelope(path: Path) -> dict[str, Any]:
return payload


def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> int:
"""Run model review and seal its verdict without publishing GitHub evidence."""
def _model_work_eligibility(
repo: str,
number: int,
expected_head: str,
*,
skip_closed_or_stale: bool,
phase: str,
) -> tuple[str, dict[str, Any], str, str] | None:
"""Return the validated review identity when this head still needs model work."""
expected = _canonical_head(expected_head)
pull_request = gate.fetch_pr(repo, number)
try:
gate.require_expected_head(pull_request, expected)
except RuntimeError:
print("Pull request is closed or stale; Noema verdict preparation skipped.")
return 0
if not skip_closed_or_stale:
raise
print(f"Pull request is closed or stale; Noema {phase} skipped.")
return None
expected_base = _canonical_base(pull_request)
actor = _reviewer_actor()
if pull_request.get("isDraft"):
print("PR is draft; Noema verdict preparation skipped.")
return 0
print(f"PR is draft; Noema {phase} skipped.")
return None
if gate.existing_noema_review(pull_request, actor):
print("Current head already has a Noema review; verdict preparation skipped.")
print(f"Current head already has a Noema review; Noema {phase} skipped.")
return None
return expected, pull_request, expected_base, actor


def admit_model_work(repo: str, number: int, expected_head: str, path: Path) -> int:
"""Record whether the current review needs the expensive model sidecar."""
eligibility = _model_work_eligibility(
repo,
number,
expected_head,
skip_closed_or_stale=False,
phase="model admission",
)
if eligibility is None:
return 0
expected, pull_request, expected_base, _actor = eligibility
repository = pull_request.get("repository")
if (
not isinstance(repository, dict)
or not isinstance(repository.get("nameWithOwner"), str)
or repository["nameWithOwner"].casefold() != repo.casefold()
or repository.get("visibility") not in ("PUBLIC", "PRIVATE", "INTERNAL")
):
raise RuntimeError("Noema repository visibility could not be verified")
_write_envelope(
path,
{
"schema_version": ENVELOPE_SCHEMA_VERSION,
"repository": repo,
"pull_request_number": number,
"expected_head": expected,
"expected_base": expected_base,
"repository_visibility": repository["visibility"].lower(),
},
)
return 0


def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> int:
"""Run model review and seal its verdict without publishing GitHub evidence."""
eligibility = _model_work_eligibility(
repo,
number,
expected_head,
skip_closed_or_stale=True,
phase="verdict preparation",
)
if eligibility is None:
return 0
expected, pull_request, expected_base, _actor = eligibility

diff, truncated = gate.fetch_diff(repo, number)
changed_files = gate.fetch_changed_files(repo, number)
Expand Down Expand Up @@ -253,6 +311,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
parser.add_argument("--pr-number", required=True, type=int)
parser.add_argument("--expected-head", required=True)
modes = parser.add_mutually_exclusive_group(required=True)
modes.add_argument("--admit-model-file", type=Path)
modes.add_argument("--prepare-verdict-file", type=Path)
modes.add_argument("--publish-verdict-file", type=Path)
return parser.parse_args(argv)
Expand All @@ -263,6 +322,8 @@ def main(argv: list[str]) -> int:
args = parse_args(argv)
if args.pr_number <= 0:
raise SystemExit("--pr-number must be positive")
if args.admit_model_file is not None:
return admit_model_work(args.repo, args.pr_number, args.expected_head, args.admit_model_file)
if args.prepare_verdict_file is not None:
return prepare_verdict(args.repo, args.pr_number, args.expected_head, args.prepare_verdict_file)
return publish_verdict(args.repo, args.pr_number, args.expected_head, args.publish_verdict_file)
Expand Down
19 changes: 16 additions & 3 deletions .github/workflows/agent-review-runtime-quality-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ on:
- ".github/workflows/agent-review-runtime-quality-ci.yml"
- ".github/workflows/noema-review.yml"
- ".github/actions/noema-review/two_phase.py"
- "scripts/ci/noema_review_gate.py"
- "tests/test_noema_review_gate.py"
- "tests/test_noema_orchestrator_workflow_contract.py"
- "tests/test_required_workflow_queue_contract.py"
- "tests/test_current_head_coalescer_self_cancellation.py"
- "tests/test_noema_reviewer_token_lifetime.py"
- "tests/test_noema_two_phase_handoff.py"
- "tests/test_noema_refreshed_app_identity.py"
Expand Down Expand Up @@ -154,6 +159,7 @@ jobs:
commercial_readiness_suite=false
exact_artifact_suite=false

changed_paths="$(git diff --name-only "$BASE_SHA...$HEAD_SHA")"
while IFS= read -r changed_path; do
case "$changed_path" in
.github/workflows/agent-review-runtime-quality-ci.yml)
Expand All @@ -175,6 +181,9 @@ jobs:
;;
.github/workflows/noema-review.yml|\
.github/actions/noema-review/two_phase.py|\
scripts/ci/noema_review_gate.py|\
tests/test_noema_review_gate.py|\
tests/test_noema_orchestrator_workflow_contract.py|\
tests/test_noema_reviewer_token_lifetime.py|\
tests/test_noema_two_phase_handoff.py|\
tests/test_noema_refreshed_app_identity.py|\
Expand Down Expand Up @@ -210,7 +219,9 @@ jobs:
queue_suite=true
review_repair_suite=true
;;
scripts/ci/current_head_run_coalescer.py)
scripts/ci/current_head_run_coalescer.py|\
tests/test_current_head_coalescer_self_cancellation.py|\
tests/test_required_workflow_queue_contract.py)
queue_suite=true
;;
.github/workflows/pr-review-fix-scheduler.yml|\
Expand Down Expand Up @@ -286,7 +297,7 @@ jobs:
exact_artifact_suite=true
;;
esac
done < <(git diff --name-only "$BASE_SHA...$HEAD_SHA")
done <<<"$changed_paths"

{
echo "noema=$noema_suite"
Expand Down Expand Up @@ -328,6 +339,8 @@ jobs:
run: |
set -euo pipefail
PYTHONPATH=. python -m pytest -q \
tests/test_noema_review_gate.py \
tests/test_noema_orchestrator_workflow_contract.py \
tests/test_noema_reviewer_token_lifetime.py \
tests/test_noema_two_phase_handoff.py \
tests/test_noema_refreshed_app_identity.py \
Expand Down Expand Up @@ -374,7 +387,7 @@ jobs:
if: steps.affected_suites.outputs.queue == 'true'
run: |
set -euo pipefail
python -m pytest -q tests/test_current_head_coalescer_self_cancellation.py
python -m pytest -q tests/test_current_head_coalescer_self_cancellation.py tests/test_required_workflow_queue_contract.py
python -m compileall -q tests/test_current_head_coalescer_self_cancellation.py

- name: Verify scheduler and contextual-orchestrator review-repair contracts
Expand Down
58 changes: 25 additions & 33 deletions .github/workflows/noema-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -580,49 +580,41 @@ jobs:
echo "::add-mask::$app_token"
echo "token=$app_token" >>"$GITHUB_OUTPUT"

- name: Validate current pull request head
- name: Admit Noema model work
if: env.PR_NUMBER != ''
id: noema_model_admission
env:
GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }}
NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app' || 'noema-review-app-oidc' }}
NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_token.outputs['app-slug']) || '' }}
NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_token.outputs['installation-id'] }}
run: |
set -euo pipefail
if ! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Noema expected head must be a full commit SHA."
exit 1
fi
pull_request_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_state="$(jq -r '.state // empty' <<<"$pull_request_json")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")"
if [ "$live_state" != "open" ] || [ "${live_head_sha,,}" != "${EXPECTED_HEAD_SHA,,}" ]; then
printf '::error::Noema review target is closed or stale. expected head=%s; live state=%s head=%s.\n' \
"$EXPECTED_HEAD_SHA" "${live_state:-missing}" "${live_head_sha:-missing}"
exit 1
admission_file="${RUNNER_TEMP}/noema-model-admission.json"
rm -f "$admission_file"
python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" \
--repo "$TARGET_REPOSITORY" \
--pr-number "$PR_NUMBER" \
--expected-head "$EXPECTED_HEAD_SHA" \
--admit-model-file "$admission_file"
if [ -f "$admission_file" ]; then
visibility="$(jq -er '.repository_visibility | select(. == "public" or . == "private" or . == "internal")' "$admission_file")"
echo "repository_visibility=$visibility" >>"$GITHUB_OUTPUT"
rm -f "$admission_file"
echo "admitted=true" >>"$GITHUB_OUTPUT"
else
echo "admitted=false" >>"$GITHUB_OUTPUT"
echo "::notice::Noema model work is unnecessary for the current pull request state."
fi

- name: Resolve Noema target repository visibility
if: env.PR_NUMBER != ''
if: env.PR_NUMBER != '' && steps.noema_model_admission.outputs.admitted == 'true'
id: target_visibility
env:
GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }}
REPOSITORY_VISIBILITY: ${{ steps.noema_model_admission.outputs.repository_visibility }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::Noema target visibility cannot be resolved without the selected repository-scoped reviewer token."
exit 1
fi
visibility=""
for target_visibility_attempt in 1 2 3 4 5 6; do
if visibility="$(
gh api "/repos/${TARGET_REPOSITORY}" --jq '.visibility // (if .private then "private" else "public" end)'
)"; then
break
fi
visibility=""
if [ "$target_visibility_attempt" -lt 6 ]; then
echo "Repository visibility lookup failed (attempt ${target_visibility_attempt}/6), possibly a transient GitHub API rate limit; retrying after backoff." >&2
sleep "$(( target_visibility_attempt * 5 ))"
fi
done
visibility="${REPOSITORY_VISIBILITY:-}"
case "$visibility" in
private|internal)
echo "require_zdr=true" >>"$GITHUB_OUTPUT"
Expand All @@ -638,7 +630,7 @@ jobs:
esac

- name: Provision contextual-orchestrator review sidecar
if: env.PR_NUMBER != ''
if: env.PR_NUMBER != '' && steps.noema_model_admission.outputs.admitted == 'true'
env:
BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }}
NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}
Expand All @@ -651,7 +643,7 @@ jobs:
bash "$GITHUB_WORKSPACE/scripts/ci/contextual_orchestrator_review_sidecar.sh"

- name: Prepare Noema model verdict
if: env.PR_NUMBER != ''
if: env.PR_NUMBER != '' && steps.noema_model_admission.outputs.admitted == 'true'
id: noema_prepare
env:
GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }}
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,12 @@ them alone proves succession.
variable in CI, so a failure class exists that cannot reproduce locally. Before calling a
scheduler change clean, run the affected tests both ways, including
`GITHUB_ACTIONS=true python3 -m pytest <paths>`.
- For consolidated CI, verify all three links: changed-path trigger, actual shell suite
selection, and the selected pytest command. A green job does not cover a changed
contract merely because its filename appears elsewhere in the workflow. Reuse the
existing single-runner job and execute its selector in regression tests; record the
hosted test count separately from a local full-suite result.
- Capture changed-file discovery before iterating it. Bash process substitution
can hide a failed `git diff` despite `set -e`; do not publish all-false suite
outputs when the base cannot be read. Execute the real selector with an invalid
base in its regression test and require failure before any selection output.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
- Raised `hourly-review-repair.yml`'s discovery ceiling from 50 to 200 while rotating deterministic 50-PR deep-inspection windows by hourly run number. The scheduler hydrates only the selected window and stops immediately after its single dispatch, preserving access to newer PRs without quadrupling expensive review/check/comment work. See `docs/doctoring/hourly-review-repair-single-file-consolidation.md`'s 2026-09-03 follow-up.

## [Unreleased]
- Reuse Noema's exact-head, base, reviewer-identity, Draft, and existing-review
eligibility checks before provisioning its model sidecar, while retaining
the same checks immediately before model work. Refs #1992.
- Include merge-scheduler entrypoint, core, and regression-test changes in
the existing runtime-quality workflow's trigger and suite selector. Scheduler
workflow edits retain queue checks and also select the full review-repair
Expand Down
24 changes: 24 additions & 0 deletions docs/doctoring/noema-orchestrator-free-zdr.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,27 @@ is not Noema review evidence.
For GitHub App credentials, reviewer identity is bound to the pinned token
mint action's app slug and numeric installation ID. PAT and OIDC credentials
continue to resolve their actor through GitHub's authenticated API.

## Draft 조기 판정

`.github` run `34045630637`의 Noema job은 sidecar 준비를 시작한 뒤
706.63초가 지나서야 Draft 상태를 확인하고 모델 작업을 건너뛰었다. 이
실행에서 불필요한 sidecar 시작은 1회였다. Issue #1992는 이 값을 관측
baseline으로 추적한다.

모델 작업 admission은 reviewer credential을 만든 뒤, repository visibility
조회와 sidecar 준비보다 먼저 실행한다. 판정은 `two_phase.py`의 기존 순서인
exact head, base, 독립 reviewer actor, Draft, 현재 head의 기존 Noema review를
그대로 공유한다. 실제 verdict 준비도 같은 판정을 다시 실행하므로 admission
뒤 상태 변경을 신뢰하지 않는다. Noema는 계속 OpenCode 승인과 독립적으로
실행된다.

Admission은 closed/stale target을 실패 처리하고 실제 verdict 준비는 기존처럼
성공적으로 건너뛴다. 공유 판정의 명시적 keyword-only flag가 두 의미를
분리한다. 이 GraphQL 판정이 같은 reviewer token으로 head/state까지 확인하므로
바로 앞의 중복 REST 검증 step은 제거했다. 실행마다 REST 호출 1회가 줄지만,
eligible 경로의 추가 GraphQL 조회가 REST와 비용이 같다고 간주하지 않는다.

로컬 회귀는 Draft와 기존 review에서 sidecar admission marker가 생기지 않는
것을 확인한다. 새 exact-head hosted 실행에서 불필요한 sidecar 시작이 0회인지
확인하기 전에는 runtime 개선이 완료됐다고 보지 않는다. Refs #1992.
1 change: 1 addition & 0 deletions scripts/ci/noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def graphql(query: str, **fields: str | int) -> dict[str, Any]:
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
repository { nameWithOwner visibility }
number
title
body
Expand Down
Loading
Loading