#!/usr/bin/env bash # Adversarial PR review runner. # # Fetches a PR's unified diff + metadata from Gitea, asks ONE model to review it # adversarially, then upserts the result as a single labeled PR comment (so # re-runs on new commits update the comment in place instead of stacking dupes). # # The ollama lane is AGENTIC: it runs the cmd/gadfly Go binary, which drives a # tool-using agent (majordomo + Ollama Cloud) over the PR's checked-out repo so # the model can read_file/grep/etc. to VERIFY findings instead of guessing from # the diff alone. The antigravity lane stays a one-shot `agy` call (agy has its # own file tools). # # Required env: # GITEA_API e.g. https://gitea.stevedudenhoeffer.com/api/v1/repos/steve/mort # GITEA_TOKEN token with repo write access (posts the comment) # PR pull request index/number # PROVIDER "ollama" | "antigravity" # MODEL model id (e.g. qwen3-coder:480b-cloud, gemini-3-pro) # # Provider-specific env: # ollama: OLLAMA_CLOUD_API_KEY, GADFLY_BIN (path to the built reviewer), # GADFLY_REPO_DIR (checked-out repo; default: this script's repo) # antigravity: `agy` on PATH with credentials already seeded (~/.gemini) # # Optional: # MAX_DIFF_CHARS diff truncation cap for the prompt (default 60000) # # This script is advisory: it never fails the job for review content. It exits # non-zero only on a usage/configuration error. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MAX_DIFF_CHARS="${MAX_DIFF_CHARS:-60000}" : "${GITEA_API:?GITEA_API required}" : "${GITEA_TOKEN:?GITEA_TOKEN required}" : "${PR:?PR required}" : "${PROVIDER:?PROVIDER required}" : "${MODEL:?MODEL required}" MARKER="" say() { echo "[gadfly-review:${PROVIDER}:${MODEL}] $*" >&2; } # jq is required for payload building / response parsing; install if missing. if ! command -v jq >/dev/null 2>&1; then say "jq not found; attempting install" { apt-get update -qq && apt-get install -y -qq jq; } >/dev/null 2>&1 \ || { sudo apt-get update -qq && sudo apt-get install -y -qq jq; } >/dev/null 2>&1 \ || { say "could not install jq"; exit 1; } fi # curl timeouts: Gitea API calls are quick. Word-split on purpose so the flags # expand as separate args. (The LLM call's own deadline lives in the reviewer # binary / agy, not here.) API_TIMEOUT="--connect-timeout 20 --max-time 30" # --- fetch PR context ------------------------------------------------------- say "fetching PR #${PR} context" DIFF="$(curl $API_TIMEOUT -fsS -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_API}/pulls/${PR}.diff" || true)" META="$(curl $API_TIMEOUT -fsS -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_API}/pulls/${PR}" || echo '{}')" TITLE="$(echo "$META" | jq -r '.title // ""')" BODY="$(echo "$META" | jq -r '.body // ""')" if [ -z "$DIFF" ]; then say "empty diff; nothing to review" exit 0 fi # Keep the FULL diff for the agentic (ollama) reviewer — it can pull the whole # thing via the get_diff tool and embeds a truncated copy in the prompt itself. # The truncated copy below is only for the one-shot antigravity prompt. FULL_DIFF="$DIFF" TRUNC_NOTE="" if [ "${#DIFF}" -gt "$MAX_DIFF_CHARS" ]; then DIFF="${DIFF:0:$MAX_DIFF_CHARS}" TRUNC_NOTE=$'\n\n[NOTE: diff truncated to '"${MAX_DIFF_CHARS}"' chars for length; review the rest manually.]' fi SYS="$(cat "${SCRIPT_DIR}/system-prompt.txt")" USR="$(printf 'PR #%s: %s\n\nDescription:\n%s\n\nUnified diff to review:\n```diff\n%s\n```%s' \ "$PR" "$TITLE" "$BODY" "$DIFF" "$TRUNC_NOTE")" # --- call the model --------------------------------------------------------- REVIEW="" case "$PROVIDER" in ollama) # Agentic lane: hand off to the cmd/gadfly binary, which runs a tool-using # agent over the checked-out repo so it can verify findings instead of # guessing from the diff. The workflow builds the binary and exports # GADFLY_BIN + GADFLY_REPO_DIR; we fall back to sane defaults for a # local run. if [ -z "${OLLAMA_CLOUD_API_KEY:-}" ]; then REVIEW="⚠️ \`OLLAMA_CLOUD_API_KEY\` is not configured; this reviewer was skipped." else BIN="${GADFLY_BIN:-gadfly}" if ! command -v "$BIN" >/dev/null 2>&1 && [ ! -x "$BIN" ]; then REVIEW="⚠️ Agentic reviewer binary not found (\`GADFLY_BIN=${BIN}\`); the workflow build step may have failed." else REPO_DIR="${GADFLY_REPO_DIR:-$(cd "${SCRIPT_DIR}/../../.." && pwd)}" DIFF_FILE="$(mktemp)" ERR_FILE="${DIFF_FILE}.err" printf '%s' "$FULL_DIFF" > "$DIFF_FILE" REVIEW="$( OLLAMA_API_KEY="$OLLAMA_CLOUD_API_KEY" \ GADFLY_MODEL="$MODEL" \ GADFLY_REPO_DIR="$REPO_DIR" \ GADFLY_DIFF_FILE="$DIFF_FILE" \ GADFLY_SYSTEM_FILE="${SCRIPT_DIR}/system-prompt.txt" \ GADFLY_TITLE="$TITLE" \ GADFLY_BODY="$BODY" \ GADFLY_MAX_DIFF_CHARS="$MAX_DIFF_CHARS" \ "$BIN" 2>"$ERR_FILE" )" rc=$? if [ "$rc" -ne 0 ] || [ -z "$REVIEW" ]; then REVIEW="⚠️ Agentic reviewer for \`${MODEL}\` failed (exit ${rc}): \`\`\` $(tail -c 1500 "$ERR_FILE" 2>/dev/null) \`\`\`" fi rm -f "$DIFF_FILE" "$ERR_FILE" fi fi ;; antigravity) if ! command -v agy >/dev/null 2>&1; then REVIEW="⚠️ Antigravity CLI (\`agy\`) not found on PATH." else FULL="$(printf '%s\n\n%s' "$SYS" "$USR")" if ! REVIEW="$(agy -p "$FULL" --model "$MODEL" 2>agy.err)"; then REVIEW="⚠️ Antigravity CLI failed: \`\`\` $(tail -c 1500 agy.err 2>/dev/null) \`\`\`" fi [ -z "$REVIEW" ] && REVIEW="⚠️ Antigravity CLI returned no output (auth/quota?)." fi ;; *) say "unknown provider: ${PROVIDER}"; exit 1 ;; esac # --- assemble comment ------------------------------------------------------- COMMENT="$(printf '%s\n### 🔭 Adversarial review — `%s` (%s)\n\n%s\n\nAutomated adversarial review. Advisory only — does not block merge.' \ "$MARKER" "$MODEL" "$PROVIDER" "$REVIEW")" POST_BODY="$(jq -n --arg b "$COMMENT" '{body:$b}')" # --- upsert by marker ------------------------------------------------------- EXISTING_ID="" page=1 while [ "$page" -le 10 ]; do CMTS="$(curl $API_TIMEOUT -fsS -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_API}/issues/${PR}/comments?limit=50&page=${page}" || echo '[]')" [ "$(echo "$CMTS" | jq 'length')" = "0" ] && break EXISTING_ID="$(echo "$CMTS" | jq -r --arg m "$MARKER" \ '.[] | select(.body != null and (.body | startswith($m))) | .id' | head -n1)" [ -n "$EXISTING_ID" ] && break page=$((page+1)) done if [ -n "$EXISTING_ID" ]; then say "updating existing comment ${EXISTING_ID}" curl $API_TIMEOUT -sS -X PATCH -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \ "${GITEA_API}/issues/comments/${EXISTING_ID}" -d "$POST_BODY" >/dev/null else say "creating new comment" curl $API_TIMEOUT -sS -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \ "${GITEA_API}/issues/${PR}/comments" -d "$POST_BODY" >/dev/null fi say "done"