Skip to content
Back to skills

Qodoloop

ASecurity

Iteratively drives a GitHub PR to zero unresolved Qodo (qodo-code-review[bot]) findings. Reads each finding's own "Agent Prompt" (Qodo hands you a ready-made fix spec per issue), applies the fix, replies to the thread explaining what was done, resolves it, pushes, and waits for Qodo's next pass. Repeats until clean or a real blocker needs a human. Use when the user wants a PR fully addressed against Qodo's review, or says "run qodoloop" / "satisfy Qodo" / "resolve Qodo comments".

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 14, 2026
ai-agentsgobashreactawscode-reviewgitapidatabase

Works with

  • claude code
  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 14, 2026

npx -y skills add oleg-koval/agent-skills --skill qodoloop --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Qodoloop?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Qodoloop
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/oleg-koval-qodoloop-agent-skills/badge)](https://www.skillsdirectory.com/skills/oleg-koval-qodoloop-agent-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: qodoloop
description: >
  Iteratively drives a GitHub PR to zero unresolved Qodo (qodo-code-review[bot]) findings. Reads
  each finding's own "Agent Prompt" (Qodo hands you a ready-made fix spec per issue), applies the
  fix, replies to the thread explaining what was done, resolves it, pushes, and waits for Qodo's
  next pass. Repeats until clean or a real blocker needs a human. Use when the user wants a PR fully
  addressed against Qodo's review, or says "run qodoloop" / "satisfy Qodo" / "resolve Qodo comments".
compatibility: Requires git and gh (GitHub CLI) authenticated, with Qodo Merge installed on the repo.
metadata:
  version: "1.2"
allowed-tools: Bash(gh:*) Bash(git:*)
---

# Qodoloop

Fix every actionable Qodo finding on a PR, answer each thread, resolve it, and keep going until Qodo has nothing left to say (or a finding needs a human call).

## Inputs

- **PR number** (optional): detect from the current branch if not given.
- **Include optional findings** (optional, default off): Qodo tags each finding "Review recommended" (actionable) or "Optional" (informational/lower-confidence). By default only "Review recommended" findings count toward done; pass `--include-optional` to also fix and resolve the optional ones.

## How Qodo actually posts (verified against a live install: don't assume otherwise)

Qodo runs as `qodo-code-review[bot]` and posts **two independent things** per review pass, not one:

1. A rollup **issue comment** titled `<h3>Code Review by Qodo</h3>`: human-readable, all findings in one place. A separate `<h3>PR Summary by Qodo</h3>` comment is a plain description, not a review: ignore it. While running, Qodo posts `<h3>Qodo is busy working</h3>` as its own comment (not an edit of a prior one).
2. A **formal PR review** (`state: COMMENTED`) carrying one **inline review comment per finding**, each a real, resolvable GitHub review thread at the finding's file/line: same machinery as any human inline comment.

**Use the inline threads as the source of truth.** They carry `isResolved` state natively (fetch via GraphQL, see `references/graphql-queries.md`), so "done" is answerable directly instead of re-parsing the rollup's HTML on every iteration. Each thread's body contains a `<details><summary>Agent Prompt</summary>` (or `**Agent Prompt**`) section with a ready-made fix spec, and a severity badge image whose `alt` text is `Remediation recommended` or `Informational` (recommended ↔ "Review recommended", informational ↔ "Optional").

Qodo has **no check run to poll**: there is nothing in `gh pr checks` to wait on. Completion means a new `qodo-code-review[bot]` comment/review appears after your push; `scripts/pr-review-state.sh` detects it.

**Rate limit is a real terminal state, not a bug.** A completed-looking comment reading "Qodo reviews are paused" / "you've reached your PR review limit" means Qodo will not review again this cycle: stop and report it, don't loop waiting for something that isn't coming.

## Review state comes from the PR, not from memory

`scripts/pr-review-state.sh` reads the PR and prints one JSON object: the current head, whether
Qodo has acted on that head (a review of it, or any comment after it was pushed), and every
unresolved review thread with its author. Every "is there work / are we done" decision in this
loop comes from its exit code, never from what you remember resolving. The script lives in this
skill's directory, `SKILL_ROOT`: the directory holding this `SKILL.md` (Claude Code prints it as the
skill's base directory when the skill loads; in Codex, derive it from the selected `SKILL.md` path).

| Exit | `verdict`  | Meaning                                                             | Do                                   |
|------|------------|---------------------------------------------------------------------|--------------------------------------|
| 0    | `clean`    | Qodo acted on the current head and no bot thread is unresolved    | exit check: done                     |
| 10   | `findings` | unresolved threads from Qodo **or any other review bot**          | run an iteration                     |
| 20   | `pending`  | Qodo has not acted on the current head yet                        | trigger if needed, then wait         |
| 30   | `limited`  | a rate-limit or paused notice from Qodo after the current head    | stop, report `blocked: rate-limited` |

Resolving the last thread does not make a PR clean: Qodo has not reviewed your push yet. Earlier
versions of this loop exited at exactly that moment, while the next round of comments was still on
its way. Human threads are listed in `.unresolved` (`is_bot: false`) and counted in `.counts.humans`;
they never block `clean`, and this loop never resolves them.

## Instructions

### 1. Identify the PR

```bash
gh pr view --json number,headRefName,headRefOid -q '{number, branch: .headRefName, sha: .headRefOid}'
```

Check out the branch if not already on it, **then require a clean working
tree** (`git status --porcelain` empty) before touching anything:

```bash
[ -z "$(git status --porcelain)" ] || { echo "working tree not clean, refusing to start: would risk committing unrelated changes"; exit 1; }
```

This is the precondition step E's scoped `git add <file>` depends on: staging
only the files a fix touched is safe *because* the tree started clean, so
nothing else could be sitting in those files. Skip this check and a file
with pre-existing local edits will have those edits swept into the fix
commit right along with it: the exact failure `git add -A` had, just
narrower.

### 2. Loop (max 5 iterations)

#### A. Read the PR state; wait for Qodo's review of the current head

One read answers both "is there work?" and "has Qodo seen my last push?". `SINCE` is the
timestamp you took right before your last push (step E); leave it empty on iteration 1. Right
after a push, add `--wait 600` straight away: the wait ends only once the bot has acted on the new
head, so threads left open on purpose cannot make the push look reviewed.

```bash
"$SKILL_ROOT/scripts/pr-review-state.sh" --repo OWNER/REPO --pr <PR> --bot qodo-code-review --since "$SINCE"
```

- Exit `10` (`findings`): go to B. On iteration 1 this is whatever Qodo (or another bot)
  already posted.
- Exit `20` (`pending`): Qodo reviews new pushes on its own, so there is nothing to post. Then wait with the same command plus `--wait 600`. Run the
  wait in the background where the host allows it (Claude Code: Bash `run_in_background`; you are
  re-invoked when it exits) so ten minutes of polling never sits in the conversation. Still `20`
  after the wait: say so out loud, check the PR for a Qodo notice explaining the silence, and wait once more. Still `20` after that: stop and
  report the timeout honestly; never claim success.
- Exit `0` (`clean`): go to the exit check. A `--wait` call returns `clean` only after a settle
  re-read 60 seconds later, so a comment landing right behind the review is still caught. A bare
  read (no `--wait`) that says `clean` is good enough only when you have not pushed since Qodo's
  last review.
- Exit `30` (`limited`): stop now and report `blocked: rate-limited`; nothing more is coming.
- Exit `1`: the read itself failed (often a transient GitHub 502). Retry once; if it fails again,
  stop and report it. An unknown state is not a clean one.

#### B. Fetch unresolved findings

The working set is every unresolved thread in the state JSON with `is_bot: true`: Qodo's
own (`is_target: true`) **and any other review bot's** (Greptile, for example). A
fix for one bot often draws a comment from another, and leaving those open is how a "finished" loop
leaves a PR that is not clean. Never act on a human's thread (`is_bot: false`); list it in the report.

Qodo's threads are `.unresolved[] | select(.is_target)` in step A's state JSON (the script already follows pagination). For each, read the full comment (`gh api repos/{owner}/{repo}/pulls/comments/<comment_id>`) and parse the finding title, severity (recommended/optional), file/line, and the **Agent Prompt** block.

Build the working set: all `recommended` threads, plus `optional` ones too if `--include-optional`.

Threads from other bots come from the state JSON (`.unresolved[] | select(.is_bot and (.is_target | not))`).
They carry no ready-made fix prompt: read the full comment
(`gh api repos/{owner}/{repo}/pulls/comments/<comment_id>`), verify its claim against the real
code, and handle it like any Qodo finding: fix it, or reply with the evidence for not fixing it.

Mark each thread as seen with an πŸ‘€ reaction on its first comment (`databaseId` from the query), so
the bot and a watching human can tell it is being worked. GitHub's reaction set is fixed; `eyes` is
the only "looking at it" value:

```bash
gh api --method POST repos/{owner}/{repo}/pulls/comments/<DATABASE_ID>/reactions -f content=eyes
```

#### C. Check exit conditions

Stop when step A's read exited `0` (`clean`) after your last push (a `--wait` read, which settles),
or exited `30` (`limited`), or the iteration cap is reached. An empty working set on its own is not
done: with no review of the current head yet, it only means Qodo has not answered your push.

Two kinds of thread stay open by design, so for Qodo `findings` can still mean done: a finding you
**blocked** (replied, deliberately left unresolved) and, without `--include-optional`, an
**Optional** finding outside the working set. Stop as done when Qodo has acted on the current head
(`.bots[0].acted_on_head` is `true` in a `--wait` read) and every open bot thread is one of those
two. Never re-work a thread already in your blocked ledger.

#### D. Fix each finding

For each thread in the working set, in order: read the Agent Prompt in full: it already names the issue, the context, the fix focus files/lines, and often the suggested fix. Apply it. Do not go beyond its stated scope. Track two lists as you go: **fixed** (thread id + the exact files you touched for it) and **blocked** (thread id + why: the prompt's suggestion was wrong, unsafe, or needs a product decision). Do not force a fix into the blocked list; do not resolve anything yet.

#### E. Commit and push

If **fixed** is empty, skip straight to F (nothing to commit). Otherwise, stage **only the files you touched in D**: never `git add -A`, which can sweep in unrelated local changes or secrets that have nothing to do with this loop:

```bash
git add <files touched by this iteration's fixes>
git commit -m "address Qodo review feedback (qodoloop iteration N)"
SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)   # step A reads bot activity after this
git push
```

Confirm the push actually succeeded before moving on: a resolved thread whose fix never made it to the branch is worse than an unresolved one.

#### F. Answer each finding

This is the "answer to the comments" half of the job, not optional cleanup, and it only runs **after** E's push is confirmed durable, so a thread is never marked resolved while its fix still only exists locally:

- **Fixed** findings: reply with one or two sentences (what you changed and why), **then** resolve:
  ```bash
  gh api graphql -f query='
    mutation($threadId: ID!, $body: String!) {
      addPullRequestReviewThreadReply(input: {pullRequestReviewThreadId: $threadId, body: $body}) {
        comment { id }
      }
    }' -f threadId="$THREAD_ID" -f body="$REPLY_TEXT"
  ```
  Pass the thread id and reply text as GraphQL **variables** (`-f threadId=... -f body=...`), never interpolated into the query string itself: a reply containing a quote, backtick, or newline breaks (or worse, injects into) a hand-built query. Then resolve with `resolveReviewThread` (see references file).
- **Blocked** findings: reply explaining exactly why (false positive / needs a human call), but **do not resolve**: leave the thread open. A blocked finding that gets silently resolved is a false "done".

Then record each disposition as a reaction on the thread's first comment: the learning signal, and
a visible marker for a human scanning the PR. πŸ‘ (`+1`) for a fixed finding, πŸ‘Ž (`-1`) for a
blocked/rebutted one. Reply first, react, then resolve: a reaction never replaces the reply.

```bash
gh api --method POST repos/{owner}/{repo}/pulls/comments/<DATABASE_ID>/reactions -f content=+1
# blocked/rebutted finding:
gh api --method POST repos/{owner}/{repo}/pulls/comments/<DATABASE_ID>/reactions -f content=-1
```

Go back to step A.

### Keep the context small; resume instead of restarting

- Carry only three things between iterations: the iteration counter, the latest state JSON, and a
  ledger of each thread you handled (fixed with its commit, rebutted, or blocked). Re-read files
  fresh each iteration; do not re-paste earlier rounds' comment bodies.
- All loop state lives on the PR, so the loop is resumable. If the session has grown long or slow,
  or a bot commented after the loop ended, run this skill again in a fresh session: step A picks up
  from the PR as it is now. Starting fresh is cheaper than dragging a long session through more
  rounds.
- After reporting done, if the host can schedule a follow-up (Claude Code: `/loop` or a scheduled
  wakeup), offer one re-check about 20 minutes later for bots that comment late.

### 3. Report

| Field              | Value |
|--------------------|-------|
| Iterations         | N     |
| Findings resolved  | N     |
| Findings blocked   | N (with reasons, left unresolved) |
| Remaining          | N (if any) |
| Human threads      | N (open, listed; never resolved by this loop) |
| Blocked            | rate-limited / timeout / human-intervention / max-iterations / none |

```text
Qodoloop complete.
  Iterations:  2
  Resolved:    4 (fixed, replied + resolved)
  Blocked:     1 (needs a human call, replied, left unresolved)
  Remaining:   0
```

## References

- `references/graphql-queries.md`: the `resolveReviewThread` / `addPullRequestReviewThreadReply` mutations, with field notes. Its `unresolvedQodoThreads` query is superseded by `scripts/pr-review-state.sh` for finding threads.

Files in this skill

  • SKILL.md13.8 KB
  • references/graphql-queries.md7.2 KB
  • scripts/pr-review-state.sh7.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.