Skip to content

fix(source-control): post-merge remediation disarms the lane's armed pull requests - #9538

Merged
kyle-sexton merged 4 commits into
mainfrom
fix/9518-remediate-disarms-lane-prs
Oct 24, 2026
Merged

kyle-sexton merged 4 commits into
mainfrom
fix/9518-remediate-disarms-lane-prs

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Member

Closes #9518

Summary

After a demotion, babysit_postmerge.py --remediate dequeued the lane's merge-queue entries but left armed, not-yet-queued lanes App pull requests armed. Under merge.lane-prs: native (#9508), GitHub could enqueue one of them before the next enable-auto-merge reader run disarmed it. Remediation now disarms them in the same run that dequeues.

Fix

  • disarm_identity_pulls() in babysit_postmerge.py reads every open pull request into the judged branch (PostMergeArmed, 100 a page, at most 10 pages, the same limit as enable-auto-merge.mjs). It calls disablePullRequestAutoMerge, the mutation enable-auto-merge.mjs disarms with, on each one whose author and autoMergeRequest.enabledBy are both the agent identity. A pull request a person armed, or one the identity did not open, stays armed.
  • A page whose nodes is not a list or whose pageInfo is missing is a read error, as in enable-auto-merge.mjs, not an empty listing. A listing past the page limit still disarms what it read, then reports the overflow as a read error.
  • remediate() runs the disarm before the dequeue and in a separate try block, so if one fails the other still runs. Disarming first means no armed pull request enters the queue after the dequeue has read it. The report gains a disarmed list. A failed read or disarm is reported as a disarm: ... error. Every pull request is still tried, and the dequeue and the demotion issue still run. With no failure in the verdict, nothing is read or written, as before.
  • babysit_merge.py reuses postmerge.DISARM_MUTATION instead of its own copy of the same mutation.
  • The guard-contract tables and their generated reference/guard-contract.md now list the disarm among the writes that --remediate gates.
  • In pr-merge.yml, the "Record a failed lane merge" step's ::error filter now also matches disarm: entries, so a failed disarm reports as an error like a failed dequeue or demotion issue, and the step's comment names the disarm. merge-job.test.mjs covers the disarm: case.

Token: the only --remediate caller is that pr-merge.yml step. It runs under the lane token for activity merge-pr, whose effect merge grants pull-requests: write (.github/actions/resolve-config/effect-grants.json). That is the same grant enable-auto-merge disarms with, so no token change is needed.

Verification

  • uv run --with pytest python -m pytest plugins/source-control/skills/babysit-prs/scripts/tests: 2743 passed, 2132 subtests passed.
  • node --test .github/scripts/pr-merge/merge-job.test.mjs: 42 passed; dropping the disarm: filter turns one red.
  • New tests in test_babysit_postmerge.py Remediation:
    • From a mix of open pull requests, only the lanes-authored, lanes-armed one is disarmed. The person-armed one, the person-authored one and the unarmed one are left alone. The disarm runs before the dequeue.
    • When one disarm fails, the error is reported, the other pull requests are still disarmed, and the dequeue and the issue still run.
    • When the queue read fails, the disarm still runs.
    • When the armed listing cannot be read, the error is reported and the dequeue still runs.
    • The armed read follows the cursor to a second page and reads the judged branch.
    • A malformed armed page is a disarm: error.
    • A listing past the page limit still disarms the lane's pull requests it read (red without the fix).
    • When the verdict has no failure, nothing is read or disarmed.
  • Mutation check: dropping either the enabledBy check or the author check turns two of the new tests red.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 23, 2026 21:33
…pull requests

babysit_postmerge.py --remediate now turns off native auto-merge on every
open pull request into the judged branch that the agent identity both
opened and armed, before it dequeues the identity's merge-queue entries.
A pull request a person armed, or one the identity did not open, is left
armed. Failures are reported as disarm: errors and do not stop the
dequeue or the demotion issue.

Closes #9518

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Record a failed lane merge" step now errors on a disarm: entry in the
remediation report, as it does for demotion issue: and dequeue: entries.
Also drops an issue number from a test comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed page of the armed listing is now a read error instead of an
empty listing, and a listing past the page limit still disarms what it
read before reporting the error. babysit_merge.py reuses the postmerge
disarm mutation, and the pr-merge.yml comment names the disarm. New tests
cover the cursor, the judged branch, a malformed page and the page limit.

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

chatgpt-codex-connector Bot commented Oct 24, 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-24T01:55:53.227665Z 0c62930 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

Copy link
Copy Markdown
Contributor

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

No security issues found in this pull request (HEAD 0c62930).

critical-findings: 0

