Autonomous PR monitor and fixer. Triages open PRs by tier, addresses thread feedback, fixes CI failures, and enables auto-merge when the 4-condition Ready-to-Merge gate passes.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
La commande reste sur une seule ligne. Faites défiler horizontalement pour la vérifier avant de la copier.
Vous préférez une copie locale ? Téléchargez les fichiers actuellement disponibles dans SkillsMP.
Affichage de SKILL.md
SKILL.md
Instructions source · Aperçu en lecture seule
name
pr-autofix
description
Autonomous PR monitor and fixer. Triages open PRs by tier, addresses thread feedback, fixes CI failures, and enables auto-merge when the 4-condition Ready-to-Merge gate passes.
allowed-tools
Bash, Read, Edit, Write, Skill
size-exception
true
user-invocable
true
/pr-autofix
Autonomous PR monitor and fixer. This file carries the whole protocol,
including the Ready-to-Merge definition below. Nothing outside it is needed
to run the command.
Triggers
Trigger phrase
Operation
pr-autofix
Triage all open PRs by tier and act
autofix this pr
Single-PR mode on the current branch's open PR
monitor open prs
Periodic triage without merging
auto-merge ready prs
Tier 1 only: enable auto-merge on land-ready PRs
address pr feedback
Tier 3/4 only: walk thread lifecycle
Process
Three phases. Tier-based dispatch decides which actions apply per PR.
Phase 1: Triage
Run test_pr_merge_ready.py for every open PR. Classify each into a tier (T1-T5) using the table below. Sort the queue by tier ascending.
Phase 2: Act per tier
Walk the queue. For each PR, apply the tier's action set. T1 first (land-ready), then T2 (CI fix), then T3/T4 (threads), then T5 (bot).
Per-PR live-state gate (BLOCKING, issue #2455). Before any action runs on a PR (any tier: arming auto-merge, pushing a CI fix, posting a thread reply), call check_pr_live_state.py and branch on the JSON envelope Data.action field. The session-start triage snapshot is stale by the time the walk reaches each row in a repo with heavy merge automation, and the consequences of acting on a stale row are concrete: armed auto-merge on a redundant PR, conflict merges into a closed branch, duplicate logic landed twice.
Lease SKIP verdicts: when exit code is 1 the lease is held by another autofix
loop. Do NOT push, do NOT arm auto-merge, do NOT post threads. The held_by
field identifies the owner so the operator can investigate a stale lease.
LIVE-STATE SKIP verdicts are binding: do NOT push commits, do NOT arm
auto-merge, do NOT run merge_pr.py on a PR this gate classifies as SKIP.
The verdict's reason field names the cause (merged, closed, draft, fully
superseded by base) for the autofix log. An ACT verdict only proves the PR is
still actionable; the four-condition Ready-to-Merge gate still applies before
any merge.
Every command that mutates a branch or PR MUST run through
run_pr_mutation_if_live. This includes base fetches, merges, rebases, pushes,
auto-merge changes, and direct merges. The wrapper performs a new GitHub query
immediately before the command and compares the result with the head and base
identity captured by the current readiness cycle. A gate from an earlier review
or validation phase is stale. Exit 75 means the wrapper logged the live-state skip
and released the lease. Other nonzero exits come from the mutation command and
retain their existing error handling.
Phase 3: Verify and gate
After all queued actions, re-check the 4-condition Ready-to-Merge gate. Enable
auto-merge only when all four conditions hold. Release each PR's lease after its
merge command (or skip) completes:
Triage all open PRs into tiers T1-T5 using test_pr_merge_ready.py.
Process T1 (land-ready) first, then T2 (CI fix), T3/T4 (threads), T5 (bot).
Before acting on any PR, call check_pr_live_state.py and skip the row when it returns Data.action=SKIP (issue #2455). The triage snapshot from step 1 goes stale fast in a repo with heavy merge automation; the gate catches PRs merged/closed mid-walk and PRs whose diff is already on main via a sibling consolidated PR.
Before any branch mutation, acquire the branch lease via pr_autofix_lease.py acquire (issue #3413). A SKIP result means another session holds the branch; exit before creating a worktree or pushing. Release the lease via pr_autofix_lease.py release when done or on error. The lease is advisory; the Force-Push Safety SHA gate is the hard backstop.
For each PR that the live-state gate cleared: address review threads, fix CI failures using known patterns, then choose the merge path from the four-condition gate.
Ready-to-Merge Definition (4 conditions, ALL required)
Branch up to date with main (mergeStateStatus not BEHIND).
All required checks pass.
All conversations addressed: READ, TRIAGED, SOLVED (if Blocking), REPLIED with course of action, RESOLVED.
mergeStateStatus == CLEAN (or UNSTABLE with documented non-required failures).
CanMerge=True from test_pr_merge_ready.py alone is insufficient. Cross-check all four conditions.
Checkout ownership for the readiness helper (issue #2443): when a PR modifies files under .claude/skills/github/scripts/pr/, run test_pr_merge_ready.py from that PR's own worktree, not from a shared checkout. A shared checkout runs whatever helper version is on its disk, which may predate the branch's fix and yield a stale CanMerge verdict. The readiness output records a ScriptCommit field with the helper revision that produced the verdict; if it does not match the PR branch's helper commit, re-run from the PR worktree before trusting the result.
Tier Definitions
Tier
Criteria
Action
T1
Branch up to date, no CI failures, no threads, CLEAN
Use the CLEAN merge path after the four-condition gate
T2
CI failures only, branch up to date
Fix CI, verify required checks pass
T3
Threads only (CI passing)
Walk full thread lifecycle, then merge
T4
Both CI failures + threads
Fix CI first, then lifecycle threads
T5
Bot PR with validation failures
Handle individually
If BEHIND, update branch against main BEFORE other actions (see doc Branch Update section).
Fix Patterns
PR description mismatch: Remove file references not in the diff (use GitHub API to PATCH body).
Branch behind main: Run each base refresh command through the late live-state wrapper:
Stale merge-state cache: test_pr_merge_ready.py sets StaleDirtySuspected=true when GitHub reports mergeable == "CONFLICTING" or mergeStateStatus == "DIRTY". This is advisory, not authoritative. A PR can merge or close during the review-fix cycle; acting on an earlier ACT result triggers a conflict merge into a deleted branch. In a worktree, use run_pr_mutation_if_live git fetch origin "$BASE", then git merge-base --is-ancestor "origin/$BASE" HEAD (exit 0 = ancestor) and a guarded run_pr_mutation_if_live git merge --no-commit --no-ff "origin/$BASE" trial merge. Both clean means the conflict is stale. Disable existing auto-merge through the wrapper and verify autoMergeRequest is null before the final guarded merge and push (issue #3913). A failing trial merge means the conflict is real: resolve via merge-resolver agent. Evidence required: both live-state verdicts, the ancestry exit code, and the trial-merge result. See doc Stale merge-state cache section (issue #2368).
Stale CI check: Push fresh commit to re-trigger; avoid --no-verify if possible.
Bot review threads: Read, triage per Thread Severity, reply with disposition, resolve via add_pr_review_thread_reply.py --resolve.
Armed auto-merge + final thread: add_pr_review_thread_reply.py --resolve posts the reply, disables armed auto-merge when that thread is the final unresolved one, then resolves the thread. If the guard cannot prove the unresolved count, the script exits 3 after posting the reply and leaves the thread unresolved so GitHub cannot merge before the completion gate.
Session validation failure: Use session-log-fixer skill.
Force-Push Safety
Before any push: verify git rev-parse "refs/heads/$BRANCH" matches the PR's expected head.sha from get_pr_context.py. (Prefer rev-parse over plain-file reads of .git/refs/heads/<branch>: rev-parse resolves loose refs AND refs that have been compacted into .git/packed-refs; a plain-file read returns "missing ref" when the branch lives only in packed-refs.) If the local ref points to a bootstrap/sandbox commit, STOP. Investigate corruption before pushing. Force-push only with explicit user authorization, using SHA-pinned source with quoted refspec:
SHA="<known-good-sha>"
BRANCH="<branch-name>"# The head.sha you already read from get_pr_context.py before starting work.
EXPECTED_REMOTE_SHA="<observed-head-sha>"
run_pr_mutation_if_live env FORCE_PUSH_OK=1 git push origin "${SHA}:refs/heads/${BRANCH}" \
--force-with-lease="refs/heads/${BRANCH}:${EXPECTED_REMOTE_SHA}"
FORCE_PUSH_OK=1 is required, not optional. A pre-push hook cannot read argv,
so the repository's non-fast-forward guard sees a rewrite and exits 1 whether
or not a lease is pinned. The variable is the one escape the force-push rule
sanctions for a pinned lease, and it narrows that single guard while every
other pre-push job runs (issue #4293). This repository's safe-push helper sets
it for you, so prefer that helper over the raw command.
Pin the lease to an explicit SHA; never use bare --force-with-lease here.
Bare --force-with-lease takes its expected value from
refs/remotes/origin/$BRANCH, and any concurrent git fetch, including one run
by a sibling agent in the same checkout, silently advances that ref to the other
agent's commit. The lease then passes and the push destroys their work. Measured
on a two-clone repro: with a fetch between the two pushes the bare form
overwrote a sibling's commit, while
--force-with-lease=refs/heads/$BRANCH:<observed-sha> rejected the identical
push with stale info (issues #3653, #3413). The rev-parse check above is a
separate read and cannot close the window between check and push; only the
pinned lease is atomic.
Quote every variable expansion. The shell does not treat : specially in a refspec; the real reason to quote is that branch names can contain characters the shell DOES treat specially (*, ?, [, whitespace), and unquoted $BRANCH will word-split or glob on those.
GitHub refuses auto-merge for UNSTABLE PRs (issue #2439) and may also reject an already-CLEAN PR because there is nothing left to wait on (issue #2450). Pick the path that matches the state:
mergeStateStatus
Path
Script
CLEAN
Auto-merge when waiting is useful; direct merge if GitHub returns the already-clean rejection
Guard set_pr_auto_merge.py --enable, then guard the merge_pr.py --strategy squash fallback
UNSTABLE with documented non-required failures
Direct merge (immediate)
Guard merge_pr.py --strategy squash
BEHIND
Update branch first, then re-classify
Guard the fetch, merge, and push separately
DIRTY/CONFLICTING
See Stale merge-state cache pattern below
merge-resolver agent if real conflict
set_pr_auto_merge.py detects the UNSTABLE and already-CLEAN rejections from GitHub's GraphQL API and emits the direct-merge fallback command in its error output (exit 3) so the operator never has to translate the generic "GraphQL request failed" message themselves.
Merge-check exit codes: test_pr_merged.py
As of issue #2308, test_pr_merged.py exits 0 on any successful query
and reports merge state in the JSON merged field. This makes the script
behave like every other shell-friendly probe: exit 0 means "I answered your
question". Branch on the JSON, not the exit code.
Earlier history: the script used to exit 100 when the PR was merged
(Skill-PR-Review-007). Treating 100 as a failure caused wasted polling loops
on PRs #2240, #2269 (#2277), and made successful merge verification look
failed on PR #2289 (#2308).
Lease acquired before per-PR action (issue #3413): pr_autofix_lease.py acquire --pull-request $PR --session $SESSION_ID. Exit 1 = SKIP (another agent holds it); exit 0 = ACT. Lease released after PR work completes or on live-state SKIP.
Tier classification recorded (T1-T5).
Branch lease acquired via pr_autofix_lease.py acquire before any branch mutation (issue #3413). SKIP result caused early exit; ACT result recorded with base_sha.
Per-PR live-state gate ran immediately before the tier's action (issue #2455): check_pr_live_state.py --pull-request $PR --skip-fetch --output-format json. Verdict Data.action=ACT recorded; Data.action=SKIP aborted the action and recorded the reason (merged, closed, draft, or fully superseded by base).
Auto-merge disarm ran after live-state ACT on any non-T1 PR (issue #3913): auto_merge_method was null or set_pr_auto_merge.py --disable succeeded and returned AutoMergeEnabled: false before any push.
Live-state gate re-ran immediately before any base refresh or conflict resolution (issue #4349): stale gate result from the start of the session is not sufficient; the PR can merge mid-cycle.
Every base refresh, rebase, push, auto-merge change, and direct merge ran through run_pr_mutation_if_live immediately before mutation (issue #4349).
Late mutation checks matched the head SHA, base ref, and base SHA captured by the current readiness cycle; any mismatch stopped mutation and restarted readiness checks.
When StaleDirtySuspected=true: set_pr_auto_merge.py disable ran and autoMergeRequest confirmed null before any base-ref refresh push (issue #3913).
All required CI checks pass (T2/T4 only).
Every review thread is READ, TRIAGED, SOLVED (if Blocking), REPLIED with course of action, and RESOLVED (T3/T4 only).
is (or with documented non-required failures).
"$PR"
"${EXPECTED_HEAD_SHA:-}"
"${EXPECTED_BASE_REF:-}"
"${EXPECTED_BASE_SHA:-}"
then
else
fi
printf
'%s'
"$late_live"
'.Data.action // empty'
if
"$late_rc"
"$late_action"
"ACT"
then
return
fi
printf
'%s'
"$late_live"
'.Data.reason // "live-state check failed"'
printf
'%s'
"$late_live"
'.Data.state // "UNKNOWN"'
printf
'%s'
"$late_live"
'.Data.head_sha // "unknown"'
printf
'%s'
"$late_live"
'.Data.base_ref // "main"'
echo
"Skipping mutation for #$PR: $late_reason"
if
"$late_state"
"MERGED"
then
echo
"Merged head SHA: $late_head"
echo
"Preserve unpushed commits or a net patch. Reapply them on a follow-up branch from current origin/$late_base."
elif
"$late_state"
"CLOSED"
then
echo
"Closed PR head SHA: $late_head"
echo
"Preserve unpushed commits or a net patch before leaving the old branch."
fi
"$SCRIPTS_DIR/pr_autofix_lease.py"
"$PR"
"$SESSION_ID"
true
return
run_pr_mutation_if_live
if
then
"$@"
return
fi
return
# late-live-state-guard:end
# SESSION_ID must be set before the loop (e.g. from the session log or a uuid).
# CLEAN path: try auto-merge only when there is pending branch-protection work to wait on.
# If GitHub rejects an already-CLEAN PR with "clean status", use the printed direct-merge fallback.
"$SCRIPTS_DIR/set_pr_auto_merge.py"
pr
enable
# Direct merge: already-CLEAN fallback or UNSTABLE state with documented non-required failures.
"$SCRIPTS_DIR/merge_pr.py"
pr
mergeStateStatus
CLEAN
UNSTABLE
Branch is up to date with main (mergeStateStatus not BEHIND).
Force-push safety check ran before any push: git rev-parse "refs/heads/$BRANCH" matched the PR's expected head.sha.
Correct merge path chosen by state: set_pr_auto_merge.py --enable for CLEAN, merge_pr.py --strategy squash for UNSTABLE with documented non-required failures (see "Merge path by mergeStateStatus" table; issue #2439).
All four Ready-to-Merge conditions hold before the merge command runs (CanMerge=True is insufficient alone).