agentleFS
tasklesstaskless/cli/.agents/skills/iterate-pr/SKILL.md

iterate-pr

Skill141 starsChanged 24 days agoMITView on GitHub

Iterate on a PR until CI passes. Use when you need to fix CI failures, address review feedback, or continuously push fixes until all checks are green. Automates the feedback-fix-push-wait cycle.

  • Commits and pushes

10 other projects use a version of this, 7 of them edited.

Adopted by

Most recently taken up by

Used with

Projects with it also use quota-axi, gh-axi and 3 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • layer5ioSkill

    quota-axi

    Report local Claude, Codex, Cursor, GitHub Copilot, and Grok quota windows via the quota-axi CLI - remaining percentages, reset times, and provider status read from local auth sources, with no routing, recommendation, or provider mutation. Use before deciding whether it is safe to keep spending a provider's quota, when the user asks about usage, rate limits, or remaining quota, or when comparing local provider headroom.

    Also in 4 projects that use this

  • layer5ioSkill

    gh-axi

    Operate GitHub through the gh-axi CLI - issues, pull requests, workflow runs, workflows, releases, repositories, labels, Projects (v2), Actions secrets and variables, search, and raw API access. Use whenever a task touches GitHub: listing or filing issues, reviewing or merging PRs, checking CI runs, triggering workflows, cutting releases, managing Projects boards, or managing Actions secrets/variables.

    Also in 4 projects that use this

  • kunchenguidSkill

    chrome-devtools-axi

    Control a Chrome browser session through the chrome-devtools-axi CLI - navigate, snapshot, click, fill forms, run JavaScript, inspect console and network, take screenshots, audit performance. Use whenever a task needs a real browser: opening or testing a web page, clicking through a flow, extracting page content, or debugging a website.

    Also in 4 projects that use this

  • kunchenguidSkill

    lavish

    Turn complex or visual agent responses into rich, reviewable HTML artifacts (HTML files) the user can annotate and send feedback on, using the lavish-axi CLI. Use when about to give a plan, comparison, diagram, table, code diff, report, or anything easier to grasp visually than as prose.

    Also in 4 projects that use this

  • MicrosoftSkill

    cut-release

    Use this skill to cut an APM release from the current worktree: assess whether the cycle since the last tag warrants a patch or minor bump (semver discipline against the merged-since-last-tag diff), sanitize the [Unreleased] CHANGELOG block into a dated version block with one concise "so what" entry per merged PR (drop internal-only churn, consolidate duplicates), bump pyproject.toml + uv.lock, run the CI-mirror lint chain, and open the release PR. Activate on "ship a release", "cut v0.x", "release prep", "bump and PR", "open release PR", "what kind of release do we need", or any phrasing that ends in opening a release PR -- even when the user does not say "skill". Stops BEFORE tagging; tagging stays a human gate that triggers the release workflow. Refuses to bump to a major (>= 1.0.0) version without explicit operator confirmation.

    Also in 4 projects that use this

The file

Show its rules and text

We split this skill into the rules it gives an agent

