fix: address review — migration comment, link ring buffer constants (#170)

This commit is contained in:
openhands 2026-02-22 17:57:39 +00:00
parent 3fceb4145a
commit 0fb1ed4bf8
10 changed files with 897 additions and 135 deletions

270
scripts/review-pr.sh Executable file
View file

@ -0,0 +1,270 @@
#!/usr/bin/env bash
# review-pr.sh — AI-powered PR review using claude CLI
#
# Usage: ./scripts/review-pr.sh <pr-number> [--force]
#
# Calls `claude -p --model sonnet` with context docs + diff.
# No tool access (pure text review), ~$0.02-0.05 per review.
#
# --force: skip the "already reviewed" check
#
# Concurrency: uses a lockfile to ensure only one review runs at a time.
# Status: writes live progress to /tmp/harb-review-status for peeking.
# Logs: /home/debian/harb/logs/review.log (auto-rotated at 100KB)
#
# Peek while running: cat /tmp/harb-review-status
# Watch log: tail -f ~/harb/logs/review.log
set -euo pipefail
# --- Environment (cron-safe) ---
export PATH="/home/debian/.nvm/versions/node/v22.20.0/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
export HOME="${HOME:-/home/debian}"
# --- Config ---
PR_NUMBER="${1:?Usage: review-pr.sh <pr-number> [--force]}"
FORCE="${2:-}"
REPO="johba/harb"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CODEBERG_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc)"
API_BASE="https://codeberg.org/api/v1/repos/${REPO}"
LOCKFILE="/tmp/harb-review.lock"
STATUSFILE="/tmp/harb-review-status"
LOGDIR="/home/debian/harb/logs"
LOGFILE="$LOGDIR/review.log"
MIN_MEM_MB=1500
TMPDIR=$(mktemp -d)
mkdir -p "$LOGDIR"
# --- Logging ---
log() {
local ts
ts="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "[$ts] PR#${PR_NUMBER} $*" | tee -a "$LOGFILE"
}
status() {
local ts
ts="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
printf '[%s] PR #%s: %s\n' "$ts" "$PR_NUMBER" "$*" > "$STATUSFILE"
log "$*"
}
cleanup() {
rm -rf "$TMPDIR"
rm -f "$LOCKFILE" "$STATUSFILE"
}
trap cleanup EXIT
# --- Log rotation (keep ~100KB + 1 archive) ---
if [ -f "$LOGFILE" ]; then
LOGSIZE=$(stat -c%s "$LOGFILE" 2>/dev/null || echo 0)
if [ "$LOGSIZE" -gt 102400 ]; then
mv "$LOGFILE" "$LOGFILE.old"
log "Log rotated (was ${LOGSIZE} bytes)"
fi
fi
# --- Memory guard ---
AVAIL_MB=$(awk '/MemAvailable/{printf "%d", $2/1024}' /proc/meminfo)
if [ "$AVAIL_MB" -lt "$MIN_MEM_MB" ]; then
log "SKIP: only ${AVAIL_MB}MB available (need ${MIN_MEM_MB}MB)"
exit 0
fi
# --- Concurrency lock ---
if [ -f "$LOCKFILE" ]; then
LOCK_PID=$(cat "$LOCKFILE" 2>/dev/null || echo "")
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
log "SKIP: another review running (PID ${LOCK_PID})"
exit 0
fi
log "Removing stale lock (PID ${LOCK_PID:-?})"
rm -f "$LOCKFILE"
fi
echo $$ > "$LOCKFILE"
# --- Fetch PR metadata ---
status "fetching metadata"
PR_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API_BASE}/pulls/${PR_NUMBER}")
PR_TITLE=$(echo "$PR_JSON" | jq -r '.title')
PR_BODY=$(echo "$PR_JSON" | jq -r '.body // ""')
PR_HEAD=$(echo "$PR_JSON" | jq -r '.head.ref')
PR_BASE=$(echo "$PR_JSON" | jq -r '.base.ref')
PR_SHA=$(echo "$PR_JSON" | jq -r '.head.sha')
PR_STATE=$(echo "$PR_JSON" | jq -r '.state')
log "${PR_TITLE} (${PR_HEAD}${PR_BASE} ${PR_SHA:0:7})"
# --- Guards ---
if [ "$PR_STATE" != "open" ]; then
log "SKIP: state=${PR_STATE}"
exit 0
fi
status "checking CI"
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API_BASE}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
if [ "$CI_STATE" != "success" ]; then
log "SKIP: CI=${CI_STATE}"
exit 0
fi
if [ "$FORCE" != "--force" ]; then
status "checking existing reviews"
EXISTING=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API_BASE}/issues/${PR_NUMBER}/comments?limit=50" | \
jq -r --arg sha "$PR_SHA" \
'[.[] | select(.body | contains("<!-- reviewed:")) | select(.body | contains($sha))] | length')
if [ "$EXISTING" -gt "0" ]; then
log "SKIP: already reviewed at ${PR_SHA:0:7}"
exit 0
fi
fi
# --- Fetch diff ---
status "fetching diff"
DIFF=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API_BASE}/pulls/${PR_NUMBER}.diff" | head -c 25000)
DIFF_STAT=$(echo "$DIFF" | grep -E '^\+\+\+ b/' | sed 's|^+++ b/||' | sort)
# --- Which context docs? ---
NEEDS_UX=false
for f in $DIFF_STAT; do
case "$f" in
landing/*|web-app/*) NEEDS_UX=true ;;
esac
done
# --- Build prompt file ---
status "building prompt"
cat > "${TMPDIR}/prompt.md" << PROMPT_EOF
# PR #${PR_NUMBER}: ${PR_TITLE}
## PR Description
${PR_BODY}
## Changed Files
${DIFF_STAT}
## PRODUCT-TRUTH.md (what we can/cannot claim)
$(cat "${REPO_ROOT}/docs/PRODUCT-TRUTH.md")
## ARCHITECTURE.md
$(cat "${REPO_ROOT}/docs/ARCHITECTURE.md")
PROMPT_EOF
if [ "$NEEDS_UX" = true ] && [ -f "${REPO_ROOT}/docs/UX-DECISIONS.md" ]; then
cat >> "${TMPDIR}/prompt.md" << UX_EOF
## UX-DECISIONS.md
$(cat "${REPO_ROOT}/docs/UX-DECISIONS.md")
UX_EOF
fi
cat >> "${TMPDIR}/prompt.md" << DIFF_EOF
## Diff
\`\`\`diff
${DIFF}
\`\`\`
## Your Task
Produce a structured review:
### 1. Claim Check
Extract every factual claim about the protocol from user-facing text in the diff.
For each, verify against PRODUCT-TRUTH.md:
- ✅ Accurate
- ⚠️ Partially true (explain)
- ❌ False (cite contradiction)
If no claims, say "No user-facing claims in this diff."
### 2. Code Review
Bugs, logic errors, missing edge cases, broken imports.
### 3. Architecture Check
Does this follow patterns in ARCHITECTURE.md?
### 4. UX/Messaging Check
Does copy follow UX-DECISIONS.md?
(Skip if no UX-DECISIONS context provided.)
### 5. Verdict
**APPROVE**, **REQUEST_CHANGES**, or **DISCUSS** — one line reason.
Be direct. No filler.
DIFF_EOF
PROMPT_SIZE=$(stat -c%s "${TMPDIR}/prompt.md")
log "Prompt: ${PROMPT_SIZE} bytes"
# --- Run claude -p ---
status "running claude (sonnet)"
SECONDS=0
REVIEW=$(claude -p \
--model sonnet \
--dangerously-skip-permissions \
--output-format text \
< "${TMPDIR}/prompt.md" 2>"${TMPDIR}/claude-stderr.log")
ELAPSED=$SECONDS
CLAUDE_EXIT=$?
if [ $CLAUDE_EXIT -ne 0 ]; then
log "ERROR: claude exited ${CLAUDE_EXIT} after ${ELAPSED}s"
log "stderr: $(cat "${TMPDIR}/claude-stderr.log" | tail -5)"
exit 1
fi
if [ -z "$REVIEW" ]; then
log "ERROR: empty review after ${ELAPSED}s"
exit 1
fi
REVIEW_SIZE=$(echo "$REVIEW" | wc -c)
log "Review: ${REVIEW_SIZE} bytes in ${ELAPSED}s"
# --- Post to Codeberg ---
status "posting to Codeberg"
COMMENT_BODY="## 🤖 AI Review
<!-- reviewed: ${PR_SHA} -->
${REVIEW}
---
*Reviewed at \`${PR_SHA:0:7}\` · [PRODUCT-TRUTH.md](../docs/PRODUCT-TRUTH.md) · [ARCHITECTURE.md](../docs/ARCHITECTURE.md)*"
POST_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/issues/${PR_NUMBER}/comments" \
-d "$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')")
if [ "${POST_CODE}" = "201" ]; then
log "POSTED to Codeberg"
else
log "ERROR: Codeberg HTTP ${POST_CODE}"
echo "$REVIEW" > "${LOGDIR}/review-pr${PR_NUMBER}-${PR_SHA:0:7}.md"
log "Review saved to ${LOGDIR}/review-pr${PR_NUMBER}-${PR_SHA:0:7}.md"
exit 1
fi
# --- Notify OpenClaw (best effort) ---
VERDICT=$(echo "$REVIEW" | grep -oP '\*\*(APPROVE|REQUEST_CHANGES|DISCUSS)\*\*' | head -1 | tr -d '*')
if command -v openclaw &>/dev/null; then
openclaw system event \
--text "🤖 PR #${PR_NUMBER} reviewed: ${VERDICT:-UNKNOWN}${PR_TITLE}" \
--mode now 2>/dev/null || true
fi
log "DONE: ${VERDICT:-UNKNOWN} (${ELAPSED}s)"