From 76e14fffe8527b0e488911a1987b59c85f7deb03 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:02:08 +0000 Subject: [PATCH 1/3] fix(source-control): confirm a merge-queue PR entered the queue after gh pr merge `gh pr merge` on a merge-queue base can return with a CLEAN PR left out of the queue. The merge phase now reads isInMergeQueue after every merge on a queue base, enqueues a CLEAN PR left out once at the pinned head with enqueuePullRequest, and reports GitHub's refusal verbatim, so a run ends merged, queued, or held with a named blocker. Closes #6630 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR --- ...6630-merge-confirm-queue-entry-4f16c791.md | 7 ++ .../skills/pull-request/SKILL.md | 2 +- .../skills/pull-request/reference/merge.md | 66 +++++++++++++++++-- 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 .changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md diff --git a/.changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md b/.changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md new file mode 100644 index 0000000000..6d3876c7e3 --- /dev/null +++ b/.changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- **`/source-control:pull-request merge` now confirms a merge-queue PR actually entered the queue.** After `gh pr merge` on a merge-queue base it reads `isInMergeQueue`; a `CLEAN` PR left out of the queue is enqueued once at the pinned head, and a refusal is reported with GitHub's error text verbatim, so a run ends merged, queued, or held with a named blocker. diff --git a/plugins/source-control/skills/pull-request/SKILL.md b/plugins/source-control/skills/pull-request/SKILL.md index 0536ace066..78680f6db4 100644 --- a/plugins/source-control/skills/pull-request/SKILL.md +++ b/plugins/source-control/skills/pull-request/SKILL.md @@ -65,7 +65,7 @@ For PR lifecycle runs spanning 3+ phases, copy `${CLAUDE_PLUGIN_ROOT}/skills/pul | `ready` | Phase 2.5 | Merge the base into the branch (never a rewrite of its history), run the code and security reviews over the PR diff in fresh-context subagents and the verify gate on the merged head, flip the draft with `gh pr ready`, then hand the PR to `/review:explain-change`, whose `digest_policy` decides whether a digest is offered or built (it gates nothing). Never arms auto-merge: ready starts the AI reviews. Refuses on a branch with no PR. See [reference/ready-for-review.md](reference/ready-for-review.md) | | `monitor` | Phase 3 | Watch CI, fix failures, evaluate comments. **Three-tier event delivery: (1) push channel** when your environment ships a GitHub-events channel (an MCP server delivering webhook events into the session), ~0 idle requests; **(2) Monitor tool** fallback (30s `gh` poll); **(3) plain `gh` polling** in cloud/headless sessions. Check the push channel FIRST per [monitor.md](reference/monitor.md) §3.0.05 before falling back | | `comments` | Phase 3.5 | Evaluate/respond to PR comments only | -| `merge` | Phase 4 | Squash merge, or merge or enqueue with `--match-head-commit` once the PR is `CLEAN` and the AI review checks have passed on that head, then wait for `MERGED` (no strategy flag behind a merge queue, never `--auto`; [reference/merge.md](reference/merge.md) §4.2.1) + worktree transition (reuse it, or release this session's lock) + verify. Never removes a worktree: that is `/source-control:worktree cleanup` | +| `merge` | Phase 4 | Squash merge, or merge or enqueue with `--match-head-commit` once the PR is `CLEAN` and the AI review checks have passed on that head, then confirm a merge-queue PR is queued (enqueue a `CLEAN` one left out, report GitHub's refusal verbatim; §4.2.2) and wait for `MERGED` (no strategy flag behind a merge queue, never `--auto`; [reference/merge.md](reference/merge.md) §4.2.1) + worktree transition (reuse it, or release this session's lock) + verify. Never removes a worktree: that is `/source-control:worktree cleanup` | | `status` | Report only | Unified status across all phases | | `full` | Phase 1-4 | Run prep → create → monitor → merge end-to-end | | `fetch-logs [--raw\|--job ]` | CI log retrieval | Pull failed-CI evidence: default = `::error`/`::warning` annotations only (cheapest); `--raw` = full ZIP dump for archive review; `--job ` = per-job plain text | diff --git a/plugins/source-control/skills/pull-request/reference/merge.md b/plugins/source-control/skills/pull-request/reference/merge.md index 40c481b4ed..e86a37958a 100644 --- a/plugins/source-control/skills/pull-request/reference/merge.md +++ b/plugins/source-control/skills/pull-request/reference/merge.md @@ -72,6 +72,10 @@ gh pr merge --squash && { When the repo deletes head branches on merge, the push fails with "remote ref does not exist"; that is expected, and it is the only failure to ignore. 4.3 deletes the local branch. Verified 2026-09-29 against [cli/cli#14007](https://github.com/cli/cli/pull/14007), which ships in gh 2.99.0 and makes `gh pr merge --delete-branch` skip the local delete when the head is checked out in the current linked worktree; earlier gh, such as 2.98.0, fails as described. Recheck when the minimum gh this plugin supports is 2.99.0 or later, at which point the split is no longer needed. +On a merge-queue base, `gh pr merge` returning is not proof the PR is queued: confirm queue entry +per [4.2.2](#422-confirm-queue-entry), with `HEAD_SHA` set to the head verified in 4.1, before +reporting the merge as done. + **Through the async merge API.** Use it instead of `gh pr merge` when the session refuses GraphQL (`gh pr merge` and `gh pr view` run over GraphQL; this is REST) or when the PR is a stack layer ([stacks.md](stacks.md)). `gh` has no command for it yet, so call it with `gh api`, pinned to the head you verified in 4.1 and never with `bypass_rules` true: @@ -146,10 +150,12 @@ fi ``` Behind a merge queue (claude-code-plugins) the queue sets the strategy, so pass no strategy flag. -There `gh pr merge` enqueues and returns; had required checks not passed it would arm auto-merge -instead, which the `CLEAN` read rules out. Never pass `--auto`, `--admin`, `--merge`/`-m` or -`--rebase`/`-r`. After enqueueing, poll until the PR merges or leaves the queue (run the loop in the -background or under a monitor): +There `gh pr merge` should enqueue and return, and had required checks not passed it would arm +auto-merge instead. It has also been seen to return, after printing that the queue sets the merge +strategy, with a `CLEAN` PR left out of the queue and auto-merge armed. Never pass `--auto`, +`--admin`, `--merge`/`-m` or `--rebase`/`-r`. Confirm queue entry per +[4.2.2](#422-confirm-queue-entry), then poll until the PR merges or leaves the queue (run the loop +in the background or under a monitor): ```bash while :; do @@ -175,6 +181,58 @@ Delete the head branch per 4.2 only once the PR reads `MERGED`. - **Recheck trigger**: a gh release that changes `gh pr merge` flags or merge-queue handling, or a rename of the AI review lanes' check names. +### 4.2.2 Confirm queue entry + +Run this after every `gh pr merge` on a merge-queue base, from 4.2 or 4.2.1. A merge run ends in +one of three states, and the report names which: merged, queued, or held with the blocker GitHub +returned. It changes what the run reports, not who may merge: it enqueues only a PR the user +already approved in 4.1 step 4 or that 4.2.1 cleared, and only at the head pinned there. + +```bash +: "${HEAD_SHA:?set HEAD_SHA to the head verified in 4.1}" +read_pr() { + gh api graphql -F owner='{owner}' -F name='{repo}' -F number= -f query=' + query($owner: String!, $name: String!, $number: Int!) { + repository(owner: $owner, name: $name) { pullRequest(number: $number) { + id state isInMergeQueue mergeStateStatus headRefOid autoMergeRequest { enabledAt } } } }' \ + --jq '.data.repository.pullRequest' +} +PR=$(read_pr) || exit 1 +[ "$(jq -r '.autoMergeRequest != null' <<<"$PR")" = true ] \ + && echo 'auto-merge is armed on this PR; tell the user (4.2.1 never arms it)' >&2 +case "$(jq -r '"\(.state) \(.isInMergeQueue) \(.mergeStateStatus)"' <<<"$PR")" in + MERGED*) echo 'merged' ;; + 'OPEN true '*) echo 'queued' ;; + 'OPEN false CLEAN') + [ "$(jq -r .headRefOid <<<"$PR")" = "$HEAD_SHA" ] \ + || { echo 'head moved since 4.1: hold and re-run readiness' >&2; exit 1; } + gh api graphql --silent -F id="$(jq -r .id <<<"$PR")" -F head="$HEAD_SHA" -f query=' + mutation($id: ID!, $head: GitObjectID!) { + enqueuePullRequest(input: {pullRequestId: $id, expectedHeadOid: $head}) { + mergeQueueEntry { position } } }' \ + || { echo 'enqueue refused: report the GitHub error above verbatim as the blocker' >&2; exit 1; } + [ "$(read_pr | jq -r '.isInMergeQueue')" = true ] \ + || { echo 'enqueue returned but the PR is not in the queue: report it' >&2; exit 1; } + echo 'queued' ;; + *) echo "not queued and not CLEAN ($(jq -c '{state,isInMergeQueue,mergeStateStatus}' <<<"$PR")): report it" >&2 + exit 1 ;; +esac +``` + +A non-zero exit is a hold: report the printed state or GitHub's error text unedited (for example +"A conversation must be resolved" names the blocker), do not retry the enqueue, and 4.3 does not +run. One exception: when the error says the PR is already queued, `gh pr merge` won the race; +re-run the block once, and a `queued` read is the result. Never pass `jump: true`. Only `queued` moves on to the poll in 4.2.1; `merged` moves on to +4.3. + +- **Pointer**: a PR joins a queue through the merge button or auto-merge: + [managing a merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue); + the mutation and its `expectedHeadOid` input: + [enqueuePullRequest](https://docs.github.com/graphql/reference/mutations#enqueuepullrequest). +- **As of**: 2026-10-11 +- **Recheck trigger**: a gh release that changes how `gh pr merge` enqueues, or a change to + `enqueuePullRequest` or its input on that page. + ## 4.3 Worktree transition and next-task setup This phase never removes a worktree; one left behind is a cleanup candidate for `/source-control:worktree cleanup`. From d0938f9612554f4d904319dc063cb83c5a391a7e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:26:54 +0000 Subject: [PATCH 2/3] fix(source-control): pin merges to the head captured in 4.1 and warn on armed auto-merge 4.1 step 2 now captures HEAD_SHA from the PR's headRefOid, and the 4.2, async and 4.2.1 merge calls pin to it instead of re-reading the live head, so 4.2.2's head-moved check compares against the verified head. A 4.2.2 hold with auto-merge armed now says a later push could merge on ci-status alone and offers gh pr merge --disable-auto without running it. Refs: #6630 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR --- ...6630-merge-confirm-queue-entry-4f16c791.md | 2 +- .../skills/pull-request/reference/merge.md | 44 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md b/.changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md index 6d3876c7e3..cdaf361bb1 100644 --- a/.changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md +++ b/.changes/source-control/fix-6630-merge-confirm-queue-entry-4f16c791.md @@ -4,4 +4,4 @@ bump: patch ### Fixed -- **`/source-control:pull-request merge` now confirms a merge-queue PR actually entered the queue.** After `gh pr merge` on a merge-queue base it reads `isInMergeQueue`; a `CLEAN` PR left out of the queue is enqueued once at the pinned head, and a refusal is reported with GitHub's error text verbatim, so a run ends merged, queued, or held with a named blocker. +- **`/source-control:pull-request merge` now confirms a merge-queue PR actually entered the queue.** After `gh pr merge` on a merge-queue base it reads `isInMergeQueue`; a `CLEAN` PR left out of the queue is enqueued once at the pinned head, and a refusal is reported with GitHub's error text verbatim, so a run ends merged, queued, or held with a named blocker. The head is now captured once in the pre-merge stale-base check and every merge call pins to it, and a hold with auto-merge armed warns that a later push could merge on `ci-status` alone and offers `gh pr merge --disable-auto`. diff --git a/plugins/source-control/skills/pull-request/reference/merge.md b/plugins/source-control/skills/pull-request/reference/merge.md index e86a37958a..fe54d94671 100644 --- a/plugins/source-control/skills/pull-request/reference/merge.md +++ b/plugins/source-control/skills/pull-request/reference/merge.md @@ -31,7 +31,12 @@ gh api --paginate "repos/{owner}/{repo}/issues//comments?per_page=100 - Any deferred items (tracked work items) 2. **Stale-base guard:** confirm the PR head is not behind its base on overlapping paths before squash-merging. `gh pr view --json mergeStateStatus,baseRefName,headRefOid` - plus `gh api repos/{owner}/{repo}/compare/...`. If + plus `gh api repos/{owner}/{repo}/compare/...`. Capture that + `headRefOid` as `HEAD_SHA` here + (`HEAD_SHA=$(gh pr view --json headRefOid -q .headRefOid)`, or `.head.sha` from + `gh api "repos/{owner}/{repo}/pulls/"` when GraphQL is refused): it is the head + every later step in this phase pins to, so a push after this point holds the merge instead of + riding along. If `behind_by > 0`, update the branch (merge-forward / `gh pr update-branch`) and re-run readiness; do **not** squash-merge a behind head. Under a non-strict ruleset, GitHub can still report `CLEAN` while the head is behind, and a stale-base squash can silently revert @@ -55,13 +60,13 @@ Default merge mode is squash: one squashed commit per PR onto the default branch In a regular checkout: ```bash -gh pr merge --squash --delete-branch +gh pr merge --squash --delete-branch --match-head-commit "$HEAD_SHA" ``` In a linked worktree (`git rev-parse --git-dir` differs from `git rev-parse --git-common-dir`), omit `--delete-branch`: on older gh it tries to check out the default branch locally, which fails while another worktree holds it, and exits 1 even though the merge succeeded. Delete the remote head branch separately, only once the PR reads `MERGED` (a merge queue or auto-merge returns before the merge lands), and through the resolved push remote, never a hardcoded `origin`: ```bash -gh pr merge --squash && { +gh pr merge --squash --match-head-commit "$HEAD_SHA" && { if [ "$(gh pr view --json state -q .state)" = MERGED ]; then REMOTE=$(bash "/scripts/resolve-remote.sh" --push ) && git push "$REMOTE" --delete else @@ -73,7 +78,7 @@ gh pr merge --squash && { When the repo deletes head branches on merge, the push fails with "remote ref does not exist"; that is expected, and it is the only failure to ignore. 4.3 deletes the local branch. Verified 2026-09-29 against [cli/cli#14007](https://github.com/cli/cli/pull/14007), which ships in gh 2.99.0 and makes `gh pr merge --delete-branch` skip the local delete when the head is checked out in the current linked worktree; earlier gh, such as 2.98.0, fails as described. Recheck when the minimum gh this plugin supports is 2.99.0 or later, at which point the split is no longer needed. On a merge-queue base, `gh pr merge` returning is not proof the PR is queued: confirm queue entry -per [4.2.2](#422-confirm-queue-entry), with `HEAD_SHA` set to the head verified in 4.1, before +per [4.2.2](#422-confirm-queue-entry), with the `HEAD_SHA` captured in 4.1 step 2, before reporting the merge as done. **Through the async merge API.** Use it instead of `gh pr merge` when the session refuses GraphQL @@ -81,7 +86,7 @@ reporting the merge as done. call it with `gh api`, pinned to the head you verified in 4.1 and never with `bypass_rules` true: ```bash -HEAD_SHA=$(gh api "repos/{owner}/{repo}/pulls/" --jq .head.sha) +: "${HEAD_SHA:?set HEAD_SHA in 4.1 step 2}" gh api -X PUT "repos/{owner}/{repo}/pulls//merge-async" \ -f sha="$HEAD_SHA" -f merge_action=direct_merge -f merge_method=squash -F bypass_rules=false ``` @@ -125,7 +130,7 @@ after arming, a push by anyone with write access would merge the new head on `ci ```bash [ "$(gh repo view --json owner --jq .owner.login)" = melodic-software ] \ || { echo 'not a melodic-software repository: wait for the user' >&2; exit 1; } -HEAD_SHA=$(gh api "repos/{owner}/{repo}/pulls/" --jq .head.sha) +: "${HEAD_SHA:?set HEAD_SHA in 4.1 step 2}" RULES=$(gh api --paginate --slurp "repos/{owner}/{repo}/rules/branches/" | jq 'add') jq -e 'any(.[]; .type == "required_status_checks" and any(.parameters.required_status_checks[]; .context == "ci-status"))' <<<"$RULES" >/dev/null \ @@ -189,7 +194,7 @@ returned. It changes what the run reports, not who may merge: it enqueues only a already approved in 4.1 step 4 or that 4.2.1 cleared, and only at the head pinned there. ```bash -: "${HEAD_SHA:?set HEAD_SHA to the head verified in 4.1}" +: "${HEAD_SHA:?set HEAD_SHA in 4.1 step 2}" read_pr() { gh api graphql -F owner='{owner}' -F name='{repo}' -F number= -f query=' query($owner: String!, $name: String!, $number: Int!) { @@ -198,32 +203,39 @@ read_pr() { --jq '.data.repository.pullRequest' } PR=$(read_pr) || exit 1 -[ "$(jq -r '.autoMergeRequest != null' <<<"$PR")" = true ] \ - && echo 'auto-merge is armed on this PR; tell the user (4.2.1 never arms it)' >&2 +AUTO=$(jq -r '.autoMergeRequest != null' <<<"$PR") +[ "$AUTO" = true ] && echo 'auto-merge is armed on this PR; tell the user (4.2.1 never arms it)' >&2 +hold() { + echo "$1" >&2 + [ "$AUTO" = true ] && echo 'auto-merge is still armed: a later push could merge on ci-status alone. +The user may disarm it with: gh pr merge --disable-auto' >&2 + exit 1 +} case "$(jq -r '"\(.state) \(.isInMergeQueue) \(.mergeStateStatus)"' <<<"$PR")" in MERGED*) echo 'merged' ;; 'OPEN true '*) echo 'queued' ;; 'OPEN false CLEAN') [ "$(jq -r .headRefOid <<<"$PR")" = "$HEAD_SHA" ] \ - || { echo 'head moved since 4.1: hold and re-run readiness' >&2; exit 1; } + || hold 'head moved since 4.1: hold and re-run readiness' gh api graphql --silent -F id="$(jq -r .id <<<"$PR")" -F head="$HEAD_SHA" -f query=' mutation($id: ID!, $head: GitObjectID!) { enqueuePullRequest(input: {pullRequestId: $id, expectedHeadOid: $head}) { mergeQueueEntry { position } } }' \ - || { echo 'enqueue refused: report the GitHub error above verbatim as the blocker' >&2; exit 1; } + || hold 'enqueue refused: report the GitHub error above verbatim as the blocker' [ "$(read_pr | jq -r '.isInMergeQueue')" = true ] \ - || { echo 'enqueue returned but the PR is not in the queue: report it' >&2; exit 1; } + || hold 'enqueue returned but the PR is not in the queue: report it' echo 'queued' ;; - *) echo "not queued and not CLEAN ($(jq -c '{state,isInMergeQueue,mergeStateStatus}' <<<"$PR")): report it" >&2 - exit 1 ;; + *) hold "not queued and not CLEAN ($(jq -c '{state,isInMergeQueue,mergeStateStatus}' <<<"$PR")): report it" ;; esac ``` A non-zero exit is a hold: report the printed state or GitHub's error text unedited (for example "A conversation must be resolved" names the blocker), do not retry the enqueue, and 4.3 does not run. One exception: when the error says the PR is already queued, `gh pr merge` won the race; -re-run the block once, and a `queued` read is the result. Never pass `jump: true`. Only `queued` moves on to the poll in 4.2.1; `merged` moves on to -4.3. +re-run the block once, and a `queued` read is the result. A hold with auto-merge armed also tells +the user that a later push could merge on `ci-status` alone (4.2.1's reason for never arming it) +and offers `gh pr merge --disable-auto` as their option; this phase does not run it. +Never pass `jump: true`. Only `queued` moves on to the poll in 4.2.1; `merged` moves on to 4.3. - **Pointer**: a PR joins a queue through the merge button or auto-merge: [managing a merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue); From 501539e4b9e369a1f6ce207032afac5addd131af Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:25:23 -0400 Subject: [PATCH 3/3] fix(source-control): pin HEAD_SHA from the compared read and hold a queued moved head - Stale-base guard reads mergeStateStatus, baseRefName and headRefOid once and uses that head for both the compare and HEAD_SHA, so the pinned head is the one the compare checked. - The confirm read holds when the PR is queued at a head other than HEAD_SHA instead of reporting it queued. - Phase body keeps the hold policy and the provider pointer; the GitHub timing and enqueue behavior stays in the provider record. - readiness.md notes that a PR left UNSTABLE by a redundant stuck status goes to the user, since the merge phase merges only on CLEAN. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WVW9hAwnSBrnFsR3kzXcB4 --- .../providers/github/pull-request/merge.md | 19 +++++++++++++------ .../skills/pull-request/reference/merge.md | 6 +----- .../pull-request/reference/readiness.md | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/plugins/source-control/reference/providers/github/pull-request/merge.md b/plugins/source-control/reference/providers/github/pull-request/merge.md index 5d183c7754..4c96d7822e 100644 --- a/plugins/source-control/reference/providers/github/pull-request/merge.md +++ b/plugins/source-control/reference/providers/github/pull-request/merge.md @@ -18,14 +18,18 @@ gh api --paginate "repos/{owner}/{repo}/issues//comments?per_page=100 ## Stale-base guard +Read the head once and use that value for both the compare and `HEAD_SHA`, so the pinned head is +the one the compare checked: + ```bash -gh pr view --json mergeStateStatus,baseRefName,headRefOid -gh api repos/{owner}/{repo}/compare/... -HEAD_SHA=$(gh pr view --json headRefOid -q .headRefOid) +read -r MSS BASE HEAD_SHA <<<"$(gh pr view --json mergeStateStatus,baseRefName,headRefOid \ + --jq '"\(.mergeStateStatus) \(.baseRefName) \(.headRefOid)"')" +echo "mergeStateStatus: $MSS" +gh api "repos/{owner}/{repo}/compare/$BASE...$HEAD_SHA" ``` -When GraphQL is refused, take `HEAD_SHA` from `.head.sha` of -`gh api "repos/{owner}/{repo}/pulls/"`. +When GraphQL is refused, read `.base.ref` and `.head.sha` from one +`gh api "repos/{owner}/{repo}/pulls/"` call instead. Update a behind branch with a merge-forward or `gh pr update-branch`. @@ -184,7 +188,10 @@ The user may disarm it with: gh pr merge --disable-auto' >&2 } case "$(jq -r '"\(.state) \(.isInMergeQueue) \(.mergeStateStatus)"' <<<"$PR")" in MERGED*) echo 'merged' ;; - 'OPEN true '*) echo 'queued' ;; + 'OPEN true '*) + [ "$(jq -r .headRefOid <<<"$PR")" = "$HEAD_SHA" ] \ + || hold 'queued at a head other than the one pinned in 4.1: hold and report it' + echo 'queued' ;; 'OPEN false CLEAN') [ "$(jq -r .headRefOid <<<"$PR")" = "$HEAD_SHA" ] \ || hold 'head moved since 4.1: hold and re-run readiness' diff --git a/plugins/source-control/skills/pull-request/reference/merge.md b/plugins/source-control/skills/pull-request/reference/merge.md index 34bf905806..649c53e3a9 100644 --- a/plugins/source-control/skills/pull-request/reference/merge.md +++ b/plugins/source-control/skills/pull-request/reference/merge.md @@ -52,8 +52,6 @@ fails the merge instead of landing unreviewed. Before any merge call, poll the P the pinned head while it reads `UNKNOWN`, and merge or enqueue only once it reads `CLEAN`; any other state, or a moved head, is a hold to report ([GitHub](../../../reference/providers/github/pull-request/merge.md#wait-for-a-known-merge-state)). -GitHub reads `UNKNOWN` just after a push or a ready flip, and a merge call on a merge-queue base -then arms auto-merge instead of enqueueing. In a regular checkout, squash-merge and delete the head branch in one call ([GitHub](../../../reference/providers/github/pull-request/merge.md#squash-merge-in-a-regular-checkout)). @@ -110,9 +108,7 @@ the merge state reads `UNKNOWN`, as in 4.2). Only then does it merge, pinned to that head with `--match-head-commit`. Behind a merge queue (claude-code-plugins) the queue sets the strategy, so pass no strategy flag. -There the merge should enqueue and return, and had required checks not passed it would arm -auto-merge instead. It has also been seen to return, after printing that the queue sets the merge -strategy, with a `CLEAN` PR left out of the queue and auto-merge armed. Never pass `--auto`, +Never pass `--auto`, `--admin`, `--merge`/`-m` or `--rebase`/`-r`. Confirm queue entry per [4.2.2](#422-confirm-queue-entry), then poll the PR's state and merge-queue membership every 30 seconds until it reads `MERGED` or leaves the queue unmerged (run the loop in the background or under diff --git a/plugins/source-control/skills/pull-request/reference/readiness.md b/plugins/source-control/skills/pull-request/reference/readiness.md index 2e4b3a0844..1e2d3e963a 100644 --- a/plugins/source-control/skills/pull-request/reference/readiness.md +++ b/plugins/source-control/skills/pull-request/reference/readiness.md @@ -74,7 +74,7 @@ List the PR's checks with name, state and bucket ([GitHub](../../../reference/pr **Gotcha: one name may show duplicate entries (`SUCCESS` check-run + stuck `PENDING` commit-status).** The checks list aggregates BOTH workflow check-runs AND external commit-statuses, so a workflow and an external app posting under the same name produce two rows: the workflow's check-run resolves cleanly, while the app's redundant commit status may never finalize and sits at `PENDING` indefinitely. When you see two rows for one name, one `pass|SUCCESS` with a `link` and one `pending|PENDING` with no link, treat the check-run as authoritative. Verify by listing every check run on the head with its name, status and conclusion, and finding the duplicated name's row in the output; the name never goes into the query ([GitHub](../../../reference/providers/github/pull-request/readiness.md#verify-a-duplicated-check-name)). -Match the name by reading the output, never by typing it into the command: a check name is chosen by whoever wrote the workflow (a fork PR names its own jobs), so inside a shell argument or a jq program it can run shell code or act as a regex. If its row reads `completed success`, the stuck commit-status is the redundant external bot. Classify it as non-blocking, document, and proceed. `mergeStateStatus=UNSTABLE` will reflect the stuck status but does NOT block merge when the repo's required checks are green. +Match the name by reading the output, never by typing it into the command: a check name is chosen by whoever wrote the workflow (a fork PR names its own jobs), so inside a shell argument or a jq program it can run shell code or act as a regex. If its row reads `completed success`, the stuck commit-status is the redundant external bot. Classify it as non-blocking, document, and proceed. `mergeStateStatus=UNSTABLE` will reflect the stuck status but does NOT block merge when the repo's required checks are green on GitHub's side. The merge phase still merges or enqueues only on `CLEAN` ([merge.md](merge.md) §4.2), so a PR held at `UNSTABLE` by such a status goes to the user. The pagination is not optional and the completeness assertion is not hygiene. See [Reading GitHub list APIs](#reading-github-list-apis).