26 rules. 1 is common, 16 are distinctive, and 9 are very specific to this project.

  • 1

    Common

    Most of the 10 edited versions we read keep these.

  • 16

    Distinctive

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Important: All scripts must be run from the repository root directory (where .git is located), not from the skill directory.

    line 21
  2. Common:

    Stop if no PR exists for the current branch.

    line 163
  3. Very specific:

    Run ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_feedback.cjs to get categorized feedback already posted on the PR.

    line 167
  4. Distinctive:

    Check for similar issues in nearby code or related files

    line 179
  5. Distinctive:

    Never silently ignore review bot feedback — always verify the finding

    line 186
  6. Distinctive:

    After processing a feedback item, acknowledge it on the PR so the trail shows what was addressed.

    line 216
  7. Very specific:

    If the gh/GraphQL call fails, log and continue — do not block the workflow.

    line 278
  8. Very specific:

    Run ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_checks.cjs to get structured failure data.

    line 282
  9. Distinctive:

    Wait if pending: If review bot checks (sentry, warden, cursor, bugbot, seer, codeql) are still running, wait before proceeding—they post …

    line 284
  10. Very specific:

    Read the log_snippet and trace backwards from the error to understand WHY it failed — not just what failed

    line 387
  11. Distinctive:

    Read the relevant code and check for related issues (e.g., if a type error in one call site, check other call sites)

    line 388
  12. Distinctive:

    Do NOT assume what failed based on check name alone—always read the logs.

    line 392
  13. Distinctive:

    Before committing, verify your fixes locally:

    line 396
  14. Distinctive:

    If you fixed a test failure: re-run that specific test locally

    line 398
  15. Distinctive:

    If you fixed a lint/type error: re-run the linter or type checker on affected files

    line 399
  16. Distinctive:

    If local verification fails, fix before proceeding — do not push known-broken code.

    line 402
  17. Very specific:

    Run node ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_checks.cjs to get current CI status

    line 414
  18. Distinctive:

    If all checks passed → proceed to exit conditions

    line 415
  19. Distinctive:

    If any checks failed (none pending) → return to step 5

    line 416
  20. Very specific:

    If checks are still pending: a.

    line 417
  21. Very specific:

    After all checks pass, do a final feedback check: sleep 10, then run node ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_feedback.cjs.

    line 422
  22. Distinctive:

    If step 7 required code changes (from new feedback after CI passed), return to step 2 for a fresh cycle.

    line 426
  23. Distinctive:

    If waiting produced code changes, return to step 2 for a fresh cycle.

    line 439
  24. Distinctive:

    Success: All checks pass, post-CI feedback re-check is clean (no new unaddressed high/medium feedback including review bot findings, and no …

    line 441
  25. Distinctive:

    Stop: No PR exists, branch needs rebase.

    line 445
  26. Very specific:

    If scripts fail, use gh CLI directly:

    line 449

“Common” is measured: kept by at least 6 in 10 of the 10 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- Forked from Sentry's iterate-pr skill (Apache-2.0): https://github.com/getsentry/skills/tree/main/skills/iterate-pr Substantially modified: stack-aware tooling, LOGAF feedback bucketing, self-review handling, and pending-reviewer tracking. -->

Iterate on PR Until CI Passes

Continuously iterate on the current branch until all CI checks pass and review feedback is addressed.

Requires: GitHub CLI (gh) authenticated, and Node (already required to work in this repo).

The scripts are zero-dependency CommonJS — stdlib only, no package to install and no second toolchain. Each one exports its logic and is covered by a *.test.cjs suite beside it, which Validate runs with node --test. When you change a script, change its test.

Important: All scripts must be run from the repository root directory (where .git is located), not from the skill directory. Use the full path to the script via ${CLAUDE_SKILL_ROOT}.

Shell gotcha: the agent's Bash tool MAY run under a non-bash shell (e.g. zsh). Always check the current shell before running commands (echo "$0" / ps -p $$ -o comm=). It matters because zsh does NOT word-split unquoted variables the way bash does — for x in $list; do … iterates once over the whole string, not per token, which can silently send a loop down the wrong path. So any multi-step orchestration (cascade rebases, loops over branches/PRs, array iteration) MUST be wrapped in a bash <<'EOF' … EOF heredoc (or bash -c) so the semantics are guaranteed regardless of the login shell — never write it as an inline loop. For destructive git fan-out (rebase + force-push loops), add a guard that refuses to push if a rebase balloons — e.g. abort when git rev-list --count <parent>..HEAD exceeds the branch's own commit count, which catches a rebase that landed on the wrong parent before it reaches the remote.

The bundled scripts sidestep all of that, and you should follow their pattern. They spawn gh and git with an argv array and no shell, so nothing is word-split, glob-expanded, or re-read for metacharacters, and a branch name containing a space or a * is just a string. They also never pipe through sed, awk, cut, or xargs: a command runs, its output is parsed in Node, and the next command is invoked from the parsed values. When you need a step they do not cover, prefer that shape — run, parse in Node, run again — over a shell pipeline whose behaviour depends on which shell you got.

Bundled Scripts

scripts/shared.cjs holds what they have in common — running gh and git, and deriving stack lineage from the open PRs. It is a library, not a command.

scripts/fetch_pr_checks.cjs

Fetches CI check status and extracts failure snippets from logs.

node ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_checks.cjs [--pr NUMBER]

Returns JSON:

{
  "pr": { "number": 123, "branch": "feat/foo" },
  "summary": { "total": 5, "passed": 3, "failed": 2, "pending": 0 },
  "checks": [
    { "name": "tests", "status": "fail", "log_snippet": "...", "run_id": 123 },
    { "name": "lint", "status": "pass" }
  ]
}

scripts/fetch_pr_feedback.cjs

Fetches and categorizes PR review feedback using the LOGAF scale.

node ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_feedback.cjs [--pr NUMBER]

Returns JSON with feedback categorized as:

  • high - Must address before merge (h:, blocker, changes requested)
  • medium - Should address (m:, standard feedback)
  • low - Optional (l:, nit, style, suggestion)
  • bot - Informational automated comments (Codecov, Dependabot, etc.)
  • resolved - Already resolved threads, and top-level comments carrying our own 🎉 acknowledgement
  • review_in_progress - A review bot's placeholder comment, posted the instant it was triggered and not yet edited to its finished form. Not feedback yet — see below.
  • review_summary - A review's top-level narration, bucketed structurally rather than by content. Surfaced and counted (summary.review_summaries), never prompted on — see below.

Review bot feedback (from Sentry, Warden, Copilot, Cursor, Bugbot, CodeQL, etc.) appears in high/medium/low with review_bot: true — it is NOT placed in the bot bucket.

That includes Claude posting as github-actions, which happens when a workflow hands claude-code-action github_token. That login is shared with informational bots, so it is decided by content: inline review threads from it, and top-level comments carrying a claude-code-action signature (Claude finished @…, a placeholder, a [View job](…/actions/runs/N) link), are review feedback. Other github-actions comments stay in bot.

A review bot's comment existing is not a completion signal, and neither is its created_at or a green check run. The Claude review bot posts a comment the instant it is triggered and edits that same comment in place as it works — a run has been observed to report success while the body still read "Review in progress" with unchecked boxes. fetch_pr_feedback.cjs detects this by checking whether the body still opens with the in-progress marker (a review that happens to discuss this behaviour mid-body is not mistaken for an unfinished one) and reports the count as summary.review_in_progress instead of bucketing the placeholder as ordinary feedback. Before treating needs_attention: 0 as "reviewed, nothing found," check summary.review_in_progress — a nonzero count means the review hasn't finished, not that it found nothing. When one or more are present, action_required says so and outranks every other bucket, since a caller must not stop just because the buckets it can already see look clean.

Self-review feedback (from the PR author) appears in high/medium/low with self_review: true. Because you can't "Request changes" on your own PR, a self-review lands as COMMENTED review summaries and ordinary review threads rather than changes-requested items — these are surfaced (not dropped). A self-review thread (an inline comment) is bucketed by content like any other, defaulting to medium absent a h:/m:/l: prefix. A self-review summary is bucketed structurally, not by content (see below), landing in review_summary absent a marker. Treat self_review items the same as any other human feedback in step 3.

A review's top-level summary is bucketed structurally, not by its content. A summary narrates a review's findings, so it always contains finding vocabulary — often negated ("not a blocker", "no security issue found") — and no content-pattern list survives that. A CHANGES_REQUESTED summary from a reviewer who isn't the PR author is always high; an explicit h:/m:/l: marker still wins over everything; absent both, it goes to its own review_summary bucket, counted as summary.review_summaries.

That bucket is deliberately not low. low means "an optional suggestion, ask the user which to address", and step 3 presents low items as a numbered list for exactly that. A review that found nothing proposes no work, so filing it there trades a false high for a false prompt and asks someone to triage a summary with nothing in it. review_summary is surfaced and counted without being actionable, the same shape review_in_progress uses. Do not prompt on these, and do not treat one as a finding. The findings a review raises still arrive separately as inline review-thread comments, which are classified by content as usual.

Each feedback item may also include:

  • thread_id - GraphQL node ID for inline review comments (used for replies)
  • pending_reviewers (in summary) - count of requested reviewers who have not submitted yet; pr.requested_reviewers lists them

scripts/resolve_pr_threads.cjs

Resolves PR review threads by their GraphQL node IDs — the bulk equivalent of the resolveReviewThread mutation below. Prefer it when closing out several threads at once.

node ${CLAUDE_SKILL_ROOT}/scripts/resolve_pr_threads.cjs THREAD_ID [THREAD_ID ...]

Returns JSON:

{
  "resolved": ["PRRT_abc123", "PRRT_def456"],
  "failed": [],
  "already_resolved": ["PRRT_ghi789"]
}

scripts/stack_status.cjs

Reports the health of a PR stack — prefer this over hand-rolled git rev-list/merge-base shell loops (which are zsh-fragile). Lineage comes from the open GitHub PRs (each PR's head → base), the shared source of truth — no local config. For every branch with a parent it prints ahead/behind vs origin, own-commit count, and whether it is cleanly stacked or DIVERGED (parent tip is not an ancestor → needs a restack). Operates purely on refs, independent of the checked-out branch.

node ${CLAUDE_SKILL_ROOT}/scripts/stack_status.cjs [--root <branch>]

scripts/propagate_stack.cjs

Cascade-rebases a branch's descendants onto their parents to carry a fix up the stack — a focused restack that (unlike a whole-stack sync) never rebases onto the latest main, so fix-propagation stays decoupled from main-reconciliation. Lineage comes from the open GitHub PRs (head → base). It is topological (parent before child) and guarded: a balloon guard resets-without-pushing if a rebase lands on the wrong parent, and it stops on the first conflict for manual reconcile. Always prefer this to an inline rebase loop.

It replays from where the child forked, not from a merge-base. This matters whenever a parent was rewritten, which is the normal case here: you fixed review feedback on the parent, and that is why you are propagating at all. A plain git rebase <parent> picks its upstream by merge-base, which after a rewrite falls back to an older common ancestor — so the parent's own superseded commits get replayed on top of their replacements. It surfaces as a conflict in files the child never touched, and the obvious resolution ("take mine") silently discards the parent's newer work. The child's own tests still pass, because the change that was lost belongs to the parent, and under rebase-and-merge that stale content then reaches main and reverts a fix that already landed.

The upstream is resolved from three sources, most reliable first: the parent's pre-rebase tip when this run rebased it, git merge-base --fork-point when the rewrite happened elsewhere (a parent restacked by hand, or an earlier run), and the parent itself when it was only appended to. The balloon guard counts from that same upstream, so its expectation cannot be inflated by the parent's superseded commits.

node ${CLAUDE_SKILL_ROOT}/scripts/propagate_stack.cjs --root <branch> [--dry-run] [--no-push] [--no-sign]

Every replayed commit is GPG-signed. A rebase writes new commits, and git signs them only when told to, so a stack signed with commit -S comes back unsigned from a plain rebase. The script passes --gpg-sign, and a signing failure (usually a locked key) is reported as one, not as a conflict. --no-sign opts out for a repository that does not sign.

One failure path leaves the repo on another branch. Normal completion, a rebase conflict, a balloon guard trip, and a push failure all checkout the branch you started on before returning. The exception is when checking out a child fails (exit 6) — most often because that branch is checked out in a worktree — which returns immediately, leaving you on whichever branch it had reached.

That matters because anything you do next inherits it as a base: branching "from main" right after one of these silently creates a branch on top of the whole stack, and the resulting PR carries every stack commit plus an OpenSpec change directory it never touched. After a non-zero exit, run git checkout main (or git checkout -B <new> origin/main), and check git log --oneline origin/main..HEAD before opening the PR.

Workflow

1. Identify PR

gh pr view --json number,url,headRefName

Stop if no PR exists for the current branch.

2. Gather Review Feedback

Run ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_feedback.cjs to get categorized feedback already posted on the PR.

3. Handle Feedback by LOGAF Priority

Auto-fix (no prompt):

  • high - must address (blockers, security, changes requested)
  • medium - should address (standard feedback)

When fixing feedback:

  • Understand the root cause, not just the surface symptom
  • Check for similar issues in nearby code or related files
  • Fix all instances, not just the one mentioned

This includes review bot feedback (items with review_bot: true). Treat it the same as human feedback:

  • Real issue found → fix it
  • False positive → skip, but explain why in a brief comment
  • Never silently ignore review bot feedback — always verify the finding

Prompt user for selection:

  • low - present numbered list and ask which to address:
Found 3 low-priority suggestions:
1. [l] "Consider renaming this variable" - @reviewer in api.py:42
2. [nit] "Could use a list comprehension" - @reviewer in utils.py:18
3. [style] "Add a docstring" - @reviewer in models.py:55

Which would you like to address? (e.g., "1,3" or "all" or "none")

Surface, never a fix and never a prompt:

  • review_summary - a review's own narration, bucketed structurally rather than by content (see step 2's category list). It is not a finding to fix and not a suggestion to triage — reply to it per "Replying to Comments" below like any other surfaced item, but do not present it in the low-priority numbered list and do not count it toward needs_attention.

Skip silently:

  • resolved threads
  • bot comments (informational only — Codecov, Dependabot, etc.)

Neither address nor skip — wait:

  • review_in_progress items. There is nothing to fix yet; the bot hasn't finished writing its findings. Do not treat these as bot noise or as a clean review. See the "Wait if pending" note in step 4.

Replying to Comments

After processing a feedback item, acknowledge it on the PR so the trail shows what was addressed. How you reply depends on whether the item is an inline thread or a top-level comment.

When to reply (both cases):

  • high and medium items — whether fixed or determined to be false positives
  • low items — whether fixed or declined by the user
  • self_review items — the same as any other human feedback
  • review_summary items — a brief acknowledgment; there is nothing to fix, but the trail should still show it was read

Inline review-thread comments (items with a thread_id):

  1. Reply with the addPullRequestReviewThreadReply GraphQL mutation (pullRequestReviewThreadId + body).
  2. Then resolve the thread with the resolveReviewThread mutation using the same threadId — a reply alone does not resolve it.
mutation {
  resolveReviewThread(input: { threadId: "<thread_id>" }) {
    thread {
      isResolved
    }
  }
}

To close out several threads in one pass, use scripts/resolve_pr_threads.cjs THREAD_ID [THREAD_ID ...] instead of repeating the mutation. Resolve threads for high/medium items that were fixed or confirmed as false positives, and low items that were fixed or explicitly declined by the user — never where the action is unclear or still pending.

Top-level comments (items WITHOUT a thread_id — review_summary items and top-level PR/issue comments, e.g. a review bot like Claude that posts its findings as one top-level comment):

There is no thread to reply into, so post a new top-level comment — and when the item carries a comment_id, also add a 🎉 reaction to the original. The reaction is the machine-readable record that the item is handled: fetch_pr_feedback.cjs checks whether we reacted and buckets the comment as resolved, so a re-run stops reporting it. Without it, every later pass re-surfaces the same comment and you have to reason about whether you already dealt with it.

gh pr comment <pr> --body "..."
gh api -X POST "repos/{owner}/{repo}/issues/comments/<comment_id>/reactions" -f content=hooray

Take the id from the item's comment_id rather than searching by body text. If you do need to look it up, use the PR-scoped endpoint with --paginate — the repo-wide repos/{owner}/{repo}/issues/comments returns every comment in the repository, and the PR-scoped one defaults to 30 per page, so an unpaginated search silently misses comments on later pages:

gh api "repos/{owner}/{repo}/issues/<pr>/comments" --paginate \
  --jq '.[] | select(.id == <comment_id>) | .id'

The reaction is scoped to us. The script does not trust the raw reaction count — anyone can react 🎉 to a comment for unrelated reasons, and treating that as handled would silently drop real feedback. It confirms the reaction belongs to the authenticated user, and fails closed: if the viewer cannot be identified, the item resurfaces. Answering twice beats dropping something.

Items without a comment_id cannot be reacted to. GitHub exposes no reactions endpoint for a pull-request review body, so review_summary items — the text of a submitted review, as opposed to an ordinary PR conversation comment — have no reaction target. For those, dedupe by scanning existing top-level comments for one whose reference marker already cites this author and snippet. In practice most review bots post their findings as an ordinary comment, which does carry a comment_id.

A PR can carry several independent top-level comments, so a bare reply is ambiguous — open every top-level reply with a reference marker identifying the comment you are addressing. Cite the author and the opening of the original, and link it when the item includes a url:

> **Re:** @<author> — "<first line of the original, ~100 chars>…"
> <item `url`, if present>

<what changed, or why it's not an issue>

*— AI Coding Agent*

Reply format (both cases):

  • 1-2 sentences: what was changed, why it's not an issue, or acknowledgment of declined items.
  • End every reply with \n\n*— AI Coding Agent*.
  • Before replying, dedupe against re-loops: for inline threads, check whether the thread already has a reply ending in *- AI Coding Agent* / *— AI Coding Agent*. For a top-level comment with a comment_id, the 🎉 reaction handles it — an item carrying acknowledged: true (bucketed as resolved) has already been answered, so skip it. For a review_summary with no comment_id, fall back to the reference-marker scan.
  • If the gh/GraphQL call fails, log and continue — do not block the workflow.

4. Check CI Status

Run ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_checks.cjs to get structured failure data.

Wait if pending: If review bot checks (sentry, warden, cursor, bugbot, seer, codeql) are still running, wait before proceeding—they post actionable feedback that must be evaluated. Informational bots (codecov) are not worth waiting for.

This applies even when the check itself has already concluded. A review bot can report its check success while its comment is still the placeholder it posted on trigger — check summary.review_in_progress from fetch_pr_feedback.cjs (step 2), not the body text or the check's conclusion, and wait for it to drop to 0 before treating that bot's feedback as final.

The OpenSpec archive signal is a label and a warning, not a check

openspec-label.yml runs on every pull request with no branches: filter. It reads the head tree and asks one question: does any directory other than archive/ exist under openspec/changes/? If one does, the PR gets the Open OpenSpec label. The predicate asks nothing about stack position, so every PR in a stack gets the same answer, and the label clears when a branch in the stack archives the change.

The tip gets more. After labelling, the workflow runs gh pr list --state open --base <this branch>; a count of zero means nothing is stacked above, so this is the last branch that can archive before the change reaches main. It then emits a ::warning:: annotation and a job-summary block listing one pnpm openspec archive <change> line per change. That is an annotation and a summary, not a PR comment, so read the OpenSpec Label job rather than waiting for something to appear in the conversation. A fork PR gets neither the label nor the tip warning: the token is read-only and the fork's head branch cannot be a base here, which would report every fork PR as the tip.

On a tip PR, treat that warning as feedback to act on before merge. Sync the deltas into openspec/specs/, archive, push. "Archive on landing" is not an option, because main only takes PRs: a change that lands unarchived needs a second PR to correct it. On taskless/marketing, #50 and #51 landed unarchived and the fix could not be pushed directly, since the main ruleset has no bypass actors, so #52 was a third PR to do what the tip should have done.

On a non-tip PR, leave the change directory alone and let the label stand. Archiving mid-stack rewrites openspec/specs/ before the work it describes has landed.

Nothing fails either way, and that is deliberate: a gate here was expected-red for as long as a forward-merging stack took to drain, and a signal expected to be red is not a signal. The channel is the label and an issue. Once no open PR's diff touches the change directory, the next push to main has openspec-tracking.yml open OpenSpec: <change> is unarchived on main, labelled Open OpenSpec, which closes itself when the change reaches openspec/changes/archive/. openspec-sweep.yml runs on a daily cron but escalates on that issue at most once per seven-day window: it acts only once the directory has gone seven days without git activity, and then only if the issue itself has been idle that long, so any comment on the thread defers the next escalation by another window.

Stacked PRs: two other check behaviours worth knowing

Changeset warns, it does not fail. It looks for a .changeset/*.md added or modified anywhere between main and this PR's head, which covers every branch below it in the stack. A warning means no changeset exists anywhere in the stack. If the change ships user-visible behaviour, add one on the BOTTOM branch rather than labelling anything skip-changeset; every branch above inherits it, since a child contains its ancestors' commits. Extend that one file as later PRs land; never add a second changeset per PR.

on: pull_request: branches: [main] tells you nothing dependable about where a workflow runs. GitHub sometimes resolves a stacked PR's eventual target and matches on that, so such a workflow may run on mid-stack PRs — and may also silently stop. Measured on the #71→#106 stack: every PR up to #102 got a Validate run, #103 and #106 got none, across 16 pull_request events that filter-less workflows handled fine. #103 reached "ready for review" as a ~93-file change never linted, typechecked, or tested in CI.

So: a workflow that must run everywhere carries no branches: filter (and names ready_for_review in types:, which is not in the default set). A workflow that must act only on the PR merging to main establishes that from the base ref or its stack position, inside the job. Either way, when judging whether a check "should even be running here," check the stack — and if a check you expected is simply absent, suspect the filter before assuming the PR is fine. An absent check reads like a passing one.

Never leave a PR on red

No PR merges with failing tests, including mid-stack. "Merging down" changes where a PR merges, not whether its tests pass — and a red branch you merge into its parent carries that red upward.

It is tempting on a stack to explain a failure as structural: this unit relocates something and the unit that consumes it lands next, so of course it fails. Treat that explanation as a bug report about the split, not a reason to proceed. If unit N alone leaves the suite red, unit N is incomplete — the change that makes it self-consistent belongs in it. In practice that means the PR that moves something also repoints every reader and updates the tests that assert the old shape; the next PR then generalizes.

This is not hypothetical. A stack here sat on 20 failures rationalized as "dispatch arrives in the next PR." The real causes were a migration that relocated files while every reader kept the old paths, and a command that discovered rules before running the migration that creates them. Both were real defects, in the released code path, that the structural story explained away. An empty scan reports success, so the symptom was "no findings" rather than an error — which is exactly why it looked like an artifact of the split.

So: never annotate a red check as expected and move on. Check the branch out, run the suite, and fix the unit until it is green on its own. The only failures that survive that treatment are ones you can point at a specific missing commit for — and if that commit is in this stack, it belongs in this PR.

5. Fix CI Failures

For each failure in the script output:

  1. Read the log_snippet and trace backwards from the error to understand WHY it failed — not just what failed
  2. Read the relevant code and check for related issues (e.g., if a type error in one call site, check other call sites)
  3. Fix the root cause with minimal, targeted changes
  4. Find existing tests for the affected code and run them. If the fix introduces behavior not covered by existing tests, extend them to cover it (add a test case, not a whole new test file)

Do NOT assume what failed based on check name alone—always read the logs. Do NOT "quick fix and hope" — understand the failure thoroughly before changing code.

6. Verify Locally, Then Commit and Push

Before committing, verify your fixes locally:

  • If you fixed a test failure: re-run that specific test locally
  • If you fixed a lint/type error: re-run the linter or type checker on affected files
  • For any code fix: run existing tests covering the changed code

If local verification fails, fix before proceeding — do not push known-broken code.

git add <files>
git commit -m "fix: <descriptive message>"
git push

7. Monitor CI and Address Feedback

Poll CI status and review feedback in a loop instead of blocking:

  1. Run node ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_checks.cjs to get current CI status
  2. If all checks passed → proceed to exit conditions
  3. If any checks failed (none pending) → return to step 5
  4. If checks are still pending: a. Run node ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_feedback.cjs for new review feedback b. Address any new high/medium feedback immediately (same as step 3) c. If changes were needed, commit and push (this restarts CI), then continue polling d. Sleep 30 seconds, then repeat from sub-step 1
  5. After all checks pass, do a final feedback check: sleep 10, then run node ${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_feedback.cjs. Address any new high/medium feedback — if changes are needed, return to step 6.

8. Repeat

If step 7 required code changes (from new feedback after CI passed), return to step 2 for a fresh cycle. CI failures during monitoring are already handled within step 7's polling loop.

Exit Conditions

Before exiting, check summary.review_in_progress. If it is > 0, do not exit — a review bot's placeholder is not a finished review, and needs_attention: 0 alongside it means "hasn't started," not "clean." Sleep 30 seconds and re-check feedback, addressing any new high/medium feedback that lands as it finishes (return to step 3).

This loop has a bound, and reaching it is a report rather than a retry. Give up after roughly ten minutes of a count that never drops, and tell the user the review appears stuck, naming the PR and the bot. A placeholder can be left behind permanently: the run can be cancelled, or its job can crash, and the comment then sits in its posted state with nobody to edit it. Waiting forever on that is the same failure as exiting early, arrived at from the other side, and it is an infrastructure problem under Ask for help rather than something more polling will fix. Re-triggering the review is usually the fix, but that is the user's call, not yours.

Then check summary.pending_reviewers. If it is > 0, reviewers have been requested but haven't submitted yet — their review may produce new feedback. Ask the user whether to wait:

  • Yes: sleep 30 seconds, re-check feedback. If new high/medium feedback appeared, address it (return to step 3). If pending_reviewers dropped to 0, proceed to exit. Repeat until reviewers complete.
  • No: proceed to the exit conditions below.

If waiting produced code changes, return to step 2 for a fresh cycle.

Success: All checks pass, post-CI feedback re-check is clean (no new unaddressed high/medium feedback including review bot findings, and no review still in progress), user has decided on low-priority items, and pending reviewers resolved or user opted to skip.

Ask for help: Same failure after 2 attempts, feedback needs clarification, infrastructure issues.

Stop: No PR exists, branch needs rebase.

Fallback

If scripts fail, use gh CLI directly:

  • gh pr checks name,state,bucket,link
  • gh run view <run-id> --log-failed
  • gh api repos/{owner}/{repo}/pulls/{number}/comments

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.