Skip to content

fix(source-control): judge a native auto-merge of a lane pull request in post-merge - #9459

Merged
kyle-sexton merged 11 commits into
mainfrom
fix/postmerge-native-auto-merge
Oct 23, 2026
Merged

kyle-sexton merged 11 commits into
mainfrom
fix/postmerge-native-auto-merge

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Closes #9447
Closes #9466

Summary

Since #9399, enable-auto-merge.mjs arms GitHub's native auto-merge on lanes App pull requests with the App token. GitHub then enqueues them under the agent identity, but no babysit-merge-pin comment is posted, because only babysit_merge.py writes one. The post-merge check treated such a merge as the gate's own. It reported read-back-unrecorded until the 6 h dispatch deadline, then failed the read-back. That failure files a demotion record and holds pr-merge for owner and Dependabot pull requests.

Fix

  • MERGED_QUERY gains a second aliased connection, autoMerge:timelineItems(itemTypes:[AUTO_MERGE_ENABLED_EVENT,AUTO_MERGE_DISABLED_EVENT],last:1). It rides in the same GraphQL request, and the existing last:1 queue-event connection that derive_auto_merge reads is left alone.
  • New native_auto_merge(): an identity merge counts as native when the identity opened it from a branch of this repository (author, isCrossRepository, as enable-auto-merge.mjs requires before it arms) and its newest auto-merge enable or disable event is an AutoMergeEnabledEvent by the identity, dated no later than the merge-queue enqueue. An arm under the identity token on any other pull request keeps the pin read-back. A gate merge keeps its pin read-back in two ways. The gate disarms an armed auto-merge before it pins and enqueues, so the newest event is then a disable. An arm made after the enqueue does not count.
  • A native merge skips the pin read and the read-back, and verifies on the dispatch and canary runs alone, as any other enqueue does. It stays by_identity, so --lane-merges-only still judges it and post-merge-dispatch starts its full run. A red full run or a red canary after it still fails it.
  • publish-promotion-evidence.mjs mirrors check() failures so demotion counts match; it reads the same fields and skips the pin read-back by the same rule, else it would still record a native merge as a pin read-back failure.
  • An identity merge whose autoMerge connection is missing or malformed makes the verdict unreadable. That holds the gate but files no record.
  • The residual in reference/safety.md and the module docstring name the exception. The source-control changelog has a fragment.
  • fix(source-control): pr-merge's affected-suites check trusts a green run on any commit named main #9466, same file: _Context.affected (green run for the affected set) and _Context._baseline (run that excuses already-failing checks) chose the newest run whose head_branch named the branch, which a run on a tag named main also reports. A new _newest_on_branch keeps the newest run whose head commit is the tip of refs/heads/<branch> or its ancestor, through the existing _Context.contains. It checks candidates newest first and stops at the first match, so a normal run costs one ancestry check.
  • Review follow-ups: the Node copy now requires one lane identity as author, enqueuer and armer, and refuses to publish on unreadable auto-merge events, matching the Python check. The scheduled, dispatched and canary runs a merge is judged on, the runs read by commit, and the lane re-run probe now keep only runs on refs/heads/<branch> (_Context.on_branch). safety.md now names enable-auto-merge.mjs as what arms auto-merge, names the pr-unblock writers that do not disarm, and lists the head and same-second residuals.

