Gemini Code Assist PR Review Loop
Overview
Use this skill to run the full GitHub PR loop: after PR creation, wait for gemini-code-assist to finish reviewing, fetch unresolved actionable review threads, acknowledge the requested fixes, implement clear fixes, verify them, commit and push to the PR branch, and ask Gemini Code Assist to re-review the latest revision.
Prefer thread-aware review data over flat PR comments. GitHub review threads preserve isResolved, isOutdated, file paths, line anchors, and diff hunks, which are necessary for reliable automation.
Thread States
Each Gemini review thread is in one of these states. The fetch script tags each thread accordingly, and the stop logic consumes the tag:
RESOLVED— Gemini or the maintainer has explicitly resolved the thread. Skip.OUTDATED— The line anchor has moved out from under the thread (the code Gemini commented on no longer exists at that position). Auto-resolved by the script. Skip.ADDRESSED_BY_REPLY— Unresolved, but the current user (or another maintainer) has posted a substantive reply (≥30 chars, not a bot, not a token "ack"). Treated as a human decision to defer/wontfix; the loop does not try to fix this thread again. The script auto-resolves these on the next pass via GraphQL (see "GitHub Write Safety" below). Opt out with--no-resolve-addressed-by-reply.UNRESOLVED— Actionable. Drives the next fix attempt.
A thread can transition UNRESOLVED → ADDRESSED_BY_REPLY → RESOLVED (reply + auto-resolve), or UNRESOLVED → fixed in code → OUTDATED → RESOLVED (line moves out, auto-resolved).
Cycle Counting
A cycle is one Gemini re-review request posted by the agent after Gemini's initial review.
- Cycle 0: Gemini's initial automatic review at PR open. Free; does not count toward the cap.
- Cycles 1–N: Each subsequent re-review request the agent posts, where
Nismax_rereview_requestsfrom~/.config/gh-gemini-review-loop/preferences.jsonor the default3. After cycleN, hard stop. - Replies posted via
repos/.../pulls/comments/{id}/repliesdo NOT count as a cycle. - Pushes to the PR branch without a re-review request do NOT count as a cycle.
- Only re-reviews posted by the agent itself count. A human pinging
@gemini-code-assistdoes not consume a cycle. The script auto-detects the agent's GitHub login viagh api user; override with--agent-login NAMEor opt out with--no-agent-filter.
The cap blocks new re-review requests and new fix cycles unless the user raises the cap. The cap does not block stale-thread cleanup, addressed-by- reply cleanup, metrics updates, terminal classification, final reports, or recording terminal state. Even at the cap, the script may still resolve outdated threads, classify fixed-pending findings, and print or record the final summary.
Severity Ordering
Gemini prefixes inline review comments with a markdown image whose alt text is the severity (critical / high / medium / low). The script parses this and orders actionable threads critical → high → medium → low → unknown, so high-severity findings are reported and fixed first. The severity tag also appears in the per-thread markdown header, e.g. ## 1. src/auth.py:42 [high].
Loop Receipt
Pass --post-receipt to leave a one-comment audit trail on the PR after the loop runs: cycles used, threads resolved (outdated + addressed-by-reply), threads still pending, and severity breakdown of remaining actionable threads. Use --dry-run --post-receipt to preview the receipt without posting.
Sticky receipt (background visibility)
For richer visibility while the loop is in flight, use --sticky-receipt instead. It maintains one comment per PR that the script edits in place across loop invocations. State persists in ~/.config/gh-gemini-review-loop/state.json (override with GGRL_STATE_DIR env var).
- First invocation: posts a fresh comment with status
RUNNING, stores its id locally. - Subsequent invocations: PATCH the same comment in place. No new comments accrete on the PR.
- Status header is configurable via
--receipt-status {running,done,stopped}. Default isRUNNINGfor sticky receipts. - Tag the final invocation with
--receipt-status done(orstopped) so the user sees the loop has finished. - Discovery fallback: if the local state file is missing, the script searches PR comments for the embedded marker and re-attaches to the existing receipt.
When --sticky-receipt is appropriate: long-running interactive loops where the user is watching the PR tab, not the chat. Always paired with --receipt-status running at cycle start, done at clean exit, stopped at stop-condition.
When the one-shot --post-receipt is appropriate: scripted/batch contexts where each invocation is independent and you want a fresh audit comment per run.
Optional Judge Eval (--judge-mode)
The Gemini review loop supports an optional OpenAI-based judge eval. It classifies each Gemini finding as one of valid_actionable / false_positive / duplicate / already_addressed / explanation_only / needs_human, plus a severity_override and recommended_action. The judge is read-only — it never resolves threads, posts comments, or pushes.
Judge eval is off by default. Nothing is sent to OpenAI unless the user explicitly opts in.
Requires an OpenAI API key resolved by key_resolver.py (env var → dotfile → macOS Keychain → Linux Secret Service). No SDK install needed — the judge uses stdlib urllib. Missing key → judge gracefully skips with a structured skipped result + one stderr hint. The loop continues unchanged.
If the user asks how to set their key permanently, recommend the OS-keystore path:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/key_resolver.py" --set
This stores in macOS Keychain (Touch ID / password-protected), Linux Secret Service, or a chmod-600 dotfile fallback. No rc-file edits, no ps leakage, no shell-restart dance. For diagnostics, suggest key_resolver.py --print-source and judge_doctor.py.
The script (fetch_gemini_threads.py) is the single source of truth. It reads ~/.config/gh-gemini-review-loop/preferences.json on every invocation and combines the saved mode with the --judge-phase the agent supplies.
Phase is auto-inferred. You do not need to pass --judge-phase: a terminal
--record-run invocation is treated as phase complete; every other fetch is
phase cycle. So on_cycle runs on every cycle fetch and on_complete runs
only at the terminal record-run, with no per-cycle flag to remember. An explicit
--judge-phase still overrides the inference.
When judge_mode=on_cycle, show the deterministic judge block every cycle.
If the judge was requested but skipped, relay the script-owned line exactly:
[loop] judge eval skipped: <reason>
Do not hand-write a replacement judge table or skip explanation unless the script fails.
Discoverability
Do not prompt for judge eval during a normal loop run. Do not prompt at session start.
One-time tip — after fetch, before fixes. On the first cycle where actionable findings are present, if judge_tip_shown is not true in the prefs file, emit this tip immediately after the findings narration line, then call mark_tip_shown() to persist judge_tip_shown: true:
[loop] cycle 1/<cap> — 4 actionable thread(s) (high: 1, medium: 3). Fixing.
[loop] Tip: judge eval can give a second opinion on these findings.
Try: "run the Gemini loop with judge eval at completion"
The tip fires at the moment the user is looking at real findings — before any fixes are applied. It appears exactly once across all future sessions.
Use README examples, --help output, and marketplace description for broader discoverability.
When to prompt
Prompt with the current runtime's choice-prompt mechanism only when the user explicitly requests judge eval without specifying a mode. In Claude Code this may be AskUserQuestion; in Codex use the available user-input flow or ask one concise question directly.
"enable judge eval" / "use judge eval" / "turn on eval"
Prompt text:
Judge eval sends Gemini findings and related PR context to OpenAI.
Choose eval mode:
- Every cycle
- At completion only
- Just this once
- Off
Persist via save_preferences(). Mapping:
- 1 →
save_preferences("on_cycle") - 2 →
save_preferences("on_complete") - 3 → do NOT save — pass
--judge-mode once --judge-phase completefor this run only - 4 →
save_preferences("off")
Preference file
~/.config/gh-gemini-review-loop/preferences.json
{
"schema_version": 2,
"judge_mode": "off",
"judge_tip_shown": true,
"max_rereview_requests": 3
}
The file is created automatically on the first script invocation with safe defaults (judge_mode: off, max_rereview_requests: 3). No manual setup is required.
Key fields:
judge_mode— controls when the OpenAI judge eval runs. Valid values:off,on_complete,on_cycle. Set via natural language ("enable judge eval") orsave_preferences(). Default:off.max_rereview_requests— persistent loop cap. Overridable per-run with--max-rereview-requests N. Default:3.judge_tip_shown— internal flag.trueafter the one-time "judge eval is available" tip has been shown in chat. Set automatically; do not edit manually.judge_model— OpenAI model used for eval. Default:gpt-4o-mini.
max_rereview_requests sets the persistent loop cap. The script reads it from ~/.config/gh-gemini-review-loop/preferences.json on every invocation. The CLI flag --max-rereview-requests N overrides it for a single invocation.
To configure the persistent cap, create or edit:
mkdir -p ~/.config/gh-gemini-review-loop
python3 - <<'PY'
import json
from pathlib import Path
path = Path.home() / ".config" / "gh-gemini-review-loop" / "preferences.json"
prefs = json.loads(path.read_text()) if path.exists() else {}
prefs["schema_version"] = 2
prefs["max_rereview_requests"] = 4
path.write_text(json.dumps(prefs, indent=2, sort_keys=True) + "\n")
PY
Or edit the JSON directly:
{
"schema_version": 2,
"max_rereview_requests": 4
}
Cost framing
gpt-4o-mini ≈ $0.001 per finding. on_complete ≈ $0.005 max per PR. on_cycle worst case depends on the configured cap (default: ≈ $0.015 for 3 cycles × 5 findings).
Verification Profile
Each repo can have an opinionated, code-derived verification profile: the
checks the loop runs at its verify step. Stored in
~/.config/gh-gemini-review-loop/preferences.json under profiles["owner/repo"].
First run (detect → preset menu → save)
Run detection after fetching findings, before the first fix attempt, so the
verification strategy is fixed before any edits. This ordering is enforced by a
PreToolUse hook that blocks edits during an active loop until a profile is
saved (see Bundled hooks). If get_profile(owner/repo)
returns None:
Detection precedence (monorepo-aware): if the repo root has a justfile with
verification recipes (test, test-*, *-tests, check, lint, typecheck,
verify) that take no required arguments, those become just <recipe> checks.
Otherwise, test directories discovered in the git tree (tests, __tests__,
spec, …) are each mapped to their nearest package marker and emitted with a
per-check working_directory. Otherwise, root single-stack detection applies.
A check may carry its own working_directory (relative to the repo root);
run_profile runs it there, falling back to the profile-level
working_directory.
- Run
detect_profile.py <repo_root>. It returns{stack, confidence, reasons, candidate_checks, presets}.presetsis an explicit, ordered, code-built option list — do not hand-roll the menu or rely on the runtime prompt UI auto-adding an option. - If
stack == "unknown"(emptycandidate_checks, emptypresets) → do not prompt or persist; use ad-hoc verification. - Reconcile against repo docs (
CLAUDE.md,CONTRIBUTING,README). If docs pin a non-standard invocation, surface it as a note beside the menu — "Repo docs pin/opt/homebrew/bin/pytest; pick Customize manually to use it." Never auto-persist an absolute path from prose. - Prompt once with the current runtime's choice-prompt mechanism, using each
presets[i].labelverbatim as an option. Example menu for a multi-check Python repo: "All detected — pytest + ruff check ." / "Tests only — pytest" / "Skip — use ad-hoc verification" / "Customize manually". - Persist the chosen preset via
judge.save_profile(...):- Has
customize == true(Customize manually) → run the free-form NL customize path; persist the user's edited checks withsource="customized". - Otherwise persist
preset["checks"]withsource=preset["source"](confirmedfor All detected,customizedfor a narrower preset,skippedfor Skip). Every persisted check isrequired: true.
- Has
Subsequent runs
A profile (including a skipped one) exists → no prompt. If source is
confirmed or customized, first render and relay the deterministic profile
intro block:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/fetch_gemini_threads.py" \
--profile-intro \
--repo OWNER/REPO
Then run run_profile.py <owner/repo> <repo_root> — it prints to_details()
JSON and exits non-zero if a required check failed; feed its verification into
--verification and the JSON into --verification-details. On skipped or
unknown stack, relay the formatter's fallback profile-intro block and use
today's ad-hoc "narrowest meaningful checks".
Customizing / un-skipping
source="skipped" suppresses automatic detection prompts only. Explicit user
intent always overrides it:
- "add mypy to this repo's verification profile" / "change the checks to X" →
edit the profile via
save_profile(..., source="customized"). - "set up a verification profile for this repo" → re-enter the detect → preset
menu → save flow and overwrite the profile, even if a
skippedmarker exists.
Gate semantics
The verify step fails iff any required check fails or times out.
Non-required failures are recorded in --verification-details but do not flip
--verification to failed. Feed ProfileRunResult.to_details() into
--verification-details and its verification field into --verification.
Before running checks, render and relay the deterministic planned-verification block:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/fetch_gemini_threads.py" \
--planned-verification \
--repo OWNER/REPO
Then run the profile runner. If no runnable profile exists, the formatter says so; continue with ad-hoc verification.
Run Metrics
After a loop completes, the script can append one JSON record to a local append-only log and print a one-screen summary.
What is stored and where
Records are appended to ~/.config/gh-gemini-review-loop/runs.jsonl (override the directory with GGRL_STATE_DIR). The file is append-only JSONL — one record per completed loop run. It is never transmitted anywhere.
Each record holds counts only: findings fetched, fixed, needs-human, addressed-by-reply, cycles used, verification result, outcome, duration (seconds), finding areas/paths, and optionally a judge-derived breakdown (only when judge mode was on). The record also includes the repo and PR number so stats can be scoped per repo.
No identity is recorded — no git author, no GitHub login, no username. The data cannot be sliced per developer and cannot become a productivity score.
The run's start timestamp and per-finding accumulation reuse the same per-PR key in state.json (see Sticky receipt) — no extra state file is needed. Judge-derived lines (e.g. false-positive counts) appear in the record and summary only when judge mode was on for that run.
--record-run (write once at loop end)
Call exactly once after the loop reaches a terminal state:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/fetch_gemini_threads.py" \
--record-run \
--fixed-count <n> \
--verification <passed|failed|skipped>
Optional additions:
--verification-details '<json>'— structured test output--outcome <clean|capped|human|regression|no_progress|verification_failed|fixed_pending_confirmation>— terminal state label--outcome-reason '<text>'— one-line explanation--gemini-confirmed— final wait/re-review completed and confirmed the latest state--gemini-unconfirmed— final wait timed out or otherwise did not confirm the latest fixes
The script fetches the current thread state, derives counts, appends the record, and prints a [loop] Summary block to stdout.
Per-cycle summary block
--record-run is terminal: it writes a record and clears the run accumulator, so it must be called exactly once at loop end. To show a mid-loop receipt during the loop — at the end of each cycle — use --cycle-summary instead:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/fetch_gemini_threads.py" \
--cycle-summary \
--fixed-count <n-this-cycle> \
--verification <passed|failed|skipped>
--cycle-summary builds the record from the accumulated run state (findings and judge verdicts unioned across cycles so far) and prints a [loop] Cycle receipt block — distinct from the terminal [loop] Summary — but it does not append to runs.jsonl and does not clear the accumulator. It is read-only and safe to call every cycle.
The receipt block also carries two sections that used to be buried in collapsed tool output. Relay the entire receipt verbatim so they reach the chat:
Verification suite:— the repo-aware checksrun_profile.pydetected (e.g.uv run pytest (root, required) → passed). Built from--verification-details; present whenever you pass profile JSON.Findings (N): X new, Y carried over— the deterministic finding list, one line per finding withpath:line [severity]and the GitHub comment URL. A finding whose content was already seen in a prior cycle is tagged· carried over from a prior cycle, so a re-posted suggestion is never miscounted as a fresh finding. This is the canonical, visible finding list — do not hand-roll a separate one or leave it inside a collapsed Bash result.
Emit a receipt at the end of every cycle. Which command depends on whether the cycle is also terminal:
- Non-terminal cycle (the loop will push, re-review, and continue): run
--cycle-summaryright after the verify step. This is REQUIRED on every such cycle — do not skip it because fixes were small or verification was skipped. - Terminal cycle (this cycle hits a stopping condition — clean, capped, human decision, regression, or no-progress): do not call
--cycle-summary. The single--record-runcall you make at loop end already prints the receipt for this cycle. Calling both would print two near-identical receipts back-to-back.
So a clean two-cycle run prints two receipts: one [loop] Cycle receipt from --cycle-summary at the end of cycle 1, one [loop] Summary from --record-run at loop end. A run that stops on cycle 1 (like a human-decision deferral) prints exactly one receipt — [loop] Summary from --record-run. Never emit two receipts on the same cycle.
Do not call --record-run more than once per loop: each call clears the accumulator, so a second call would undercount findings_fetched and reset the duration. Use --cycle-summary for all mid-loop visibility.
Script-owned human blocks
For human-readable loop narration, prefer deterministic script output and relay it verbatim. Do not hand-write or paraphrase these blocks unless the script fails:
- profile intro (
--profile-intro) - planned verification (
--planned-verification) - judge eval table or judge skip line
- cycle receipt (
--cycle-summary) - terminal summary (
--record-run) - semantic-risk note (
--semantic-risk, repeatable) - next options for non-clean terminal outcomes
- wait heartbeat (
--wait-chunk-secondspending output, or--wait-heartbeat)
Before push or before Gemini confirms the re-review, use the script's fixed-
pending wording. Locally fixed work must appear as Fixed locally plus
Awaiting push/re-review confirmation, not as Remaining valid actionable.
For non-clean terminal summaries, rely on the deterministic Next options:
section emitted by the formatter. Do not replace it with free-form prose.
Semantic risk note
--semantic-risk is a manual / heuristic v1 signal. Pass it when a fix changes
behavior in ways that passing tests may not fully cover, especially:
- public function signature changes
- return-shape changes
- password/auth/security behavior
- database query behavior
- exception behavior
- public API behavior
Example:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/fetch_gemini_threads.py" \
--cycle-summary \
--fixed-count <n-this-cycle> \
--verification passed \
--semantic-risk "hash_password(password) -> hash_password(password, salt)" \
--semantic-risk "get_user() now returns one row instead of a list"
Relay the resulting [loop] Semantic risk note (manual / heuristic) block
verbatim. Do not present it as deterministic detection.
Color and JSON output
Human-readable [loop] blocks are purple/magenta by default. Do not strip ANSI
color from normal human-readable loop output; the [loop] prefix remains
present for searchability and accessibility.
Color must never appear in JSON, GitHub comments, files, metrics, receipts
stored on disk, or any machine-readable output. For every --json or
--format json command, stdout is machine JSON only: parse stdout with
json.loads(...), treat logs and warnings as stderr, and do not relay raw JSON
unless the user explicitly asked to see machine output.
--stats (read-only)
Print aggregated stats for the current repo from runs.jsonl and exit. Never touches GitHub.
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/fetch_gemini_threads.py" --stats
Options: --stats-window N (default 10 most-recent runs), --stats-all-repos, --format json.
Run metrics and --stats are local-only — stored under ~/.config/gh-gemini-review-loop/, never posted to GitHub, and contain no identity.
Progress Narration
This is enforced mechanically: a PreToolUse:Bash hook (loop_summary_gate.py) blocks every git push while a Gemini loop is active and the summary is stale. The hook does NOT fire for --record-run (the terminal receipt is exempt). Trying to push without summarizing first returns exit code 2 and explains the fix.
Violating the letter of this rule violates the spirit.
While the loop is running, the agent MUST emit one-line status updates to the user-facing chat at each phase transition. The format is [loop] cycle N/<cap> — <phase>.
Mechanical backstop layer (three independent enforcers):
loop_summary_gate.py(PreToolUse:Bash) — Blocksgit pushwhenupdate_seq > last_summary_seq. Exit code 2 with an error message tells the agent exactly which--cycle-summarycommand to run first. This is the primary gate: the agent cannot push a new cycle's commits without first emitting the summary.loop_summary_hook.py(Stop) — On every turn end, if a loop advanced without a summary being emitted, runs--cycle-summary --auto-snapshotand surfaces the result through the runtime hook output (systemMessagein Claude Code). Catches the terminal-cycle gap (between--record-runoutput existing in Bash tool output and the agent relaying it to the user).Memory feedback — Saved in the session memory to reinforce the rule across future sessions.
Required narration points:
| Phase | Narration line |
|---|---|
| Before script fetch | [loop] session cycle N — re-review cap: M/K consumed. Fetching threads from PR #<num>... where N is the cycle number within this session (1-based), M is how many re-reviews already exist on the PR, K is the cap. This is the key fix for the "cycle 1/4 but cap is 3/4" confusion: always show both session-local position and cap state separately. |
| After fetch, before fixes | [loop] session cycle N — <K> actionable thread(s) (severity: <breakdown>). Fixing. + judge eval tip if first time (see Discoverability) + if judge ran: copy the entire [loop] judge eval (phase): … block from script stdout verbatim (header + one row per thread) |
| After fix attempt, before verify | [loop] session cycle N — fixes applied. Verifying via profile runner. |
| After verify | [loop] session cycle N — verified (<test summary>). |
| Before push | [loop] session cycle N — committing and pushing <commit-sha>... |
| Before push (HARD GATE) | Run --cycle-summary and print its full output ([loop] Cycle receipt block). Only then push. |
| After push, before re-review | [loop] session cycle N — pushed. Requesting Gemini re-review. Cap now M/K. |
| After final re-review request | Wait for Gemini after REREVIEW_AT before terminal recording. If wait succeeds, record with --gemini-confirmed; if it times out, record with --gemini-unconfirmed. |
| During any Gemini wait | Run chunked waits (--wait-chunk-seconds); after each non-ready chunk relay the script's heartbeat block verbatim, then start the next chunk. Never background the wait; never go silent for more than ~90s. |
| Stop condition triggered | [loop] STOP — <stop-condition>: <one-line explanation>. |
| Loop complete (all clean) | [loop] DONE — 0 actionable threads remaining. Cycles used: N/<cap>. |
| Loop complete / stopped (after DONE/STOP) | [loop] Summary block (from --record-run) — print the full stdout; this is the terminal receipt |
Terminal thread breakdown (when remaining_actionable > 0)
After printing the [loop] Summary block, when there are remaining actionable threads, render them in three separate buckets — never a single mixed "for human review" table. The buckets map directly to why each thread is still open:
Always reference a thread by its GitHub comment URL (e.g.
https://github.com/<owner>/<repo>/pull/<n>#discussion_r3374837147), which the
receipt's Findings block already prints per finding. Never surface the bare
discussion_r… / GraphQL node token on its own — it is opaque to the user and
not clickable. If you need to point at a thread to resolve, give the URL and,
when useful, the file:line.
Bucket 1 — Human decision required
Threads where the judge verdict was needs_human. These require a product/format/design call, not a code change. For each thread:
Human decision required
1. <file>:<line> · <GitHub comment URL>
Finding: <what Gemini flagged, verbatim or closely paraphrased>
Why human: <concrete reason — format consistency, security policy, product behavior tradeoff>
The agent did not auto-fix this because <specific reason: changes report format behavior / requires policy decision / both options are valid>.
Options:
- <option A>
- <option B>
Example:
Human decision required
1. main.py:828 · https://github.com/Owner/Repo/pull/9#discussion_r3369882171
Finding: OWASP tags appear in console and JSON reports, but not in Markdown.
Why human: this changes report format behavior. Both choices are valid.
The agent did not auto-fix this because it is a product decision, not a safe mechanical fix.
Options:
- Add OWASP tags to Markdown output for consistency.
- Keep Markdown simpler; document that OWASP tags are JSON/console only.
Bucket 2 — Remaining because cap was reached
Threads that the judge classified as valid_actionable but the loop ran out of cycles before addressing them. Do not label these as "human review" and do not downgrade their severity — they remain valid, fixable issues. Do not use "low priority" unless the judge, reviewer, or user explicitly said so.
Remaining because cap was reached
1. <file>:<line> · <GitHub comment URL>
Finding: <one-sentence description>
Judge: valid_actionable (conf <N>)
Reason not fixed: cap reached
Suggested handling: fix in next PR, or bump cap and re-run the loop
2. ...
Bucket 3 — Already fixed but still unresolved on GitHub
Threads where you applied a code fix in this loop, but the GitHub thread still shows UNRESOLVED. These are not open work items — they will become OUTDATED on the next Gemini review.
Already fixed but still unresolved on GitHub
1. <file>:<line> · <GitHub comment URL>
Finding: <what Gemini originally flagged>
Status: fix applied in this session
Why still shown: code change shifts the line anchor; thread auto-resolves as OUTDATED on next Gemini review
Omit any bucket with zero entries. If every remaining thread fits bucket 1, just print bucket 1. Never print an empty bucket header.
Classification logic (in order of priority):
- Judge verdict
needs_human→ bucket 1 - Thread from a prior Gemini review where you applied a code fix at that file/line in this session → bucket 3
- All other
valid_actionablethreads (new from latest review, or no fix attempted) → bucket 2
After the buckets — next step suggestions:
Use the deterministic Next options: section emitted by the terminal summary
formatter. Do not hand-write the options unless the script fails.
Verification routing:
Always run verification through run_profile.py when a profile is confirmed for the repo:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/fetch_gemini_threads.py" \
--planned-verification \
--repo owner/repo
python3 "$GGRL_PLUGIN_ROOT/skills/gh-gemini-review-loop/scripts/run_profile.py" owner/repo /path/to/repo
Relay the planned-verification block before running checks. Feed the runner's
verification field into --verification and its full JSON output into
--verification-details. Do not call uv run pytest or any test runner
directly — route through the profile so metrics get consistent structured
details. This matters even when you know the profile command (the runner times
it, captures structured output, and sets the right exit code for downstream
processing).
Skip narration only when running in pure non-interactive batch mode (e.g. gh pr create chained into a script that captures output for later — but in interactive Claude Code or Codex sessions, never skip).
Rationale: in interactive Claude Code sessions, the user is watching the chat. Silent loops feel broken even when they're working. One line per phase is the right cadence — enough to show progress without burying signal.
When the user explicitly wants visibility outside the chat (e.g. they'll step away from the terminal, or other reviewers will look at the PR while the loop runs), pair the chat narration with --sticky-receipt. See Sticky receipt above.
Bundled hooks
The plugin ships three hooks (hooks/hooks.json) that turn the most-skipped
agent obligations into mechanical guarantees. All are network-gated by local
state.json (free no-ops outside an active loop) and all fail open: a hook
error never wedges the session.
| Event | Script | What it guarantees |
|---|---|---|
PreToolUse (Bash) |
loop_summary_gate.py |
Blocks git push while a loop is active and summary_is_stale(). Exit code 2 with the exact --cycle-summary command to run. Primary gate — the agent physically cannot push without summarizing first. |
PreToolUse (Edit/Write/MultiEdit) |
loop_profile_gate.py |
Blocks edits while a loop is active for the repo and no verification profile is saved, so the verify strategy is fixed before any fix. Saving any profile — including Skip — clears the gate. |
Stop |
loop_summary_hook.py |
If a loop advanced this turn without a [loop] Summary, emits the authoritative --cycle-summary. Dedup-aware via update_seq/last_summary_seq — silent when the agent already summarized. Read-only; never records or clears. |
These mean the agent should still narrate and summarize as documented, but a lapse no longer costs the user visibility or breaks the profile-before-fixes ordering.
Variations (user-prompt → flag mapping)
When the user phrases the request differently, dispatch to the right flag combination. This table is authoritative; if a phrasing isn't here, fall back to defaults.
| User intent | Phrasing examples | Pass to script |
|---|---|---|
| Default loop | "run the gemini loop" / "handle gemini feedback" / "yeet this PR" | (no extra flags) |
| High-severity only | "only fix high severity" / "skip the nits" / "just the important stuff" | --min-severity high |
| Medium and above | "skip low-priority comments" | --min-severity medium |
| Critical only | "just the critical findings" | --min-severity critical |
| Strict severity filter | "only what Gemini flagged as high — ignore unmarked" | --min-severity high --drop-unknown-severity |
| Audit-only | "summarize Gemini comments" / "read-only review" / "show me what's pending" | --dry-run --post-receipt --no-resolve-outdated --no-resolve-addressed-by-reply |
| More cycles once | "be persistent" / "do 4 cycles" | --max-rereview-requests 4 |
| Fewer cycles once | "one cycle only" / "don't loop, just fix once" | --max-rereview-requests 1 |
| Persistent cap | "always use 4 cycles" / "configure the cap max to 4" | Set max_rereview_requests in ~/.config/gh-gemini-review-loop/preferences.json |
| Specific PR | "handle PR https://github.com/..." | --pr <URL> |
| Different bot login | "handle review comments from google-gemini-code-assist" | --author google-gemini-code-assist |
| Post status without acting | "leave a status comment without touching anything" | --post-receipt --no-resolve-outdated --no-resolve-addressed-by-reply |
| Live status comment | "show me a live status comment on the PR" / "I want background visibility" | --sticky-receipt --receipt-status running per cycle; --sticky-receipt --receipt-status done at the final invocation |
| Loop + judge at completion | "run the Gemini loop with judge eval at completion" / "with judge eval at completion" | save_preferences("on_complete"). Phase is auto-inferred (complete at --record-run). No prompt. |
| Loop + judge every cycle | "run the Gemini loop with judge eval on every cycle" / "with judge eval on every cycle" | save_preferences("on_cycle"). Phase is auto-inferred (cycle per fetch, complete at --record-run). No prompt. |
| Judge just this once | "run judge eval just this once" / "with judge eval just this once" | --judge-mode once --judge-phase complete. No save. No prompt. |
| Enable judge eval (no mode) | "enable judge eval" / "use judge eval" / "turn on eval" | Show the runtime choice prompt; act on answer. |
| Explain judge eval | "what is judge eval?" / "how does judge eval work?" | Explain it. Do not enable it. |
| Disable judge for this run | "skip the judge this time" | --judge-mode off |
| Change saved preference | "change my eval preference" / "reset judge mode" | Show the runtime choice prompt; overwrite prefs file. |
| Default loop with saved judge mode | (no special phrasing — agent reads saved prefs) | No --judge-phase needed — phase is auto-inferred. Script obeys saved mode. |
| History investigation | "show me all Gemini threads ever, including resolved" | --include-resolved --include-outdated --include-addressed-by-reply --no-resolve-outdated --no-resolve-addressed-by-reply |
| Local stats | "show Gemini loop stats" / "loop stats for this repo" / "how's the loop doing here" | --stats |
| Set up verification profile | "set up a verification profile for this repo" / "configure checks for this repo" | Run detect_profile.py, show preset menu, then persist the chosen preset |
| Customize profile | "add mypy to this repo's checks" / "change the verification checks to X" | Edit checks, save_profile(..., source="customized") |
| Skip profile | "skip verification profile" / "use ad-hoc checks for this repo" | save_profile(repo, source="skipped") — suppress automatic re-prompt |
Run metrics and --stats are local-only — stored under ~/.config/gh-gemini-review-loop/, never posted to GitHub, and contain no identity.
If the user explicitly opts out of any default behavior (e.g. "don't auto-resolve anything"), respect it for the rest of the session via --no-resolve-outdated --no-resolve-addressed-by-reply.
This skill does NOT support multi-bot loops (CodeRabbit, Copilot, etc.). It is opinionated for gemini-code-assist only. If the user asks for a different bot, change --author, but severity parsing and addressed-by-reply heuristics are calibrated for Gemini's output format.
Stopping Conditions
Stop the loop and report status instead of pushing or asking Gemini again when any condition is true:
Cap reached — Gemini has already been asked to re-review the PR up to the configured cap.
All clean — There are no
UNRESOLVEDactionable Gemini threads after stale-thread cleanup.Human decision required — All remaining
UNRESOLVEDthreads are informational, duplicate, contradictory, or require a human product/design/security decision.Test regression — Tests fail after a fix attempt and the failure is not clearly caused by the latest Gemini-addressing change.
No progress — A thread that was UNRESOLVED in the previous cycle is still UNRESOLVED after a fix attempt AND the surrounding code/hunk was not changed AND no substantive maintainer reply (as defined in Thread States) was posted on it. This catches genuine stuckness — distinct from ADDRESSED_BY_REPLY, which is intentional deferral and should not trip this condition.
The script detects this mechanically: when the actionable thread fingerprint (SHA256 of thread ids + bodies) is identical to the previous cycle's, it prints:
[loop] no_progress: actionable thread set is unchanged since the previous cycle — no fix landed. Stop with: --record-run --outcome no_progress ...When this line appears in script stdout, stop immediately: call
--record-run --outcome no_progress --outcome-reason 'no code change resolved any open thread'and do not push or request another review.
If a thread was deliberately deferred via a substantive reply (state ADDRESSED_BY_REPLY), treat it as condition 3 (human decision), not condition 5 (no progress). The loop must not re-try the same fix on the same thread cycle after cycle.
Do not run more than the configured fix/re-review cap per PR. If the loop stops because the cap is reached, do not request another re-review or start another fix cycle unless the user raises the cap. Still run stale-thread cleanup, terminal classification, metrics recording, and the final script-generated summary.
Resuming After t
…(truncated)