diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index ebc5f6827..6386453b1 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -26,7 +26,7 @@ concurrency: jobs: review: runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 env: # GH_PAT is a fine-grained PAT with PR read/write across the repos you # want the agent to act on. Stored as a repo secret. @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout agent repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install Claude Code CLI run: npm install -g @anthropic-ai/claude-code @@ -54,15 +54,24 @@ jobs: - name: Enumerate candidate PRs id: list + env: + # Cap how many PRs one cron tick reviews to stay within timeout. + # Override via repo variable: gh variable set MAX_PRS --body 20 --repo don-petry/self + MAX_PRS: ${{ vars.MAX_PRS || '10' }} run: | set -euo pipefail if [ -n "${PR_URL_OVERRIDE}" ]; then echo "${PR_URL_OVERRIDE}" > prs.txt else - bash scripts/list-prs.sh > prs.txt + bash scripts/list-prs.sh > prs_all.txt + TOTAL=$(wc -l < prs_all.txt | tr -d ' ') + head -n "$MAX_PRS" prs_all.txt > prs.txt + if [ "$TOTAL" -gt "$MAX_PRS" ]; then + echo "::notice::Backlog has $TOTAL PRs but MAX_PRS=$MAX_PRS — reviewing first $MAX_PRS only. Remaining will be picked up in subsequent runs." + fi fi echo "count=$(wc -l < prs.txt | tr -d ' ')" >> "$GITHUB_OUTPUT" - echo "--- candidate PRs ---" + echo "--- candidate PRs (max $MAX_PRS) ---" cat prs.txt || true - name: Review each PR (council + synth) diff --git a/AGENT.md b/AGENT.md index 7fd1bdf09..3091561e8 100644 --- a/AGENT.md +++ b/AGENT.md @@ -123,13 +123,20 @@ gh workflow run pr-review.yml --repo don-petry/self -f dry_run=false ## Tuning -- **Risk rules** live in `prompts/review-pr.md`. Edit there. +- **Risk rules** — edit `prompts/shared.md` (taxonomy), or per-lens in + `prompts/council/{security,correctness,maintainability}.md`. - **Cron frequency** — change the `cron:` line in the workflow file. - **Scope** — edit `scripts/list-prs.sh` to add/remove queries (e.g. to include PRs from a specific org, or to exclude certain repos). +- **Max PRs per run** — defaults to 10 per cron tick to stay within the 60-min + job timeout (~5 min per PR with 3 council members). Override: + `gh variable set MAX_PRS --body 15 --repo don-petry/self` +- **Models** — change model IDs in `scripts/review-one-pr.sh` (`run_member` + calls and the synthesis invocation). See the script for current assignments. ## Cost -Hourly cron × ~30 days = ~720 runs/month. If most hours have zero candidate PRs -the cost is just GitHub Actions minutes (~10s each). Anthropic API cost scales -with PR count and diff size — expect a few cents per non-trivial PR review. +Uses the Claude Max plan via OAuth token — no per-token API billing. GitHub +Actions cost is ~720 runs/month (hourly × 30 days). Runs with zero candidate +PRs finish in ~10s. Each PR reviewed costs ~5 min of runner time (4 model +invocations: 3 council + 1 synthesis). diff --git a/scripts/list-prs.sh b/scripts/list-prs.sh index 742b22550..4533be1a6 100755 --- a/scripts/list-prs.sh +++ b/scripts/list-prs.sh @@ -1,20 +1,18 @@ #!/usr/bin/env bash -# Enumerate open PRs the agent should consider reviewing. +# Enumerate open, non-draft PRs the agent should consider reviewing. # # Two buckets, deduped: # 1. PRs authored by @me across all repos (self-review). # 2. PRs where @me is a requested reviewer across all repos. # -# Output: one PR URL per line on stdout. +# Output: one PR URL per line on stdout. Drafts are excluded. set -euo pipefail -ME="$(gh api user --jq '.login')" - -# gh search prs respects $GH_TOKEN. --json url is portable across versions. authored=$(gh search prs \ --state open \ --author "@me" \ + --draft=false \ --limit 100 \ --json url \ --jq '.[].url') @@ -22,13 +20,11 @@ authored=$(gh search prs \ review_requested=$(gh search prs \ --state open \ --review-requested "@me" \ + --draft=false \ --limit 100 \ --json url \ --jq '.[].url') -# Skip drafts — agent should not review WIP PRs. -# We re-query each URL's isDraft via gh pr view in the review step too, -# but filtering here saves API calls for the obvious cases. { printf '%s\n' "$authored" printf '%s\n' "$review_requested" diff --git a/scripts/review-one-pr.sh b/scripts/review-one-pr.sh index 35f9740c0..d028bfb2a 100755 --- a/scripts/review-one-pr.sh +++ b/scripts/review-one-pr.sh @@ -30,9 +30,12 @@ export PR_HEAD_SHA echo " head SHA: $PR_HEAD_SHA" # 2. Idempotency: look for our marker at this SHA in existing reviews+comments +# Extract the most recent SHA from our review marker in existing PR comments/reviews. +# Uses (array + array) to concatenate safely when either is empty, then iterates +# .body with null guard. The 2>/dev/null catches GraphQL field-access errors. EXISTING_MARKER_SHA=$( gh pr view "$PR_URL" --json reviews,comments \ - --jq '[.reviews[].body, .comments[].body] | .[] | select(. != null)' 2>/dev/null \ + --jq '((.reviews // []) + (.comments // [])) | .[].body | select(. != null)' 2>/dev/null \ | grep -oE '