Verification

  • uvx --with pytest-xdist pytest -n auto plugins/source-control/skills/babysit-prs/scripts/tests/: 2616 passed. The new NativeAutoMerge cases cover:
    • a native merge with no pin, past the deadline, verifies with no failure, and the pin callable is never called;
    • one with no dispatch run asks for one;
    • --lane-merges-only judges it;
    • a red full run after its re-run, and a red canary, still fail it;
    • a gate merge with no auto-merge event still fails its read-back;
    • the pin stays required for a disarm by the identity, an arm after the enqueue, an arm by a person, and an arm by a User named like the App slug;
    • a missing or malformed autoMerge connection reads as unreadable.
  • fix(source-control): pr-merge's affected-suites check trusts a green run on any commit named main #9466 tests: a newer green dispatch on a commit off main no longer sets the affected-set base (assert_called_once_with(OTHER_SHA, LATER_SHA)), and a red baseline on a commit off main no longer excuses a check (the red schedule run fails). Both fail with the ancestry check removed.
  • node --test on publish-promotion-evidence.test.mjs and resolve-promotion-evidence.test.mjs: 110 passed; the new native case fails with the read-back gate reverted.
  • node --test .github/scripts/pr-merge/*.test.mjs: 214 passed. scripts/run-ruff.sh check and format --check are clean.
  • The new MERGED_QUERY ran against this repository's main: 100 pull requests on page 1 in one GraphQL call. feat(source-control): hold only the takeover set on the babysit HIGH floor #9413, whose auto-merge a person armed, reads AutoMergeEnabledEvent at 14:40:08Z before its enqueue at 14:52:09Z, with no disable event after the merge. Gate-enqueued fix(wizard): fit generate's intro and closing to non-browser wizards, add gate and exit hooks #9401 reads an empty autoMerge.
  • Left out: a live native merge by the lanes App, because none has happened yet. Not addressed here: a push to an armed lanes pull request after the arm (by the address-feedback or apply-patch App writers) still merges natively without a head comparison; ci-status re-runs the hold rules on the merge-group head.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 23, 2026 12:24
… in post-merge

A lanes App pull request that GitHub's native auto-merge lands posts no
babysit-merge-pin comment, yet its enqueue is the identity's, so the
post-merge check waited for a pin read-back and failed it at the dispatch
deadline, filing a demotion record.

The merged-pull-request query now also reads the newest auto-merge enable
or disable event through a second aliased timelineItems connection. When
that event is the identity's enable, made no later than the enqueue, the
merge skips the pin read and verifies on the dispatch and canary runs
alone. It stays an identity merge, so post-merge-dispatch still starts its
full run, and a red full run or canary still fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vidence

publish-promotion-evidence.mjs mirrors the post-merge check's failures so
demotion counts match, and still owed a pin for a lane's native
auto-merge. It now reads the same aliased auto-merge events and skips the
pin read-back by the same rule.

Both copies also require that the identity opened the pull request from
a branch of this repository, as enable-auto-merge.mjs does before it
arms, so an arm under the identity token on any other pull request keeps
its pin read-back. The pin requirement is decided once in check(), an
arm at the enqueue time and an unrelated merge with no auto-merge events
are covered, and the AutoMerge comment names the native case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…otion evidence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 23, 2026 16:43
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T16:45:37.819901Z d106d32 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 62fdc202db1868ea9fb2fdba33dfd1a8181c884a: 0 finding(s), 0 as inline comments.

Incremental review of 62fdc20 against b1ee5b9 found no issues. I only read the diff and the code around it. I ran no tests, and the PR description's test counts are author-claimed and unverified.

  • publish-promotion-evidence.mjs: the change adds only a comment. It names a residual: the branch tip is read before the run listings, while check() reads it after. The code at publish-promotion-evidence.mjs:313-323 matches the comment.
  • github-fake.mjs: the new onTop, unknownCompare and tip options answer compare and ref requests the way the script reads them.
    • A compare of <run>...<tip> where the run is in onTop returns behind, which the script treats as off the branch.
    • unknownCompare returns an undefined status, which the script refuses with "whether that run is on main is unknown".
    • tip: null gives a non-SHA tip, which hits the "has no tip commit" refusal.
    • The compare ordering is consistent: unknownCompare is checked first, then onTop as the base (behind), then offBranch or onTop as the head (ahead).
  • Tests: the two new tests cover the behind and unreadable-ancestry/tip paths that the earlier off-branch tests missed.
    • A red run on a commit on top of the tip is not charged.
    • An unreadable tip or compare refuses the publish.
  • Closing issues fix(source-control): judge native lane auto-merges without a pin read-back #9447 and fix(source-control): pr-merge's affected-suites check trusts a green run on any commit named main #9466: nothing in this incremental change affects the criteria already reviewed.
  • Manifests, hold classes: this change touches no manifests and no hold classes (cost, trust-gate, critical-security).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-security-review: 1 inline finding(s) on d106d32ff67e3b96a2ca57b70edff934206e101c; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py Outdated
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 62fdc202db1868ea9fb2fdba33dfd1a8181c884a: 0 finding(s), 0 as inline comments.

No security issues found in the incremental change since b1ee5b9. I read .claude-lane/incremental.diff and the PR context, and skipped the deep review under skip gate 2.

The change has no security-relevant surface:

  • publish-promotion-evidence.mjs gains only a comment that names a residual race. The tip is read before the run listings, so a run on a commit that lands between the two reads counts as off-branch. That direction fails closed. It can leave a run uncharged, which is the same outcome as the existing off-branch handling, and it cannot reach an unchecked action. No logic changed.
  • fixtures/.../github-fake.mjs is a test fake. It adds the onTop, unknownCompare and tip options and compare-response branches.
  • publish-promotion-evidence.test.mjs adds two tests. One covers a run on a commit on top of the tip, where compare answers behind. The other covers an unreadable tip or unknown ancestry, where the publish refuses.

No manifest files, workflow files, trust-gate paths or standing instructions are touched, so there are no manifest <field> lines.

The PR context text is data. It holds no embedded directives aimed at this lane.

critical-findings: 0

kyle-sexton and others added 3 commits October 23, 2026 12:46
The native_auto_merge docstring said the arm's expectedHeadOid still
bound the head. It binds only the head at arm time; a writer's push after
the arm leaves auto-merge armed, so name that residual instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_Context.affected and _Context._baseline picked the newest run whose
head_branch named the branch. A run on a tag named main reports that
too, so a writer's own commit could supply the plan script that sets the
affected set, or the baseline that excuses a red check. Both now take
the newest run whose head commit is the tip of refs/heads/<branch> or its
ancestor, through _Context.contains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the branch

- publish-promotion-evidence.mjs refuses to publish when the lane
  identity's auto-merge events cannot be read, as the post-merge check
  reads them unreadable, and requires one lane identity as author,
  enqueuer and armer.
- The scheduled, dispatched and canary runs a merge is judged on, the
  runs read by commit, and the lane re-run probe keep only runs whose
  head commit is on refs/heads/<branch>, so a red run on a tag named
  like the branch cannot fail a merge.
- safety.md names enable-auto-merge.mjs as what arms and re-arms lanes
  App pull requests, names the pr-unblock branch writers that do not
  disarm, and adds the head and same-second residuals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 1 inline finding(s) on a93c9d269b426ee018169db182d4bd84d611d8bb; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py Outdated
kyle-sexton and others added 3 commits October 23, 2026 13:23
publish-promotion-evidence.mjs kept post-merge runs by head_branch name
alone, so a red dispatch on a tag named main was a gate failure there
while the post-merge check ignores it. It now keeps a run only when its
commit is the tip of refs/heads/<branch> or its ancestor, as
_Context.on_branch does.

Tests now cover each filter that no test killed when removed: the Node
on-branch filter, the Node author-is-enqueuer conjunct, the
dispatch_runs_at filter, and the lane re-run probe filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…robe

Only a run the probe could mark pays for the ancestry read, so an
unreadable compare on an old or already re-run run no longer ends the
probe for newer runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-security-review: 1 inline finding(s) on 1a32cc908a2ccb292e7d8330f001666fcc2f9610; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

… a behind run

Each test fails when its path is removed:
- the missing-tip refusal;
- the unknown-compare-status refusal;
- a red run on a commit on top of the tip (compare `behind`), which a
  tag named main one commit past the branch reads as, being ignored.

The test fake gains `tip`, `onTop` and `unknownCompare` options. A
comment records that the tip is read before the run listings here and
after them in check().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 3121c6e Oct 23, 2026
34 checks passed
@kyle-sexton
kyle-sexton deleted the fix/postmerge-native-auto-merge branch October 23, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant