pm-autofix-pr
Skill2 starsChanged 4 months agoMITView on GitHub
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.
- Deletes or force-pushes
- Commits and pushes
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-crSkill
- pm-deepenSkill
- pm-planSkill
- pm-riskSkill
- pm-simplifySkill
- pm-triageSkill
- rebase-prSkill
- review-offlineSkill
- upscaleSkill
- vibeSkill
- wigoSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
28 rules. About 0 are common, 13 are distinctive, and 15 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈13
Distinctive · estimate
This author's own choices. Worth a look.
15
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 31
ghCLI: must be installed and authenticated (gh auth status). - Very specific: line 87
Run gh auth status. If it exits non-zero (not installed, not authenticated, or unreachable), stop immediately with this message:
- Distinctive: line 107
If a GitHub MCP server happens to be connected in this session, note it for opportunistic use later (thread resolution only — see "GitHub …
- Very specific: line 151
Errors: if any gh/gh api call fails, accumulate the error message into an errors list.
- Distinctive: line 166
This is the most important step: Every time PR state is fetched, evaluate reviewer feedback before waiting on CI.
- Very specific: line 174
Primary Evaluator: If SUBAGENT_TOOL, spawn one clean-context subagent per item with the Agent tool.
- Very specific: line 180
If ADVISOR is false, skip this call entirely — every item's Primary verdict is final, subject to the confidence rule below.
- Very specific: line 266
Precondition: before entering 5b: the worktree must be clean (git status --porcelain empty).
- Very specific: line 270
Snapshot the worktree: Run git status --porcelain and record it as pre_fix_status — at this point it must be empty (the 5b precondition).
- Distinctive: line 271
Apply the change: Read the relevant source/error context and edit files:
- Distinctive: line 275
Categorize what this FIX touched: Run git status --porcelain again.
- Very specific: line 283
Verify with git status --porcelain that the worktree is once again empty.
- Very specific: line 287
After all FIX items have been processed, the worktree is clean and COMMITTED_ITEMS lists every successful fix with its own sha.
- Very specific: line 297
After this conversion, every blocked item has an explicit PR reply and (best-effort) a tracking issue, so the "Always Reply" core principle …
- Distinctive: line 301
5d. Push the iteration's commits: After 5b finishes, decide whether there is anything to push without depending on undefined refs:
- Very specific: line 305
If the branch has an upstream (git rev-parse --abbrev-ref --symbolic-full-name @{u} exits 0), run git push.
- Very specific: line 306
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 …
- Distinctive: line 311
If the rebase succeeds, re-run pre-commit checks for each rebased commit (using git rebase --exec is acceptable, or by replaying 5b's …
- Distinctive: line 312
If the rebase fails (conflicts), run git rebase --abort to leave the worktree clean, record the abort in the final summary, jump straight …
- Distinctive: line 327
Use the right channel:
- Distinctive: line 333
5f. Wait for CI only after feedback is answered: If any fetched feedback item still lacks an evaluation decision and an outcome reply, …
- Very specific: line 368
Fixed point reached: only when all five conditions above print PASS (condition 5's DEGRADED (PASS) counts as PASS here — see the …
- Distinctive: line 387
When every file is resolved, git add the resolved paths and run the Step 5c pre-commit checks.
- Distinctive: line 403
test assertions whose expected value is derived from mutable state on the base branch — a hash, count, date, or other literal that will …
- Distinctive: line 405
Ask for a verdict: CLEAN or ISSUES, with each issue concrete enough to act on (file:line, what's wrong, why it's a regression and not a …
- Very specific: line 409
If the other call (Primary or advisor, whichever is still live) has a valid verdict, proceed on it alone: CLEAN → condition 5 passes, …
- Very specific: line 431
If the window elapses without a change and the most recent fetch's errors list is empty, proceed to Step 7.
- Distinctive: line 509
Do not ask the user anything at the end.
≈ “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
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), every feedback item has a reply documenting the outcome, and the run's own fix commits have cleared an independent regression-review pass. 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: a primary evaluator forms the verdict, cross-checked by the advisor when one is available in this session (see Step 0a). 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/unauthenticated gh, 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
ghCLI must be installed and authenticated (gh auth status). This is now the primary path for all GitHub interaction, including fetching failed-job log tails (gh run view --job <id> --log-failed, still the only way to get raw Actions log output). The skill stops at preflight ifghis missing or unauthenticated.- GitHub MCP server is optional. If one happens to be connected in the host session, use it opportunistically for thread resolution — the one operation where it's a genuinely better fit than
gh(see "GitHub CLI Commands Used" below). Its absence never blocks the skill. (Comment/reply posting always goes throughgh— see the note under "GitHub CLI Commands Used" for why.) - A native subagent tool (the
Agent/Tasktool) is preferred for the primary evaluator in Step 4 and Step 5i, giving each verdict a clean context rather than the loop's own accumulated history. Its absence never blocks the skill — both steps fall back to evaluating inline instead (slower, not different). - The advisor — whatever this session's harness calls its second-opinion/reviewer-model feature — is optional and cross-checks the primary evaluator's verdict in Step 4 and Step 5i when present. Its absence never blocks the skill; those steps proceed on the primary evaluator's verdict alone (see each step's degraded-mode handling).
Configuration
Override via prompt arguments (e.g., /pm-autofix-pr 10 --ci-timeout 30 --monitor 0).
| Parameter | Default | Description |
TOTAL_CI_BUDGET | 15 | Overall wall-clock ceiling (minutes) on all CI-wait (Step 5f) + monitoring (Step 6) time for the whole run, initialized once at run start (Step 0b) — not reset by later pushes or monitoring cycles. CI_TIMEOUT and MONITOR_DURATION below remain the per-phase bounds, but each wait is clamped to whichever of TOTAL_CI_BUDGET or the per-phase bound remains, whichever is smaller. Once it reaches zero, the run aborts through Step 7 with ci-timeout (which also posts a resume-pointer PR comment — see Step 7) regardless of how recently the last commit was pushed or how many fix/push cycles happened inside it. |
MONITOR_DURATION | 10 | Minutes to watch for new issues after convergence. 0 to skip. Also bounded by whatever of TOTAL_CI_BUDGET remains. |
CI_TIMEOUT | 20 | Minutes to wait for CI before aborting (no user prompt — exits through Step 7 with ci-timeout). Also bounded by whatever of TOTAL_CI_BUDGET remains — with the defaults above, TOTAL_CI_BUDGET (15) binds first. |
POLL_INTERVAL | 30 | Seconds between PR-state re-fetches while waiting for CI or monitoring; also the --interval passed to gh pr checks --watch where supported (see Step 5f). |
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 (per-push or total budget exhausted), unresolvable merge conflict, rebase conflict, persistent push failure, an unrecoverable self-caught regression fix, or monitoring window elapsed. Every exit goes through Step 7. The skill never prompts the user.
GitHub CLI Commands Used
gh / gh api is the primary path for all GitHub interaction — no bundled scripts. A GitHub MCP server, if already connected in the session, may be used as an opportunistic fast path for the one operation marked below; never wait for or require one. All comment/reply posting always goes through gh, even when an MCP is connected — see the note below the table for why.
<tmpfile> and <titlefile> below always mean a file allocated via mktemp for that specific write (e.g. mktemp /tmp/reply-body-XXXXXX) — never a fixed literal path. Two concurrent /pm-autofix-pr invocations on the same host would otherwise race on a shared filename, letting one invocation's generated body or title get overwritten by the other's before gh reads it. Capture the exact path mktemp returns, reuse it across the write and the gh call, then rm -f it afterward.
| Operation | Primary (gh) | Opportunistic MCP fast path |
| Preflight / current user | gh auth status; gh api user -q .login for GH_USER | — |
| Auto-detect the PR for the current branch | gh pr list -R {owner}/{repo} --json number,headRefName,url --head <branch> (see Step 1) | — |
| Fetch PR details and mergeability | gh api repos/{owner}/{repo}/pulls/{n} (mergeable, mergeable_state, head.sha, base.ref, …) | — |
| Fetch CI check runs | gh api repos/{owner}/{repo}/commits/{sha}/check-runs | — |
| Fetch review threads (with resolution state) | gh api graphql — REST has no isResolved field, so this one needs GraphQL regardless of MCP (see references/api-patterns.md) | — |
| Fetch review summaries | gh api repos/{owner}/{repo}/pulls/{n}/reviews | — |
| Fetch PR conversation comments | gh api repos/{owner}/{repo}/issues/{n}/comments | — |
| Reply on an inline review thread | gh api repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -F body=@<tmpfile> (body written to a temp file first, so no shell-escaping of the reviewer-authored text is needed — no jq or other extra binary required) | — |
| Reply on a review summary / PR conversation comment | gh pr comment <n> -R {owner}/{repo} --body-file <tmpfile> / gh issue comment <n> -R {owner}/{repo} --body-file <tmpfile> | — |
| Resolve a review thread after a fix | gh api graphql — resolveReviewThread mutation (GraphQL-only; no REST equivalent) | MCP's thread-resolve method, if connected — same GraphQL mutation under the hood |
| File a tracking issue (DEFER outcome) | gh issue create -R {owner}/{repo} --title "$(cat <titlefile>)" --body-file <tmpfile> | — |
Why posting never uses MCP, even opportunistically: GH_USER (used for all self-authored-comment filtering) is captured from gh api user. If a connected MCP server posts a comment under a different authenticated identity, that reply would never be recognized as self-authored on a later re-fetch — the skill could then evaluate and reply to its own past outcome messages indefinitely, breaking convergence. Since gh api -F body=@<tmpfile> / gh pr comment --body-file <tmpfile> already avoid the shell-escaping problem MCP's typed body field solved, MCP's only remaining advantage for posting was skipping a temp file — not enough to justify the risk. Thread resolution has no such coupling (it authors no content attributable to an identity), so it keeps the MCP fast path.
See references/api-patterns.md for exact commands, JSON shapes, and pagination/vocabulary notes.
Workflow
Step 0a: Detect available evaluation capabilities
This skill runs under any harness. It never checks which one — only what it can call from where it happens to be running:
- Native subagent tool — if a native
Agent/Tasktool is available, setSUBAGENT_TOOL = true; Step 4 and Step 5i's primary evaluator spawns a clean-context subagent with it (Agenttool,subagent_type: "Explore"or an equivalent read-capable general agent). If not, setSUBAGENT_TOOL = false; those steps evaluate inline instead — slower, not different. - Advisor — if this session has a consultable advisor (however this harness exposes that — an
advisortool, a documented advisor feature, etc.), setADVISOR = true. If not, setADVISOR = false. Neither state blocks the skill; Step 4 and Step 5i both define theirADVISOR = falsebehavior explicitly.
Neither check requires installing anything or shelling out to another CLI. Record both flags now; every later step reads them rather than re-probing.
Step 0b: Preflight — verify gh
Run gh auth status. If it exits non-zero (not installed, not authenticated, or unreachable), stop immediately with this message:
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.
Then capture GH_USER via gh api user -q .login (used to filter out self-authored comments later — see the [bot]-suffix note in references/api-patterns.md when comparing this login against GraphQL-sourced author.login values).
Also initialize TOTAL_CI_BUDGET_REMAINING to TOTAL_CI_BUDGET minutes here — run-scoped state captured once at the start of the run, like GH_USER, not on first entry into Step 5f. This matters because some runs never enter Step 5f at all: Step 3's "nothing to fix" path goes straight to Step 6, and Step 5d's "no commits this iteration" path skips to Step 5g and from there can also reach Step 6 — both would reference an uninitialized counter if the budget were only set up inside Step 5f's own prose.
For the same reason, also probe here — once, not per-iteration and not inside Step 5f's own prose — whether Step 5f's waiting primitive (item 1) can use a blocking watch: run gh pr checks --help 2>&1 | grep -q -- '--watch' and check for a timeout-equivalent binary (command -v timeout first, falling back to command -v gtimeout, the name Homebrew's coreutils installs on stock macOS). Record the result as WATCH_USABLE (true only if both checks pass) and, when true, the resolved binary name as TIMEOUT_BIN. Both Step 5f and Step 6 read these once-per-run values; neither re-probes, including on the Step 3 → Step 6 direct path that never visits Step 5f.
Also initialize ALL_COMMITTED_ITEMS = [] here, for the same reason TOTAL_CI_BUDGET_REMAINING lives here rather than in Step 3: it is a run-wide accumulator (one entry per successful FIX commit across every iteration, appended to in Step 5b, consumed by Step 7's budget-exhaustion comment) that must persist and only grow for the whole run — unlike the other lists Step 3 (re)initializes below, which are safely re-derived from each fetch.
Also initialize REGRESSION_FIXES = [] and LAST_REGRESSION_FINDINGS = [] here, same run-wide-persistence reasoning: REGRESSION_FIXES accumulates one entry per self-caught regression fix for Step 7's summary, the same way ALL_COMMITTED_ITEMS does for reviewer-triggered fixes, and LAST_REGRESSION_FINDINGS must survive across Step 5i invocations so a finding that recurs after being "fixed" can be recognized (Step 5i's stale-loop bound) rather than re-derived fresh each time.
Also initialize BLOCKED_REGRESSIONS = [] here, for the same run-wide-persistence reasoning: it accumulates one entry per synthetic regression:<n> FIX that Step 5b could not land (pre-commit still failing after its one sub-fix), for Step 7's summary — see Step 5i step 4 for why these cannot go through Step 5b's normal automated-fix-failed DEFER conversion the way a reviewer-triggered blocked FIX does.
Also initialize MERGE_COMMITS = [] here, for the same run-wide-persistence reasoning as ALL_COMMITTED_ITEMS: Step 5h appends to it across however many merge-conflict resolutions happen over the run, and both Step 5g's condition 5 and Step 5i's regression-review scope (below) need to read it — initializing it at its first append site inside Step 5h, as an LLM-executed runbook literally reads X = [], would look like a fresh empty list on every Step 5h entry and silently drop earlier merges from both of those readers.
Also initialize REGRESSION_PASSES_REMAINING = 3 here — a small, run-wide cap on how many times Step 5i may re-enter after an ISSUES verdict (mirroring Step 5h's existing two-strike "Bounded attempt" idiom for repeated merge-conflict resolutions). This exists because TOTAL_CI_BUDGET_REMAINING only ever charges CI-wait time (Step 5f item 4) — never the wall-clock cost of the evaluator calls, fix, and commit a regression cycle itself takes — so on a repo with fast or already-terminal CI, a reviewer that keeps finding plausible-but-different ISSUES every pass (never repeating verbatim, so the exact-repeat stale-loop dedup in Step 5i step 4 never fires either) can drive many regression-review/fix/push cycles while barely denting the tracked budget. A small re-entry cap bounds this independently of how much (or little) time CI waits actually charge.
If a GitHub MCP server happens to be connected in this session, note it for opportunistic use later (thread resolution only — see "GitHub CLI Commands Used" below); its absence is not a preflight failure and no check for it is needed here.
Step 1: Identify the PR
gh's own ambient repo resolution (bare gh pr view, no -R) can be ambiguous on a checkout with multiple GitHub remotes and no gh repo set-default — in the worst case gh prompts for the base repository, which this skill can never do (Never Prompt the User). So every tier below except the deliberate last-resort (3c) resolves and passes an explicit -R {owner}/{repo} rather than relying on ambient resolution.
- Get the current branch:
git rev-parse --abbrev-ref HEAD. - If a PR number was provided as argument, resolve its repo the same way as 3a (derive
{owner}/{repo}fromorigin), then callgh api repos/{owner}/{repo}/pulls/<n>(if the resolved PR'shead.refdoesn'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'sgit fetch <BASE_REMOTE> <base.ref>would run withBASE_REMOTEunset 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.refmatches the local branch:- 3a. Origin lookup. Parse
git remote get-url originto{owner}/{repo}(stripgit@github.com:,https://github.com/, trailing.git). Rungh pr list -R {owner}/{repo} --head <branch> --state open --json number,url,headRefName,baseRefName,headRepositoryOwner, then keep only PRs whoseheadRepositoryOwner.loginequals{owner}(the owner just parsed fromorigin). This filter is load-bearing, not belt-and-braces:gh pr list's--headfilter matches on branch name only — it does not support theowner:branchqualifier (gh pr list --help:"<owner>:<branch>" syntax not supported) — and-Ronly scopes which repo's PR list is searched, not which fork owns each result's head. It is needed at 3a and not only at 3b becauseoriginis frequently the base repository itself: a maintainer or push-access contributor who cloned it directly has noupstreamremote at all, so 3b never runs and 3a is the only tier that fires. 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 happens to share the same name (verified empirically: a singlegh pr list -R <owner>/<repo> --head <branch>against a busy public repo returned open PRs from several distinct fork owners, all cross-repository). Since Step 4 validates onlyhead.ref, an unfiltered match would let the skill fetch, comment on, and triage that stranger's PR whenever the local branch's own PR is absent or closed. A deleted or inaccessible head repository yields an empty/missinglogin, which correctly fails the comparison. If the filter leaves no PRs, treat 3a as "no PR found" and fall through — including the uncommon case where the branch's own PR head lives in a fork registered under some remote name other thanorigin/upstream, which 3c exists to catch. - 3b. Upstream lookup (fork workflow). If step 3a returned no PRs after its owner filter and
git remote get-url upstreamexists, parse it the same way to{upstream_owner}/{upstream_repo}and rungh pr list -R {upstream_owner}/{upstream_repo} --head <branch> --state open --json number,url,headRefName,baseRefName,headRepositoryOwner, then keep only PRs whoseheadRepositoryOwner.loginequals{origin_owner}(the owner parsed fromoriginin 3a) — the same filter as 3a, load-bearing for the same reasons stated there. If the filter leaves no PRs, treat 3b as "no PR found" and fall through to 3c. - 3c. Ambient
gh pr viewfallback. If both explicit-remote lookups return nothing (uncommon remote naming, or the base repo isn't reachable viaorigin/upstream), run baregh pr view --json number,url,headRefName,baseRefNameand letgh's own resolution find it. Parse{base_owner}/{base_repo}directly out of the returnedurl(https://github.com/{owner}/{repo}/pull/{number}) rather than depending on version-specific JSON field names for the base repository. Treat any non-zero exit (includingghbeing unable to resolve a single unambiguous repo) as "no PR found" — stop and tell the user there is no open PR for the current branch.
- 3a. Origin lookup. Parse
- Validate — this runs for both the explicit-number path (step 2) and the auto-detected path (step 3) — by calling
gh api repos/{owner}/{repo}/pulls/<pullNumber>on the resolved{owner, repo, pullNumber}to retrievetitle,body,head.ref,head.sha,base.ref, andhtml_url(the base repository'sfull_nameis{owner}/{repo}, the same pair just queried). Confirmhead.refmatches the local branch. Capturebase.reffor Step 5d's first-push fallback and Step 5h's base-branch merge, and captureBASE_REMOTE— the fetch source for the base repository, which is not alwaysorigin. Resolve it by matchinggit remote -vURLs to{base_owner}/{base_repo}(apply the samegit@github.com:/https://github.com// trailing-.gitstripping as 3a):originon the 3a origin path,upstreamon the 3b fork path, and on the 3c path the remote matching the{base_owner}/{base_repo}parsed fromurl. If no local remote points at the base repository (common for a fork checkout with noupstream), setBASE_REMOTEto the base repo's clone URLhttps://github.com/{base_owner}/{base_repo}.git—git fetchaccepts 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 gh/gh api calls (paginate where applicable) and merge into a single state object. All REST calls below use the {owner}/{repo} and pullNumber captured in Step 1:
| State field | Source |
head_sha, base.ref | gh api repos/{owner}/{repo}/pulls/<n> → .head.sha, .base.ref |
has_merge_conflict | Same call → .mergeable (bool or null) and .mergeable_state (string) — this is the REST vocabulary, distinct from the uppercase MERGEABLE/CONFLICTING values gh pr view --json mergeable returns via GraphQL; use the REST call so this table's vocabulary and the retry logic below stay correct. If mergeable is null, GitHub is still computing mergeability in the background — re-fetch 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 | gh api repos/{owner}/{repo}/commits/<head_sha>/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) — the check run's .id here is the job id gh run view --job expects. Other failure types are non-fixable — report them. |
review_threads | gh api graphql with a reviewThreads query (see below and references/api-patterns.md) — REST's pulls/<n>/comments endpoint has no thread-resolution concept at all (isResolved only exists in the GraphQL schema), so this is the one piece of PR state that requires GraphQL regardless of whether an MCP is connected. Paginate reviewThreads(first: 100, after: $cursor). 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(last: 20) (not first: — the goal is the latest reviewer comment, and GraphQL connections default to oldest-first; last: 20 also bounds the query since nested pagination of comments-within-threads isn't practical in one gh api graphql call) as latestReviewerComment, sorting by createdAt if order is not guaranteed and excluding author.login == GH_USER. 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 | gh api repos/{owner}/{repo}/pulls/<n>/reviews. Apply supersession (see below). |
pr_comments | gh api repos/{owner}/{repo}/issues/<n>/comments. Drop entries where .user.login == GH_USER. |
REST and GraphQL disagree on bot logins: for the same comment, REST's user.login carries a [bot] suffix (e.g. "chatgpt-codex-connector[bot]") while GraphQL's author.login for the identical comment (matched by databaseId) omits it ("chatgpt-codex-connector") — verified on a live PR in this repo. GH_USER is captured via REST (gh api user -q .login in Step 0b) and self-filtering compares it against both REST-sourced logins (review_summaries, pr_comments) and GraphQL-sourced logins (review_threads). Rather than reasoning about which side is which, strip a trailing [bot] from both GH_USER and the login being compared before every self-authorship check — this is cheap, always correct, and avoids depending on which identity type (personal account, GitHub App, Actions bot) gh happens to be authenticated as in a given run.
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 gh/gh api 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 asthread:<thread.id> review_summaries, keyed asreview:<review.id>pr_comments, keyed ascomment:<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). (ALL_COMMITTED_ITEMS is initialized once in Step 0b, not here — see that step for why: unlike this paragraph's other lists, which are safely re-derived every time "Step 3's calls" are re-run from Step 5f/5g/6, ALL_COMMITTED_ITEMS must persist and only grow across the whole run.) This is distinct from the per-iteration COMMITTED_ITEMS Step 5b/5d/5e use to isolate each iteration's own push and replies — do not widen COMMITTED_ITEMS itself to run scope, or 5d's empty-iteration short-circuit and 5e's reply-once guarantee both break.
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, run Step 5g's checklist condition 4 (the resolved-upstream ahead-commit check, printed as PASS/FAIL) before declaring anything — a resumed run can find nothing new to fix while a prior, interrupted run still left local commits unpushed on this exact branch, and that would otherwise slip straight past every other gate below. If it prints FAIL, go to Step 5d to push — with Step 5d checking for ahead commits directly, it pushes these even though COMMITTED_ITEMS is empty this iteration, then falls through to 5e (a no-op here), 5f (wait for the CI this push triggers), and 5g, which re-runs this same condition as part of its own checklist; this paragraph itself is not re-entered. Once condition 4 prints PASS, 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 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 run the Primary Evaluator (see below). 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.
Primary Evaluator. If
SUBAGENT_TOOL, spawn one clean-context subagent per item with theAgenttool. If notSUBAGENT_TOOL, evaluate the item yourself, inline, instead — slower, not different.Provide the comment, code context, PR title/description, and changed files summary. Ask for (or, inline, produce) a FIX / DEFER / REJECT verdict with category, confidence (high/low), and reasoning. See
references/comment-evaluation.mdfor the full prompt template. Collect every item's verdict before moving on to the advisor check below — do not consult the advisor per item.Advisor check (only if
ADVISOR). Once every feedback item in this pass has a Primary verdict, state them all in-transcript in one batch — item key, the comment being evaluated, the Primary verdict, its confidence, and its reasoning — then consult the advisor once for the whole batch. The advisor takes no separate prompt; it reviews the conversation as it stands, so the batch must already be stated before you call it. Ask it (in the same in-transcript framing) to flag any item where it would land on a different verdict, and why.If
ADVISORis false, skip this call entirely — every item's Primary verdict is final, subject to the confidence rule below.
Decision logic (from references/comment-evaluation.md):
| Primary | Advisor (if consulted) | Action |
| FIX | FIX (or not flagged) | FIX — apply code change in this PR |
| FIX | DEFER | FIX — both consider the feedback valid; apply it now instead of filing an issue |
| DEFER | FIX | FIX — same as above, either order |
| REJECT | REJECT (or not flagged) | REJECT — reply with rationale, no code change, no issue |
| DEFER | DEFER (or not flagged) | DEFER — file tracking issue, reply with link |
| any other disagreement (either side REJECT, the other FIX or DEFER) | DEFER — file tracking issue | |
any verdict, ADVISOR false, low confidence | — | DEFER — file tracking issue |
The rule leans toward action without being reckless: fix when the evaluator(s) agree the change belongs in this PR, and also on a FIX/DEFER split — both consider the feedback valid, so the disagreement is only about timing and fixing now avoids issue churn. Only reject when there is no advisor disagreement and no concern worth tracking. Every remaining disagreement involving a REJECT vote files an issue so nothing is silently dropped. When no advisor is available to catch a shaky call, a low-confidence Primary verdict routes to DEFER instead of being acted on unchecked — this is the compensating control for running without a second opinion; a high-confidence Primary verdict with no advisor available stands as given. 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 (see "GitHub CLI Commands Used" and references/api-patterns.md for exact commands):
- Inline review thread: reply to
commentId = latestReviewerComment.databaseIdviagh api repos/{owner}/{repo}/pulls/<n>/comments/<commentId>/replies -F body=@<tmpfile>with the body written to a temp file first (nojqor other JSON-building tool needed —gh api -F key=@pathreads and correctly encodes the file's raw contents). Always post throughgh, even if a GitHub MCP is connected — see "GitHub CLI Commands Used" for why. - Review summary or PR conversation comment:
gh pr comment <n> -R {owner}/{repo} --body-file <tmpfile>(orgh issue comment) with the body written to a temp file first. 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 + body_hash(body) for review summaries (REST reviews have no updated_at, and submitted_at doesn't change on a body edit — see references/api-patterns.md), 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 {if ADVISOR: an AI reviewer, cross-checked by an advisor | else: an AI reviewer}. If you disagree, please reply and we'll re-evaluate._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 stateorAdd 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 evaluation}. _Filed automatically by `pm-autofix-pr` after triage{if ADVISOR: , cross-checked by an advisor}._- Write the title to its own temp file first, as a single non-empty line — the title, like the body, is derived from untrusted reviewer feedback, and interpolating it directly into
--title "<title>"lets shell metacharacters in the feedback (backticks,$(...), quotes) execute as commands or simply break the invocation. Rungh issue create -R {owner}/{repo} --title "$(cat <titlefile>)" --body-file <tmpfile> [--label deferred-from-pr]— write$(cat <titlefile>)verbatim into the command text; never splice the title text itself into the command string. The file's content becomes inert command-substitution output inside a double-quoted argument, not re-parsed shell syntax. If the repo has nodeferred-from-prlabel, drop--labeland retry rather than pre-creating it. Capture the returned issue number and URL by parsinggh issue create's stdout (it prints the new issue's URL, from which the number is the trailing path segment). 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 → the
.../repliesendpoint; review summary / PR comment →gh pr comment/gh 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}toDEFERRED_ITEMSfor the Step 7 summary.
Issue-creation failure fallback. If gh issue create 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 --porcelainand record it aspre_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 --porcelainagain. Capture:modified_paths— entries with status codesM,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 bothmodified_pathsanduntracked_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 toCOMMITTED_ITEMS = []with{item_key, sha, files, validation}— also append the same entry to the run-wideALL_COMMITTED_ITEMS(initialized in Step 0b) so Step 7's exhaustion comment can report every commit landed this run, not just this iteration's. 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 (-fdso newly-created subdirectories are removed too).- Verify with
git status --porcelainthat the worktree is once again empty; if it is not, abort the entire loop withexit reason: dirty-worktreerather than risk contaminating later FIXes.
Record the item under
BLOCKED_ITEMS = []with{item_key, files, pre_commit_error_tail}— plusfinding_summarywhenitem_keystarts withregression:(carried over from theTHIS_PASS_FINDINGSentry Step 5i step 4 is routing; every other item_key has nofinding_summaryof its own and omits the field). Continue with the next FIX item; blocked items will be turned intoautomated-fix-failedDEFER 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 — except synthetic regression items. A BLOCKED_ITEMS entry whose item_key starts with regression: (Step 5i's self-caught findings, routed through this same flow) cannot go through the DEFER conversion below: that conversion quotes "the reviewer's original feedback" and posts a reply on "the original thread / review summary / PR conversation comment," and a synthetic regression item has neither — there is no reviewer text to quote and no thread to reply on. Pull these out of BLOCKED_ITEMS first and append them (with their carried-over finding_summary, above) to the run-wide BLOCKED_REGRESSIONS (initialized in Step 0b) instead; do not run the DEFER flow for them. The run then falls through to 5d/5e for whatever did commit this pass (if anything) and exits directly to Step 7 with exit reason: regression-fix-failed — see Step 5i step 4's own routing for this exit and for how LAST_REGRESSION_FINDINGS is handled on a pass with blocked items; this paragraph only covers the reply-target carve-out, not the exit itself.
For every remaining entry in BLOCKED_ITEMS (the reviewer-triggered 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_tailcaptured in 5b. Set the**Why deferred:**line toautomated-fix-failed — <one-line of the pre-commit error>. - File the tracking issue with
gh issue create(as in Step 5a'), post the DEFER reply on the original thread / review summary / PR conversation comment using theautomated-fix-failedprefix from Step 5a' and ending withTracked as #<issue_number> (<issue_html_url>)., then record the item inOUTCOME_MARKERSand append it toDEFERRED_ITEMS— exactly like an evaluator-driven DEFER. Apply the same retry +TODO: file as a separate issuefallback if issue creation 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:
- Check for ahead commits directly — using the same upstream-resolution Step 5g's condition 4 uses (resolve
@{u}; with no upstream configured, treat every local commit as ahead) — rather than relying onCOMMITTED_ITEMSalone: it is scoped to this iteration and reads empty whenever Step 5b didn't run this iteration, even when a previous, interrupted run already made commits that were never pushed (the case Step 3's "nothing to fix" short-circuit routes here to handle). IfCOMMITTED_ITEMSis empty for this iteration and there are no ahead commits, 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, orautomated-fix-failedDEFER conversions creates no new commits and therefore no new CI events, so waiting for CI in 5f would just spin untilCI_TIMEOUTand exit as a failure even when we have actually reached the fixed point. - Otherwise (either
COMMITTED_ITEMSis non-empty, or ahead commits exist despite this iteration producing none), push:- If the branch has an upstream (
git rev-parse --abbrev-ref --symbolic-full-name @{u}exits 0), rungit 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 a successful push (either case above), reset the per-push
CI_TIMEOUTclock (Step 5f) back toCI_TIMEOUTminutes — it is scoped to "time since the most recent push," so it re-arms on every push.TOTAL_CI_BUDGET_REMAININGis a separate, run-wide counter and is never reset here.
- If the branch has an upstream (
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 --execis acceptable, or by replaying 5b's checks on the rebased tree), then push again. - If the rebase fails (conflicts), run
git rebase --abortto 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. Skip this for entries whose item_key starts with regression: (Step 5i's self-caught fixes, routed through this same commit flow with a synthetic key) — there is no reviewer thread to reply to for a bug the loop found in its own commits; those are recorded in REGRESSION_FIXES and surfaced in Step 7's summary instead.
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: reply to
commentId = latestReviewerComment.databaseId(the numeric REST ID of the thread's most recent reviewer comment — not the thread's GraphQLid) viagh api repos/{owner}/{repo}/pulls/<n>/comments/<commentId>/replies -F body=@<tmpfile>— always throughgh, even if a GitHub MCP is connected (see "GitHub CLI Commands Used"). After the reply succeeds, resolve the thread: primary pathgh api graphqlwith theresolveReviewThreadmutation andthreadId = <thread.id>(the GraphQL node ID from thereview_threadsquery in Step 3); if a GitHub MCP is already connected, its thread-resolve method may be used instead — it performs the same mutation. If both the reply and the resolve succeed, add the thread toADDRESSED_THREAD_IDS. - Review summary or PR conversation comment:
gh pr comment <n> -R {owner}/{repo} --body-file <tmpfile>(orgh issue comment). Start the body with@reviewer Regarding your <review/comment> (<short identifier>):, then include the fixed outcome. If the reply succeeds, add the item key toREPLIED_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 waiting. Once feedback is answered, wait for CI to change:
TOTAL_CI_BUDGET_REMAINING was already initialized once in Step 0b, at run start — not here. It is never reset by a later push or by a monitoring cycle (Step 6); every wait anywhere in the run, in either step, draws down the same counter, which is what closes the "resets per push" and "monitoring windows chain unboundedly" gaps described in the issue this behavior was added for.
At the top of each wait, compute budget_this_wait = min(minutes remaining on the per-push CI_TIMEOUT clock, TOTAL_CI_BUDGET_REMAINING). If that is <= 0, skip straight to the timeout check below without waiting.
- Waiting primitive — prefer a blocking watch over a manual sleep loop:
WATCH_USABLEandTIMEOUT_BINwere already probed once, in Step 0b — not here, not per iteration. Reuse those values. IfWATCH_USABLE, wait withTIMEOUT_BIN <chunk>s gh pr checks <n> -R {owner}/{repo} --watch --interval <POLL_INTERVAL>, where<chunk>ismin(budget_this_wait in seconds, 300)— capping every watch invocation at 5 minutes even when more budget remains, so control returns periodically to check the four channels--watchnever looks at (review threads, review summaries, PR conversation comments, mergeability).TIMEOUT_BINexits 124 when the chunk elapses with checks still pending; treat that the same as "nothing changed this wait" below. A clean exit (0) fromgh pr checks --watchmeans the checks reached a terminal state — re-fetch immediately rather than waiting out the rest of the chunk.- If
WATCH_USABLEis false, fall back to the original loop: sleepPOLL_INTERVALseconds once (viaBashwithsleep <n>), then continue to item 2 below — the same single-sleep-per-pass shape every other iteration of this wait loop already has.budget_this_waitbounds the fallback the same way it bounds the watch path: by the outer loop (items 2-5) repeatedly re-entering item 1 and sleeping again until the timeout check fires — item 1 itself never sleeps for the fullbudget_this_waitin one call. A--watch-capableghwithout a timeout binary must use this fallback — invokinggh pr checks --watchunbounded, with nothing to cap it, would block pastbudget_this_waitand defeat the run-wide budget just as surely as a command-not-found failure would. gh pr checks --watchis a sleep replacement only, never a state source. Its own status output uses the GraphQLCheckConclusionStatevocabulary (uppercase — seereferences/api-patterns.md's "CI check runs" section), not the REST vocabulary the rest of this skill relies on. Whichever waiting path was used, always re-run Step 3's five calls next to rebuild authoritative state — do not parsegh pr checks' own output.
- Re-run Step 3's 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_shachanged (new push from another contributor).has_merge_conflictflipped from false to true (the base branch advanced and now conflicts with the PR) — re-enter to run Step 5h.- Any check run's
conclusiontransitioned from null/in_progressto a terminal value, or any check run was added/removed. - Review thread count, review summary count, or PR conversation comment count changed.
- Any review thread's latest comment
updatedAt, or any PR conversation comment'supdated_at, advanced past the previous snapshot. These fields are real and reliable on their respective endpoints. - Any review summary's
hash(body)differs from the previous snapshot's — notupdated_at: the RESTpulls/{n}/reviewsresponse has noupdated_atfield at all (onlysubmitted_at, which does not change when a review body is edited), so a body-edited review would never be detected otherwise. This mirrors theOUTCOME_MARKERSscheme already used for edited reviews after a REJECT/DEFER outcome.
- Subtract this wait's actual elapsed wall-clock time from both
TOTAL_CI_BUDGET_REMAININGand the per-pushCI_TIMEOUTclock. - If nothing changed and CI is still in flight, loop back to step 1 — unless the timeout check below now fires. Exception: if this wait exited via a clean (
0)--watchreturn (checks were already in a terminal state when the wait started) and step 3 found no change event, sleep the remainder ofPOLL_INTERVALbefore looping back to step 1's watch call, rather than re-entering--watchimmediately — then subtract this sleep's duration from bothTOTAL_CI_BUDGET_REMAININGand the per-pushCI_TIMEOUTclock too, the same way step 4 already does for the wait itself. Without this floor and its own deduction, an already-terminal check set makes--watchreturn almost instantly every time, turning "loop back to step 1" into a tight spin of Step 3's five API calls with no actual wait between them, and — if the floor sleep's own time weren't separately charged — into wall-clock time the run-wide budget never sees, since step 4's deduction already ran before this exception fires. This is not a rare edge case: Step 6 hits it on every iteration, since it is only ever entered once checks are already terminal (see Step 6) — Step 5f itself can also hit it in the narrower push-to-check-creation race window, where--watchsees the previous commit's already-terminal checks before GitHub creates new ones for the latest push.
Timeout check. Abort the loop if either: the per-push CI_TIMEOUT minutes have elapsed with no terminal CI conclusion for at least one previously-pending check, or TOTAL_CI_BUDGET_REMAINING has reached zero, regardless of how recently the last commit was pushed. Record which of the two conditions actually fired as TIMEOUT_TRIGGER — total-budget if TOTAL_CI_BUDGET_REMAINING reached zero (checked first, since that's the harder deadline once it's hit), otherwise per-push — so Step 7's PR comment can report the real cause instead of always blaming the total budget. Record ci-timeout in the final summary and jump to Step 7 (which posts a resume-pointer PR comment before exiting). Do not prompt the user.
This wait-and-refetch 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 interactive CLI coding sessions this skill targets. gh pr checks --watch changes how the wait is spent, not how state is detected.
5g. Re-fetch state and check for fixed point. Re-run Step 3's 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 checklist. Every time 5g runs, evaluate all five conditions below and print each as PASS/FAIL with the concrete evidence that proves it — not just the verdict — with one exception: condition 5 has a third possible print, DEGRADED (PASS) (see condition 5's own text), which counts as a PASS for every purpose below (the fixed-point gate, the exit-reason selection, and Step 7's summary) but is called out separately so a degraded single-evaluator confirmation stays visible rather than reading as an ordinary two-evaluator PASS. This is what keeps a resumed or reviewed run legible instead of trailing off mid-analysis with no record of what was actually still open:
- CI green —
ci_failuresis empty after filtering outcancelled(the only non-success conclusion treated as informational). Any remaining entry — includingtimed_out,startup_failure,action_required, and non-github-actionsfailure— blocks convergence and is reported to the user. PrintPASSwith the list of check names + conclusions, orFAILwith the list of still-failing checks. - Zero unresolved feedback — no reviewer feedback item remains without an evaluation decision and an outcome reply. Print
PASSwith the count of items evaluated this run, orFAILwith the list of items still missing a reply. - No merge conflicts —
has_merge_conflictis false. This is a hard gate: never declare a fixed point while the PR is conflicted. Anull/indeterminatemergeableis not "false"; it was recorded inerrorsabove, so printFAILhere too (retry rather than converge). PrintPASSwithmergeable_state, orFAILand fall through to the "Merge conflict present" branch below. - Zero unpushed commits — resolve the branch's actual upstream the same way Step 5d does: run
git rev-parse --abbrev-ref --symbolic-full-name @{u}. If it exits0, compare withgit log @{u}..HEAD --oneline; a non-zero exit from thatgit logisFAIL, neverPASS— a fatal error and a genuinely clean result both print empty stdout, so the exit status decides it, not the output alone. If the@{u}resolution itself exits non-zero, the branch has no upstream yet — treat every local commit as unpushed (FAILwhenevergit log HEAD --onelineis non-empty). Print the resolved ref (or "no upstream") and the command's output verbatim as evidence. Empty output on a zero exit isPASS; anything else isFAIL— return to 5d to push, then re-run this checklist. This check exists because control flow alone (5d pushes before 5g runs) is not proof: a prior run's push can fail, be skipped, or be interrupted in a way this explicit check catches even when every other signal looks clean — this is what a stale unpushed-commits state from a prior run looks like from inside 5g. - No regressions in this run's own commits — only evaluate this condition once conditions 1-4 above already print
PASS(no point spending a review pass on a diff that CI, feedback triage, or a conflict merge is about to change anyway). If bothALL_COMMITTED_ITEMSandMERGE_COMMITSare empty (this invocation hasn't authored any commit of its own yet — e.g. Step 4's "nothing to fix" short-circuit before any iteration ran, and no base-branch conflict was ever merged in), printPASSvacuously without spawning a reviewer: there is nothing of this run's authorship to check for self-inflicted regressions, and CI already vets whatever commits already existed on the branch coming in. A non-emptyMERGE_COMMITSalone (a run that only resolved a base-branch conflict, with no Step 5b FIX) is not vacuous — Step 5h's hand-resolved conflicts are exactly the kind of self-authored, regression-prone change this condition exists to catch, per this PR's own motivating incidents. Otherwise run Step 5i's regression-review pass. PrintPASSwhen every call this pass made agreed CLEAN, orDEGRADED (PASS)when Step 5i step 3's no-advisor or no-verdict handling resolved this pass without a finding (see step 3); printFAILwith the combined finding list when any call returns ISSUES, then follow Step 5i's routing rather than converging.
Fixed point reached only when all five conditions above print PASS (condition 5's DEGRADED (PASS) counts as PASS here — see the checklist's own note above) → 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, or Step 5i reports the same regression finding twice in a row despite an intervening fix, or Step 5i's REGRESSION_PASSES_REMAINING re-entry cap reaches zero on a pass with genuinely new (never-repeating) findings → 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 theBASE_REMOTEcaptured in Step 1 — the base repository's remote or clone URL, not necessarilyorigin. For a fork PR the base lives onupstream(or a direct URL), and fetchingorigin/<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_HEADis exactly what step 1 just fetched, so this merges the correct base branch whetherBASE_REMOTEis a named remote or a URL. (Equivalentlygit merge --no-edit <BASE_REMOTE>/<base.ref>whenBASE_REMOTEis a named remote.) - If the merge is clean (exit 0 — the branch was merely behind, or the divergent changes auto-merged):
git mergehas 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>thengit 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. Confirmgit status --porcelainis 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 mergesetsORIG_HEADto the pre-merge commit) and jump to Step 7 withexit reason: merge-conflict, noting that merging the base broke the pre-commit checks. Do not push a broken merge.
- 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
- If the merge conflicts (non-zero exit;
git diff --name-only --diff-filter=Ulists 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 addthe resolved paths and run the Step 5c pre-commit checks. On success, complete the merge withgit 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 --abortto restore the clean pre-merge worktree and jump to Step 7 withexit reason: merge-conflict. Do not leave a half-resolved merge in place and do not prompt the user.
- Read every conflicted file, reason about both sides (
- Push the merge. Push with the same handling as Step 5d (upstream check,
git pull --rebaserecovery on a rejected push, exponential backoff on network error) — including resetting the per-pushCI_TIMEOUTclock on success, exactly as Step 5d does. This publishes the merge commit and triggers a fresh CI run. - Record it. Append
{sha, base_ref, conflicted_files}to the run-wideMERGE_COMMITS(initialized in Step 0b) 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.
5i. Regression-review pass. The mechanic behind Step 5g's checklist condition 5. Every other checklist condition depends on an external signal — CI, a reviewer, GitHub's mergeability check — catching a problem; this one exists because past runs shipped self-inflicted regressions (a swallowed clock event in a deferral implementation, a hardcoded test literal that broke once main advanced) that no external signal caught until a later, separate reviewer pass, forcing a follow-up PR. This step is that reviewer pass, run inline instead of after the fact.
Runs only when Step 5g's conditions 1-4 already print PASS and at least one of ALL_COMMITTED_ITEMS or MERGE_COMMITS is non-empty (see condition 5's own text for the vacuous-empty case, and for why no point reviewing a diff that other in-flight work is about to change).
Both passes happen in this single invocation, not across separate loop iterations. An earlier version of this step persisted a "clean streak" across 5g re-entries — the first CLEAN verdict left the streak at 1, condition 5 kept printing FAIL, and control had nowhere productive to go: no new commit had landed, so Step 5f's wait-for-external-change primitive had nothing to wait on and the run spun until ci-timeout. Getting independent verification without that dead wait means reviewing the diff fully now, not the same commits reviewed twice, later:
- Run the Primary Evaluator, reusing Step 4's evaluator infrastructure (Step 0a's
SUBAGENT_TOOLflag: a fresh, clean-contextAgent-tool spawn if true, an inline self-review if false) for a review of the diff ofALL_COMMITTED_ITEMSandMERGE_COMMITScombined (every commit this invocation has authored so far, run-wide — not just this iteration's, and not just Step 5b's FIXes). For eachALL_COMMITTED_ITEMSsha, diff it normally; for eachMERGE_COMMITSsha, usegit show --remerge-diff <sha>(git ≥ 2.36; theortmerge strategy that Step 5h's plaingit merge/git commitalready use) rather than a plain diff,git log -p, orgit show --cc. A bare diff against a merge commit's first parent silently omits the hand-resolved hunks entirely, and--cc(dense combined diff) has its own silent gap: it suppresses any region where the merge result matches either parent, so a resolution that took one side wholesale — exactly the failure mode Step 5h's own instructions warn against ("Never blindly keep one side") and this condition exists to catch — produces zero diff hunks, letting the review return CLEAN on an empty input rather than on an actual review.--remerge-diffinstead shows the real diff between the machine-remerged tree and what was actually committed, so a wholesale-side resolution (or any other silent drop) is visible. If the resulting diff for a given sha is genuinely empty (a clean, unmodified auto-merge Step 5h never hand-touched), evaluate it as CLEAN directly rather than passing an empty diff into evaluation — an empty prompt input risks tripping the no-verdict path in step 3 below into a spuriousDEGRADED (PASS). On git < 2.36 (--remerge-diffunsupported), fall back to--ccand accept its wholesale-side blind spot as a known limitation rather than blocking the review. Reviewing onlyALL_COMMITTED_ITEMSwould mean a run that only resolved a base-branch conflict never gets its hand-authored resolution reviewed at all (see Step 5g condition 5's non-vacuous case above); reviewingstart-of-run..HEADinstead of these two specific sets was considered and rejected, because after a base merge that range also carries the entire base branch's own delta, which would have the evaluator flag other contributors' pre-existing code as this run's regressions. A clean-context spawn reviewing a diff it did not author is a stronger independence property than the orchestrator grading its own commits from the same contaminated context that produced them, and it costs one bounded subagent call per 5g pass (plus, ifADVISOR, one advisor consultation) instead of an open-ended wait. Prompt: review only for correctness bugs and regressions the fix commits themselves introduced — not style, not scope, not anything Step 4's evaluation already triages from reviewer feedback. Explicitly ask the Primary Evaluator to check for, at minimum:
- dropped, swallowed, or silently-discarded events or state on any code path the diff touches (the swallowed-clock-event failure shape from the incident above);
- test assertions whose expected value is derived from mutable state on the base branch — a hash, count, date, or other literal that will drift as
mainadvances — rather than a fixed, self-contained expectation (the hardcoded-test-literal failure shape from the same incident history).
Ask for a verdict: CLEAN or ISSUES, with each issue concrete enough to act on (file:line, what's wrong, why it's a regression and not a style nit).
Combine. If
ADVISOR: state the Primary Evaluator's CLEAN/ISSUES verdict and its findings in-transcript, then consult the advisor once, asking it to independently confirm CLEAN or flag ISSUES against the same diff. When both agree CLEAN, condition 5 is satisfied for this 5g pass — nothing further to do. When either is ISSUES, union their findings intoTHIS_PASS_FINDINGS, deduping entries that name the same file:line and the same defect. IfADVISORis false, the Primary Evaluator's verdict is used alone: CLEAN satisfies condition 5 but is marked degraded (single-evaluator confirmation only — seeDEGRADED_EVALUATORSbelow); ISSUES populatesTHIS_PASS_FINDINGSdirectly, not marked degraded — a finding needing a fix stands on its own regardless of how many reviewers produced it.No-verdict evaluator. Any evaluation call this step makes (the Primary Evaluator, or the advisor consultation) that times out, exits nonzero, or returns output with no parseable CLEAN/ISSUES verdict has produced no usable input. Retry that one call once (same framing, same diff). If the retry also produces no parseable verdict:
- If the other call (Primary or advisor, whichever is still live) has a valid verdict, proceed on it alone: CLEAN → condition 5 passes, marked degraded; ISSUES → its findings alone populate
THIS_PASS_FINDINGSand proceed to step 4 below as usual. - If no call this pass produced a parseable verdict at all — the Primary Evaluator failed with no advisor available to fall back on, or both the Primary Evaluator and the advisor failed — condition 5 is a degraded PASS with zero findings for this pass. Do not spin retrying indefinitely, and do not treat a non-verdict as ISSUES.
Set
DEGRADED_EVALUATORS(per-pass state, overwritten each time this step runs — not a run-wide accumulator likeLAST_REGRESSION_FINDINGS) to record why this pass is degraded:no-advisorwhenADVISORis false and the Primary Evaluator alone confirmed CLEAN, or the identity of whichever call (primary/advisor) produced no verdict after its retry — so Step 7's "Regression review" status line can reportDEGRADED (<DEGRADED_EVALUATORS>)instead of a plainPASS; leave it empty on any pass where every call this step made returned a parseable verdict.- If the other call (Primary or advisor, whichever is still live) has a valid verdict, proceed on it alone: CLEAN → condition 5 passes, marked degraded; ISSUES → its findings alone populate
On any ISSUES: compare
THIS_PASS_FINDINGSagainstLAST_REGRESSION_FINDINGS(run-wide state from Step 0b, holding whatever the previous Step 5i invocation last found — empty if this is the first). If they match — the fix applied last time didn't actually resolve what was flagged — that is the stale-loop trigger cross-referenced from Step 5g's "Stale loop detected" bullet: recordstale_loopand jump to Step 7 rather than looping forever chasing a fix the reviewer keeps rejecting. Otherwise, decrementREGRESSION_PASSES_REMAINING(initialized in Step 0b). If it has reached zero, this pass's ISSUES are different findings than last time (the exact-repeat dedup above didn't catch them) but the run has still re-entered this step too many times — recordstale_loopwithTHIS_PASS_FINDINGSlisted as the outstanding findings and jump to Step 7, rather than treating "never repeats verbatim" as license to keep going indefinitely. Otherwise, treat each finding inTHIS_PASS_FINDINGSas its own FIX item with a syntheticitem_keyofregression:<n>(no reviewer thread backs it) — no Step 4 triage, no FIX/DEFER/REJECT ambiguity call: the loop authored the bug, so the loop fixes it, unconditionally. Route each through Step 5b's existing single-item flow (apply, 5c pre-commit, commit — which appends the usual{item_key, sha, files, validation}entry toCOMMITTED_ITEMS/ALL_COMMITTED_ITEMS, same as any other FIX), additionally append{sha, finding_summary}toREGRESSION_FIXESfor Step 7's dedicated table. Once every FIX item from this pass has been attempted, setLAST_REGRESSION_FINDINGSto whichever findings inTHIS_PASS_FINDINGSlanded a commit (all of them, on the common all-succeeded path) — this is what lets the next Step 5i invocation detect an exact repeat; skipping this update on the success path would silently disable the exact-repeat stale-loop trigger for every future pass. Then go to 5d to push (5d seesCOMMITTED_ITEMSnon-empty from these entries and pushes normally) and 5e (which skips the reply for anyregression:key — see 5e). The next 5g pass re-runs this regression review from scratch against the updated diff.If any regression item is blocked (5c's pre-commit check still fails after its one sub-fix), Step 5b routes it to
BLOCKED_REGRESSIONSinstead of the normalautomated-fix-failedDEFER conversion — see Step 5b's carve-out for why a synthetic finding has no reviewer thread to reply on.LAST_REGRESSION_FINDINGSis still only set to whichever findings landed a commit (the general rule above already excludes a blocked finding, since it never lands); if every finding this pass blocked,LAST_REGRESSION_FINDINGSis unchanged from its prior value — a pass that landed nothing didn't actually resolve (or fail to resolve) anything, so overwriting it would corrupt a later resumed run's stale-loop comparison. Once every FIX item from this pass has been attempted, a non-emptyBLOCKED_REGRESSIONSmeans the run stops here — do not re-enter Step 5i again this invocation. Fall through to 5d/5e for whatever did commit (if anything, per the general routing above), then exit directly to Step 7 withexit reason: regression-fix-failed(do not route back through Step 5g's condition 5 — see condition 5's own text, which delegates unconditionally to "Step 5i's routing" and defers to this exit rather than defining its own): the loop authored these bugs and could not land its own fix, so it surfaces the failure rather than guessing further.
Bound. Beyond the exact-repeat stale-loop trigger in this step's own numbered step 4 above, a reviewer that keeps finding different plausible-sounding issues every pass (never repeating verbatim) isn't caught by that dedup. TOTAL_CI_BUDGET_REMAINING is not a reliable backstop for this case: it only ever charges the wait portion of Step 5f (item 4), never the wall-clock cost of Step 5i's own evaluator calls, Step 5b's fix/commit, or Step 5d's push — so on a repo with fast or already-terminal CI, a cycle can charge only seconds to the budget while actually costing much more real time, letting many more regression/fix/push cycles happen than the configured TOTAL_CI_BUDGET minutes would suggest. REGRESSION_PASSES_REMAINING (initialized to 3 in Step 0b, decremented in step 4 above) is what actually bounds this case: once it reaches zero, the run exits via stale-loop regardless of how much CI-wait budget remains, the same way Step 5h's "Bounded attempt" caps repeated merge-conflict resolutions.
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 — which, per Step 5g, already means condition 5's regression review resolved (vacuously or otherwise) within that same 5g pass, so no separate regression check is needed here.
The effective monitor window is min(MONITOR_DURATION, TOTAL_CI_BUDGET_REMAINING) — the same shared counter Step 5f draws down, initialized once in Step 0b and never reset by convergence (this holds even when Step 6 is entered directly from Step 3's "nothing to fix" exit, without ever passing through Step 5f). If that effective window is <= 0 (the run reached a fixed point but exhausted the total budget getting there), skip monitoring entirely and proceed straight to Step 7 as monitoring-timeout: the PR genuinely has no open work, there is just no budget left to keep watching it — still a success exit, and no resume-pointer comment is needed since nothing is pending.
Report: "All issues resolved. Monitoring for {effective window} minutes..."
Wait for change for up to the effective window using Step 5f's waiting primitive only — the probe/chunk/watch-or-sleep selection (item 1), the re-fetch (item 2), and the change comparison (item 3) — but never Step 5f's Timeout check paragraph, which is scoped to the ci-timeout exit reason and does not apply here. Compare counts, updated_at markers, review-summary body hashes (and head_sha, and has_merge_conflict) against the previous snapshot — same signals as Step 5f's "change event" list. Track a dedicated MONITOR_DURATION_REMAINING clock, initialized to the effective window at Step 6 entry; subtract each wait's elapsed time from both it and TOTAL_CI_BUDGET_REMAINING, the same way Step 5f item 4 subtracts from both TOTAL_CI_BUDGET_REMAINING and the per-push CI_TIMEOUT clock. If either reaches <= 0 during one of these direct Step 6 waits, that is monitoring-timeout — a success exit, per the paragraph above — never ci-timeout. 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. Because that sub-loop runs Step 5f, it draws on TOTAL_CI_BUDGET_REMAINING and can itself hit Step 5f's own ci-timeout Timeout check if the total budget runs out before the sub-loop reconverges — this is what stops flapping monitor-detect-fix cycles from chaining forever. The sub-loop's duration is charged only to TOTAL_CI_BUDGET_REMAINING, not to MONITOR_DURATION_REMAINING — a fix detour narrows the shared run-wide budget but does not by itself consume the leftover monitor window. After fixing, resume monitoring with min(MONITOR_DURATION_REMAINING, TOTAL_CI_BUDGET_REMAINING), not a fresh MONITOR_DURATION.
A non-empty errors list (Step 3's error accumulation, including an indeterminate mergeable == null) is never itself evidence of "no change": counts, timestamps, head_sha, and has_merge_conflict can look identical to the previous snapshot even though mergeability was never actually confirmed. If the window elapses while the most recent fetch's errors list is non-empty, do not declare monitoring-timeout success yet — perform one more Step 3 re-fetch, mirroring Step 5g's gate. If that re-fetch's errors list comes back empty and no other change is detected, the window has genuinely elapsed clean — proceed to Step 7 as monitoring-timeout. If errors is still non-empty, treat it as a change event and re-enter the evaluate + fix loop (Step 4 → Step 5g), so Step 5g's own retry-after-30-seconds gate resolves the ambiguous state instead of the window silently expiring into a false success.
If the window elapses without a change and the most recent fetch's errors list is empty, proceed to Step 7.
Step 7: Final Summary
Budget-exhaustion PR comment. If the exit reason is ci-timeout, post a PR comment before printing the local summary below, so the state and the resume path are visible on GitHub without anyone needing to re-run the skill just to read local output. Write the body to a mktemp'd temp file first (the same discipline as every other reply in this skill — see "GitHub CLI Commands Used") and post with gh pr comment <n> -R {owner}/{repo} --body-file <tmpfile>. Branch on TIMEOUT_TRIGGER's value (recorded by Step 5f's timeout check) to pick the matching branch of the {if TIMEOUT_TRIGGER == ...} conditional below, so the comment names whichever bound actually fired, rather than always blaming the total budget:
Autofix paused: {if TIMEOUT_TRIGGER == total-budget: total CI budget exhausted (`TOTAL_CI_BUDGET` = {TOTAL_CI_BUDGET} min) | if TIMEOUT_TRIGGER == per-push: CI check wait exceeded `CI_TIMEOUT` ({CI_TIMEOUT} min) for this push}.
**Fixes landed this run:** {ALL_COMMITTED_ITEMS shas + one-line summaries, or "none"}
**Feedback resolved:** {count of threads resolved / DEFER issues filed this run, or "none"}.
**Still pending:** {check runs from the most recent Step 3 fetch without a terminal `.conclusion` yet, and/or entries in `ci_failures` that never resolved, and/or unresolved feedback items, and/or merge conflicts, whichever apply}.
Resume with `/pm-autofix-pr {pullNumber}`.ALL_COMMITTED_ITEMS is the run-wide accumulator (Step 0b/Step 5b), not the current iteration's COMMITTED_ITEMS, so the "Fixes landed this run" line above reports every commit the run made before exhausting the budget, not just the final iteration's.
This comment is authored by GH_USER — the same identity every other reply in this skill already posts under — so Step 3's existing pr_comments self-filter (drops entries where .user.login == GH_USER, [bot]-suffix stripped on both sides per the note under Step 3) already keeps a later, resumed run from misreading its own exhaustion comment as reviewer feedback; no new filtering logic is needed. If the comment post itself fails, do not retry indefinitely or block the exit — log the failure and proceed to print the local summary regardless.
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 | regression-fix-failed
### 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 |
### Regression Fixes (self-caught by Step 5i, no reviewer involved)
| Commit | Finding |
|--------|---------|
| a1b2c3d | Deferred-run branch dropped the pending clock event instead of replaying it after the concurrency cap lifted |
### Blocked Regression Fixes (self-caught by Step 5i, pre-commit failed — `exit reason: regression-fix-failed`)
| Finding | Files | Pre-commit failure |
|---------|-------|---------------------|
| Hardcoded expected hash drifted once `main` advanced | test/regression_test.py | test: assertion mismatch |
### 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)
- Unpushed commits: PASS (no commits ahead of the resolved upstream) / FAIL (list the commit shas + subjects still ahead — or, with no upstream configured, every local commit)
- Regression review: PASS (Primary, and the advisor if consulted, agreed CLEAN) / DEGRADED (`DEGRADED_EVALUATORS` reason — no advisor available, or a call returned no parseable verdict after retry) / vacuous (no commits this run) / FAIL (list the latest combined findings)
- Issue creation failures: 0 / K (each requires manual filing — see Deferred table)The last Step 5g checklist evaluation (or, for any exit that never reached 5g — e.g. dirty-worktree/push-failure, an early ci-timeout on a first push, a merge-conflict failing out of 5h, or a rebase-conflict failing out of 5d — a fresh run of its five checks) is the source for this block — every exit prints the same five PASS/FAIL conditions, not just fixed-point, so a stale-loop or ci-timeout exit is exactly as legible as a success.
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) ormonitoring-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(also posts the budget-exhaustion PR comment above),merge-conflict,rebase-conflict,push-failure,dirty-worktree,regression-fix-failed: print "Autofix exited without converging — see summary above for required follow-up." For amerge-conflictexit, add: "Merge conflicts with<base.ref>could not be resolved automatically — resolve them manually, then re-run." For aregression-fix-failedexit, add: "A self-caught regression fix in<sha or 'the working tree' if reverted>failed pre-commit and could not be landed automatically — see the Blocked Regression Fixes table above, fix it manually, then re-run." Do not loop again, do not prompt.
References
references/api-patterns.md—gh/gh apicommand signatures, expected response shapes, polling-loop semantics, supersession algorithm, push and rebase handling, issue-creation flow, the opportunistic-MCP thread-resolution fast pathreferences/comment-evaluation.md— Full evaluation prompt templates, FIX/DEFER/REJECT decision matrix, DEFER and REJECT taxonomies, ambiguity-to-DEFER policy
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.

