Files
steve c3d09d3bd4
Build & push image / build-and-push (push) Successful in 6s
feat: live status-board comment + full-fleet dogfood (#1)
Phase 3: one consolidated, live-updating PR comment aggregating every
model's per-lens progress (queued -> running -> finished + verdict), so
the swarm's progress is visible at a glance and a watcher can tell when
it's done. Opt-in statusWriter in the binary (atomic writes) + a
background status-board.sh renderer wired through entrypoint.sh; default
on, GADFLY_STATUS_BOARD=0 to disable.

Also restores gadfly's dogfood swarm to the full cloud fleet (9 cloud +
M5; M1 dropped as too slow) matching mort, and folds in the 3 real bugs
the swarm found on its own PR (skip-binary stuck-waiting, panic-stuck
lens, busy-loop on bad poll interval). All 36 findings graded via the
gadfly MCP (18 real / 18 false-positive).

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Steve Dudenhoeffer <steve@stevedudenhoeffer.com>
Co-committed-by: Steve Dudenhoeffer <steve@stevedudenhoeffer.com>
2026-06-27 19:00:12 +00: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"