|
| 1 | +#!/usr/bin/env bash |
| 2 | +# gemini-code-review.sh — one-shot code review on Gemini 3.5 Flash via liteLLM. |
| 3 | +# |
| 4 | +# Self-contained, repo-agnostic worker for the /make-no-mistakes:gemini-code-review |
| 5 | +# command. Resolves a diff, sends it + a condensed review rubric to |
| 6 | +# gemini-3.5-flash in a SINGLE completion through a transient liteLLM proxy, and |
| 7 | +# prints review markdown to stdout. NO nested Claude Code agent runs on Gemini, |
| 8 | +# so there is no tool-call-translation fragility — just one completion. An Opus |
| 9 | +# orchestrator (the command) then curates the output against the local repo's |
| 10 | +# CLAUDE.md rules. |
| 11 | +# |
| 12 | +# Usage: |
| 13 | +# gemini-code-review.sh # diff <base>...HEAD (base auto-detected) |
| 14 | +# gemini-code-review.sh 245 # gh pr diff 245 |
| 15 | +# gemini-code-review.sh --uncommitted # git diff HEAD (staged + unstaged) |
| 16 | +# gemini-code-review.sh my-branch # git diff <base>...my-branch |
| 17 | +# gemini-code-review.sh a..b # git diff a..b |
| 18 | +# [--base <branch>] # override auto-detected base |
| 19 | +# |
| 20 | +# Secret: requires GEMINI_API_KEY in the environment. Provide it WITHOUT leaking |
| 21 | +# it into logs via this plugin's own secret helpers: |
| 22 | +# /secret-input (stage the key once) |
| 23 | +# /secret-use GEMINI_API_KEY -- bash <this-script> [args] |
| 24 | +# |
| 25 | +# Requires: litellm, jq, curl, git (+ gh for PR mode). |
| 26 | +set -euo pipefail |
| 27 | + |
| 28 | +PORT="${GEMINI_REVIEW_PORT:-4100}" |
| 29 | +MODEL="gemini/gemini-3.5-flash" |
| 30 | +BASE="" |
| 31 | +TARGET="" |
| 32 | + |
| 33 | +while [ $# -gt 0 ]; do |
| 34 | + case "$1" in |
| 35 | + --base) [ -z "${2:-}" ] && { echo "ERROR: --base requiere un valor." >&2; exit 1; }; BASE="$2"; shift 2 ;; |
| 36 | + --uncommitted) TARGET="--uncommitted"; shift ;; |
| 37 | + -h|--help) sed -n '2,26p' "$0"; exit 0 ;; |
| 38 | + *) TARGET="$1"; shift ;; |
| 39 | + esac |
| 40 | +done |
| 41 | + |
| 42 | +for bin in litellm jq curl git; do |
| 43 | + command -v "$bin" >/dev/null 2>&1 || { echo "ERROR: falta '$bin' en PATH." >&2; exit 1; } |
| 44 | +done |
| 45 | +if [ -z "${GEMINI_API_KEY:-}" ]; then |
| 46 | + echo "ERROR: GEMINI_API_KEY no está en el entorno." >&2 |
| 47 | + echo "Stage it once with /secret-input, then run via:" >&2 |
| 48 | + echo " /secret-use GEMINI_API_KEY -- bash \"$0\" $*" >&2 |
| 49 | + exit 1 |
| 50 | +fi |
| 51 | + |
| 52 | +# --- auto-detect base branch (repo-agnostic) --- |
| 53 | +if [ -z "$BASE" ]; then |
| 54 | + BASE="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@' || true)" |
| 55 | + if [ -z "$BASE" ]; then |
| 56 | + for b in develop main master; do |
| 57 | + git show-ref --verify --quiet "refs/remotes/origin/$b" && BASE="$b" && break |
| 58 | + done |
| 59 | + fi |
| 60 | + [ -z "$BASE" ] && BASE="main" |
| 61 | +fi |
| 62 | + |
| 63 | +# --- resolve diff --- |
| 64 | +LABEL="" |
| 65 | +if [ -z "$TARGET" ]; then |
| 66 | + LABEL="${BASE}...HEAD"; DIFF="$(git diff "${BASE}...HEAD")" |
| 67 | +elif [ "$TARGET" = "--uncommitted" ]; then |
| 68 | + LABEL="uncommitted (git diff HEAD)"; DIFF="$(git diff HEAD)" |
| 69 | +elif [[ "$TARGET" =~ ^[0-9]+$ ]]; then |
| 70 | + command -v gh >/dev/null 2>&1 || { echo "ERROR: 'gh' requerido para modo PR." >&2; exit 1; } |
| 71 | + LABEL="PR #${TARGET}"; DIFF="$(gh pr diff "$TARGET")" |
| 72 | +elif [[ "$TARGET" == *..* ]]; then |
| 73 | + LABEL="$TARGET"; DIFF="$(git diff "$TARGET")" |
| 74 | +else |
| 75 | + LABEL="${BASE}...${TARGET}"; DIFF="$(git diff "${BASE}...${TARGET}")" |
| 76 | +fi |
| 77 | +if [ -z "${DIFF// }" ]; then echo "ERROR: diff vacío para '${LABEL}'." >&2; exit 2; fi |
| 78 | +DIFF_CHARS=${#DIFF} |
| 79 | +[ "$DIFF_CHARS" -gt 600000 ] && echo "WARN: diff grande (${DIFF_CHARS} chars) — el review puede perder profundidad." >&2 |
| 80 | + |
| 81 | +# --- rubric (repo-agnostic; the command curates against the repo's CLAUDE.md) --- |
| 82 | +read -r -d '' RUBRIC <<'RUBRIC_EOF' || true |
| 83 | +You are a battle-tested senior engineer doing a rigorous code review of a diff. |
| 84 | +Review as if you will debug this at 3 AM during an incident. Read EVERY changed |
| 85 | +line. You are given ONLY the diff — do not ask to run commands or open other |
| 86 | +files; review what is shown and flag where you'd want to see more. |
| 87 | +
|
| 88 | +Hunt for: |
| 89 | +- Logic: race conditions, null/undefined access without guards, missing awaits, |
| 90 | + off-by-one, inverted/incorrect boolean logic, wrong equality, bad type coercion. |
| 91 | +- State/data-flow: stale closures, missing effect/memo deps, direct mutation of |
| 92 | + shared/immutable state, lost error context in catch blocks. |
| 93 | +- Edge cases: empty/zero/negative, empty-string vs null vs undefined, encoding, |
| 94 | + timezones, large-input performance, concurrency. |
| 95 | +- Architecture: single-responsibility violations, oversized units, missing types, |
| 96 | + magic numbers/strings, leftover debug logs / commented-out code, dead code. |
| 97 | +- Tests: new logic without tests; missing error/empty/edge coverage. |
| 98 | +- Security: secrets/keys committed or logged, privileged credentials in client |
| 99 | + code, unvalidated input, missing authz/ownership checks, SQL injection, XSS via |
| 100 | + raw/unsafe HTML injection, SSRF on server-side fetch, missing rate limiting. |
| 101 | +
|
| 102 | +Output EXACTLY this markdown: |
| 103 | +
|
| 104 | +## Gemini Code Review — {LABEL} |
| 105 | +
|
| 106 | +### Files Reviewed |
| 107 | +| File | +/- | Risk | Notes | |
| 108 | +|------|-----|------|-------| |
| 109 | +
|
| 110 | +### Findings |
| 111 | +#### Critical (blocks merge) |
| 112 | +- **[Category]** `file:line` — issue -> fix |
| 113 | +#### Major (fix before merge) |
| 114 | +- ... |
| 115 | +#### Minor (recommended) |
| 116 | +- ... |
| 117 | +
|
| 118 | +### Missing Tests / Changes |
| 119 | +- ... |
| 120 | +
|
| 121 | +### Verdict: Approve | Request Changes | Block |
| 122 | +| Critical | Major | Minor | |
| 123 | +|----------|-------|-------| |
| 124 | +| N | N | N | |
| 125 | +
|
| 126 | +If a section is empty, write "None.". Be specific with file:line; no vague advice. |
| 127 | +RUBRIC_EOF |
| 128 | +RUBRIC="${RUBRIC/\{LABEL\}/$LABEL}" |
| 129 | + |
| 130 | +# --- transient liteLLM proxy (self-generated config + master key; kill only ours) --- |
| 131 | +PROXY_LOG="$(mktemp -t gcr-proxy.XXXXXX.log)" |
| 132 | +PROXY_CFG="$(mktemp -t gcr-config.XXXXXX.yaml)" |
| 133 | +export LITELLM_MASTER_KEY="sk-gcr-local-$$" |
| 134 | +cat > "$PROXY_CFG" <<CFG |
| 135 | +model_list: |
| 136 | + - model_name: $MODEL |
| 137 | + litellm_params: |
| 138 | + model: $MODEL |
| 139 | + api_key: os.environ/GEMINI_API_KEY |
| 140 | +general_settings: |
| 141 | + master_key: os.environ/LITELLM_MASTER_KEY |
| 142 | +litellm_settings: |
| 143 | + drop_params: true |
| 144 | +CFG |
| 145 | +STARTED=false |
| 146 | +ready() { curl -fsS "http://127.0.0.1:${PORT}/health/readiness" >/dev/null 2>&1; } |
| 147 | +cleanup() { $STARTED && [ -n "${PID:-}" ] && kill "$PID" 2>/dev/null || true; rm -f "$PROXY_LOG" "$PROXY_CFG"; } |
| 148 | +trap cleanup EXIT INT TERM |
| 149 | + |
| 150 | +if ! ready; then |
| 151 | + echo "Levantando liteLLM proxy (gemini-3.5-flash) en 127.0.0.1:${PORT}..." >&2 |
| 152 | + litellm --config "$PROXY_CFG" --host 127.0.0.1 --port "$PORT" >"$PROXY_LOG" 2>&1 & |
| 153 | + PID=$!; STARTED=true |
| 154 | + for _ in $(seq 1 40); do ready && break; sleep 1; done |
| 155 | + ready || { echo "ERROR: el proxy no arrancó. Log:" >&2; tail -8 "$PROXY_LOG" >&2; exit 4; } |
| 156 | +fi |
| 157 | + |
| 158 | +# --- one completion --- |
| 159 | +# `-sS` (NOT -fsS): on an HTTP 4xx/5xx the body is captured + diagnosed below, |
| 160 | +# instead of curl failing silently and masking the real API error (quota, bad key). |
| 161 | +BODY="$(jq -n --arg m "$MODEL" --arg sys "$RUBRIC" \ |
| 162 | + --arg usr "Review this diff (${LABEL}):"$'\n\n'"\`\`\`diff"$'\n'"${DIFF}"$'\n'"\`\`\`" \ |
| 163 | + '{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}]}')" |
| 164 | +RESP="$(curl -sS "http://127.0.0.1:${PORT}/v1/chat/completions" \ |
| 165 | + -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \ |
| 166 | + -H "Content-Type: application/json" -d "$BODY")" \ |
| 167 | + || { echo "ERROR: no se pudo conectar al proxy." >&2; exit 5; } |
| 168 | + |
| 169 | +CONTENT="$(printf '%s' "$RESP" | jq -r '.choices[0].message.content // empty')" |
| 170 | +if [ -z "$CONTENT" ]; then |
| 171 | + echo "ERROR: respuesta sin contenido:" >&2 |
| 172 | + printf '%s\n' "$RESP" | jq -r '.error // .' >&2 2>/dev/null || printf '%s\n' "$RESP" >&2 |
| 173 | + exit 6 |
| 174 | +fi |
| 175 | + |
| 176 | +OUT="$(mktemp -t gemini-code-review.XXXXXX.md)" |
| 177 | +printf '%s\n' "$CONTENT" | tee "$OUT" |
| 178 | +echo "[saved: $OUT · model: gemini-3.5-flash · diff: $LABEL · ${DIFF_CHARS} chars]" >&2 |
0 commit comments