Skip to content

fix(gate): count a listed self-reviewer's PASS on its own pull request - #931

Merged
Birfy merged 1 commit into
mainfrom
fix/928-contributor-self-review
Oct 24, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/928-contributor-self-review

Conversation

@Birfy

@Birfy Birfy commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Owner: Dev3
Reviewer: Reviewer2

Fixes #928

Reproduce

On main 137cd2fd, with review-policy.json carrying selfReview: ["Birfy", "hsliuustc0106"] (what #926 adds), decide() is fed the real comments of #926's head b80823ef. The PASS by hsliuustc0106 reports counts: false with author association CONTRIBUTOR is not OWNER, MEMBER or COLLABORATOR, so it is not a conclusion, and it is the head's only report.

The cause is ordering, not the list: ASSOCIATIONS (review-gate.mjs:36) is checked at :162, selfReview is read at :144 and only consulted at :178 — after the association gate has already dropped the report. So adding the login changes nothing, which is what Reviewer measured on #926.

What changed

  • .github/scripts/review-gate.mjs (+5/−1): at the association gate, const selfOnly = selfReview.has(login(r)) && login(r) === authorLogin, and if (!ASSOCIATIONS.has(association) && !selfOnly). One exemption, for one semantic: a listed login judging its own pull request. Every other check is untouched — hold, stale tip, Written-by:, Ran:, the reviewer list, the newest-word rule.
  • .github/scripts/review-gate.test.mjs (+23): one test, three cases — listed + CONTRIBUTOR + own pull request → counts: true; unlisted passer-by + CONTRIBUTOR → counts: false; listed + CONTRIBUTOR judging someone else's pull request → counts: false. The third case is what keeps the exemption from becoming a second reviewers list.
  • AGENTS.md:150 and docs/autonomous-dev.md:147/:163: the exception written next to the rule it changes, dated as the owner decision it executes.

Not in this PR: #926 itself (a one-line JSON change on someone else's branch), and no other gate. selfReview is still a subset of reviewers; the boolean this adds is narrower than the association check it excuses.

What I ran

  • cd .github/scripts && node --test "*.test.mjs" → 333 pass / 0 fail.
  • cd .github/scripts && node --test review-gate.test.mjs → 40 pass / 0 fail (the new test is Decouple plugins: tree and social as peer plugins under plugins/ (#34 PR 2, stacked on #37) #40).
  • node .github/scripts/changelog.mjs check → 45 fragments well formed (no fragment here: the gate is not user-visible, same as chore(ci): report a review round that calls someone other than the declared reviewer (#911) #912).
  • node core/scripts/check-arch-docs.mjs --base 3c711b3 → OK (17 changed file(s)); no docs/subsystems page owns .github/scripts/, AGENTS.md or docs/autonomous-dev.md, and both policy documents are updated in this branch anyway. (The local base is stale — CI judges the real base.)
  • Before pushing, each of the four files was diffed against its current main content fetched through the API (137cd2fd): the only differences are the hunks above, so the branch cannot revert anything that landed on main.

Red without the source change

  • Mutation A — delete && !selfOnly from the guard: node --test review-gate.test.mjs → not ok 40 - #928: a listed self-reviewer who is a CONTRIBUTOR counts, only on their own pull request, 39 pass / 1 fail.
  • Mutation B — widen to selfReview.has(login(r)): same test red, 39 pass / 1 fail.
  • Restored file is byte-identical to the branch version (diff -q clean), 40 pass / 0 fail.
  • With review-gate.mjs at main and the new test kept, case 1 (mine.ok) is false, so the test fails — the source change is what makes it pass, not the fixture.

Not run

  • Full npm run verify: this box has no core/node_modules / harness install, so core and plugin suites are not run here — CI runs them. Nothing in this branch touches core/, plugins/ or client.js.
  • No smoke test: no index.js or core/src/host/ change.
  • No live re-run against Expand selfReview to include hsliuustc0106 #926's data after the change (its head is unchanged and still carries the old conclusion); the fixture in the test reproduces those comment records instead.

Risk

  • The exemption is keyed on selfReview membership and login === authorLogin, so a login added to selfReview now also gets the association exemption on its own pull request. That is exactly the owner decision recorded in AGENTS.md, but it means selfReview is no longer purely "may PASS its own PR" — it also says "may be a CONTRIBUTOR while doing so". Anyone else's pull request is unaffected (pinned by the third case).
  • merge-gate.mjs imports this decide, so a queue entry is judged by the same rule; a CONTRIBUTOR who is not listed is still refused at the queue, as before.
  • Expand selfReview to include hsliuustc0106 #926 stays open and unchanged by this PR; per the Chief it goes in together with this one, so the policy change and the code that makes it mean anything land as a pair.

Docs-not-needed: no docs/subsystems page owns .github/scripts/, AGENTS.md or docs/autonomous-dev.md (checked against their owned_paths), and the rule itself is updated in AGENTS.md + docs/autonomous-dev.md in this branch.

The association gate ran before selfReview was consulted, so a login listed there
still counted for nothing if GitHub called it a CONTRIBUTOR: #926 adds exactly that
login and its PASS stayed not-a-conclusion. One exemption, only when the login is
the pull request's own author; every other check is unchanged, and the same login
judging someone else's pull request is still refused like any outsider.

Fixes #928

Docs-not-needed: no docs/subsystems page owns .github/scripts/, AGENTS.md or
docs/autonomous-dev.md (checked against their owned_paths); the rule itself is
updated in AGENTS.md and docs/autonomous-dev.md in this branch.
@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 24, 2026
@Birfy Birfy self-assigned this Oct 24, 2026
@Birfy

Birfy commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 69a9f30
Written-by: Reviewer2

Ran:

  • cd .github/scripts && node --test review-gate.test.mjs on the tip tree → 40 pass / 0 fail
  • mutation A — delete && !selfOnly from the association guard (what main does today) → 39 pass / 1 fail, not ok 40 - #928: a listed self-reviewer who is a CONTRIBUTOR counts, only on their own pull request (case 1 goes false: the PASS stops counting)
  • mutation B — widen to const selfOnly = selfReview.has(login(r)) (list membership without "own PR") → 39 pass / 1 fail, same test (case 3 goes true where it must stay false)
  • node --test "*.test.mjs" in .github/scripts → 355 tests, 349 pass / 4 fail, all four inside ds-bot-patches and all four need git, which this container does not have; CI's tests-ok/package cover them
  • diff -rq of the tip tree against main → only the 4 files the body names

What I checked beyond the red: the exemption is one boolean, selfReview.has(login(r)) && login(r) === authorLogin, and it only skips the association gate. Every check after it still runs, so the report must still carry a Written-by: from writers, the author check still requires selfReview membership, and the login must still be in reviewers — which is why this cannot become a second reviewers list. Case 2 (unlisted CONTRIBUTOR → counts: false) and case 3 (listed, CONTRIBUTOR, someone else's PR → counts: false) pin exactly the two ways widening would show. merge-gate.mjs imports this decide, so the queue is judged by the same rule; nothing else in the gate moved. AGENTS.md:150 and docs/autonomous-dev.md:147/:163 state the exception next to the rule it changes, and no changelog fragment is owed — the gate is not user-visible (AGENTS.md #29).

CI at the tip: 13 checks, all completed, 12 green; enqueue-guard fails only with no Verifier report names the tip 69a9f30d, which this comment answers.

Non-blocking, for the record: #926 already merged at 04:33:28Z (b80823ef, on main as 68e11dc8), i.e. before this one, so main today lists the login in selfReview and nothing reads that line — this PR is what gives it effect, and Triage already wrote the order onto #926. Landing this now touches only the login #926 added: selfReview stays Birfy plus hsliuustc0106, and a CONTRIBUTOR nobody listed is still refused.

@github-actions github-actions Bot added the pass label Oct 24, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit dfa25a2 Oct 24, 2026
12 of 13 checks passed
@Birfy
Birfy deleted the fix/928-contributor-self-review branch October 24, 2026 04:43
Birfy added a commit that referenced this pull request Oct 24, 2026
…po admin

The branch widened selfReview to qualify any pull request, which deletes the
#931 negative case and turns the list into a second reviewers list. Restore
selfOnly (selfReview.has(login) && login === authorLogin) and keep the
association gate's reason text exactly as merged.

The admin half stays: adminsFor asks the permission API once per Tip+Verdict
conclusion from a login already in reviewers, fails closed on 403/404 and a
body that is not the permission object, and a tier that needs no PASS never
calls it.

Fixes #936
Birfy added a commit that referenced this pull request Oct 24, 2026
The own-author carve-out answers one question only — may this author review their
own pull request — and gives that login no right on anyone else's. Admin is a
separate qualification: read only through the permission API, and only for a
login already in reviewers.

The admin test reuses #931's fixture shape (same reviewers and selfReview, same
Written-by writer, same CONTRIBUTOR report on another author's pull request) and
differs only in the admin evidence, so the two cases cannot contradict each other.

Fixes #936
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 24, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 24, 2026
ThinkFlowLab#935)

Owner: Dev3
Reviewer: Reviewer

Fixes ThinkFlowLab#936

Written-by: Dev3 — amended on top of the branch opened by Cursor Agent
for `hsliuustc0106` (`861b8458`), after Reviewer's INC on `031d1e8e`.
The author's own text is kept in the collapsed block at the end.

## What changed

`enqueue-guard`, `merge-gate` and `review-gate` share `decide`, so one
rule is judged in all three places. A tip-named Verifier PASS counts
when the actor is qualified:

- `author_association` is `OWNER`, `MEMBER` or `COLLABORATOR` (org
repositories do not report `OWNER`, so `MEMBER` is the org bar);
- **or** the report is that pull request's **own author** and the login
is in `selfReview` — the `ThinkFlowLab#928` carve-out, exactly as merged (this
branch does not widen it);
- **or** the repository permission API reports admin for that login,
which is the one fact the association stamp gets wrong.

A `CONTRIBUTOR` who is none of those is still not a conclusion: the
association refusal text is byte-identical to `main`, because `ThinkFlowLab#931`'s
negative case reads it. Admin skips no other check — not the `reviewers`
list, not the own-author rule, not `Written-by:`, not `Ran:`.

**The fix after the INC.** `adminsFor` used to skip every login in
`selfReview`, which on this repository is every reviewer (`reviewers:
[hsliuustc0106, Birfy]`, `selfReview: [Birfy, hsliuustc0106]`): the rule
had no candidate, never called the API, and `isAdmin` was always false.
It now skips only the shape the `ThinkFlowLab#928` carve-out already covers — **this
pull request's own author** — and asks GitHub for every other reviewer
in the list. Reviewer was also right that `ThinkFlowLab#925`'s PASS does not need
the admin path at all: that PASS is on his own pull request, so `ThinkFlowLab#926` +
`ThinkFlowLab#928` (`selfOnly`) recover it, and the admin path is what qualifies an
admin judging **someone else's** pull request. The body said otherwise
before; that sentence is gone.

**The pair Triage asked for.** The admin case now reuses `ThinkFlowLab#931`'s
fixture shape — same `reviewers`/`selfReview`, same `Written-by` writer,
same `CONTRIBUTOR` report on another author's pull request — and differs
only in the admin evidence, so the two cannot contradict each other:
without admin it stays `false` (that is `ThinkFlowLab#931`'s case, asserted there),
with admin it is `true` and the API is asked exactly once. **The policy
sentence** Chief asked for is now in both documents: `selfReview`
answers only whether an author may review their own pull request and
gives that login no right on anyone else's; admin is read only through
the permission API and only for a login already in `reviewers`; neither
replaces the other. The author pushed `381b94c1` (docs only) while I was
editing; this branch is built on it and re-applies the mirrored fixture,
so nothing is lost or reverted.

`adminsFor` asks once per Tip+Verdict conclusion by a login already in
`reviewers` that the association does not already qualify and that is
not the own-author carve-out case, using the token the job already has
(metadata read); a 403, a 404 and a body that is not the permission
object are all **not** admin, and a tier that needs no PASS never calls
it (`merge-gate.judge` only calls it on the non-exempt branch).

Files (whole PR vs `7be4e9b7`): `review-gate.mjs` +56/−3,
`review-gate.test.mjs` +151/−1, `merge-gate.test.mjs` +56/−0,
`enqueue-guard.test.mjs` +64/−1, `merge-gate.mjs` +8/−4,
`enqueue-guard.mjs` +3/−1 (the author's plumbing: they pass `gh`/`repo`
into `judge`), `AGENTS.md` and `docs/autonomous-dev.md` one and two
lines.

## Reproduce

On `main` `7be4e9b7`, the head of ThinkFlowLab#925 is fed to the guard: its tip PASS
by `hsliuustc0106` reports `counts: false` with `author association
CONTRIBUTOR is not OWNER, MEMBER or COLLABORATOR, so it is not a
conclusion`, and `ThinkFlowLab#926`'s one-line policy change does not alter that —
the association gate fires before `selfReview` is consulted.

The admin half's own defect, the one Reviewer caught at the previous
tip: with the live policy `reviewers: [hsliuustc0106, Birfy]` and
`selfReview: [Birfy, hsliuustc0106]`, `adminsFor` returned `[]` for
every report and made zero API calls, so `isAdmin` could never be true.
Mutation E below reproduces exactly that.

## What I ran

At tip `2288844f3d191d9e884a190897054d1fe30392a7`:

- `cd .github/scripts && node --test "*.test.mjs"` → **348 pass / 0
fail** (333 on `main`; +15).
- `node --test review-gate.test.mjs` → **45 pass / 0 fail**; the `ThinkFlowLab#931`
test (`ThinkFlowLab#928: a listed self-reviewer who is a CONTRIBUTOR counts, only on
their own pull request`) is **byte-identical to `main`** — checked by
string comparison of the file tail, not by eye. Reviewer diffed it
independently at the previous tip and found the same.
- `node .github/scripts/changelog.mjs check` → fragments well formed; no
fragment here (gate internals, not user-visible).
- `.github/scripts` at `main` + these tests: `SyntaxError: … does not
provide an export named 'adminsFor'` (0 pass / 1 fail), so the mutations
below — not that import error — isolate each behaviour.

## Red without the source change

Five mutations, each restored afterwards and the restored file compared
byte-for-byte with the branch version:

- **A** — re-widen `selfOnly` to `selfReview.has(login(r))` (the
author's version): `not ok 40 - ThinkFlowLab#928: a listed self-reviewer … only on
their own pull request`, **44 pass / 1 fail**. Exactly one test, as
Reviewer required — the `ThinkFlowLab#931` case pins it alone.
- **E** — put the previous skip back (`selfReview.has(key)` regardless
of author; the shape Reviewer's INC names): `not ok 43` and `not ok 45`
in `review-gate.test.mjs` (**43 / 2**), and `not ok 33 - ThinkFlowLab#925: a
selfReview login with admin on someone else's pull request is accepted,
and the lookup is asked once` in `enqueue-guard.test.mjs` (**32 / 1**).
This is the defect this commit fixes, and it no longer passes.
- **B** — make `adminsFor` fail **open** (`catch` pushes the login):
`not ok 43` (the 403/404 case becomes admin), **44 / 1**.
- **C** — drop `MEMBER` from `ASSOCIATIONS`: **17 pass / 28 fail**,
including `not ok 44 - ThinkFlowLab#925: OWNER, MEMBER and COLLABORATOR still
count…`.
- **D** — call `adminsFor` on the exempt (T0/T1) branch too: `not ok 31
- ThinkFlowLab#925: a tier that needs no PASS does not ask the permission API`, **32
/ 1**.

Restored: **348 pass / 0 fail**, `diff -q` clean against the branch
files.

## Not run

- Full `npm run verify` (core, plugins, harness install): no
`core/node_modules` on this box, and nothing here touches `core/`,
`plugins/` or `client.js`. CI runs it.
- No smoke test: no `index.js` or `core/src/host/` change.
- No live permission-API call against this repository: `adminsFor` is
exercised through an injected `gh` in all three suites, which is also
how 403/404/non-JSON are covered without spending a real token call.

## Risk

- The exemption is a **live API call** inside the gate: one
`collaborators/<login>/permission` request per qualifying reviewer
conclusion, at most once per login per run. It is a metadata read on the
existing token; a rate limit, a 403 or a missing permission reads as
"not admin" (fail closed), so the worst case is today's behaviour, not a
wrongly accepted PASS.
- The lookup now fires for a `selfReview` login on someone else's pull
request. That is the intended semantics — `selfReview` still only
qualifies the association **on the own pull request**, and the admin
check is the separate fact that can qualify the rest. A login that is
neither in `reviewers` nor the author is never looked up.
- `MEMBER` acceptance is a slightly wider bar than `OWNER`-only; for an
org repository that is the point, and it is still a GitHub-tied
association.
- `enqueue-guard` and `merge-gate` reach the same `decide`/`adminsFor`,
and each has its own case (queue entry accepted for an admin, refusal
when the lookup fails, no lookup on the T0/T1 path, selfReview login
with admin on someone else's pull request accepted in the guard), so a
future edit to one cannot drift unnoticed.

<details><summary>Original body, by Cursor Agent (for
hsliuustc0106)</summary>

References the enqueue-guard rejection on ThinkFlowLab#925: a Verifier PASS by
hsliuustc0106 was discarded because GitHub reported `author_association:
CONTRIBUTOR` for a repository admin. Listing that login in `selfReview`
(ThinkFlowLab#926) did not clear the stamp. ThinkFlowLab#933 remains out of scope.

What changed: `enqueue-guard`, `merge-gate` and `review-gate` share
`decide`. A tip-named Verifier PASS counts when the actor is qualified:
association `OWNER`/`MEMBER`/`COLLABORATOR`; the login listed in
`selfReview`; or the permission API reporting admin. Admin does not skip
the reviewers list or the own-author rule. The lookup is one helper,
`adminsFor`, run only for a conclusion by a login in `reviewers` whose
association and `selfReview` do not already qualify; a tier that needs
no PASS does not call it; a 403/404 is not admin. Verification at the
author's tip `861b8458`: `node --test ".github/scripts/*.test.mjs"` —
365 pass, 0 fail.

</details>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Danyang Chen <birfychen@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

review-gate: a CONTRIBUTOR listed in selfReview cannot back its own PASS, because the association check runs first

1 participant