Phase 1: auditor (audit-task.sh + Gitea Actions workflow); runner crash-loop fix documented

This commit is contained in:
danny8632
2026-05-12 07:02:55 +00:00
parent f3c38f1017
commit a296b87065
2 changed files with 223 additions and 0 deletions

176
agents/audit-task.sh Executable file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env bash
# audit-task.sh <owner>/<repo> <pr-number>
#
# 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 <owner>/<repo> <pr-number>" >&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"

View File

@@ -0,0 +1,47 @@
# Drop into each agent-managed project repo as .gitea/workflows/auditor.yml.
# Requires the project to have these Gitea Actions secrets configured:
# AUDITOR_SSH_KEY — private ed25519 key whose public counterpart is in
# agent@dev-01:~/.ssh/authorized_keys
#
# The workflow SSH's into dev-01 (192.168.1.29) and runs audit-task.sh, which
# uses claude headless to review the PR against its linked issue's Done
# criteria, then posts the audit as a PR comment.
name: Auditor
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
audit:
runs-on: ubuntu-latest
container:
image: debian:bookworm-slim
steps:
- name: Install ssh + curl
run: |
apt-get update -qq
apt-get install -y -qq openssh-client curl jq ca-certificates
- name: Audit PR via dev-01
env:
AUDITOR_KEY: ${{ secrets.AUDITOR_SSH_KEY }}
REPO: ${{ github.repository }}
PR_NUM: ${{ github.event.pull_request.number }}
run: |
set -e
[ -n "$AUDITOR_KEY" ] || { echo "ERROR: AUDITOR_SSH_KEY secret not set"; exit 1; }
mkdir -p ~/.ssh
printf '%s\n' "$AUDITOR_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
# Trust dev-01's host key — collected at runtime; LAN-only path
ssh-keyscan -H 192.168.1.29 >> ~/.ssh/known_hosts 2>/dev/null
ssh -i ~/.ssh/id_ed25519 \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
agent@192.168.1.29 \
"PATH=\$HOME/.local/bin:/usr/local/bin:\$PATH MAX_WALLCLOCK=10m /usr/local/bin/audit-task.sh '$REPO' '$PR_NUM'"