rebase-pr
Skill2 starsChanged 4 months agoMITView on GitHub
This skill should be used when the user asks to "rebase pr", "rebase onto main", "rebase this branch", "rebase and force push", "resolve rebase conflicts", "update the branch from main", "my PR has conflicts", "bring the PR up to date with main", or wants a PR branch rebased onto its base branch with conflicts resolved by hand, the project's quality gate re-run, and a safe force-push. Also triggered by the /rebase-pr command.
- Commits and pushes
2 other projects use a version of this.
Adopted by
Most recently taken up by
- dmzoneill/skillstheir own version, first seen Oct 24, 2026
- zzacong/agent-skillstheir own version, first seen Oct 4, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- dmzoneill/skills★ 2same name
- zzacong/agent-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository20 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- auto-merge-dependabotSkill
- best-ofSkill
- codestorySkill
- codex-2nd-opinionSkill
- cpSkill
- extract-design-systemSkill
- investigateSkill
- is-skillSkill
- pm-autofix-prSkill
- pm-crSkill
- pm-deepenSkill
- pm-planSkill
- pm-riskSkill
- pm-simplifySkill
- pm-triageSkill
- review-offlineSkill
- upscaleSkill
- vibeSkill
- wigoSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
15 rules. About 0 are common, 3 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 62
Every call passes -R "$PR_REPO" explicitly (resolved in Step 1) rather than relying on gh's ambient repo resolution.
- Very specific: line 143
Otherwise: look it up. Query every pair in REPO_CANDIDATES and require exactly one match across the combined results — do not stop at the …
- Very specific: line 186
Check both sides and enumerate each: git remote get-url --all <r> for fetch, git remote get-url --push --all <r> for push.
- Distinctive: line 259
Read the project's own instructions first — CLAUDE.md / AGENTS.md.
- Distinctive: line 326
Verify before continuing: All three must hold:
- Distinctive: line 333
If any fails, go back to item 1 rather than continuing over a half-resolved tree.
- Very specific: line 338
If a replayed commit becomes empty because the base already contains it, GIT_EDITOR=true git rebase --skip, and record it for the Step 7 …
- Very specific: line 341
If a conflict cannot be resolved confidently — a semantic conflict whose correct outcome is genuinely unclear, or markers that cannot be …
- Very specific: line 348
Run each GATE command from Step 3 as its own invocation, in order, observing each exit status.
- Very specific: line 349
Never &&-chain them, and do not stop at the first failure — a full picture is what makes the next step's classification possible.
- Very specific: line 380
If the gate cannot be made green because a genuine regression resists fixing, stop with exit reason: gate-failure — do not force-push a …
- Very specific: line 438
No failures but any check in bucket: pending → not terminal.
- Very specific: line 441
Every check terminal (pass / skipping / cancel) and none failing → terminal success.
- Very specific: line 447
No checks reported at all → not yet conclusive.
- Very specific: line 450
exit reason: no-checks: if the set is still empty when it elapses.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rebase PR
Rebase a GitHub PR branch onto its base branch, resolve every conflict by hand, re-run the
project's quality gate, and force-push without clobbering a concurrent writer. One invocation,
gh CLI only, no GitHub MCP dependency.
Core Principle: Never Bulk-Resolve a Conflict
A rebase conflict is resolved file by file, by reading both sides and writing a coherent
result. Never run git checkout ORIG_HEAD -- ., git checkout --ours ., git checkout --theirs .,
git restore --source=... ., or any other bulk path operation while a rebase is in progress — each
one silently discards resolutions already made for the other files in the same conflict.
Core Principle: The Lease Is Taken Before the Rebase
--force-with-lease only protects a concurrent writer if the value it is armed with predates that
writer's push. Fetching the branch and then computing the lease anchor arms the lease with the
other writer's commit, so the push succeeds and destroys their work — the exact failure the lease
exists to prevent. Capture EXPECTED_REMOTE_SHA before the rebase (Step 2); every later fetch
of that branch is for inspection only. See references/force-push-safety.md.
Core Principle: Gate Steps Never Chain
Quality-gate commands run as separate invocations, never &&-chained. A flaky test must not
silently skip the build step that follows it — each step's own exit status has to be observed.
Prerequisites
ghCLI, installed and authenticated (gh auth status). This is the only GitHub dependency; the skill stops at preflight if it is missing. A GitHub MCP server is never required and never waited for (same posture aspm-autofix-pr).- A clean worktree. The skill refuses to start with uncommitted tracked changes — a rebase over a dirty tree is how resolutions get lost.
Configuration
Override via prompt arguments (e.g. /rebase-pr 42 --watch-ci --ci-budget 20).
| Parameter | Default | Description |
WATCH_CI | off | Whether to wait for post-push CI (Step 8) instead of exiting immediately after the push. Enable with --watch-ci. |
CI_BUDGET | 15 | Wall-clock ceiling (minutes) on the whole Step 8 wait, initialized once when Step 8 is entered. Only meaningful with --watch-ci. |
POLL_INTERVAL | 30 | Seconds between check re-fetches; also the --interval passed to gh pr checks --watch. |
GitHub CLI Commands Used
| Operation | Command |
| Preflight | gh auth status |
| Resolve the PR for the current branch | gh pr list -R "$PR_REPO" --head <branch> --state open --json number,headRefName,headRepository,headRepositoryOwner,baseRefName,url |
| Read PR head/base/mergeability | gh pr view <n> -R "$PR_REPO" --json number,headRefName,baseRefName,mergeable,headRepository,headRepositoryOwner,url,title |
| Report what was rebased | gh pr comment <n> -R "$PR_REPO" --body-file <tmpfile> |
| Wait for post-push CI (opt-in) | gh pr checks <n> -R "$PR_REPO" --watch --interval <POLL_INTERVAL> |
Every call passes -R "$PR_REPO" explicitly (resolved in Step 1) rather than relying on gh's
ambient repo resolution. On a checkout with several GitHub remotes and no gh repo set-default,
ambient resolution can prompt for the base repository — and this skill has no way to answer a
prompt. pm-autofix-pr Step 1 documents the same hazard at greater length.
gh pr view --json mergeable returns the GraphQL vocabulary — MERGEABLE, CONFLICTING,
UNKNOWN (uppercase) — not REST's mergeable/mergeable_state pair. Treat it as a hint about
whether a rebase is needed, never as the authority on whether one succeeded; git rebase's own exit
status is the authority.
<tmpfile> always means a path from mktemp (e.g. mktemp /tmp/rebase-pr-body-XXXXXX), never a
fixed literal, so two concurrent invocations cannot race on the same filename. rm -f it after.
Bundled Scripts
Invoke by absolute path. When loaded as a plugin that prefix is
${CLAUDE_PLUGIN_ROOT}/rebase-pr/scripts/; if ${CLAUDE_PLUGIN_ROOT} is unset (skill installed
standalone), resolve scripts/ relative to this SKILL.md. Both scripts are self-contained — this
skill installs on its own, so they share no library with any other skill in the repo.
scripts/capture-lease.sh <remote> <branch>— Step 2. Fetches the branch, prints the lease anchor, and classifies local-vs-remote divergence.scripts/safe-force-push.sh <remote> <branch> <expected-sha>— Step 7. Re-inspects the remote, stands down if it moved, otherwise pushes with the lease armed to<expected-sha>.
Workflow
Step 0: Preflight
gh auth status. On non-zero exit, stop with:ghCLI not available. This skill requires the GitHub CLI (gh), authenticated. Install it from https://cli.github.com/ and rungh auth login, then re-run.git status --porcelain --untracked-files=no. If non-empty, stop and report the dirty paths — commit or set them aside first. Do not stash on the user's behalf: the stash stack is shared across worktrees and another session may pop it.- Confirm no rebase is already in progress: both
git rev-parse --git-path rebase-mergeandgit rev-parse --git-path rebase-applymust name a path that does not exist. If one does, stop — finish orgit rebase --abortthat rebase first.
Step 1: Identify the PR
BRANCH=$(git rev-parse --abbrev-ref HEAD). A detached HEAD stops the run. Also capture the tracked head name, which is what GitHub knows the branch as:TRACKED=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)→ strip the leading<remote>/. The two differ whenever the local branch was renamed (review-42trackingorigin/feature), a configuration Step 2 explicitly supports.Enumerate the configured remotes —
git remotefor the names, thengit remote get-url --all <r>for each — and reduce their URLs toowner/repopairs (stripgit@github.com:,https://github.com/, and a trailing.git). Deduplicate the pairs case-insensitively; GitHub owner and repository names are not case-sensitive. Call that setREPO_CANDIDATES, and record which remote each pair came from.Do not require a remote named
originorupstream. Git permits any remote name, and item 5 below already resolves the push and base remotes by URL match rather than by name. A perfectly ordinary layout — the base repository asorigin, the fork asforkor<username>, noupstreamat all — would otherwise dead-end: the origin query's head-repository filter discards the cross-repository PR, and a bullet keyed on the literal nameupstreamnever fires, so a checkout whose only irregularity is its remote names gets zero matches and a hard stop.Order
REPO_CANDIDATESbase-repository-first, since that is where PRs live: a remote namedupstreamfirst if present, thenorigin, then the remoteTRACKEDtracks, then the rest ingit remoteorder. The names only prioritize; none is required.If a PR number was given, resolve which repository it belongs to before reading it. A PR number is scoped to the repository the PR was opened against — the base repository, which in a fork checkout is not the fork. Query every pair in
REPO_CANDIDATESfor that number and require exactly one distinct base repository to answer — the same rule the auto-detect path below applies, for the same reason. Do not take the first that answers: PR numbers are per-repository, so a fork and its base can both have a PR N, and item 2's ordering is only a heuristic over remote names — with the base remote named something likecentraland the fork asorigin, the fork is queried first andPR_REPO/BASE_REFwould bind to its unrelated PR. Head-repository validation does not catch that, because the fork legitimately is inREPO_CANDIDATES; and if both PRs share the fork's head branch, Step 2 exits 0, Step 4 rebases onto the wrong base, and Step 7 force-pushes with the lease raising no objection. Item 2 already deduplicates the candidate pairs, so a remote aliasing the same repository does not trip the exactly-one rule. More than one distinct base repository answering → stop and say so; the argument may then be given asowner/repo#Nor a full PR URL, which names the repository outright and resolves the ambiguity. Read the PR withgh pr view <n> -R "$PR_REPO" --json number,headRefName,baseRefName,mergeable,headRepository,headRepositoryOwner,url,title, then validate thatheadRepository.nameWithOwneris inREPO_CANDIDATES(item 5 resolves it to a specific remote); if it matches none, stop rather than pushing somewhere unverified.Otherwise look it up. Query every pair in
REPO_CANDIDATESand require exactly one match across the combined results — do not stop at the first that answers. A fork branch can carry a PR against the fork and a PR against upstream at the same time;-Rscopes each query to one repository, so a first-answer-wins order can silently select the fork-local PR, takeBASE_REFfrom it, and then let Step 7 force-push the shared head branch — rewriting the upstream PR onto the wrong base, with the lease raising no objection because the head branch never moved.For each candidate pair, run
gh pr list -R <pair> --head <name> --state open --json number,headRefName,headRepository,headRepositoryOwner,baseRefName,urlonce per branch name (below), then keep only PRs whoseheadRepository.nameWithOwneris itself inREPO_CANDIDATES— i.e. the head lives in a repository this checkout actually has a remote for. That filter is load-bearing, not belt-and-braces:--headmatches on branch name alone (ghrejects theowner:branchqualifier), and a base repository's PR list includes every cross-repository PR opened from a fork — so an unfiltered lookup can return a stranger's PR whose head branch merely shares a name with yours. Rebasing and force-pushing someone else's PR is not a recoverable mistake. Match the fullowner/repopair, never the owner alone: one account can own several repositories carrying the same branch name.Query both branch names.
gh pr list --headfilters on the PR's head branch and does not translate a local alias, so a renamed checkout searched by$BRANCHalone finds nothing and the run stops — even though the skill supports that configuration. Query$BRANCHand$TRACKEDwhen they differ. Union rather than replace:git checkout -b foo origin/mainleaves@{u}pointing atmain, and querying--head maininstead offoocould match a stranger's PR against the default branch.Deduplicate the combined matches by
(base repository, PR number)before applying the exactly-one rule. Two remotes frequently name the same repository — a maintainer who cloned the base directly and also added anupstreamalias for it — and$BRANCH/$TRACKEDcan both match the one real PR; the candidate-set dedupe in item 2 handles the former, this handles the rest. Without it, discovery would stop as falsely ambiguous on entirely ordinary setups.Zero matches after filtering, or more than one from distinct base repositories, → stop and say so; never guess. On exactly one match, set
PR_REPOto the repository whose query produced it.- Capture
PR_NUMBER,HEAD_REF(headRefName), andBASE_REF(baseRefName) — the base branch is whatever the PR says it is, not a hardcodedmain. - Resolve the two remotes by matching
git remote -vURLs (stripgit@github.com:,https://github.com/, and a trailing.git):PUSH_REMOTE— the remote every endpoint of which resolves toheadRepository.nameWithOwner, the fullowner/repopair of the head repository. This is where the force-push goes; for a fork PR that is the fork, not the base repository. If no remote matches the pair, stop rather than guessing — a lease is only as meaningful as the ref it is armed against.Check both sides and enumerate each:
git remote get-url --all <r>for fetch,git remote get-url --push --all <r>for push.- Both sides, because
git remote -v's(fetch)and(push)rows name different repositories onceremote.<r>.pushurlis set, and matching either alone can select a remote that pushes to the fork but fetches from upstream. The lease uses both halves — the scripts fetch the branch through the fetch URL to capture and compare the anchor, while the rewrite goes out through the push URL. - Enumerate, because a remote may carry several
remote.<r>.urlorremote.<r>.pushurlentries andgit pushwrites to all the effective ones, while plainget-urlprints only the first — so a single-URL check inspects one of N destinations while claiming to have validated the remote. Getting this wrong is not a clean failure:safe-force-push.shpushes by remote name, so the intended repository is rewritten and the second destination rejects, and Step 7 then reports exit 5 "push rejected" over a rewrite that already published. - Match the full
owner/repopair, never the owner alone: one account can own several repositories carrying a branch of this name, and the owner-only test cannot tell them apart — the lease would be captured against, and the push aimed at, the wrong one, safely overwriting a branch that has nothing to do with this PR.
A split remote is otherwise fail-safe rather than destructive — a missing branch exits 2, a foreign same-named branch exits 3 or 4, and git's own
--force-with-lease=<branch>:<sha>check rejects a stranger-derived anchor as stale info — but the diagnosis is misleading, so validate up front.- Both sides, because
BASE_REMOTE— the remote holding the base repository, resolved the same way by URL. For a same-repo PR it is the same remote asPUSH_REMOTE. It is fetch-only, so only its normal URLs need to resolve to the base repository. If no local remote points there, use the clone URLhttps://github.com/<base_owner>/<base_repo>.git;git fetchaccepts a URL in place of a name.
- The local branch name need not match
HEAD_REF— a fork checkout often names it differently, and Step 7 targetsHEAD_REFonPUSH_REMOTEexplicitly. What must be established is that this checkout is the PR branch, which Step 2 verifies: descending from the PR head is not proof of being it. A follow-up branch cut from the PR tip descends from it too, and force-pushing that HEAD would publish commits the PR never contained — with the lease raising no objection, since the remote has not moved.
Step 2: Capture the lease anchor, then fetch the base
scripts/capture-lease.sh "$PUSH_REMOTE" "$HEAD_REF"stdout is EXPECTED_REMOTE_SHA. Act on the exit code:
- 0 — the local tip equals the remote head. Proceed.
- 3 — the local branch is strictly ahead (unpushed local commits) and tracks
<PUSH_REMOTE>/<HEAD_REF>, so it really is the PR branch. Proceed, and say in Step 7's comment that the push also published those commits. - 5 — HEAD is ahead of the PR head but this checkout does not track it: it is a different branch
that merely descends from the PR tip. Stop with
exit reason: branch-mismatch. Check out the PR head first (gh pr checkout <n>) and re-run; do not push a HEAD whose identity as the PR branch was never established. - 4 — the local branch is behind or has diverged: another writer pushed to this branch. Stand down. Report their commits (the script prints the range) and stop. Do not reset, do not fast-forward silently — reconciling someone else's commits with the local ones is the user's call.
- 2 — usage error, missing remote branch, or an unreachable remote. Relay stderr and stop.
Record EXPECTED_REMOTE_SHA and ORIGINAL_HEAD=$(git rev-parse HEAD). Then fetch the base:
git fetch "$BASE_REMOTE" "refs/heads/$BASE_REF"
BASE_SHA=$(git rev-parse FETCH_HEAD)refs/heads/ is not optional. A bare "$BASE_REF" is a refspec, and git's ref-resolution rules
try refs/tags/ before refs/heads/ — so in a repo carrying a tag named like the base branch,
git fetch <remote> main writes the tag's commit to FETCH_HEAD (* tag main -> FETCH_HEAD;
reproduced on git 2.55). BASE_SHA would then be the tag, Step 4 would rebase the PR onto it, and
Step 7's lease would raise no objection — the head branch never moved. The PR gets silently rewritten
onto the wrong history, which is also what would break Step 4's claim that rebasing $BASE_SHA pins
the operation to what Step 2 fetched. Both bundled scripts fetch refs/heads/<branch> for the same
reason.
Step 3: Discover the quality gate
Read the project's own instructions first — CLAUDE.md / AGENTS.md. Collect them from two
directions, because either alone misses real rules:
- Baseline: walk from the working directory up to the repo root.
- Scoped: list the paths this PR actually changes,
git diff --name-only "$BASE_SHA"...HEAD, and for each one walk its directory up to the repo root, collecting everyCLAUDE.md/AGENTS.mdon the way. Deduplicate (symlink-aware —AGENTS.mdis frequently a symlink toCLAUDE.md).
The upward-only baseline is what misses a monorepo: run from the repo root, it never descends into
packages/foo/AGENTS.md, so a PR touching only that package would be gated by root-level checks
while the package's own mandated checks never run — and then force-pushed. The changed-path walk is
what finds them.
Merge with closest-wins precedence: the repo-root file is the baseline, a nearer file overrides it on conflict and may add checks the baseline omits. When a scoped file changes or adds a command, surface a one-line divergence note — which file, which command — so the gate stays auditable rather than silently flattened. Use exactly the commands these files state. Only if they state none, infer from the manifests:
| Manifest | Gate commands (run in this order, separately) |
package.json | the format/lint/typecheck/test/build scripts that exist, via the lockfile's package manager (npm run, pnpm, yarn) |
pyproject.toml | uv run ruff format --check ., uv run ruff check ., uv run ty check, uv run pytest — or the hatch/bare-pytest equivalents when uv.lock is absent |
Cargo.toml | cargo fmt --check, cargo clippy, cargo test, cargo build |
go.mod | gofmt -l ., go vet ./..., go test ./..., go build ./... — gofmt -l recurses, but exits 0 even when it lists unformatted files, so judge that step by its output, not its exit status |
Makefile | whichever of make fmt, make lint, make test, make check, make ci exist |
.pre-commit-config.yaml | pre-commit run --all-files |
Re-run this discovery after Step 4's rebase, unconditionally. Everything it reads can change
underneath it: not just CLAUDE.md / AGENTS.md, but every manifest and lockfile in the table below
— a base that adds a lint script to package.json, or switches package manager by replacing the
lockfile, redefines the gate without touching an instruction file at all. Narrowing the trigger to
instruction files would leave GATE derived from the pre-rebase tree in exactly those cases. The
walk is a handful of file reads with no side effects, so re-running it costs nothing worth
conditioning on; if the merged command set comes back identical, say so in one line and move on.
The reason it must run twice rather than only once, later, is that this walk reads the checked-out
tree — at Step 3 that is still the pre-rebase PR branch, so a package's instruction file the base
added after the PR diverged is invisible here, and the gate would run without the checks that file
mandates before the branch is force-pushed. That is the same failure the paragraph above argues
against, arriving from the other direction. Post-rebase discovery is a strict superset: it sees
base-added and PR-added instructions and manifests, which reading them from BASE_SHA alone would
not.
Record the resulting list as GATE. Never assume npm run *. Also note any known-failure list the
project's instructions record — see references/quality-gate.md for why that list belongs in the
repo rather than in this skill.
Step 4: Rebase
git rebase "$BASE_SHA"Rebasing onto the fetched SHA (not <remote>/<branch>) pins the operation to exactly what Step 2
fetched. On exit 0 with no conflicts, go to Step 5.
On a conflict, loop until the rebase completes:
git diff --name-only --diff-filter=Ulists the conflicted files. Read every one.- Resolve each by hand. In a rebase the sides are inverted relative to a merge:
--oursis the base branch being replayed onto,--theirsis your own commit. Reason about content, not about which label sounds like "mine". Keep the PR's intent and the base's independent change; never discard a side wholesale, and never leave a<<<<<<<,=======, or>>>>>>>marker behind. - Bulk operations are forbidden here — see the first Core Principle.
git add -- <resolved paths>(name the paths; notgit add -A).- Verify before continuing. All three must hold:
git diff --name-only --diff-filter=U— empty (nothing unmerged left).git diff --name-only— empty (nothing resolved-but-unstaged).git grep -nE '^(<{7}|\|{7}|={7}|>{7})( |$)' -- <resolved paths>— no hits. The|||||||alternative matters: undermerge.conflictStyle = diff3orzdiff3git emits a base section delimited by it, and a pattern covering only the other three markers passes a resolution that still carries the base text. If any fails, go back to item 1 rather than continuing over a half-resolved tree.
GIT_EDITOR=true git rebase --continue. The override is mandatory: a real editor has nothing to attach to in a headless run and the command would hang. No git invocation in this skill may open an editor — baregit rebase -iis therefore forbidden, and the one place a todo list is needed (Step 6's autosquash) stubs bothGIT_EDITORandGIT_SEQUENCE_EDITOR.- If a replayed commit becomes empty because the base already contains it,
GIT_EDITOR=true git rebase --skip, and record it for the Step 7 comment.
If a conflict cannot be resolved confidently — a semantic conflict whose correct outcome is
genuinely unclear, or markers that cannot be cleanly removed — run git rebase --abort, confirm
git rev-parse HEAD is back at ORIGINAL_HEAD, and stop with exit reason: rebase-conflict,
naming the file and what was ambiguous. A half-rebased branch is never left behind and never pushed.
Step 5: Re-run the quality gate
Run each GATE command from Step 3 as its own invocation, in order, observing each exit status.
Never &&-chain them, and do not stop at the first failure — a full picture is what makes the next
step's classification possible.
Step 6: Classify failures, fix only regressions
For each failing command, classify it once (see references/quality-gate.md):
REGRESSION — the rebase caused it. Fix it and
git addthe fix. If it belongs to the branch tip,git commit --amend --no-edit. If it belongs to an earlier commit, do not reach forgit rebase -i— it opens an editor and hangs. Use the non-interactive equivalent:git commit --fixup=<sha> GIT_SEQUENCE_EDITOR=true GIT_EDITOR=true git rebase --autosquash "$BASE_SHA"(On git older than 2.45,
--autosquashneeds--interactivealongside it; withGIT_SEQUENCE_EDITOR=trueaccepting the already-reordered todo list, that stays non-interactive.) After any fix — amend, autosquash, or follow-up commit — re-run every command inGATE, not just the one that failed. A fix that satisfies the failing step can break a step that passed earlier: a lint fix that reformats a file the build reads, an autosquash that replays every later commit onto new content. Re-running only the failing command leaves that breakage unobserved, and Step 7 would then force-push it — contradicting this step's own closing rule, since the skill would not know the gate was broken. If the replay conflicts, Step 4's conflict rules apply unchanged. Adding a plain follow-up commit is always an acceptable alternative when the fix does not belong to any single commit.- PRE-EXISTING — it fails on the base branch too. Do not chase it. Verify once, with
evidence: on a clean worktree
git switch --detach "$BASE_SHA", run only the failing command, thengit switch -back to the branch. Record the evidence (command, exit status, one-line symptom) for Step 7's comment.
If the gate cannot be made green because a genuine regression resists fixing, stop with
exit reason: gate-failure — do not force-push a branch whose gate you know is broken.
Step 7: Force-push safely
scripts/safe-force-push.sh "$PUSH_REMOTE" "$HEAD_REF" "$EXPECTED_REMOTE_SHA"The script re-fetches the branch, compares the remote head against the anchor from Step 2, and only
then pushes with --force-with-lease=<branch>:<expected-sha>. Exit codes:
- 0 — pushed.
- 4 — the remote head moved off the anchor while the rebase ran. Stand down. The script prints the intervening commits; diff their change against yours and report both to the user. Do not re-run with a refreshed anchor: that is precisely the mistake this ordering prevents.
- 5 — the push was rejected anyway (branch protection, or a writer racing inside the push
window). Report the rejection verbatim; do not retry with
--force. - 2 / 3 — usage error, or the worktree went dirty / a rebase is still in progress. Fix and re-run.
Then comment on the PR with what actually happened — write the body to a mktemp file and post with
gh pr comment "$PR_NUMBER" -R "$PR_REPO" --body-file <tmpfile>, then rm -f it. The body covers: the base SHA
rebased onto, each conflicted file and the resolution taken, any skipped-as-empty commits, the gate
results, and each PRE-EXISTING failure with its evidence.
Step 8: Wait for CI (opt-in)
Skipped unless --watch-ci was passed; by default the run ends at Step 7 with the PR URL and a
summary. When enabled, reuse the pattern pm-autofix-pr already implements rather than inventing a
second one:
- Probe once:
gh pr checks --help 2>&1 | grep -q -- '--watch', andcommand -v timeoutfalling back tocommand -v gtimeout(the name Homebrew's coreutils installs on macOS). Both must succeed to use the blocking watch; record the binary asTIMEOUT_BIN. Initialize
CI_BUDGET_REMAINING = CI_BUDGET * 60once, here — seconds, since every consumer below is in seconds.CI_BUDGETitself is configured in minutes (see Configuration), so using it unconverted would expire the 15-minute default after about 15 seconds. Each wait isTIMEOUT_BIN <chunk>s gh pr checks "$PR_NUMBER" -R "$PR_REPO" --watch --interval <POLL_INTERVAL>where<chunk>ismin(CI_BUDGET_REMAINING, 300). Classify every exit status —timeoutpasses the child's status through unchanged, and here the status is the signal:Exit Meaning Action 0every check finished successfully terminal — done 124the chunk elapsed with checks still pending keep waiting if budget remains 8checks still pending (the non-watch read in item 3 reports this) keep waiting if budget remains any other nonzero unknown — do not guess terminal for the wait: re-read gh pr checks "$PR_NUMBER" -R "$PR_REPO" --json name,state,bucket,linkand let that decide (see below)The nonzero row deliberately does not name a cause, because the status alone cannot distinguish one.
gh help exit-codesgives1for any failure and4for "authentication required";2is a cancelled command; and since the call is wrapped inTIMEOUT_BIN, that binary's own125/126/127surface here too. A repo with no CI at all exits1("no checks reported on the … branch"). So decide from the re-read, not from the number. Classify on thebucketfield, whichghdocuments as categorizingstateintopass,fail,pending,skipping, orcancel—statealone carries no pending discriminator, and throwing that signal away is what would let the wait end before CI ran:- Any check in
bucket: fail→exit reason: ci-failed, naming them. - No failures but any check in
bucket: pending→ not terminal. Keep waiting while budget remains; only on exhaustion is thisci-timeout. (The exit-8row above already treats pending this way; the re-read must agree with it.) - Every check terminal (
pass/skipping/cancel) and none failing → terminal success.cancelgroups here deliberately:ghexits nonzero only on failed or pending counts, so a cancel-only rollup already exits0and ends the wait via the table's exit-0row without ever reaching this re-read — classifying it as unsuccessful here would make the two paths contradict each other. It also matchespm-autofix-pr, wherecancelledis the one non-success conclusion treated as informational. This wait decides terminality, not merge-readiness. - No checks reported at all → not yet conclusive. This is the ordinary push-to-registration
race:
ghexits 1 with "no checks reported" until the workflows register. Allow a bounded grace period —min(2 × POLL_INTERVAL, CI_BUDGET_REMAINING)— re-reading across it, and only declareexit reason: no-checksif the set is still empty when it elapses. The rebase and push have already succeeded either way; there is simply nothing to wait for. - The re-read itself fails (auth expired, API unreachable,
ghbroken) →exit reason: gh-unavailable, quotinggh's stderr. Never report failing checks the re-read did not show.
Distinguishing pending is also what keeps this aligned with
pm-autofix-pr, which gates on a terminal.conclusionper check run rather than on the absence of failures.What must not happen is the wait treating a red CI run as "still pending" and reporting a budget timeout instead. (
pm-autofix-prcan ignore the exit status because it treats the watch as a pure sleep replacement and always re-fetches authoritative state afterwards; Step 8's re-read plays the same role here — the status only decides when to stop waiting, never what happened.)Subtract each wait's actual elapsed time from
CI_BUDGET_REMAINING.- Any check in
- Without the blocking watch, fall back to one
sleepper pass plus agh pr checks "$PR_NUMBER" -R "$PR_REPO"re-read, applying the same exit classification as the table above. Sleepmin(POLL_INTERVAL, CI_BUDGET_REMAINING), not a flatPOLL_INTERVAL— an unclamped final sleep overshoots the advertised ceiling by up to one interval. - On budget exhaustion, exit degraded, not silently: post a PR comment naming what was rebased,
which checks are still pending, and the exact resume command (
/rebase-pr <n> --watch-ci), then stop withexit reason: ci-timeout.
Failure Modes
| Situation | Handled in | Exit reason |
gh missing or unauthenticated | Step 0 | stop at preflight |
| Dirty worktree, or a rebase already in progress | Step 0 | dirty-worktree |
| No open PR for the branch, or several | Step 1 | stop, no guess |
| Another writer pushed before the rebase started | Step 2, exit 4 | concurrent-writer |
| HEAD descends from the PR head but is not the PR branch | Step 2, exit 5 | branch-mismatch |
| Conflict cannot be resolved confidently | Step 4 | rebase-conflict (after git rebase --abort) |
| Gate red from a regression that resists fixing | Step 6 | gate-failure (no push) |
| Remote head moved while the rebase ran | Step 7, exit 4 | concurrent-writer |
| Push rejected despite a valid lease | Step 7, exit 5 | push-failure |
| CI red on the rebased branch | Step 8 | ci-failed (failing checks named) |
| The branch has no CI checks at all | Step 8 | no-checks (rebase and push already succeeded) |
gh cannot report check state (auth, API, broken CLI) | Step 8 | gh-unavailable (stderr quoted) |
| CI still pending when the budget runs out | Step 8 | ci-timeout (degraded comment posted) |
Consistency With the Sibling PR Skills
Two conventions are shared with pm-autofix-pr so the three PR workflows do not drift; when either
changes there, change it here too.
gh-first, MCP-optional (issue #44): the command table above isghonly, and a GitHub MCP server is never a precondition.pm-autofix-pr's "GitHub CLI Commands Used" section is the reference implementation.- Bounded CI waiting (issue #45): Step 8's probe, 5-minute watch chunks, single run-wide budget,
and degraded-exit comment mirror
pm-autofix-prStep 0b / Step 5f / Step 7. This skill keeps the wait opt-in and single-phase — it has no fix loop to re-enter, so it needs no per-push clock.
pm-autofix-pr resolves base-branch conflicts by merging the base in (no force-push, history
preserved for reviewers). This skill rebases and force-pushes. Pick by what the PR needs: merge when
review is in flight and the history is being read, rebase when a linear history is wanted and the
branch is safe to rewrite.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

