| name | pm-autofix-pr |
| description | This skill should be used when the user asks to "autofix pr", "fix pr locally", "fix ci failures", "fix review comments", "iterate on pr", "fix failing checks", "fix pr comments", "make ci green", "fix the build", "address reviewer feedback", or wants to iteratively fix CI failures and review comments on a GitHub PR from the local CLI. |
| user-invocable | true |
Autofix PR
Iteratively fix CI failures and address reviewer feedback on a GitHub PR until a true fixed point is reached — all CI green, the PR free of merge conflicts with its base branch, every feedback item triaged into one of three outcomes (FIX, DEFER, REJECT), and every feedback item has a reply documenting the outcome. A single invocation handles everything end-to-end without user input.
Core Principle: Three Outcomes per Feedback Item
Not every review comment deserves a code change in this PR, and not every rejected comment is worthless. Before touching code, evaluate every review thread, review summary, and PR conversation comment on its merits with two independent AI reviewers — the local host model (whichever harness you are running in) and a cross-harness model (Claude ↔ Codex; whichever one you are not). Each item is triaged into exactly one of:
- FIX — correct and in scope for this PR → change the code in this PR, reply with the commit.
- DEFER — correct but out of scope, or a minor/diminishing-returns nit not worth churn in this PR → file a tracking issue, reply on the PR with a link to the new issue.
- REJECT — wrong, unrelated, already-handled, or pure style preference with no project backing → reply with a rationale, no code change, no issue.
DEFER is the safety valve that lets the skill say "not now" to legitimate-but-low-value feedback without losing it. Use it for: pickiness on naming/style where the current code is reasonable, micro-optimizations, refactor requests for working code, doc requests for internal helpers, and anything correct but outside the PR's stated scope.
Core Principle: Always Reply
Every reviewer feedback item must get an explicit reply before the skill can converge. FIX gets a reply that says what was fixed, where it was fixed, and the commit that contains it. DEFER gets a reply with the rejection rationale plus a link to the filed tracking issue. REJECT gets a reply that says no code change was made and why. A missing reply is still unfinished work, even if the code and CI are already green.
Core Principle: Never Prompt the User
This skill runs end-to-end without asking the user anything once invoked. There is no "begin fixing?" confirmation, no "ambiguous feedback, how should I handle it?", no "pre-commit failed, retry?", no "merge conflict, resolve it?", no "stale loop, continue?", no "CI timeout, keep waiting?". Every decision point has a deterministic auto-action defined below; uncertain feedback defaults to DEFER (file an issue and let humans resolve later); merge conflicts with the base branch are auto-resolved by merging the base in (Step 5h); unrecoverable conditions exit cleanly with a final summary. The only way the skill stops mid-flight is by reaching the fixed point, hitting a hard precondition failure (missing GitHub MCP, missing cross-harness CLI, no PR for the branch), or hitting an unrecoverable error (unresolvable merge conflict, rebase conflict, persistent push failure). Each exit goes through Step 7's summary.
Prerequisites
- GitHub MCP server must be configured in the host session (Claude Code or Codex CLI). The skill stops at preflight if it isn't available.
gh CLI is still required for one thing only: fetching failed-job log tails (gh run view --job <id> --log-failed). The MCP has no equivalent. All other GitHub interaction goes through the MCP.
- Both harness CLIs must be installed:
claude (Claude Code, npm install -g @anthropic-ai/claude-code) and codex (Codex CLI, npm install -g @openai/codex). The dual-evaluator step calls whichever one is not the host. The skill stops at preflight if the cross-harness CLI is missing.
Configuration
Override via prompt arguments (e.g., /pm-autofix-pr 10 --ci-timeout 30 --monitor 0).
| Parameter | Default | Description |
|---|
MONITOR_DURATION | 10 | Minutes to watch for new issues after convergence. 0 to skip. |
CI_TIMEOUT | 20 | Minutes to wait for CI before aborting (no user prompt — exits through Step 7 with ci-timeout). |
POLL_INTERVAL | 30 | Seconds between PR-state re-fetches while waiting for CI or monitoring. |
LOG_TAIL_LINES | 500 | Lines of CI failure log to inspect. |
There is no iteration limit. The loop runs until one of: fixed point reached, stale loop detected, CI timeout, unresolvable merge conflict, rebase conflict, persistent push failure, or monitoring window elapsed. Every exit goes through Step 7. The skill never prompts the user.
MCP Tools Used
All GitHub interaction is direct MCP tool calls — no bundled scripts.
| Tool | Purpose |
|---|
mcp__github__get_me | Preflight gate; also returns the current user's login for self-comment filtering. |
mcp__github__list_pull_requests | Auto-detect the PR for the current branch. |
mcp__github__pull_request_read | Fetch PR details, check runs, review comments/threads, reviews, conversation comments, status, and mergeability (mergeable / mergeable_state). Polled every POLL_INTERVAL seconds while waiting on CI or in the monitoring window. |
mcp__github__add_reply_to_pull_request_comment | Post replies on inline review threads (FIX, DEFER, and REJECT replies). |
mcp__github__add_issue_comment | Post PR-level replies for review summaries and PR conversation comments. |
mcp__github__pull_request_review_write (method="resolve_thread") | Resolve threads after a fix is pushed. |
mcp__github__issue_write (method="create") | File a tracking issue for each DEFER outcome (out-of-scope, diminishing-returns, ambiguous, or automated-fix-failed). |
Workflow
Step 0a: Detect the host harness
This skill runs under either Claude Code or Codex CLI. The orchestrator (you) is the local host; the dual-evaluator step delegates the second opinion to the cross-harness CLI.
Self-identify before doing anything else:
- If you are Claude (Opus / Sonnet / Haiku) → host is
claude, cross-harness is codex.
- If you are Codex (GPT-5.x) → host is
codex, cross-harness is claude.
Verify the cross-harness CLI is installed: run command -v <cross-harness-binary> via Bash. If it's missing, stop immediately with:
<cross-harness> CLI not installed. This skill needs both harnesses to run the dual-evaluator step. Install it with npm install -g @anthropic-ai/claude-code (Claude) or npm install -g @openai/codex (Codex), then re-run.
Capture for use in evaluator prompts and rejection bodies:
LOCAL_LABEL — e.g. "Claude Opus 4.6" or "Codex GPT-5.4". Use the most specific identifier you know about yourself; fall back to the family name ("Claude", "Codex GPT-5.x") if unsure.
REMOTE_LABEL — the cross-harness model label. Same precision rule.
Per-host invocation table (referenced by Step 4):
| Host | Local Evaluator (clean-context spawn of own model) | Cross-harness Evaluator |
|---|
claude | Agent tool with model="opus" | Skill tool with skill="codex-2nd-opinion" |
codex | Bash: codex exec --full-auto --sandbox read-only --ephemeral - < /tmp/eval-XXXX (10-min timeout) | Bash: claude -p --permission-mode auto --output-format text < /tmp/eval-XXXX (10-min timeout) |
For Bash-based evaluator spawns, write the prompt to a mktemp /tmp/eval-XXXXXX file, run the command with stdin redirection, capture stdout, then rm -f the temp file.
Step 0b: Preflight — verify the GitHub MCP
Call mcp__github__get_me. If the tool is unavailable in the session or the call errors, stop immediately with this message:
GitHub MCP not available. This skill requires the GitHub MCP server. Enable it in your host's MCP settings (.mcp.json, ~/.claude/settings.json, or the Codex equivalent) and re-run. See https://github.com/github/github-mcp-server for setup.
Do not fall back to gh for the workflow. On success, capture login as GH_USER (used to filter out self-authored comments later).
Step 1: Identify the PR
- Get the current branch:
git rev-parse --abbrev-ref HEAD.
- If a PR number was provided as argument, resolve its repo via
mcp__github__pull_request_read method=get (requires owner/repo — derive them from origin as in step 3a; if the resolved PR's head.ref doesn't match the local branch, warn and continue with the user's explicit number). Then skip the auto-detect in step 3 and go straight to step 4 so the same validation and base capture (base.ref, base.repo, BASE_REMOTE) runs — otherwise Step 5h's git fetch <BASE_REMOTE> <base.ref> would run with BASE_REMOTE unset even for a same-repo PR.
- Auto-detect the PR. Try the following resolution strategies in order and stop at the first that yields exactly one open PR whose
head.ref matches the local branch:
- 3a. Origin lookup. Parse
git remote get-url origin to {owner}/{repo} (strip git@github.com:, https://github.com/, trailing .git). Call mcp__github__list_pull_requests with head={owner}:{branch}, state=open, perPage=5.
- 3b. Upstream lookup (fork workflow). If step 3a returned no PRs and
git remote get-url upstream exists, parse it the same way to {upstream_owner}/{upstream_repo} and call mcp__github__list_pull_requests against that repo with head={origin_owner}:{branch} (PRs from a fork use the fork owner as the head prefix).
- 3c.
gh pr view fallback. If both MCP lookups fail and gh is available, run gh pr view --json number,headRepositoryOwner,headRepository,baseRepositoryOwner,baseRepository,url to let gh resolve the base repo via git config. On success, treat the returned baseRepositoryOwner.login / baseRepository.name as the PR's owner/repo. If gh is not installed or returns nothing, stop and tell the user there is no open PR for the current branch.
- Validate — this runs for both the explicit-number path (step 2) and the auto-detected path (step 3) — by calling
mcp__github__pull_request_read method=get on the resolved {owner, repo, pullNumber} to retrieve title, body, head.ref, head.sha, base.ref, base.repo (the base repository's full_name), and url. Confirm head.ref matches the local branch. Capture base.ref for Step 5d's first-push fallback and Step 5h's base-branch merge, and capture BASE_REMOTE — the fetch source for the base repository, which is not always origin. Resolve it by matching git remote -v URLs to {base_owner}/{base_repo} (apply the same git@github.com: / https://github.com/ / trailing-.git stripping as 3a): origin on the 3a origin path, upstream on the 3b fork path, and on the 3c gh path the remote matching baseRepositoryOwner/baseRepository. If no local remote points at the base repository (common for a fork checkout with no upstream), set BASE_REMOTE to the base repo's clone URL https://github.com/{base_owner}/{base_repo}.git — git fetch accepts a URL in place of a remote name, so Step 5h works either way.
Step 2: Read Project Pre-commit Requirements
Find CLAUDE.md (or AGENTS.md) by walking from working directory to repo root. Extract only explicitly stated pre-commit commands: format, lint, type-check, test, build. If none are stated, skip pre-commit checks entirely.
Step 3: Fetch PR State
Issue these MCP calls (paginate where applicable) and merge into a single state object:
| State field | Source |
|---|
head_sha | pull_request_read method=get → head.sha |
has_merge_conflict | pull_request_read method=get → mergeable (bool or null) and mergeable_state (string). If mergeable is null, GitHub is still computing mergeability in the background — re-fetch method=get up to 3 times with a 3-second sleep between tries, and only treat a definitive true/false as authoritative. Set has_merge_conflict = true when mergeable == false or mergeable_state == "dirty" (the PR conflicts with its base branch). Any other terminal state (clean, blocked, behind, unstable, has_hooks, unknown) sets has_merge_conflict = false — those don't indicate a content conflict and don't block the fixed point. If mergeable is still null after the re-fetches, leave has_merge_conflict = false but record the indeterminate state in errors so Step 5g re-fetches instead of declaring a false fixed point. |
ci_failures | pull_request_read method=get_check_runs → keep entries whose conclusion ∈ {failure, timed_out, cancelled, startup_failure, action_required}. For each failure whose app.slug == "github-actions", mark fixable=true and fetch the log tail with the Bash command shown just below this table (kept out of the cell so the shell pipe stays a real pipe). Other failure types are non-fixable — report them. |
review_threads | pull_request_read method=get_review_comments (paginate via perPage=100, after). Split into unresolved = [t for t in threads if not t.isResolved] and resolved_thread_ids = [t.id for t in threads if t.isResolved]. For each unresolved thread, take the last non-self element of comments (sorted by createdAt if order is not guaranteed and author.login != GH_USER) as latestReviewerComment. Drop self-only threads whose latestReviewerComment is absent from feedback_items; they are author notes, not reviewer feedback, and must not be dereferenced later. |
review_summaries | pull_request_read method=get_reviews. Apply supersession (see below). |
pr_comments | pull_request_read method=get_comments. Drop entries where user.login == GH_USER. |
The ci_failures log-tail fetch lives here rather than inside the table cell
above: an escaped \| renders correctly on GitHub but is a literal argument, not
a pipe operator, when this raw Markdown is executed — so the command is kept in a
fenced block where the pipe stays a real shell pipe.
gh run view --job <check_run.id> --log-failed 2>&1 | tail -<LOG_TAIL_LINES>
Supersession algorithm for reviews: group reviews by user.login. Within each group, sort by submitted_at ascending. Find the index of the latest APPROVED or DISMISSED review (or -1 if none). Discard everything at or before that index. From the remainder, keep only CHANGES_REQUESTED or COMMENTED reviews with non-empty body and user.login != GH_USER. The result is the actionable summary list.
Errors: if any MCP call fails, accumulate the error message into an errors list. Do not abort — downstream steps tolerate partial state and re-fetch.
Build feedback_items from:
- unresolved review threads with a non-null
latestReviewerComment, keyed as thread:<thread.id>
review_summaries, keyed as review:<review.id>
pr_comments, keyed as comment:<comment.id>
Initialize ADDRESSED_THREAD_IDS with resolved_thread_ids. Initialize REPLIED_ITEM_KEYS = {} for review summaries and PR conversation comments that already received an outcome reply during this invocation. Initialize OUTCOME_MARKERS = {} (item_key → latest_reviewer_marker_at_outcome) covering both REJECTED and DEFERRED items so a later reviewer edit re-enters evaluation. Initialize DEFERRED_ITEMS = [] (one entry per filed tracking issue, used by Step 7's summary).
Print the initial assessment as a status line — Found N CI failures, M reviewer feedback items, and merge conflicts: yes/no. Begin processing. — and proceed unconditionally. Never wait for a confirmation: the skill is fully automatic from this point on.
If there is nothing to fix (no CI failures, no unanswered feedback), has_merge_conflict is false, and the errors list is empty, report the PR is clean and proceed to Step 6 (monitoring). If has_merge_conflict is true, go to Step 5h even when there is no other work. If errors is non-empty — including an indeterminate mergeable == null recorded in Step 3, or any failed Step 3 MCP fetch — do not declare the PR clean and do not enter monitoring; fall through to Step 5g's retry path (report the fetch failures and retry after 30 seconds) so the ambiguous state resolves before any success exit. A false "clean" here would otherwise skip Step 5g's error gate entirely under --monitor 0.
Step 4: Evaluate Every Feedback Item
This is the most important step. Every time PR state is fetched, evaluate reviewer feedback before waiting on CI. Do not defer review handling until checks finish.
For each feedback item not already answered, gather context, then spawn two subagents in parallel. Inline threads are already answered when thread.id ∈ ADDRESSED_THREAD_IDS; review summaries and PR conversation comments are already answered when item_key ∈ REPLIED_ITEM_KEYS; previously rejected or deferred feedback is already answered when item_key ∈ OUTCOME_MARKERS and its reviewer marker has not changed.
- Inline review threads: use
latestReviewerComment, then read the referenced file and code context.
- Review summaries: parse the body into concrete asks; read the PR diff, PR description, and any files mentioned by the review.
- PR conversation comments: parse the body into concrete asks; read referenced files, logs, or diff context as needed.
-
Local Evaluator — runs the host model in a clean context. Use the row from Step 0a's per-host invocation table that matches your host:
- Claude host: Agent tool with
model="opus".
- Codex host: Bash with
codex exec --full-auto --sandbox read-only --ephemeral - < /tmp/eval-XXXXXX (10-minute timeout). Write the prompt via mktemp first; rm -f after.
Provide the comment, code context, PR title/description, and changed files summary. Ask for a FIX / DEFER / REJECT verdict with category, confidence, and reasoning. See references/comment-evaluation.md for the full prompt template.
-
Cross-harness Evaluator — runs the other model. Use the matching row from Step 0a's invocation table:
- Claude host: Skill tool with
skill="codex-2nd-opinion" (the user-level skill in this repo, frontmatter name: codex-2nd-opinion).
- Codex host: Bash with
claude -p --permission-mode auto --output-format text < /tmp/eval-XXXXXX (10-minute timeout; --permission-mode auto keeps claude from prompting when run headless inside the loop). Same mktemp / rm -f discipline as above.
Pass the same evaluation prompt as the Local Evaluator. Ask for the same verdict format.
Claude host — DO NOT invoke any of the following — they look superficially related but are the wrong tool and will produce different output:
codex:rescue / Skill tool with skill="codex:rescue" — this delegates rescue/fix work, not opinion-gathering.
codex:codex-rescue — the rescue subagent in the Agent tool, same problem.
codex:setup, codex:codex-cli-runtime, codex:gpt-5-4-prompting, codex:codex-result-handling — internal helpers, not user-facing review tools.
The only correct invocation is the Skill tool with skill="codex-2nd-opinion". If codex-2nd-opinion is not in the available-skills list, stop and report — do not substitute another skill.
Codex host — DO NOT call codex exec again as the cross-harness evaluator (that is the Local Evaluator). The cross-harness step must be claude -p, never another codex exec.
Decision logic (from references/comment-evaluation.md):
| Local | Cross-harness | Action |
|---|
| FIX | FIX | FIX — apply code change in this PR |
| FIX + DEFER | (either order) | FIX — both agree the feedback is valid; apply it now instead of filing an issue |
| REJECT | REJECT | REJECT — reply with rationale, no code change, no issue |
| DEFER | DEFER | DEFER — file tracking issue, reply with link |
| any other combination | | DEFER — file tracking issue (any remaining disagreement, all involving a REJECT vote, defaults to DEFER) |
The rule leans toward action without being reckless: fix when both evaluators agree the change belongs in this PR, and also when one votes FIX and the other DEFER — both consider the feedback valid, so the disagreement is only about timing and fixing now avoids issue churn. Only reject when both agree there is no concern worth tracking. Every remaining disagreement (any combination with a REJECT vote) files an issue so nothing is silently dropped. This matches the "Three Outcomes" core principle.
Ambiguous feedback (open questions, architectural suggestions with multiple alternatives, requests that depend on undocumented context) is auto-classified as DEFER without consulting the user. The filed issue is the durable artifact a human can resolve later; the PR reply tells the reviewer where the discussion has moved. Do not block the loop on user input.
Step 5: The Triage and Fix Loop
Loop until fixed point or unrecoverable abort. Process each feedback item exactly once per fetch cycle through the outcome flow that matches its Step 4 verdict.
5a. REJECT flow (verdict = REJECT). Compose a rejection body using the prefix table below and reply through the right channel:
- Inline review thread: call
mcp__github__add_reply_to_pull_request_comment with commentId = latestReviewerComment.databaseId.
- Review summary or PR conversation comment: call
mcp__github__add_issue_comment with issue_number = pullNumber. Start the body with @reviewer Regarding your <review/comment> (<short identifier>): and quote or summarize the specific ask being rejected.
Do not resolve rejected inline threads — they stay unresolved so the reviewer can push back. Record the item in OUTCOME_MARKERS as item_key → latest_reviewer_marker_at_outcome using a mutable marker: latestReviewerComment.databaseId + updatedAt for inline threads, review.id + updated_at for review summaries when available, review.id + body_hash(body) for review summaries when no update timestamp exists, and comment.id + updated_at for PR conversation comments. Do not add it to ADDRESSED_THREAD_IDS; suppression depends on the recorded reviewer marker staying current.
Rejection body format:
{prefix} {reason}
_This assessment was made by two independent AI reviewers ({LOCAL_LABEL} and {REMOTE_LABEL}). If you disagree, please reply and we'll re-evaluate._
Substitute {LOCAL_LABEL} / {REMOTE_LABEL} with the values captured in Step 0a (e.g. "Claude Opus 4.6" and "Codex GPT-5.4", in either order depending on the host).
Prefixes by REJECT category:
| Category | Prefix |
|---|
not-an-issue | **Not an issue** — |
unrelated | **Unrelated to this PR** — |
not-relevant | **Not applicable** — |
style-preference | **Style preference (no change)** — |
already-handled | **Already handled (no change)** — |
| (default) | **No action taken** — |
5a'. DEFER flow (verdict = DEFER). The feedback is legitimate but does not belong in this PR — file a tracking issue, then reply with a link.
-
Build the issue title from the feedback's primary ask: a short imperative phrase, e.g. Refactor extractTokens() to share parser state or Add retry logic to HTTP client.
-
Build the issue body:
Deferred from #{pullNumber}: {one-line summary}.
Original feedback by @{reviewer} on PR #{pullNumber} ({pr_url}):
> {quoted feedback, blockquoted}
**Context:** {file path:line, or short note on where this applies}.
**Why deferred:** {scope-creep | diminishing-returns | ambiguous} — {one-sentence rationale from the evaluators}.
_Filed automatically by `pm-autofix-pr` after dual-evaluator triage by {LOCAL_LABEL} and {REMOTE_LABEL}._
-
Call mcp__github__issue_write with method="create", owner, repo, title, body, and (if applicable) labels=["deferred-from-pr"]. Capture the returned number and html_url.
-
Compose the PR reply with the matching prefix below, ending with Tracked as #{new_issue_number} ({issue_html_url}).
Prefixes by DEFER category:
| Category | Prefix |
|---|
scope-creep | **Out of scope for this PR** — |
diminishing-returns | **Deferred (diminishing returns)** — |
ambiguous | **Deferred for separate discussion** — |
automated-fix-failed | **Deferred (automated fix failed pre-commit)** — |
| (default) | **Deferred** — |
-
Reply through the same channel as REJECT (inline thread → add_reply_to_pull_request_comment; review summary / PR comment → add_issue_comment). Do not resolve inline threads — the reviewer can push back if the deferral is wrong.
-
Record the item in OUTCOME_MARKERS (same marker scheme as REJECT). Append {item_key, issue_number, issue_url, category, title} to DEFERRED_ITEMS for the Step 7 summary.
Issue-creation failure fallback. If mcp__github__issue_write fails (rate limit, permissions, transient error) — retry once after 60 seconds. If the retry also fails, do not block the loop: post the DEFER reply with TODO: file as a separate issue — automated issue creation failed (<error summary>). instead of the tracked-issue link, and append {item_key, issue_number=null, ...} to DEFERRED_ITEMS so the final summary surfaces the gap. The reviewer's concern is still acknowledged in writing.
5b. FIX flow (verdict = FIX) and CI failures. Process each FIX item individually — apply, check, commit — before moving to the next. This isolates each item in its own commit so a pre-commit failure can be reverted cleanly without touching earlier successful fixes (the revert combines git restore for tracked changes and git clean -fd for untracked files the FIX created — both are safe because only the in-flight item's changes are in the worktree).
Precondition before entering 5b: the worktree must be clean (git status --porcelain empty). If it is not, fail loudly and jump to Step 7 with exit reason: dirty-worktree — there is no safe way to attribute the existing changes to a specific FIX item.
For each FIX item in feedback_items whose verdict is FIX (CI failures included), in sequence:
-
Snapshot the worktree. Run git status --porcelain and record it as pre_fix_status — at this point it must be empty (the 5b precondition).
-
Apply the change. Read the relevant source/error context and edit files:
- CI failures: read the failed-job log tail, identify failing file/line, fix.
- Inline review threads: read the referenced file, apply the requested change.
- Review summaries / PR conversation comments: locate files, apply the parsed asks.
-
Categorize what this FIX touched. Run git status --porcelain again. Capture:
modified_paths — entries with status codes M, A, D, R, T (tracked changes/renames/deletes).
untracked_paths — entries with status code ?? (new files this FIX created).
-
Run pre-commit checks for this item (Step 5c).
-
On pre-commit success: stage the touched files by name (never git add -A) — staging both modified_paths and untracked_paths. Commit with a descriptive message that names the feedback item (e.g. Fix null check in extractTokens (review thread #PRRT_xxx)), capture the resulting short-sha, and add the FIX item to COMMITTED_ITEMS = [] with {item_key, sha, files, validation}.
-
On pre-commit failure (5c returned a hard fail after the sub-fix attempt): revert this item completely so the next FIX starts from a clean worktree:
git restore --source=HEAD --staged --worktree -- <modified_paths> to undo tracked modifications/renames/deletions (also unstages anything pre-commit staged).
git clean -fd -- <untracked_paths> to delete files this FIX created (-fd so newly-created subdirectories are removed too).
- Verify with
git status --porcelain that the worktree is once again empty; if it is not, abort the entire loop with exit reason: dirty-worktree rather than risk contaminating later FIXes.
Record the item under BLOCKED_ITEMS = [] with {item_key, files, pre_commit_error_tail}. Continue with the next FIX item; blocked items will be turned into automated-fix-failed DEFER entries (with their own tracking issues) at the end of the loop iteration.
After all FIX items have been processed, the worktree is clean and COMMITTED_ITEMS lists every successful fix with its own sha. Each entry's sha is what 5e quotes in the corresponding "Fixed in <sha>" reply.
Convert each blocked FIX into an automated-fix-failed DEFER before leaving 5b. For every entry in BLOCKED_ITEMS (the items 5b reverted because pre-commit refused them), run the Step 5a' DEFER flow with category="automated-fix-failed":
- Title:
Auto-fix failed pre-commit: <one-line summary of the original feedback>.
- Issue body: include the reviewer's original feedback (quoted), the file paths the FIX touched, and the
pre_commit_error_tail captured in 5b. Set the **Why deferred:** line to automated-fix-failed — <one-line of the pre-commit error>.
- File the tracking issue with
mcp__github__issue_write, post the DEFER reply on the original thread / review summary / PR conversation comment using the automated-fix-failed prefix from Step 5a' and ending with Tracked as #<issue_number> (<issue_html_url>)., then record the item in OUTCOME_MARKERS and append it to DEFERRED_ITEMS — exactly like an evaluator-driven DEFER. Apply the same retry + TODO: file as a separate issue fallback if issue_write fails.
After this conversion, every blocked item has an explicit PR reply and (best-effort) a tracking issue, so the "Always Reply" core principle holds for blocked FIXes too. Clear BLOCKED_ITEMS for the iteration; do not include their entries in 5e (which only iterates COMMITTED_ITEMS).
5c. Pre-commit checks (from Step 2) — invoked by 5b for the current in-flight item only. Run in order: format → lint → type-check → test → build. If a formatter modifies files, stage them. If a check fails, attempt one sub-fix (does not count as a loop iteration). If the sub-fix also fails, do not ask the user — return a hard fail to 5b, which handles the revert and continues with the next FIX item. The check is bounded to this single item because earlier successful items are already committed and out of the worktree.
5d. Push the iteration's commits. After 5b finishes, decide whether there is anything to push without depending on undefined refs:
- If
COMMITTED_ITEMS is empty for this iteration (no FIX produced a commit), there is nothing to push — skip directly to Step 5g (re-fetch and check fixed point), not 5f. An iteration that only emits REJECT replies, DEFER replies, or automated-fix-failed DEFER conversions creates no new commits and therefore no new CI events, so waiting for CI in 5f would just spin until CI_TIMEOUT and exit as a failure even when we have actually reached the fixed point.
- Otherwise, push:
- If the branch has an upstream (
git rev-parse --abbrev-ref --symbolic-full-name @{u} exits 0), run git push.
- If the branch has no upstream yet (first push of this branch — common for a freshly-created PR), run
git push -u origin <head.ref> using the branch name captured in Step 1. Do not consult any base-branch ref; the upstream gets set during this push.
On rejected push (upstream has new commits), auto-recover without prompting:
- Run
git pull --rebase.
- If the rebase succeeds, re-run pre-commit checks for each rebased commit (using
git rebase --exec is acceptable, or by replaying 5b's checks on the rebased tree), then push again.
- If the rebase fails (conflicts), run
git rebase --abort to leave the worktree clean, record the abort in the final summary, jump straight to Step 7. Exit with summary; the user must resolve the divergence manually.
On network error during push, retry with exponential backoff (2s, 4s, 8s, 16s). After the fourth failure, jump to Step 7 with the failure recorded — do not prompt.
5e. Reply to every addressed feedback item. For each entry in COMMITTED_ITEMS (the per-item commits 5b/5d produced this iteration), post an outcome reply that quotes that item's own short-sha — never a different item's sha, since each FIX has its own commit. An item only counts as addressed once its reply is posted.
Reply body format:
Fixed in `<short-sha>`.
Changed: <file/function/behavior summary>.
Validation: <pre-commit check, targeted test, or reason validation was not run>.
Use the right channel:
- Inline review thread: call
mcp__github__add_reply_to_pull_request_comment with commentId = latestReviewerComment.databaseId (the numeric REST ID of the thread's most recent reviewer comment — not the thread's GraphQL id). After the reply succeeds, call mcp__github__pull_request_review_write with method="resolve_thread" and threadId = <thread.id> (the GraphQL node ID from get_review_comments). If both calls succeed, add the thread to ADDRESSED_THREAD_IDS.
- Review summary or PR conversation comment: call
mcp__github__add_issue_comment with issue_number = pullNumber. Start the body with @reviewer Regarding your <review/comment> (<short identifier>):, then include the fixed outcome. If the reply succeeds, add the item key to REPLIED_ITEM_KEYS.
This step is mandatory — never skip it. If a reply or resolve call fails with 403/429, wait 60s and retry once. After a failed retry, continue the code loop if needed, but do not count that feedback item as addressed and do not declare convergence; it must reappear on the next fetch/retry cycle until a reply is posted.
5f. Wait for CI only after feedback is answered. If any fetched feedback item still lacks an evaluation decision and an outcome reply, re-enter Step 4 immediately instead of polling. Once feedback is answered, poll for change:
- Sleep
POLL_INTERVAL seconds (via Bash with sleep <n>).
- Re-run Step 3's MCP calls to rebuild PR state.
- Compare against the previous snapshot. Treat any of the following as a "change event" worth re-entering Step 4 / Step 5:
head_sha changed (new push from another contributor).
has_merge_conflict flipped from false to true (the base branch advanced and now conflicts with the PR) — re-enter to run Step 5h.
- Any check run's
conclusion transitioned from null/in_progress to a terminal value, or any check run was added/removed.
- Review thread count, review summary count, or PR conversation comment count changed, or any of their
updated_at (or per-comment updatedAt) timestamps advanced past the previous snapshot. This catches new comments, edited comments, and new reviews uniformly.
- If nothing changed and CI is still in flight, loop back to step 1.
Track wall-clock elapsed time since the last commit was pushed. If CI_TIMEOUT minutes elapse with no terminal CI conclusion for at least one previously-pending check, abort the loop — record ci_timeout in the final summary and jump to Step 7. Do not prompt the user.
This polling path is the only mechanism the skill uses to detect new state. It does not depend on <github-webhook-activity> envelopes or any subscription tool; those are not available in the standard Claude Code CLI or Codex CLI sessions this skill targets.
5g. Re-fetch state and check for fixed point. Re-run Step 3's MCP calls. Filter out threads whose ID is in ADDRESSED_THREAD_IDS and PR-level feedback whose key is in REPLIED_ITEM_KEYS. For each item in OUTCOME_MARKERS, suppress it only if its latest reviewer marker still matches the value recorded at the prior REJECT/DEFER outcome; if a later reviewer reply exists, remove the item from OUTCOME_MARKERS and treat it as fresh feedback to re-evaluate in Step 4. If the merged state has a non-empty errors list (including an indeterminate mergeable == null), do not declare a fixed point — report the fetch failures and retry after 30 seconds.
Fixed point reached if all of:
ci_failures is empty after filtering out cancelled (the only non-success conclusion treated as informational). Any remaining entry — including timed_out, startup_failure, action_required, and non-github-actions failure — blocks convergence and is reported to the user.
- No reviewer feedback item remains without an evaluation decision and an outcome reply.
has_merge_conflict is false — the PR has no merge conflicts with its base branch. This is a hard gate: never declare a fixed point while the PR is conflicted. If has_merge_conflict is true, do not converge — run Step 5h to resolve the conflict, then continue the loop. (A null/indeterminate mergeable is not "false"; it was recorded in errors above, so this branch retries rather than converging.)
→ Proceed to Step 6.
Merge conflict present (has_merge_conflict true) → run Step 5h, then continue the loop.
Stale loop detected if the same CI checks are failing with similar error patterns as the previous iteration, or two consecutive iterations resolved base-branch conflicts yet the PR is still reported non-mergeable → record stale_loop in the final summary and jump to Step 7. Do not prompt the user. (BLOCKED_ITEMS cannot accumulate across iterations because Step 5b now converts each blocked FIX into an automated-fix-failed DEFER and clears the list.)
New issues found → run Step 4 (evaluate new feedback) and continue the loop.
5h. Resolve merge conflicts with the base branch. Runs whenever the fetched state has has_merge_conflict == true. The PR branch has diverged from its base (base.ref, captured in Step 1) in a way GitHub can't auto-merge; left alone the PR is unmergeable and must never be reported as a fixed point. Resolve by merging the base branch into the PR branch — merge, not rebase, because the PR branch is already published and a merge avoids a force-push and preserves the reviewer-visible commit history.
Precondition: the worktree must be clean (git status --porcelain empty). Step 5b leaves it clean; if it is not, jump to Step 7 with exit reason: dirty-worktree.
- Fetch the base.
git fetch <BASE_REMOTE> <base.ref>, using the BASE_REMOTE captured in Step 1 — the base repository's remote or clone URL, not necessarily origin. For a fork PR the base lives on upstream (or a direct URL), and fetching origin/<base.ref> would merge the fork's stale copy of the branch — or fail outright — leaving GitHub's conflict unresolved and dead-ending the loop.
- Merge the base in.
git merge --no-edit FETCH_HEAD — FETCH_HEAD is exactly what step 1 just fetched, so this merges the correct base branch whether BASE_REMOTE is a named remote or a URL. (Equivalently git merge --no-edit <BASE_REMOTE>/<base.ref> when BASE_REMOTE is a named remote.)
- If the merge is clean (exit 0 — the branch was merely behind, or the divergent changes auto-merged):
git merge has already created the merge commit. Now run the Step 5c pre-commit checks on the merged tree.
- On success → if Step 5c staged or modified any files (a formatter run or the one allowed sub-fix), fold them into the merge commit with
git add -- <changed paths> then git commit --amend --no-edit, so the tree you push is exactly the tree that passed validation and no stray changes are left to trip the next iteration's clean-worktree precondition. Confirm git status --porcelain is empty, then go to step 6 (push).
- On a pre-commit failure that a single sub-fix can't clear, undo the merge with
git reset --hard ORIG_HEAD (git merge sets ORIG_HEAD to the pre-merge commit) and jump to Step 7 with exit reason: merge-conflict, noting that merging the base broke the pre-commit checks. Do not push a broken merge.
- If the merge conflicts (non-zero exit;
git diff --name-only --diff-filter=U lists the conflicted files): resolve each conflicted file, preserving this PR's intent while incorporating the base's changes.
- Read every conflicted file, reason about both sides (
ours = this PR, theirs = the base), and edit to a coherent resolution that keeps both the PR's change and any independent base change. Never blindly keep one side, and never leave a conflict marker (<<<<<<<, =======, >>>>>>>) behind.
- When every file is resolved,
git add the resolved paths and run the Step 5c pre-commit checks. On success, complete the merge with git commit --no-edit.
- If resolution can't be done confidently — a semantic conflict the skill can't reason about, markers it can't cleanly remove, or pre-commit still failing after one sub-fix — run
git merge --abort to restore the clean pre-merge worktree and jump to Step 7 with exit reason: merge-conflict. Do not leave a half-resolved merge in place and do not prompt the user.
- Push the merge. Push with the same handling as Step 5d (upstream check,
git pull --rebase recovery on a rejected push, exponential backoff on network error). This publishes the merge commit and triggers a fresh CI run.
- Record it. Append
{sha, base_ref, conflicted_files} to MERGE_COMMITS = [] for the Step 7 summary, then continue the loop (return to Step 5f to wait for the new CI run, or Step 5g to re-check the fixed point).
Bounded attempt. Successful merges count toward stale-loop detection (Step 5g): if two consecutive iterations resolve base conflicts but the PR is still reported non-mergeable — e.g. the base keeps advancing and re-conflicting faster than the loop can converge — stop with exit reason: stale-loop rather than merging forever.
Step 6: Monitoring Phase
Skip if MONITOR_DURATION is 0 or if there are CI failures, unanswered feedback items, or a merge conflict (has_merge_conflict true). Monitoring is only entered from a true fixed point.
Report: "All issues resolved. Monitoring for {MONITOR_DURATION} minutes..."
Poll for change for up to MONITOR_DURATION minutes using the same loop as Step 5f: sleep POLL_INTERVAL seconds, re-run Step 3's MCP calls, compare counts and updated_at markers (and head_sha, and has_merge_conflict) against the previous snapshot. If a change is detected within the window — including the base advancing to introduce a new merge conflict — re-enter the evaluate + fix loop (Step 4 → Step 5, with Step 5h handling any new conflict) with a fresh sub-loop. After fixing, resume monitoring with the remaining time. If the window elapses without a change, proceed to Step 7.
Step 7: Final Summary
Print:
## Autofix PR Summary
### PR: #<number> — <title>
### Iterations: N
### Exit reason: fixed-point | stale-loop | ci-timeout | merge-conflict | rebase-conflict | push-failure | dirty-worktree | monitoring-timeout
### Changes Made (FIX outcomes)
| Iteration | Commit | Fixes Applied | Replies Posted |
|-----------|--------|---------------|----------------|
| 1 | abc123 | Fixed lint error in foo.ts, addressed review on bar.ts | @reviewer fixed thread in bar.ts via abc123 |
| 2 | def456 | Fixed test failure in baz_test.py | n/a |
### Merge Conflict Resolutions (base merged in — Step 5h)
| Iteration | Merge Commit | Base | Conflicted Files Resolved |
|-----------|--------------|------|---------------------------|
| 2 | 9f8e7d6 | main | src/parser.ts, src/index.ts |
### Deferred Feedback (DEFER outcomes — issue filed)
| Feedback | Category | Tracking Issue | Reply |
|----------|----------|----------------|-------|
| @reviewer on file.ts:42 | scope-creep | #123 | Posted DEFER reply with link |
| @reviewer (review summary) | diminishing-returns | #124 | Posted DEFER reply with link |
| @reviewer on util.ts:88 | scope-creep | _none — issue creation failed_ | Posted DEFER reply with TODO note |
### Rejected Feedback (REJECT outcomes — no change, no issue)
| Feedback | Category | Reason | Reply |
|----------|----------|--------|-------|
| @reviewer on file.ts:42 | not-an-issue | Code is correct as-is | Posted no-change rationale |
### Blocked Items (FIX attempted but pre-commit failed)
| Item | Pre-commit failure | Tracking Issue |
|------|--------------------|----------------|
| @reviewer on parser.ts:201 | type-check: tsc TS2322 | #125 |
### Current Status
- CI: All passing / N failures remaining (list each: name, conclusion, log link)
- Mergeable: yes / no — merge conflicts with `<base.ref>` remaining (list conflicted files if the run exited on `merge-conflict`)
- Reviewer feedback: All answered / M items still missing replies (list each)
- Issue creation failures: 0 / K (each requires manual filing — see Deferred table)
Do not ask the user anything at the end. The skill exits unconditionally after printing the summary:
- Success exits —
fixed-point (CI green, no merge conflicts, all feedback answered) or monitoring-timeout (reached only by passing through the same green-CI / no-conflict / answered-feedback gate before entering Step 6, so a clean window-elapse is also success): print "PR is ready for re-review."
- Failure exits —
stale-loop, ci-timeout, merge-conflict, rebase-conflict, push-failure, dirty-worktree: print "Autofix exited without converging — see summary above for required follow-up." For a merge-conflict exit, add: "Merge conflicts with <base.ref> could not be resolved automatically — resolve them manually, then re-run." Do not loop again, do not prompt.
References
references/api-patterns.md — MCP tool signatures, expected response shapes, polling-loop semantics, supersession algorithm, push and rebase handling, issue-creation flow, log-fetching gap
references/comment-evaluation.md — Full evaluation prompt templates, FIX/DEFER/REJECT decision matrix, DEFER and REJECT taxonomies, ambiguity-to-DEFER policy