| name | unblock-prs |
| description | Clear what is blocking the open pull requests that already have auto-merge enabled — rebase the one that is out-of-date with the base branch, watch its checks, fix what fails — one PR at a time, and never merge anything. Use when asked to unblock or look after the open PRs, rebase branches behind main, watch CI, or fix a failing check on a PR. |
Unblock open pull requests
Clear the obstacles in front of the pull requests that are already queued to
merge, so GitHub can merge them itself.
Scope. Only pull requests with auto-merge already enabled
(autoMergeRequest is not null). That flag is the author's statement that the
PR should land as soon as it is able to; everything else is still someone's work
in progress and is none of this skill's business.
Two jobs, and no third. Rebase a branch that is out-of-date with main, and
fix a failing check. Never merge. No gh pr merge, no --auto, no enabling
auto-merge on a PR that lacks it, no review approvals. When a PR is green and
still unmerged, report why — do not finish it by hand.
One at a time. Merging any PR moves main, which puts every other open
branch back to BEHIND. A batch rebase therefore runs a full CI matrix per
branch and throws all but the first away. Never rebase a second PR while one is
still in flight.
Invoking this skill is the explicit instruction CLAUDE.md asks for before
pushing: for one PR at a time, you may push to that PR's branch, force-pushing
with --force-with-lease. Nothing on main, and no merges.
The loop
Survey → pick one → rebase → watch → fix → it merges itself → survey again.
Stop and report instead of continuing when a failure needs a decision the user
has to make.
1. Survey
gh pr list --state open --json number,title,headRefName,baseRefName,isDraft,author,mergeStateStatus,autoMergeRequest \
--jq '[.[] | select(.autoMergeRequest != null)]'
That filter is the scope rule, and a draft PR cannot have auto-merge, so it does
most of the exclusion by itself. Still drop any PR whose baseRefName is not
main. Releases here go through semantic-release straight from main, so
unlike a Changesets repository there is no bot-authored version PR to skip.
chore/pnpm-update does have auto-merge, enabled by the bot, so it is in scope —
but pnpm-update.yml force-pushes that branch daily. If it moves under you, do
not fight it: re-survey and take its new state.
mergeStateStatus says what is blocking each one:
BEHIND — exactly the "out-of-date with the base branch" banner. This is the
one the loop rebases.
BLOCKED — up to date, but a required check is failing or still pending, or a
review is missing. Go to step 3 or 4; do not rebase it, that only restarts the
same matrix.
DIRTY — the rebase will conflict. Step 2b.
UNKNOWN — GitHub has not finished computing the merge state. Query again a
few seconds later; do not read it as up to date.
CLEAN — nothing is blocking it and auto-merge is on, so it is already
merging. Leave it alone.
2. Pick exactly one, and rebase it
Take whichever PR the user named; otherwise the one most likely to go green
unattended — small, already reviewed, oldest first among equals. Everything else
waits, untouched. A PR sitting at BEHIND costs nothing.
2a. The normal case
gh pr update-branch --rebase <number>
This rebases server-side. It does not touch the working tree, needs no checkout,
and fails cleanly instead of leaving a half-finished rebase behind. It does not
merge anything, and it leaves auto-merge armed. Confirm the head commit actually
moved and that checks are queued on the new SHA:
gh pr view <number> --json headRefOid,mergeStateStatus,autoMergeRequest
gh pr checks <number>
If the local clone has that branch checked out, it is now stale. Only resync it
when git status --porcelain is empty — never discard uncommitted work:
git fetch origin <branch> && git reset --hard "origin/<branch>"
2b. When it conflicts
gh pr update-branch fails, or mergeStateStatus was DIRTY. Rebase locally,
from a clean working tree:
git fetch origin main
git switch <branch>
git rebase origin/main
git push --force-with-lease="<branch>:<sha-before-the-rebase>" origin <branch>
Name the expected SHA in --force-with-lease. A bare --force-with-lease
compares against the remote-tracking ref, which is only as fresh as the last
fetch. Resolve conflicts by keeping the intent of both sides; if the resolution
is not obvious, stop and ask rather than guessing. git rebase --abort puts
everything back.
pnpm-lock.yaml is the usual conflict on chore/pnpm-update. Do not hand-edit
it: take one side, then run pnpm install --no-frozen-lockfile and commit what
it writes.
Note that a force-push does not disarm auto-merge; if autoMergeRequest did come
back null, say so and stop rather than re-enabling it.
3. Watch the checks
Every workflow here triggers on push, so the rebase started a fresh matrix.
Watch it in the background:
gh pr checks <number> --watch --fail-fast --interval 30
--required narrows the output to the checks that gate the merge;
github/rulesets/main.json is that list, and it is shorter than what the PR
page shows. test (test:cov), test-typescript-versions (*), codecov/* and
Validate PR title all run but are not required, so a red one there does not
hold the merge — fix it anyway, but do not report the PR as blocked on it.
When the required checks all pass, GitHub squashes and merges the PR on its own
(allow_auto_merge, squash_merge_commit_title: PR_TITLE) — that is the
intended ending, and nothing here should race it.
Do not start the next PR while this one is being watched.
4. Fix a failing check
Find the failing job and read only its failing step:
gh pr checks <number> --json name,bucket,link --jq '.[] | select(.bucket=="fail")'
gh run view --job <job-id> --log-failed
The check name is the command. style-check (X), lint-and-build (X) and
test (X) all run pnpm run X, so lint-and-build (codemod:full) reproduces as
pnpm run codemod:full. Four checks do not follow that rule:
| Check | What to run |
|---|
test-node-versions (<ver>) | pnpm run build then pnpm run test on that Node |
test-typescript-versions (<ver>) | pnpm run build:min, then npx -y -p typescript@<ver> tsc -p ./test/dist/named/tsconfig.json |
backup-repository-settings | pnpm run repo-settings:backup, then look for a dirty tree |
Validate PR title | The PR title is not Conventional Commits — gh pr edit --title |
A squash merge takes the PR title as the commit title
(squash_merge_commit_title: PR_TITLE), so the fix for that last one is the
title itself, not the branch.
Three things about reproducing the rest:
test (*) runs pnpm run build:min first in CI. Do the same locally, or
the tests run against a stale dist/ and fail for the wrong reason.
style-check (doc) and style-check (check:ext) do not build, so they read
whatever dist/ you already have.
test (test:browser) needs Playwright — pnpm exec playwright install chromium — and CI retries it once before calling it failed, so a single red
run there may just be flake. Re-read the log before changing code.
- Every
style-check, lint-and-build and test job ends with
assert-repo-is-clean. So fmt:full, doc, codemod:full and lint:fix
fail by changing files, and the fix is to run the command locally and commit
what it wrote. A green run of the command with a dirty tree afterwards is still
a failure.
Reproduce locally before pushing — a speculative fix costs another full matrix,
which is the cost this whole loop exists to avoid. Fix the cause: CLAUDE.md
rules hold, so no file-level eslint-disable, no loosening eslint.config.mts,
no as any. Then pnpm run fmt,
commit with a Conventional Commits message, push to the branch, and go back to
watching. Auto-merge survives the push, so a green result merges the PR without
another command. Do not re-rebase for a fix unless main has moved.
lint-and-build, test and the two compatibility matrices all gate on
check-should-run-type-checks, which diffs against origin/main. A job that
reports green having skipped its steps is expected on a docs-only branch, not a
problem to chase.
5. It merged — then go round again
Confirm with gh pr view <number> --json state,mergedAt. The remote branch is
deleted automatically (delete_branch_on_merge); locally, git fetch --prune,
and if that branch was checked out, switch back to main and pull.
If every required check is green and the PR is still open, something outside this
skill's two jobs is holding it — a missing review, a required check that never
reported, auto-merge disabled behind your back. Report that and stop. Do not
merge it.
Once it does merge, main has moved and every remaining PR reads BEHIND again.
That is expected, not a regression. Return to step 1 and pick the next single PR.
6. Report
One line per PR, in the order handled: number, what was done (rebased and merged
by GitHub / rebased and waiting / fix pushed / left alone and why), and where its
checks stand. Name any PR left failing and what the failure is. Do not report a
run as green while checks are still pending, and say plainly which PRs were never
reached and which were out of scope for lacking auto-merge.