I read the full diff (8 files) and the PR context. I also read the helpers the new code relies on in babysit_postmerge.py (_graphql, _login_key, _split_repo, _actor_key) and in babysit_merge_policy.py (policy.actor_key).

  • Scope of the new write. disarm_identity_pulls disarms a pull request only when both its author and autoMergeRequest.enabledBy match the agent identity. Both use the same exact-match actor comparison as the existing dequeue (policy.actor_key against _login_key(identity)). That comparison adds [bot] for Bot actors, so a User who owns the bare slug app cannot be mistaken for the App app[bot]. A pull request a person opened or armed stays armed. The change only turns auto-merge off, so it cannot widen what merges.
  • Injection. The owner, repo, branch, cursor and pull request id reach gh api graphql as separate -f name=value arguments, with no shell or string interpolation. _split_repo validates the owner/repo pair. The DISARM_MUTATION string is a constant, now shared by babysit_merge.py, with the same text as before.
  • Authorization. The only caller is the existing pr-merge.yml step, which already runs under the lane token with pull-requests: write. This diff changes no permissions:, triggers, action pins or tokens. Its workflow edits are two comment lines and the disarm: addition to the jq error filter.
  • Failure handling. The disarm step has its own try block, so a failed read or write does not skip the dequeue or the demotion issue. Overflow past the page limit still disarms what it read, then reports an error. A missing branch is reported as an error and does not raise an unhandled exception. disarm: errors are now surfaced by the record step, so a failed disarm does not go unreported.
  • Instruction surface and manifests. The guard-contract table and its generated doc only add the disarm to the list of writes --remediate gates. No standing instruction is removed or softened. No plugin.json or marketplace.json file changed, so there are no manifest <field> lines. The .changes file is a changelog entry.
  • PR context. It contains no embedded instructions. The closing issue body was omitted by the wrapper.

I did not consult docs/conventions/instruction-exception-register/README.md, because the diff deletes or narrows no instruction. No trust-gate path is touched, so I set no hold.

@kyle-sexton
kyle-sexton enabled auto-merge October 24, 2026 01:55
@github-actions

Copy link
Copy Markdown
Contributor

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

No findings. I read the full diff (.claude-lane/pr.diff) and the PR context, and ran no tests or build commands myself.

What I checked

  • Closing issue fix(source-control): post-merge remediation also disarms armed lane pull requests #9518. Its one criterion is: "Remediation disarms every armed lanes App pull request in the same run that dequeues, with a test, and a person-armed pull request (not the lanes App's) is left alone."
    • disarm_identity_pulls is called from remediate() before the dequeue.
    • It disarms only a pull request that the agent identity both authored and armed.
    • Tests cover each case: a mix of pull requests (person-armed, person-authored and unarmed ones stay armed), a failed disarm, a failed queue read, an unreadable listing, a malformed page, cursor paging, and the page-limit overflow.
    • One test asserts the disarm write comes before the dequeue write.
    • The criterion is met, and tests would fail if it broke.
  • Identity matching. The author and enabledBy checks use _actor_key on both sides, compared with _login_key(identity). That is the same comparison dequeue_identity_entries uses, so a Bot login gets its [bot] suffix consistently.
  • Error handling.
    • remediate() runs the disarm in its own try, so a disarm failure doesn't stop the dequeue or the demotion issue.
    • A page-limit overflow still disarms what it read, then raises PostMergeReadError.
    • Per-pull-request mutation failures are collected and raised together as PostMergeWriteError.
    • _graphql accepts the error class that disarm_identity_pulls passes to it.
  • Shared mutation. babysit_merge.py already imports babysit_postmerge as postmerge, so DISARM_MUTATION = postmerge.DISARM_MUTATION adds no new import cycle. The mutation text is unchanged.
  • Workflow.
    • The jq filter in pr-merge.yml now matches disarm: errors, and merge-job.test.mjs covers it.
    • The guard-contract markdown and guard_contract.py were updated together.
  • Manifests. The diff changes no plugin.json or marketplace.json, so no manifest <field> lines apply. The changelog fragment is bump: patch, which fits a bug fix with no new user-facing surface.
  • Hold classes. The change adds no model spend or trigger, touches no trusted-actor, CODEOWNERS or .gitmodules paths, and has no critical security finding. No hold is set.

The test results in the PR description (2743 Python tests, 42 Node tests) are the author's claims. I did not run them.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 06c5490 Oct 24, 2026
112 of 113 checks passed
@kyle-sexton
kyle-sexton deleted the fix/9518-remediate-disarms-lane-prs branch October 24, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(source-control): post-merge remediation also disarms armed lane pull requests

1 participant