Skip to content
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.

### Maturin download failures close every transport response

- Refactor the bounded Maturin asset downloader so successful and rejected
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 @@ -61,6 +61,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.

### 2026-09-30 central coverage owner stack delta

| Gap ID | 상태 | exact-head evidence | causal owner / next gate |
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/pr_review_fix_scheduler.py
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