Files
gadfly/scripts/status-board.sh
T
steve a1b0691a1e
Build & push image / build-and-push (pull_request) Successful in 9s
fix: fold in gadfly's own review findings (3 real bugs)
The dogfood swarm reviewed PR #1; folding in the warranted findings
(graded via the gadfly MCP — 18 real / 18 false-positive across the 4
completed reviewers):

- entrypoint.sh: finalize a never-written status file when run.sh skips
  the binary (empty diff / no key / missing binary). The pre-seed stayed
  {started:0, done:false}, so the board showed that model "waiting to
  start" forever and the N/N counter never completed — breaking the
  board's own "tell when everything is finished" invariant.
  (glm-5.2, correctness — the strongest finding.)
- main.go: recover() in the per-lens goroutine. A panic previously
  crashed the whole binary (killing every other lens's output) and left
  the lens stuck "running" on the board. Now it's recorded as an errored
  result and the lens is marked finished. (glm-5.2 + minimax-m3.)
- status-board.sh: coerce a non-numeric GADFLY_STATUS_POLL_SECS back to
  12. Under `set -uo pipefail` a bad `sleep "$POLL"` failed silently and
  the loop spun, hammering the Gitea API. (glm-5.2, error-handling.)

The remaining real findings (sanitizer collision, page-10 pagination,
markdown-injection via PR-controlled lens names, cosmetic blank line)
were graded trivial and left as-is — documented in the finding notes.

gofmt clean, go vet quiet, go build + go test -race green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:56:41 -04:00

138 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Live status board for a Gadfly review.
#
# Each model process (the cmd/gadfly binary) publishes its per-lens progress to
# $GADFLY_STATUS_DIR/<model>.json as lenses go queued -> running -> finished.
# This script polls that directory and upserts ONE consolidated PR comment that
# aggregates every model's per-lens status, so a human (or an agent watching the
# PR) can see the whole swarm's progress at a glance and know when it's done —
# instead of staring at N separate "⏳ Reviewing…" placeholders.
#
# It is advisory and best-effort: a failed render/post is logged and retried on
# the next tick; nothing here can fail the review or block a merge. It runs in
# the background from entrypoint.sh and exits once the $GADFLY_STATUS_DIR/.done
# sentinel appears (the entrypoint touches it after all model lanes finish),
# after one final render.
#
# Required env:
# GITEA_API https://HOST/api/v1/repos/OWNER/REPO
# GITEA_TOKEN token with repo write access (posts the comment)
# PR pull request number
# GADFLY_STATUS_DIR directory holding the per-model <model>.json files
# Optional:
# GADFLY_STATUS_POLL_SECS render/upsert interval (default 12)
set -uo pipefail
: "${GITEA_API:?GITEA_API required}"
: "${GITEA_TOKEN:?GITEA_TOKEN required}"
: "${PR:?PR required}"
: "${GADFLY_STATUS_DIR:?GADFLY_STATUS_DIR required}"
POLL="${GADFLY_STATUS_POLL_SECS:-12}"
# Guard against a non-numeric poll interval: with `set -uo pipefail` (no set -e)
# a bad `sleep "$POLL"` would fail silently and the `while :` loop would spin,
# hammering the Gitea API. Coerce anything non-integer (or <1) back to 12.
case "$POLL" in ''|*[!0-9]*) POLL=12 ;; esac
[ "$POLL" -ge 1 ] 2>/dev/null || POLL=12
DONE_FILE="${GADFLY_STATUS_DIR}/.done"
MARKER="<!-- gadfly-status-board -->"
API_TIMEOUT="--connect-timeout 20 --max-time 30"
BOARD_ID="" # cached comment id, so we PATCH in place instead of re-searching
say() { echo "[gadfly-status-board] $*" >&2; }
command -v jq >/dev/null 2>&1 || { say "jq not found; status board disabled"; exit 0; }
# render_section FILE -> markdown for one model (its header + per-lens bullets).
# Reads the JSON the binary writes; tolerates a half-written/missing file by
# emitting nothing (jq exits non-zero -> caller skips it this tick).
render_section() {
jq -r '
def icon(state; errored):
if state == "finished" then (if errored then "⚠️" else "✅" end)
elif state == "running" then "🔄"
else "⏸️" end;
def lensline:
"- " + icon(.state; (.errored // false)) + " **" + .name + "** — " +
( if .state == "finished" then (if (.errored // false) then "could not complete" else (.verdict // "done") end)
elif .state == "running" then "running"
else "queued" end );
( [.lenses[] | select(.state == "finished")] | length ) as $fin
| ( .lenses | length ) as $tot
| ( if .done then "✅ done"
elif $tot == 0 then "⏳ waiting to start"
else "⏳ " + ($fin|tostring) + "/" + ($tot|tostring) + " lenses" end ) as $sum
| "#### `" + .model + "` · " + .provider + " — " + $sum + "\n"
+ ( if $tot == 0 then "- ⏸️ _no lenses reported yet_"
else ([.lenses[] | lensline] | join("\n")) end )
' "$1" 2>/dev/null
}
# render_body -> the full consolidated comment body (marker + header + sections).
render_body() {
local f sections="" total=0 done=0 ts
shopt -s nullglob
local files=("${GADFLY_STATUS_DIR}"/*.json)
shopt -u nullglob
for f in "${files[@]}"; do
local sec
sec="$(render_section "$f")" || continue
[ -z "$sec" ] && continue
total=$((total + 1))
if [ "$(jq -r 'if .done then 1 else 0 end' "$f" 2>/dev/null)" = "1" ]; then
done=$((done + 1))
fi
sections="${sections}${sec}"$'\n\n'
done
ts="$(date -u '+%Y-%m-%d %H:%M:%SZ')"
if [ "$total" -eq 0 ]; then
sections="_Waiting for reviewers to start…_"$'\n'
fi
printf '%s\n## 🪰 Gadfly — live review status\n\n%d/%d reviewers finished · updated %s\n\n%s\n<sub>Live status board. Findings are posted in each model'\''s own comment. Advisory only — does not block merge.</sub>' \
"$MARKER" "$done" "$total" "$ts" "$sections"
}
# find_existing -> id of the board comment if it already exists (paginate by
# marker). Used once, to recover the comment across container restarts.
find_existing() {
local page=1 cmts id
while [ "$page" -le 10 ]; do
cmts="$(curl $API_TIMEOUT -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/issues/${PR}/comments?limit=50&page=${page}" 2>/dev/null || echo '[]')"
[ "$(echo "$cmts" | jq 'length' 2>/dev/null || echo 0)" = "0" ] && break
id="$(echo "$cmts" | jq -r --arg m "$MARKER" \
'.[] | select(.body != null and (.body | startswith($m))) | .id' 2>/dev/null | head -n1)"
[ -n "$id" ] && { echo "$id"; return; }
page=$((page + 1))
done
echo ""
}
# upsert BODY — PATCH the cached/known board comment, else POST a new one and
# cache its id. A failed PATCH (e.g. comment deleted) clears the cache so the
# next tick re-discovers or re-creates it.
upsert() {
local body="$1" post_body resp
post_body="$(jq -n --arg b "$body" '{body:$b}')"
[ -z "$BOARD_ID" ] && BOARD_ID="$(find_existing)"
if [ -n "$BOARD_ID" ]; then
if ! curl $API_TIMEOUT -fsS -X PATCH -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
"${GITEA_API}/issues/comments/${BOARD_ID}" -d "$post_body" >/dev/null 2>&1; then
say "patch of comment ${BOARD_ID} failed; will re-discover"
BOARD_ID=""
fi
else
resp="$(curl $API_TIMEOUT -fsS -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
"${GITEA_API}/issues/${PR}/comments" -d "$post_body" 2>/dev/null || echo '{}')"
BOARD_ID="$(echo "$resp" | jq -r '.id // ""' 2>/dev/null)"
fi
}
say "starting (poll ${POLL}s, dir ${GADFLY_STATUS_DIR})"
while :; do
upsert "$(render_body)"
[ -f "$DONE_FILE" ] && break
sleep "$POLL"
done
say "done"