Phase 1: auditor (audit-task.sh + Gitea Actions workflow); runner crash-loop fix documented
This commit is contained in:
176
agents/audit-task.sh
Executable file
176
agents/audit-task.sh
Executable 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"
|
||||
47
auditor/workflow-template.yml
Normal file
47
auditor/workflow-template.yml
Normal 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'"
|
||||
Reference in New Issue
Block a user