Background
OpenAI PR Review decides pr-linkage from GraphQL closingIssuesReferences, the list behind GitHub's Development sidebar. Since about 2026-09-30 07:00 UTC GitHub has stopped turning closing keywords into those references: h2vivi/firmwares PRs #1477 and #1479 and GizClaw/gizos PR #617 all carry Closes #N in the body, the Issue timeline shows the cross-reference with willCloseTarget: false, and closingIssuesReferences stays empty, while PRs opened before 07:00 UTC with the same wording are linked. Re-saving the body or changing the keyword does not help, and there is no public API to create the link. Every such PR is blocked with Pull request must natively close at least one same-repository Issue although its body states the Issue it closes.
The reviewer should not depend on when or whether GitHub indexes the keyword. The PR body is already part of the readiness snapshot and states the same fact.
Goal
Take the Issues a pull request closes from the closing keywords in its body instead of from GitHub's Development links. A PR whose body says Closes #123 for a readable same-repository Issue satisfies pr-linkage, and the named Issues feed the Issue review, the sub-issue coverage rule, and plan conformance exactly as the native set did.
Non-goals
- Do not change how Issue events find the PRs to refresh:
openai-pr-review-dispatch.yml keeps using closedByPullRequestsReferences; an unlinked PR is refreshed by its next event or an @codex review comment.
- Do not change Issue-format rules, native parent / sub-issue / dependency checks, review-thread handling, limits, model, or effort.
- Do not add a caller input or secret; do not consult Development links as a second source.
Code Changes Tree
.github/
├── scripts/
│ ├── pr-readiness/
│ │ ├── closing-issues.mjs # parse closing keywords from the body, read the referenced Issues in one GraphQL request, normalize them
│ │ ├── verify.mjs # rebuild the snapshot from body-declared closing Issues instead of closingIssuesReferences
│ │ ├── common.mjs # reword the pr-linkage blockers for body-declared linkage
│ │ └── test.mjs # cover the parser, the collector, the bound, API failures, and stale linkage in the verifier
│ └── pr-review/
│ ├── run.mjs # reword the PR-stage prompt from native to body-declared linkage
│ └── test.mjs # run the discussion step with a closing body, a non-Issue reference, and an API failure
└── workflows/
├── codex-openai-review.yml # collect linked Issues through closing-issues.mjs, check out its review-request dependency, reword defaults
└── openai-pr-review-dispatch.yml # reword the default readiness instructions
README.md # document the keyword grammar, ignored contexts, unreadable references, and the 100-reference bound
Design
Closing references
closingIssueReferences(body, repository) returns the distinct references in body order. A reference is one of GitHub's closing keywords (close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved, any case, optional colon), spaces or tabs, then #N, owner/repo#N, or https://github.com/owner/repo/issues/N. The keyword must not continue a word (hotfixes #1 declares nothing) and the number must not continue into a word. Text inside fenced code, inline code, block quotes (through the existing stripQuotedText), indented code blocks, and HTML comments declares nothing, so a PR-template placeholder or a pasted log cannot satisfy the rule. #N resolves against the PR's repository.
Reading the Issues
collectClosingIssues reads at most 100 references with one aliased GraphQL request (iK: repository(owner, name) { issue(number) { … } }) carrying the same fields as before: title, body, state, Issue Type, parent, sub-issues, blockedBy, blocking, each with the 100-node bound. A reference that is not a readable Issue — a pull request number, a missing number, an inaccessible repository, reported as NOT_FOUND — is ignored, as GitHub ignores it. Any other GraphQL error, or a response without data, fails context collection closed. Issues are de-duplicated by their canonical nameWithOwner#number. linked_issue_count is the number of Issues read, or the number of references when the body names more than 100, so the existing too-many-closing-issues blocker reports the truncation.
Call sites
The reusable workflow's context step and verify.mjs both call the module, which removes their duplicated Issue normalization. The context step imports it from the reviewer checkout (REVIEWER_SOURCE_DIR) and sends the request with github.request('POST /graphql') to receive partial data with NOT_FOUND errors. Both reviewer checkouts add .github/scripts/review-request to their sparse set because the module imports stripQuotedText. analyzePullRequest and the snapshot shape are unchanged; the body was already hashed, so a changed closing reference still invalidates the snapshot and the publication-time verifier still rejects it.
Test And Acceptance Criteria
Acceptance Criteria
- A PR whose body contains
Closes #N for an open same-repository Issue passes pr-linkage without any Development link, and that Issue is reviewed.
- A body with only a plain mention, a quoted or commented keyword, or a reference to a pull request or missing number still reports
missing-closing-issue.
- More than 100 references report
too-many-closing-issues; a GraphQL failure other than NOT_FOUND fails closed.
- Removing or changing the closing reference, or editing the named Issue, while a review runs is rejected by the verifier.
- Existing caller YAML needs no new input, secret, or permission.
Validation
node .github/scripts/pr-review/test.mjs
node .github/scripts/review-request/test.mjs
node .github/scripts/pr-readiness/test.mjs
node .github/scripts/issue-review/test.mjs
actionlint -ignore 'property "workflow_(repository|sha)" is not defined' .github/workflows/*.yml
git diff --check
- Live read: run the collector against h2vivi/firmwares PR #1479's body and confirm it returns Issue #1478 while a pull-request number, a missing number, and an inaccessible repository are ignored.
Background
OpenAI PR Reviewdecidespr-linkagefrom GraphQLclosingIssuesReferences, the list behind GitHub's Development sidebar. Since about 2026-09-30 07:00 UTC GitHub has stopped turning closing keywords into those references: h2vivi/firmwares PRs #1477 and #1479 and GizClaw/gizos PR #617 all carryCloses #Nin the body, the Issue timeline shows the cross-reference withwillCloseTarget: false, andclosingIssuesReferencesstays empty, while PRs opened before 07:00 UTC with the same wording are linked. Re-saving the body or changing the keyword does not help, and there is no public API to create the link. Every such PR is blocked withPull request must natively close at least one same-repository Issuealthough its body states the Issue it closes.The reviewer should not depend on when or whether GitHub indexes the keyword. The PR body is already part of the readiness snapshot and states the same fact.
Goal
Take the Issues a pull request closes from the closing keywords in its body instead of from GitHub's Development links. A PR whose body says
Closes #123for a readable same-repository Issue satisfiespr-linkage, and the named Issues feed the Issue review, the sub-issue coverage rule, and plan conformance exactly as the native set did.Non-goals
openai-pr-review-dispatch.ymlkeeps usingclosedByPullRequestsReferences; an unlinked PR is refreshed by its next event or an@codex reviewcomment.Code Changes Tree
Design
Closing references
closingIssueReferences(body, repository)returns the distinct references in body order. A reference is one of GitHub's closing keywords (close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved, any case, optional colon), spaces or tabs, then#N,owner/repo#N, orhttps://github.com/owner/repo/issues/N. The keyword must not continue a word (hotfixes #1declares nothing) and the number must not continue into a word. Text inside fenced code, inline code, block quotes (through the existingstripQuotedText), indented code blocks, and HTML comments declares nothing, so a PR-template placeholder or a pasted log cannot satisfy the rule.#Nresolves against the PR's repository.Reading the Issues
collectClosingIssuesreads at most 100 references with one aliased GraphQL request (iK: repository(owner, name) { issue(number) { … } }) carrying the same fields as before: title, body, state, Issue Type, parent, sub-issues,blockedBy,blocking, each with the 100-node bound. A reference that is not a readable Issue — a pull request number, a missing number, an inaccessible repository, reported asNOT_FOUND— is ignored, as GitHub ignores it. Any other GraphQL error, or a response without data, fails context collection closed. Issues are de-duplicated by their canonicalnameWithOwner#number.linked_issue_countis the number of Issues read, or the number of references when the body names more than 100, so the existingtoo-many-closing-issuesblocker reports the truncation.Call sites
The reusable workflow's context step and
verify.mjsboth call the module, which removes their duplicated Issue normalization. The context step imports it from the reviewer checkout (REVIEWER_SOURCE_DIR) and sends the request withgithub.request('POST /graphql')to receive partial data withNOT_FOUNDerrors. Both reviewer checkouts add.github/scripts/review-requestto their sparse set because the module importsstripQuotedText.analyzePullRequestand the snapshot shape are unchanged; the body was already hashed, so a changed closing reference still invalidates the snapshot and the publication-time verifier still rejects it.Test And Acceptance Criteria
Acceptance Criteria
Closes #Nfor an open same-repository Issue passespr-linkagewithout any Development link, and that Issue is reviewed.missing-closing-issue.too-many-closing-issues; a GraphQL failure other thanNOT_FOUNDfails closed.Validation
node .github/scripts/pr-review/test.mjsnode .github/scripts/review-request/test.mjsnode .github/scripts/pr-readiness/test.mjsnode .github/scripts/issue-review/test.mjsactionlint -ignore 'property "workflow_(repository|sha)" is not defined' .github/workflows/*.ymlgit diff --check