iterate-pr · git:20260930.106186c · 2026-09-30 · sha256 cc54b11a3b81fa21

iterate-pr git:20260930.106186cA

Immutable. This exact content never changes and is served at /api/v1/blob/cc54b11a3b81fa21.

---
name: iterate-pr
description: 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.
---

<!--
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.

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

Returns JSON:

```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](https://develop.sentry.dev/engineering-practices/code-review/#logaf-scale).

```bash
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.

**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.

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

Returns JSON:

```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.

```bash
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.

```bash
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

```bash
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.

```graphql
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.

```bash
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:

```bash
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.

```bash
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`