Skip to content
Draft
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Change-request autofix fails closed on unknown merge state

- The shared change-request gate now requires `mergeStateStatus` to be exactly `CLEAN` or `HAS_HOOKS`. Missing, empty, and unknown REST fallback values cannot authorize autofix or RCA dispatch. Proposed in ContextualWisdomLab/.github#1492.

### Failed-check finding names the Strix sandbox instead of the gateway

- `opencode-review-dispatch.yml`'s `emit_strix_provider_failure_finding` rendered one fixed finding for every `STRIX_PROVIDER_UNAVAILABLE` line, whose Root cause read "The contextual-orchestrator gateway or its discovered provider pool was unavailable for this run". `#1953` had just given the Strix sandbox bootstrap failure its own second verdict token (`STRIX_SANDBOX_UNAVAILABLE`) precisely because that attribution is wrong for it -- the sandbox container never reaches its Caido proxy, so the run dies before the gateway serves anything -- and this consumer re-applied the wrong attribution one step downstream, into the review findings and the failure census. The emitter now branches on the second token: a sandbox verdict gets a finding that names Strix's sandbox, says the verdict does not name the gateway, and tells the reader not to change gateway or provider configuration on its strength. A `STRIX_PROVIDER_UNAVAILABLE` line without the token keeps its existing text verbatim, so the gateway class has no regression surface. No test covered this finding text at all before (`gateway or its discovered provider pool` matched nothing under `tests/`); `tests/test_opencode_dispatch_strix_sandbox_finding.py` now runs the production emitter from the published run block and pins both directions plus the no-signal case. Refs #1953, #1935.
Expand Down
33 changes: 33 additions & 0 deletions docs/doctoring/unknown-merge-state-fail-closed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Unknown merge state does not authorize change-request automation

검토 기준일: **2026-09-07**

## Problem

GitHub can temporarily report an unresolved mergeability state after a push.
The REST fallback normalizes that condition to an empty value. Treating the
empty value as clean allows automatic change-request handling without positive
mergeability evidence.

## Decision

The shared gate accepts only `CLEAN` and `HAS_HOOKS`. Missing, empty, or
unknown values return no clean review body, so neither autofix nor RCA dispatch
is authorized. Known dirty states retain the same behavior.

## Verification contract

`test_change_request_gates_fail_closed_on_unknown_merge_state` covers empty,
unknown, and absent values across the normalized body, autofix, and RCA entry
points. Hosted exact-head checks remain mandatory.

## Status

**Proposed** in ContextualWisdomLab/.github#1492. Protected `main` remains the
release authority.

## Reference

GitHub. (n.d.). *REST API endpoints for pull requests*. GitHub Docs. Retrieved
September 7, 2026, from
https://docs.github.com/en/rest/pulls/pulls
6 changes: 6 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다.

### 2026-09-07 unknown merge-state fail-closed amendment

- **Gap:** an empty or absent REST fallback `mergeStateStatus` can pass the change-request gate as if mergeability were proven.
- **Action:** ContextualWisdomLab/.github#1492 requires `CLEAN` or `HAS_HOOKS` explicitly before autofix or RCA classification.
- **Status:** Proposed; exact-head hosted Checks, independent review, ordinary protected integration, and post-merge current-main verification remain required.

## 1. 근거와 범위

### 1.1 우선순위가 높은 근거
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/pr_review_fix_scheduler.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def latest_current_head_opencode_review(pr: dict[str, Any]) -> dict[str, Any] |
def _clean_change_request_body(pr: dict[str, Any]) -> str | None:
"""Return normalized exact-head OpenCode review text for a clean PR."""
merge_state = str(pr.get("mergeStateStatus") or "").upper()
if merge_state and merge_state not in {"CLEAN", "HAS_HOOKS"}:
if merge_state not in {"CLEAN", "HAS_HOOKS"}:
return None
review = latest_current_head_opencode_review(pr)
if review is None:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_pr_review_fix_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,37 @@ def test_change_request_requires_current_head_opencode_review():
assert not fix.change_request_is_autofixable(stale_review_pr)


def test_change_request_gates_fail_closed_on_unknown_merge_state():
"""Unknown or missing merge state cannot authorize automatic repair."""
head = "a" * 40
body = "Actionable source-backed finding with suggested diff."
current_review = {
"state": "CHANGES_REQUESTED",
"author": {"login": "opencode-agent"},
"commit": {"oid": head},
"body": body,
}

for merge_state in ("", "UNKNOWN"):
pull_request = make_pr(
headRefOid=head,
mergeStateStatus=merge_state,
reviews={"nodes": [current_review]},
)
assert fix._clean_change_request_body(pull_request) is None
assert not fix.change_request_is_autofixable(pull_request)
assert not fix.change_request_requires_rca(pull_request)

pull_request = make_pr(
headRefOid=head,
reviews={"nodes": [current_review]},
)
del pull_request["mergeStateStatus"]
assert fix._clean_change_request_body(pull_request) is None
assert not fix.change_request_is_autofixable(pull_request)
assert not fix.change_request_requires_rca(pull_request)


def test_process_queue_dispatches_same_repo_current_head(monkeypatch, capsys):
"""The queue path dispatches one same-repository autofix."""
pr = make_pr()
Expand Down
Loading