You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lane F, part of step F6 (design: "Checking whether the issue is already fixed"). The pre-merge already-fixed check now follows the rules decided on 2026-09-30 for the pre-PR check (#84), by running that check itself.
What changes
github/merge.ts: GhMergeGateway no longer reads the REST issue timeline. That check counted every same-repository PR that mentioned the issue and returned unknown for any cross-repository reference. It now calls an AlreadyFixedGateway: by default GhAlreadyFixedGateway for the same repository, using the merge gateway's own runner (since Give the merge and issue gh runners the allowlisted environment #86: the allowlisted environment, with 0.25 s and 0.15 s stop waits). The rules are the pre-PR check's:
A cross-reference counts only when it would close the issue (willCloseTarget). A PR that only mentions the issue does not count, in this repository or another. A closing PR in another repository is a match, no longer unknown.
When the PR's base is not the default branch, a PR in this repository into that same base that references the issue also counts.
Manual links (both sides, connect and disconnect replayed in order), the closed state, and new base-branch commits count. The old merge check read none of these.
The merge-specific exclusion is kept. This PR is passed as the task's own PR, matched by repository and number: excluded while open, a match once merged (AGENTS.md: exclude the subject only in the states the exclusion is for).
Inputs. The base branch is the PR's baseRefName. The commit scan starts at the PR's baseRefOid: merging requires that commit to equal the reviewed snapshot's base, which the pre-PR check also scanned from. No own commits are passed, because the task's commits reach the base branch only through this merge.
Timing. The check starts as soon as the PR is read, alongside the rule reads, rather than after them. A fresh inspection on cli/cli fell from about 3.4 s to 2.1 s. The check stops MERGE_CHECK_SETTLE_MS (0.65 s: both of the merge runner's grace periods plus 250 ms, defined in github/merge.ts) before the inspection deadline, so its processes settle inside the 6-second and 12-second inspection budgets. A caller's abort during the check settles within the merge runner's 0.4 s, as Give the merge and issue gh runners the allowlisted environment #86 requires for every gh call of the gateway. A check stopped that way is unknown. If the rule reads fail, the check is stopped and awaited before the inspection rejects. A caller's own abort still propagates as the caller's reason.
Fails closed. An outcome other than clear or found, or any error, is unknown. An injected check must name the merge repository; one that names another repository, or none, is refused at construction. The merge repository is now validated with the shared REPOSITORY pattern.
Scope. The only GraphQL query is the pre-PR check's. It reads nothing on ProjectV2 except __typename, so the repo scope is enough.
runner/merge.ts: the blocker now says why it blocks. A match names what was found: at most five closes, PRs and commits, by repository, number, state and short SHA, never a commit message. An unknown result gives the check's reason, for example a base more than 250 commits behind. Once this PR has merged, only the pr-state blocker is shown: the check would otherwise count the merged PR itself as the fix.
Docs: docs/implementation/guarded-merge.md describes the new check, its inputs, its deadline and its known gaps. The "pre-merge check" item is removed from the "does not do" list in pull-request-opening.md.
Not in this PR
Earlier PRs of the task. The merge gateway knows only the configured PR number, so another still-open PR of the same task still blocks merging. The old check did the same. The pre-PR check excludes these PRs using the Store's PR records.
Base branch names. A name outside the BRANCH pattern in github/validate.ts makes the check unknown, which blocks merging; it fails closed.
Rebase (Add pre-merge rebase and head-bound command checks #22). Once a rebase moves the PR's base, the scan must still cover the commits the rebase brought under the PR. This is recorded in the deferred section of guarded-merge.md.
Inspection timeout. The merge-queue reads (queueWatermark, inspectQueue) still check the caller's signal before the deadline's, so a deadline that fires first can be reported as the caller's abort. This code is unchanged here.
Evidence
Final head d7bc309, after merging main at aafd153 (#86):
Full local suite: 1351 passed, 27 failed, 2 skipped (1380), on a machine with a load average near 200 from other sessions. None of the failures is in a changed module. 18 were timeouts and the rest Docker contention (a network pool overlapping another container's). Every failing non-Docker file then passed in full when re-run: test/history.test.ts, test/review.test.ts, test/runner-planning-feedback.test.ts, test/runner-shutdown.test.ts and test/store.test.ts (115 of 115). Of the 9 real-Docker failures, two are the supervisor tests that fail on main without this branch, and four passed when re-run. The other three failed again under the same load; on main (f4b8336), "refuses to seed a repository with a chain of in-checkout links that ends outside it, and leaves no storage behind" fails the same way, and the other two ("refuses to seed a repository with a chain that leaves through a target this host lacks…", "returns the adapter handle at once and cancels it during network creation") passed there, so they depend on load. CI (test) passed on this head.
Old code against the new tests. With the old github/merge.ts, 8 of the new tests fail.
Mutation checks. Each mutation below was applied alone, and at least one test failed each time:
the check started after the rule reads instead of alongside them;
the check not stopped, or not awaited, when the rule reads fail;
an injected check without repository accepted;
the already-fixed blocker shown for a merged PR;
the found detail, the unknown reason, the five-match cap, the time-out reason, or the coordinator's use of the detail dropped;
a closing commit's SHA left at full length;
a late check answer used after the early stop, or a state returned after the caller aborted;
a repository name of 40 hex characters shortened, or the check started with no settle time left (both found by Copilot and failing before the fix);
a clear answer without a valid base head accepted (found by Copilot, failing before the fix);
the caller's abort reason replaced by the inspection timeout (found by Copilot, failing before the fix).
Live, read-only.GhMergeGateway.inspect ran against an open cli/cli PR and five cli/cli issues (two closed, three open). There was no scope error; the closed issues were found and the open ones clear. The check read the PR's base branch trunk and scanned from its baseRefOid. Nothing was written to GitHub.
10. Medium: the check's SIGTERM-to-SIGKILL and pipe grace could push an inspection about 1.5 s past its deadline. Low: only this PR is excluded, not the task's earlier PRs; the default runner wiring was untested; new blockers for unusual branch names and after this PR merges; ProjectV2 runtime scope not proven live; same-tick deadline resolution; no test or detail for the blocker text; an injected check without repository skips the repository guard; one doc sentence's reasoning; deleted REST tests.
bd070d4 (deadline, runner wiring test, docs)
2
Local /code-review, high effort
bd070d4
5. The check started only after four sequential reads, leaving it about 2 s of a fresh 6 s inspection (measured live); the blocker text did not cover a reference into a non-default base; an injected check without repository skipped the repository guard; the settle time was computed in merge.ts from the check's constants; a merged PR showed an already-fixed blocker.
76c271b
3
Local /code-review, high effort
76c271b
2: the check's reason and matches were dropped before the blocker, so a PR whose base is more than 250 commits behind only said "could not be completed"; the repository pattern duplicated REPOSITORY.
93e77b7
4
Local /code-review, high effort
93e77b7
1: the merge-read fake was copied into five tests.
d391305
5
Local /code-review, high effort
d391305
None.
—
6
Copilot
d391305
1: a close by a commit reached the blocker with the full 40-character SHA, against the documented short-SHA detail.
164f690
7
Copilot (summary only, no inline thread)
164f690
1: an answer that arrived after the check's early stop was still used, and rule reads that answered after an abort still produced a state (merge.ts lines 140 and 162). Reproduced with a fake-timer test.
2eae9d2
8
Copilot (summary only, no inline threads)
2eae9d2
2: the closer SHA shortening also cut a repository name of 40 hex characters; a PR read that ended past the stop point still started the check, whose processes could settle after the deadline. Both reproduced with tests.
8742c6d
9
Local /code-review, high effort
8742c6d
None.
—
10
Copilot
8742c6d
1: a clear or found answer without a valid baseHead was accepted, though clear removes the blocker.
Conflict in github/merge.ts. #86 moved the merge's gh calls to the allowlisted runner with 0.25 s and 0.15 s stop waits, and documents that an aborted merge click settles by 14.4 s. The check's own runner waits 1.5 s, so it now uses the merge runner, and the early stop is built from the merge's grace periods. A test fails if the check gets another runner.
fb80731
12
Copilot
fb80731
1: when the caller aborted and the inspection deadline also passed before a stopped gh call settled, inspect() reported the timeout instead of the caller's reason (pre-existing code, reachable now that calls wait for gh to stop).
d7bc309
13
Copilot
d7bc309
None ("Approval recommended").
—
Review-lesson audit
Finding
Classification
Grace periods could overrun the inspection deadline
Covered: AGENTS.md, Guarded external actions, "Count the subprocess shutdown grace periods … inside that overall deadline." Fixed with an early stop (now MERGE_CHECK_SETTLE_MS) and a fake-timer test.
Default runner wiring untested
Covered: AGENTS.md, Review readiness, "A test's setup must leave the state the production path would." Fixed with a test that the default check has its own runner.
Only this PR is excluded
One-off: the merge gateway has no Store access. The old check behaved the same way. Recorded under "Not in this PR".
Unusual base branch names and post-merge blocker
One-off: both fail closed. The branch-name case is documented. The post-merge blocker was removed in round 2.
ProjectV2 runtime scope
One-off: the query matches #84's, which was verified with a default repo-scope token. A refusal would make the check unknown, so it fails closed.
Same-tick deadline resolution
Declined: this existed before the change for every read. status() re-checks the caller signal afterwards.
Blocker text: no test, no detail (rounds 1–3)
One-off: fixed in rounds 2 and 3. The blocker names the matches or the reason, and tests assert the text.
Injected check without repository (rounds 1 and 2)
Declined in round 1 (it matched the publisher's guard), then fixed in round 2: the merge now requires the name.
Doc reasoning for the scan start
One-off: fixed in guarded-merge.md.
Deleted REST tests
One-off: the REST timeline is gone. The equivalent malformed-input cases are covered in test/already-fixed.test.ts, and fail-closed cases through the merge are in test/merge.test.ts.
The check ran after the sequential rule reads (round 2)
Covered: AGENTS.md, Guarded external actions, "Budget a multi-stage validation across all sequential stages." Fixed by running the check alongside the rule reads.
Settle time computed outside the check (round 2)
One-off: the settle time moved next to the grace periods it is built from. After #86 those are the merge runner's, so it is MERGE_CHECK_SETTLE_MS in github/merge.ts.
Repository pattern duplicated (round 3)
One-off: github/validate.ts exists for this; the merge now uses REPOSITORY.
Test fake copied into five tests (round 4)
One-off: shared as mergeReads.
Full SHA in a commit close (round 6)
One-off: the formatter shortened commit matches but not the close description; it now shortens both.
Late answers after an abort (round 7)
Covered: AGENTS.md, Async jobs and polling, "Never apply a background response without proving it is still current." Reproduced and fixed; answered in a PR comment.
SHA shortening too broad (round 8)
One-off: a fix from round 6 matched more than the text it was for; it now matches only the commit <sha> form.
Check started with no settle time left (round 8)
Covered: AGENTS.md, Guarded external actions, "Count the subprocess shutdown grace periods … inside that overall deadline." The round-1 fix bounded the stop, not the start.
Clear answer without its base head (round 10)
Covered: AGENTS.md, Guarded external actions, "For safety-critical API responses, require and validate every requested field before any early return, including terminal-success paths."
Caller reason replaced by the timeout (round 12)
Covered: AGENTS.md, Async jobs and polling, "Preserve the original timeout, cancellation, and shutdown reason through every layer."
…edGateway
GhMergeGateway's own REST-timeline check counted every same-repository
PR that mentioned the issue and returned unknown for any cross-repository
reference. The merge now runs the pre-PR check with the same rules
(decided 2026-09-30): a cross-reference counts only when it would close
the issue; with a non-default base, a PR here into that base that
references the issue also counts; manual links, the closed state and
new base-branch commits count. This PR stays the task's own PR,
excluded only while open. The scan starts at the PR's base commit, and
the check stops early enough for its gh processes to settle before the
inspection deadline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le reads
- Start the already-fixed check as soon as the PR is read, in parallel with
the rule reads; stop and await it if those reads fail. A fresh inspection
fell from about 3.4 s to 2.1 s on cli/cli, leaving the check its time.
- Move CHECK_SETTLE_MS next to the grace periods it is built from.
- Require an injected check to name the merge repository.
- Show no already-fixed blocker once this PR has merged.
- Blocker text: "refers to it" also covers a reference into a non-default base.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Carry the check's matches (closes, PRs, short commit SHAs; at most five,
never a commit message) or its unknown reason into the merge blocker. A
long-lived PR whose base is more than 250 commits behind now says so.
- Validate the merge repository with the shared REPOSITORY pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mit's SHA
A close by a commit reached the blocker as "commit <40 hex>", against the
documented short-SHA detail. describeMatches now shortens a full SHA in the
close description to 12 characters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recheck abort signals before accepting delayed check responses
github/merge.ts:140
The early-stop signal is only consulted in the rejection path. If a check resolves after observing or ignoring that abort, its clear/found response is still accepted, contradicting the requirement that a check stopped at CHECK_SETTLE_MS becomes unknown. Re-check both cancellation sources immediately after the await before applying the response.
This issue also appears on line 162 of the same file.
…an abort
Copilot (summary only) noted that a check answer arriving after the early
stop was still used, and that rule reads answering after an abort still
produced a state. Reproduced both with fake-timer tests. A stopped check is
now unknown whatever it answers, and the inspection rejects with the abort's
reason once its reads end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot review on 164f690 (summary only, no inline thread): "Recheck abort signals before accepting delayed check responses" (github/merge.ts lines 140 and 162).
Reproduced and fixed in 2eae9d2. The new fake-timer test "does not accept a check answer or rule reads that arrive after an abort" failed before the fix. It covers:
a check that ignores its early stop and answers clear: the result is now unknown, "The check did not finish in time.";
a caller abort followed by a late clear: the inspection now rejects with the caller's reason;
rule reads that answer after the caller aborted: the inspection now rejects instead of returning a state with rulesKnown: false.
The caller's abort is checked once, after both the check and the rule reads have ended (signal?.throwIfAborted() in #inspectNow). A second check inside #alreadyFixed was tried and removed, because no test could fail without it: the inspection-level check already covers that case.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
Late-started checks can exceed the inspection deadline, and valid hexadecimal repository names can be misreported.
Review effort: Balanced Findings: None
Previously missed (2)
In code that hasn't changed since last review
Only shorten SHAs in commit closer descriptions
github/merge.ts:69
This regex shortens any standalone 40-hex token in the whole close description, including a valid owner or repository name (for example owner/aaaaaaaa…#9), so the blocker can report the wrong repository. Limit shortening to the commit <sha> form produced for commit closers.
Avoid starting checks when no settlement window remains
github/merge.ts:136
If the initial pr view consumes the budget past deadlineAt - CHECK_SETTLE_MS, this clamps the delay to zero but still starts the already-fixed check before the timer callback runs. Its subprocesses can then require the full kill/pipe grace period and settle after the inspection deadline, contradicting the bounded inspection contract. Return unknown without starting the check when no settlement window remains; add a regression where the PR read finishes after that cutoff.
…SHA shortening
Copilot (summary only) found two cases, both reproduced with tests:
- A PR read that ended past the stop point still started the check, whose
processes could then settle after the inspection deadline. With no settle
time left, the check is now not started and is unknown.
- The closer SHA shortening matched any 40-hex token, so a repository name
of 40 hex characters was cut. Only the "commit <sha>" form is shortened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot review on 2eae9d2 (summary only, no inline threads). Both findings were reproduced and fixed in 8742c6d:
"Only shorten SHAs in commit closer descriptions" (github/merge.ts:69). The new test "shortens only a closing commit SHA, not a repository name that looks like one" (a closer in owner/aaaa… with 40 as) failed before the fix. Only the commit <sha> form at the start of the close description is shortened now.
"Avoid starting checks when no settlement window remains" (github/merge.ts:136). The new fake-timer test "does not start the merge check when no time is left for its processes to settle", in which the PR read ends 10 ms past the stop point, failed before the fix. With no settle time left, the check is not started and is unknown: "No time was left to run the check."
A clear or found answer without a valid baseHead was accepted, though
clear removes the merge blocker. It now counts as unknown ("The check
returned an invalid result.").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reason
When the caller aborted and the inspection deadline also passed before a
stopped gh call settled, inspect() reported the timeout instead of the
caller's reason. It now rethrows the combined signal's reason, which is
whichever abort came first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lane F, part of step F6 (design: "Checking whether the issue is already fixed"). The pre-merge already-fixed check now follows the rules decided on 2026-09-30 for the pre-PR check (#84), by running that check itself.
What changes
github/merge.ts:GhMergeGatewayno longer reads the REST issue timeline. That check counted every same-repository PR that mentioned the issue and returnedunknownfor any cross-repository reference. It now calls anAlreadyFixedGateway: by defaultGhAlreadyFixedGatewayfor the same repository, using the merge gateway's own runner (since Give the merge and issue gh runners the allowlisted environment #86: the allowlisted environment, with 0.25 s and 0.15 s stop waits). The rules are the pre-PR check's:willCloseTarget). A PR that only mentions the issue does not count, in this repository or another. A closing PR in another repository is a match, no longerunknown.baseRefName. The commit scan starts at the PR'sbaseRefOid: merging requires that commit to equal the reviewed snapshot's base, which the pre-PR check also scanned from. No own commits are passed, because the task's commits reach the base branch only through this merge.MERGE_CHECK_SETTLE_MS(0.65 s: both of the merge runner's grace periods plus 250 ms, defined ingithub/merge.ts) before the inspection deadline, so its processes settle inside the 6-second and 12-second inspection budgets. A caller's abort during the check settles within the merge runner's 0.4 s, as Give the merge and issue gh runners the allowlisted environment #86 requires for everyghcall of the gateway. A check stopped that way isunknown. If the rule reads fail, the check is stopped and awaited before the inspection rejects. A caller's own abort still propagates as the caller's reason.clearorfound, or any error, isunknown. An injected check must name the merge repository; one that names another repository, or none, is refused at construction. The merge repository is now validated with the sharedREPOSITORYpattern.__typename, so thereposcope is enough.runner/merge.ts: the blocker now says why it blocks. A match names what was found: at most five closes, PRs and commits, by repository, number, state and short SHA, never a commit message. Anunknownresult gives the check's reason, for example a base more than 250 commits behind. Once this PR has merged, only thepr-stateblocker is shown: the check would otherwise count the merged PR itself as the fix.docs/implementation/guarded-merge.mddescribes the new check, its inputs, its deadline and its known gaps. The "pre-merge check" item is removed from the "does not do" list inpull-request-opening.md.Not in this PR
Earlier PRs of the task. The merge gateway knows only the configured PR number, so another still-open PR of the same task still blocks merging. The old check did the same. The pre-PR check excludes these PRs using the Store's PR records.
Base branch names. A name outside the
BRANCHpattern ingithub/validate.tsmakes the checkunknown, which blocks merging; it fails closed.Rebase (Add pre-merge rebase and head-bound command checks #22). Once a rebase moves the PR's base, the scan must still cover the commits the rebase brought under the PR. This is recorded in the deferred section of
guarded-merge.md.Inspection timeout. The merge-queue reads (
queueWatermark,inspectQueue) still check the caller's signal before the deadline's, so a deadline that fires first can be reported as the caller's abort. This code is unchanged here.Evidence
Final head
d7bc309, after mergingmainataafd153(#86):npx tsc --noEmit -p .: clean.test/merge.test.ts,test/already-fixed.test.ts,test/publish.test.ts,test/runner-shutdown.test.ts,test/gh-env.test.ts: 363 passed.test/history.test.ts,test/review.test.ts,test/runner-planning-feedback.test.ts,test/runner-shutdown.test.tsandtest/store.test.ts(115 of 115). Of the 9 real-Docker failures, two are the supervisor tests that fail onmainwithout this branch, and four passed when re-run. The other three failed again under the same load; onmain(f4b8336), "refuses to seed a repository with a chain of in-checkout links that ends outside it, and leaves no storage behind" fails the same way, and the other two ("refuses to seed a repository with a chain that leaves through a target this host lacks…", "returns the adapter handle at once and cancels it during network creation") passed there, so they depend on load. CI (test) passed on this head.github/merge.ts, 8 of the new tests fail.main;execFilerunner; after: the check's own slower-to-stop runner);repositoryaccepted;clearanswer without a valid base head accepted (found by Copilot, failing before the fix);GhMergeGateway.inspectran against an open cli/cli PR and five cli/cli issues (two closed, three open). There was no scope error; the closed issues werefoundand the open onesclear. The check read the PR's base branchtrunkand scanned from itsbaseRefOid. Nothing was written to GitHub.Review rounds
repositoryskips the repository guard; one doc sentence's reasoning; deleted REST tests.bd070d4(deadline, runner wiring test, docs)/code-review, high effortbd070d4repositoryskipped the repository guard; the settle time was computed inmerge.tsfrom the check's constants; a merged PR showed an already-fixed blocker.76c271b/code-review, high effort76c271bREPOSITORY.93e77b7/code-review, high effort93e77b7d391305/code-review, high effortd391305d391305164f690164f690merge.tslines 140 and 162). Reproduced with a fake-timer test.2eae9d22eae9d28742c6d/code-review, high effort8742c6d8742c6dclearorfoundanswer without a validbaseHeadwas accepted, thoughclearremoves the blocker.a3ac0b8a3ac0b8main(#86)fb80731github/merge.ts. #86 moved the merge'sghcalls to the allowlisted runner with 0.25 s and 0.15 s stop waits, and documents that an aborted merge click settles by 14.4 s. The check's own runner waits 1.5 s, so it now uses the merge runner, and the early stop is built from the merge's grace periods. A test fails if the check gets another runner.fb80731fb80731ghcall settled,inspect()reported the timeout instead of the caller's reason (pre-existing code, reachable now that calls wait forghto stop).d7bc309d7bc309Review-lesson audit
MERGE_CHECK_SETTLE_MS) and a fake-timer test.repo-scope token. A refusal would make the checkunknown, so it fails closed.status()re-checks the caller signal afterwards.repository(rounds 1 and 2)guarded-merge.md.test/already-fixed.test.ts, and fail-closed cases through the merge are intest/merge.test.ts.MERGE_CHECK_SETTLE_MSingithub/merge.ts.github/validate.tsexists for this; the merge now usesREPOSITORY.mergeReads.commit <sha>form.No new AGENTS.md rule is needed.
🤖 Generated with Claude Code