#!/usr/bin/env bash # audit-task.sh / # # Reads a Gitea PR, finds the linked issue (via "Resolves #N" in the PR body), # asks claude to audit the diff against the issue's done criteria, and posts # a structured review comment on the PR. # # Env knobs: # MODEL default sonnet (the auditor's model) # MAX_WALLCLOCK default 10m # GITEA_URL default https://gitea.dannyhaslund.dk # GITEA_TOKEN_FILE default /etc/agent/gitea-token set -euo pipefail REPO="${1:-}" PR_NUM="${2:-}" [[ -z "$REPO" || -z "$PR_NUM" ]] && { echo "usage: $0 / " >&2; exit 64; } MODEL="${MODEL:-sonnet}" MAX_WALLCLOCK="${MAX_WALLCLOCK:-10m}" GITEA_URL="${GITEA_URL:-https://gitea.dannyhaslund.dk}" GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-/etc/agent/gitea-token}" LOG_ROOT="${LOG_ROOT:-/var/agent/logs}" [[ -r "$GITEA_TOKEN_FILE" ]] || { echo "missing $GITEA_TOKEN_FILE" >&2; exit 65; } GITEA_TOKEN="$(cat "$GITEA_TOKEN_FILE")" mkdir -p "$LOG_ROOT" LOG_FILE="$LOG_ROOT/audit-pr${PR_NUM}-$(date -u +%Y%m%dT%H%M%S).log" log() { printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*" | tee -a "$LOG_FILE"; } # ---------- 1. Fetch PR ---------- log "Fetching PR $REPO#$PR_NUM" pr="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/v1/repos/$REPO/pulls/$PR_NUM")" pr_title="$(jq -r .title <<<"$pr")" pr_body="$(jq -r .body <<<"$pr")" pr_head="$(jq -r .head.ref <<<"$pr")" pr_base="$(jq -r .base.ref <<<"$pr")" # ---------- 2. Find linked issue ---------- issue_num="$(grep -oiE '(resolves|closes|fixes)\s+#[0-9]+' <<<"$pr_body" | head -1 | grep -oE '[0-9]+' || true)" if [[ -n "$issue_num" ]]; then log "Linked issue: #$issue_num" issue_body="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_URL/api/v1/repos/$REPO/issues/$issue_num" | jq -r .body)" else log "WARN: no linked issue found in PR body; auditing against PR description only" issue_body="(no linked issue; audit against PR description and general code quality)" fi # ---------- 3. Get diff ---------- DIFF_FILE="$(mktemp)" trap 'rm -f "$DIFF_FILE" "$AUDIT_OUT" 2>/dev/null' EXIT curl -fsS -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_URL/api/v1/repos/$REPO/pulls/$PR_NUM.diff" > "$DIFF_FILE" diff_size="$(wc -c < "$DIFF_FILE")" log "Diff size: $diff_size bytes" # Truncate to 32 KB if too large if [[ $diff_size -gt 32768 ]]; then log "Truncating diff to 32 KB for claude" head -c 32768 "$DIFF_FILE" > "${DIFF_FILE}.trim" echo -e "\n\n[... diff truncated at 32KB ...]" >> "${DIFF_FILE}.trim" mv "${DIFF_FILE}.trim" "$DIFF_FILE" fi # ---------- 4. Audit via claude ---------- SCHEMA='{ "type": "object", "required": ["verdict", "summary", "criteria"], "properties": { "verdict": { "type": "string", "enum": ["approve", "request_changes", "comment"] }, "summary": { "type": "string", "minLength": 10 }, "criteria": { "type": "array", "items": { "type": "object", "required": ["criterion", "status"], "properties": { "criterion": { "type": "string" }, "status": { "type": "string", "enum": ["pass", "fail", "unclear"] }, "note": { "type": "string" } } } }, "issues_found": { "type": "array", "items": { "type": "string" } } } }' PROMPT="You are the auditor agent in an autonomous coding pipeline. Review the pull request against the linked issue's Done criteria. Be strict but fair: - pass = the diff clearly satisfies the criterion - fail = the diff clearly does not, or contradicts it - unclear = cannot determine from the diff alone (e.g. requires running tests) Set verdict: - approve = all criteria pass, no significant issues - comment = mostly fine but some criteria are unclear or minor issues - request_changes = at least one criterion fails, or critical issues issues_found is for code-quality concerns the issue did NOT specify but that matter for merge: security holes, broken syntax, obvious bugs, dependency problems. Do NOT bikeshed style. Do not call any tools. Produce the JSON from the prompt text alone. # Linked issue body $issue_body # PR title $pr_title # PR body $pr_body # Diff ($pr_head into $pr_base) $(cat "$DIFF_FILE") Return only the JSON matching the schema." AUDIT_OUT="$(mktemp)" log "Invoking claude (model=$MODEL, timeout=$MAX_WALLCLOCK)" set +e timeout --signal=INT --kill-after=30s "$MAX_WALLCLOCK" \ claude -p "$PROMPT" \ --model "$MODEL" \ --output-format json \ --json-schema "$SCHEMA" \ --allowedTools "" \ < /dev/null > "$AUDIT_OUT" 2>>"$LOG_FILE" rc=$? set -e log "claude exited rc=$rc" if [[ $rc -ne 0 ]]; then log "ERROR โ€” auditor failed; posting fallback comment" curl -fsS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ -d "$(jq -nc --arg b "agent auditor: failed to produce an audit (claude rc=$rc). Log: \`$LOG_FILE\`" '{body:$b}')" \ "$GITEA_URL/api/v1/repos/$REPO/issues/$PR_NUM/comments" > /dev/null exit 1 fi # ---------- 5. Render review comment ---------- audit="$(jq -c '.structured_output' "$AUDIT_OUT")" verdict="$(jq -r .verdict <<<"$audit")" summary="$(jq -r .summary <<<"$audit")" criteria_md="$(jq -r '.criteria // [] | map("- **" + (.status|ascii_upcase) + "** โ€” " + .criterion + (if .note then " _" + .note + "_" else "" end)) | join("\n")' <<<"$audit")" issues_md="$(jq -r '.issues_found // [] | if length == 0 then "_none_" else map("- " + .) | join("\n") end' <<<"$audit")" # Verdict badge case "$verdict" in approve) badge=":white_check_mark: APPROVE" ;; comment) badge=":speech_balloon: COMMENT" ;; request_changes) badge=":no_entry: REQUEST_CHANGES" ;; *) badge=":grey_question: $verdict" ;; esac review_body="$(printf '## Auditor review โ€” %s\n\n%s\n\n### Criteria\n\n%s\n\n### Other issues found\n\n%s\n\n---\n_model: %s ยท log: `%s`_\n' \ "$badge" "$summary" "$criteria_md" "$issues_md" "$MODEL" "$LOG_FILE")" # ---------- 6. Post comment ---------- log "Posting review (verdict=$verdict)" curl -fsS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ -d "$(jq -nc --arg b "$review_body" '{body:$b}')" \ "$GITEA_URL/api/v1/repos/$REPO/issues/$PR_NUM/comments" > /dev/null log "Done. verdict=$verdict" echo "$verdict"