c3d09d3bd4
Build & push image / build-and-push (push) Successful in 6s
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>
138 lines
5.9 KiB
Bash
Executable File
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"
|