geminiloop · diff

v1.1 to v1.2

87 added, 87 removed. Audit A to A.

---
name: geminiloop
description: >
Iteratively satisfy Gemini Code Assist's review on a GitHub PR, but treat every Gemini comment
SKEPTICALLY (Gemini Code Assist is materially more error-prone than Greptile): verify each finding
against the real code first, fix ONLY the genuinely-correct ones, and rebut + resolve false
- positives WITHOUT changing correct code. Repeat until no unresolved Gemini comments remain. Use
+ positives WITHOUT changing correct code. Repeat until Gemini has reviewed the latest push and nothing is left unresolved. Use
when the user wants to clear a PR's Gemini Code Assist review.
compatibility: GitHub only (Gemini Code Assist is a GitHub app). Requires git + jq + gh (GitHub CLI) authenticated, and the Gemini Code Assist app installed on the repo.
metadata:
- version: "1.1"
+ version: "1.2"
allowed-tools: Bash(gh:*) Bash(git:*)
---
# Geminiloop
Drive a GitHub PR until Gemini Code Assist has no unresolved comments, **but do not cargo-cult
its suggestions.** Gemini is often confidently wrong. Every comment is a *claim to verify*, not an
instruction to obey. A wrong suggestion applied is worse than the comment itself.
## How Gemini differs from other review bots
- **No check-run, no score.** Gemini does not publish a `X/5` confidence or a `greptile` check. It
posts a **PR review** (state `COMMENTED`) authored by `gemini-code-assist[bot]` with a
`## Code Review` summary body plus inline review comments. Detection is by polling the *reviews*
- endpoint, not a check-run. "Satisfied" = zero unresolved comments (each either fixed or rebutted),
- since there is no numeric target.
+ endpoint, not a check-run. "Satisfied" = Gemini has reviewed the latest push and no bot comment is left
+ unresolved (each either fixed or rebutted), since there is no numeric target.
- **Auto-reviews on push/open.** It reviews automatically when a PR opens or gets new commits;
`/gemini review` forces a fresh pass.
- **Severity, not confidence.** Inline comments carry a priority badge (a `![critical]` /
`![high]` / `![medium]` / `![low]` shields image) at the top of the body. Weight `critical`/`high`
seriously; treat `medium`/`low` as usually-skippable nits unless clearly correct.
- **Higher false-positive rate.** This is the whole point of the skill: bias toward *rebut* over
*change*.
## Not for
- GitLab / Perforce (Gemini Code Assist is GitHub-only). For other review bots this catalog ships
`copilotloop`, `codexloop`, `coderabbitloop`, and `qodoloop`; for CI failures rather than review comments, use
`ci-fix-loop`.
+ ## 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
+ Gemini 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` | Gemini acted on the current head and no bot thread is unresolved | exit check: done |
+ | 10 | `findings` | unresolved threads from Gemini **or any other review bot** | run an iteration |
+ | 20 | `pending` | Gemini has not acted on the current head yet | trigger if needed, then wait |
+ | 30 | `limited` | a rate-limit or paused notice from Gemini after the current head | stop, report `blocked: rate-limited` |
+
+ Resolving the last thread does not make a PR clean: Gemini 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.
+
## 1. Identify the PR
```bash
gh pr view --json number,headRefName,headRefOid -q '{number,branch:.headRefName,head:.headRefOid}'
```
Switch to the PR branch if not already on it. Capture `OWNER`/`REPO` (`gh repo view --json owner,name`).
## 2. The loop (max 5 iterations)
Keep an explicit iteration counter and stop at 5: the cap is a real bound to enforce, not a
figure of speech. Each pass through A–G is one iteration; on hitting the cap, go straight to the
report and list what is still unresolved rather than starting a sixth.
- ### A. Ensure a fresh Gemini review on the current head
-
- Gemini auto-reviews new commits, but force a deterministic pass and record the head SHA:
-
- ```bash
- HEAD_SHA=$(gh pr view <PR> --json headRefOid -q .headRefOid)
- # Only trigger if no gemini review already exists for this exact SHA.
- # gh cannot combine --slurp with --jq, so pipe to `jq -s`: it folds every page
- # into one array. Without it the filter runs once per page and HAVE becomes a
- # multi-line count that never equals "0", silently skipping the trigger.
- HAVE=$(gh api repos/{owner}/{repo}/pulls/<PR>/reviews --paginate |
- jq -s --arg head "$HEAD_SHA" \
- '[.[][] | select(.user.login=="gemini-code-assist[bot]" and .commit_id==$head)] | length')
- if [ "$HAVE" = "0" ]; then gh pr comment <PR> --body "/gemini review"; fi
- ```
+ ### A. Read the PR state; wait for Gemini's review of the current head
- Poll for the review of THIS head to land. No check-run exists, so poll the reviews endpoint, and
- poll it on a **deadline**, never `while true`: a review that never arrives must end the skill with
- an honest timeout, not hang it.
+ One read answers both "is there work?" and "has Gemini 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
- # 10-minute deadline, one retry, then give up.
- wait_for_review() { # $1 = attempt label
- local deadline=$(( SECONDS + 600 ))
- while [ "$SECONDS" -lt "$deadline" ]; do
- R=$(gh api repos/{owner}/{repo}/pulls/<PR>/reviews --paginate |
- jq -s --arg head "$HEAD_SHA" \
- '[.[][] | select(.user.login=="gemini-code-assist[bot]" and .commit_id==$head)] | sort_by(.submitted_at, .id) | last')
- if [ -n "$R" ] && [ "$R" != "null" ]; then return 0; fi
- echo "waiting for Gemini review of $HEAD_SHA ($1)..."; sleep 15
- done
- return 1
- }
-
- if ! wait_for_review "first wait"; then
- echo "no Gemini review after 10m, retrying once" # say the retry out loud
- gh pr comment <PR> --body "/gemini review"
- if ! wait_for_review "after retry"; then
- echo "Gemini did not review $HEAD_SHA after a retry; stopping and reporting."
- exit 1 # honest timeout, never a success claim
- fi
- fi
+ "$SKILL_ROOT/scripts/pr-review-state.sh" --repo OWNER/REPO --pr <PR> --bot gemini-code-assist --since "$SINCE"
```
- Report the retry in the final summary; two silent timeouts are the failure mode this guard exists
- to prevent.
+ - Exit `10` (`findings`): go to B. On iteration 1 this is whatever Gemini (or another bot)
+ already posted.
+ - Exit `20` (`pending`): post `/gemini review` (`gh pr comment <PR> --body "/gemini review"`). 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, post `/gemini review` once more, 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 Gemini'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 the findings
- - **Summary** (the `## Code Review` body): the review `.body` from the object above: read the
+ The working set is every unresolved thread in the state JSON with `is_bot: true`: Gemini'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. Treat another bot's thread with exactly the same verify-first rules
+ below. Never act on a human's thread (`is_bot: false`); list it in the report.
+
+ - **Summary** (the `## Code Review` body): the latest review `.body` from this bot (`gh pr view <PR> --json reviews`): read the
overall take and the severity spread.
- - **Unresolved inline comments** on the current head:
+ - **Unresolved inline comments**: `.unresolved[] | select(.is_bot)` in step A's state JSON lists
+ each thread's `thread_id`, `comment_id`, `path`, `line` and a short excerpt, across every page of
+ threads. Read the full comment before judging it:
```bash
- gh api repos/{owner}/{repo}/pulls/<PR>/comments --paginate \
- --jq '.[] | select(.user.login=="gemini-code-assist[bot]") | {id, path, line, body}'
+ gh api repos/{owner}/{repo}/pulls/comments/<COMMENT_ID> --jq '{user: .user.login, path, line, body}'
```
- Also pull the review threads + their resolved state via GraphQL (see step F) so you only act on
- unresolved ones.
-
Mark each unresolved comment as seen, so the bot and a watching human can tell it is being handled.
GitHub's reaction set is fixed; `eyes` is the only "looking at it" value:
```bash
gh api --method POST repos/{owner}/{repo}/pulls/comments/<COMMENT_ID>/reactions -f content=eyes
```
### C. Critically evaluate EACH comment (the core of this skill)
For every comment, **verify the claim against the actual code and repo conventions before touching
anything.** Read the file, the surrounding code, the types, and any call sites. Then classify:
1. **CORRECT + actionable**: the finding is real and the fix improves the code. → fix it (step D).
2. **FALSE POSITIVE / technically wrong**: the claim doesn't hold. → do **NOT** change code; write a
specific, evidence-based reply (cite the exact code/line/behavior that disproves it), then resolve.
3. **Valid but out-of-scope / stylistic nit** that conflicts with repo convention or the PR's intent
→ briefly decline with a reason, then resolve. Do not expand the PR's scope to satisfy a nit.
**Hard rules:**
- **Never modify correct code just to silence Gemini.** Prefer a reasoned rebuttal.
- When uncertain whether a claim holds, **investigate** (read more code, run the type-checker / tests)
rather than assume Gemini is right. Default to skepticism.
- If a suggested change would break other call sites, alter public behavior, or contradict a verified
repo convention, it is a category-2 rebuttal, not a fix.
- Never fabricate identifiers to satisfy a comment (e.g. a Linear/ticket prefix). If Gemini asks for a
ticket reference and none exists, say so; do not invent one.
**Gemini's common failure modes to watch for (default these to category 2):**
- Hallucinated APIs, options, or framework behavior stated as fact.
- "Add a null/undefined check" where the type already guarantees presence.
- Suggestions that compile-break or break other callers.
- Security/perf warnings with no actual exploit path or measurable cost.
- Restating library/framework semantics incorrectly.
- Style demands that contradict the repo's existing, consistent pattern.
### D. Apply fixes: category 1 only
Make the minimal correct change. Re-run the local gate if the repo has one (typecheck/tests) before
moving on.
### E. Commit and push FIRST, before resolving anything
Order matters. A resolved thread is a claim that the fix is on the branch, so the push has to
succeed before the claim is made: otherwise a failed commit or push leaves the PR unfixed with the
finding marked resolved, and nobody looks at it again.
If step D changed code:
```bash
# Stage ONLY the files your fixes touched: never `git add -A`, which sweeps up
# unrelated work and untracked secrets sitting in the worktree.
git status --short # look before you stage
git add <path> [<path>...] # the files named in the findings you fixed
git commit -m "address gemini review feedback (geminiloop iteration N)"
+ SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ) # step A reads bot activity after this
git push
```
Author the commit per the repo's norms (e.g. the user's identity; no AI attribution if that is the
convention). Confirm the push actually landed before continuing:
```bash
git rev-parse HEAD
gh pr view <PR> --json headRefOid -q .headRefOid # must match
```
If they differ, stop: the fix is not on the PR, so nothing may be resolved yet.
### F. Reply to and resolve every addressed thread
- Only now, with the fixes pushed, reply and resolve. Fetch unresolved threads, **following
- pagination**: a PR with more than 100 threads will otherwise look clean while unresolved findings
- sit on page two:
-
- ```bash
- # Loop until hasNextPage is false, passing endCursor back in as $cursor.
- CURSOR=null
- while : ; do
- PAGE=$(gh api graphql -F cursor="$CURSOR" -f query='
- query($cursor: String) {
- repository(owner: "OWNER", name: "REPO") {
- pullRequest(number: PR_NUMBER) {
- reviewThreads(first: 100, after: $cursor) {
- pageInfo { hasNextPage endCursor }
- nodes { id isResolved comments(first: 1) { nodes { databaseId author { login } path body } } }
- }
- }
- }
- }')
- echo "$PAGE" # collect nodes from every page before deciding the PR is clean
- PI='.data.repository.pullRequest.reviewThreads.pageInfo'
- [ "$(echo "$PAGE" | jq -r "$PI.hasNextPage")" = "true" ] || break
- CURSOR=$(echo "$PAGE" | jq -r "$PI.endCursor")
- done
- ```
+ Only now, with the fixes pushed, reply and resolve. Take thread and comment ids from step A's state
+ JSON (`.unresolved[]`): the script already follows pagination, so do not hand-roll a threads query.
Reply on a thread's comment via `gh api repos/{owner}/{repo}/pulls/<PR>/comments -f body="..." -F in_reply_to=<comment_id>`,
then resolve:
```bash
gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "THREAD_ID"}) { thread { isResolved } } }'
```
Then record the disposition as a reaction on the bot's own comment: the learning signal, and a
visible marker for a human scanning the PR:
- 👍 (`+1`): the finding was accepted and fixed.
- 👎 (`-1`): the finding was rebutted as a false positive. **This is the correction signal.**
```bash
gh api --method POST repos/{owner}/{repo}/pulls/comments/<COMMENT_ID>/reactions -f content=+1
# rebutted finding:
gh api --method POST repos/{owner}/{repo}/pulls/comments/<COMMENT_ID>/reactions -f content=-1
```
Reply first, then react, then resolve: the reaction is not a substitute for the reply, and a
resolved thread with no reply loses the reasoning.
- Resolve a thread only for comments authored by `gemini-code-assist[bot]` that you have fixed or
- rebutted: never blanket-resolve, and never resolve a human reviewer's thread.
+ Resolve a thread only for a bot comment in your working set (this bot's or another
+ review bot's) that you have fixed or rebutted: never blanket-resolve, and never resolve a human reviewer's thread.
Threads you are **rebutting** need no push, so they may be replied to and resolved regardless of
whether step D changed code.
- ### G. Re-review
+ ### G. Back to A
- Pushing re-triggers Gemini automatically; go back to **A** with the new head SHA. If step D changed
- nothing (all comments were rebutted), skip the push, ensure all threads are resolved, and exit.
+ Go back to **A**. After a push, A waits for Gemini's review of the new head (pushing re-triggers
+ it when auto-review is on; otherwise A triggers it). If this iteration changed no code (every finding
+ rebutted), there is no new head to wait for: A's fresh read says `clean` or shows what is still open.
## 3. Exit conditions
+ Decide from a fresh state read, never from what you remember resolving.
+
Stop when **any** is true:
- - Zero unresolved `gemini-code-assist[bot]` comments remain, and every comment this round was fixed
- or rebutted+resolved. (There is no score to hit: this is "done".)
- - Max iterations (5) reached: report what remains.
- - Gemini never responded after one retry: report the timeout honestly; do not claim success.
+ - **Done**: the latest `pr-review-state.sh --wait 600` call, made after your last push, exited `0`
+ (`clean`); with no push since the bot's last review, a bare read that exits `0` is enough. Open human threads do not block this; list them in the report.
+ - **Max iterations (5)** reached: report what remains.
+ - **Timeout**: still `pending` after the retry in step A. Report it honestly; do not claim success.
+ - **Rate limited**: exit `30`. Report `blocked: rate-limited`.
+ ## 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.
+
## 4. Report
```text
Geminiloop complete.
PR: #<n>
Iterations: N
Comments fixed: N (genuinely-correct findings)
Comments rebutted: N (false positives / nits, resolved with rationale)
- Remaining: 0
+ Remaining: N (bot threads still open)
+ Human threads: N (open, listed below; this loop never resolves them)
+ Final state: clean | timeout | rate-limited | iteration cap
```
If it stopped at max iterations, list the remaining threads with your current assessment
(fix-pending vs disputed) so a human can arbitrate.