From b0e6e9aa1732208a6e06b8f81fad3733757cf47f Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Tue, 21 Apr 2026 12:31:47 +0100 Subject: [PATCH] meta: add PR review skill and ignore .prs/ output directory --- .claude/skills/review-prs/SKILL.md | 59 ++++++++++++++++++++ .claude/skills/review-prs/sync.sh | 88 ++++++++++++++++++++++++++++++ .gitignore | 2 + 3 files changed, 149 insertions(+) create mode 100644 .claude/skills/review-prs/SKILL.md create mode 100755 .claude/skills/review-prs/sync.sh diff --git a/.claude/skills/review-prs/SKILL.md b/.claude/skills/review-prs/SKILL.md new file mode 100644 index 000000000..dec56809f --- /dev/null +++ b/.claude/skills/review-prs/SKILL.md @@ -0,0 +1,59 @@ +--- +name: review-prs +tools: Read, Grep, Glob, Bash +--- + +Review all open PRs on pyinfra-dev/pyinfra and maintain review files in `.prs/`: + +## Steps + +1. **Run sync script**: Execute `bash .claude/skills/review-prs/sync.sh` to clean up stale reviews and get the list of PRs to update/create. + +2. **For each PR** in the sync output (Update + New only, NOT Unchanged), launch agents in parallel (batches of 3-5) to review: + - Each agent gets: repo name, PR number, whether it's an update or new review. + - Agent fetches the PR diff via `gh pr diff --repo pyinfra-dev/pyinfra {number}`. + - Agent also fetches `updatedAt` via `gh pr view --repo pyinfra-dev/pyinfra {number} --json updatedAt --jq .updatedAt` and includes it in the review header as `**PR Updated:** {timestamp}`. + - Agent reads relevant source code files being modified (use Read/Grep, never assume). + - Agent writes the review file to `.prs/{number}.md`. + +3. **Review format** for each PR file (`.prs/{number}.md`): + +```markdown +# PR #{number} - {title} + +**Author:** {author} +**URL:** https://github.com/pyinfra-dev/pyinfra/pull/{number} +**Review Date:** {YYYY-MM-DD} +**PR Updated:** {updatedAt ISO timestamp from GH API} + +## Verdict: {GO | NO-GO | NEEDS-DISCUSSION} + +{1-2 sentence summary of what the PR does} + +## Issues + +{Only if there are problems. Each issue should be a concise bullet with a code snippet if relevant. Skip this section entirely if no issues.} + +## Notes + +{Optional minor observations, nits, or suggestions. Keep brief.} +``` + +## Review guidelines + +- **Be minimal**: Only highlight actual problems or risks. Don't pad reviews with praise or restatements. +- **Code snippets**: When referencing problematic code, include the relevant snippet (keep short). +- **Deep inspection**: Always read the actual source code being modified. Never assume behavior - trace code paths, check callers, verify types. Use Grep/Read extensively. +- **Verdict options**: + * **GO** = safe to merge as-is or with trivial nits only + * **NO-GO** = has bugs, security issues, breaking changes, or significant design problems + * **NEEDS-DISCUSSION** = requires maintainer input on approach/design + +## Parallelism + +- Process multiple PRs in parallel using Agent tool where possible (batch into groups of 3-5). +- Each agent should be given the full context: repo name, PR number, review format, and instruction to deeply inspect code. + +## Final Summary + +Once all review updates are complete, output a summary table of PRs grouped by verdict. Re-read the PR files to collect updated verdicts for each. diff --git a/.claude/skills/review-prs/sync.sh b/.claude/skills/review-prs/sync.sh new file mode 100755 index 000000000..b47eb2f8e --- /dev/null +++ b/.claude/skills/review-prs/sync.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Syncs .prs/ review files against open GitHub PRs. +# Deletes reviews for merged/closed PRs, outputs a plan for what to update/create. +# Skips re-review when PR hasn't been updated since the last review. +set -euo pipefail + +REPO="pyinfra-dev/pyinfra" +PRS_DIR="$(git rev-parse --show-toplevel)/.prs" + +mkdir -p "$PRS_DIR" + +# Fetch all open PR numbers (updated in last year) +SINCE=$(date -v-1y +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -d '1 year ago' +%Y-%m-%dT%H:%M:%SZ) +OPEN_PRS=$(gh pr list --repo "$REPO" --state open --limit 200 --json number,title,author,updatedAt \ + --jq "[.[] | select(.updatedAt >= \"$SINCE\")] | sort_by(.number)") + +OPEN_NUMBERS=$(echo "$OPEN_PRS" | jq -r '.[].number') + +# Find existing review files +EXISTING=() +for f in "$PRS_DIR"/*.md; do + [ -f "$f" ] || continue + num=$(basename "$f" .md) + [[ "$num" =~ ^[0-9]+$ ]] && EXISTING+=("$num") +done + +# Delete stale reviews (merged/closed) +DELETED=() +for num in "${EXISTING[@]}"; do + if ! echo "$OPEN_NUMBERS" | grep -qx "$num"; then + rm "$PRS_DIR/$num.md" + DELETED+=("$num") + fi +done + +# Categorize: update (changed), unchanged (skip), or new +UPDATE=() +UNCHANGED=() +NEW=() +while IFS= read -r num; do + if [ -f "$PRS_DIR/$num.md" ]; then + # Compare stored updatedAt with current PR updatedAt + stored_ts=$(sed -n 's/^\*\*PR Updated:\*\* //p' "$PRS_DIR/$num.md" 2>/dev/null || echo "") + current_ts=$(echo "$OPEN_PRS" | jq -r ".[] | select(.number == $num) | .updatedAt") + if [ -n "$stored_ts" ] && [ "$stored_ts" = "$current_ts" ]; then + UNCHANGED+=("$num") + else + UPDATE+=("$num") + fi + else + NEW+=("$num") + fi +done <<< "$OPEN_NUMBERS" + +# Output markdown summary +echo "# PR Review Sync" +echo "" +echo "**Repo:** $REPO" +echo "**Date:** $(date +%Y-%m-%d)" +echo "" + +if [ ${#DELETED[@]} -gt 0 ]; then + echo "## Deleted (merged/closed)" + for num in "${DELETED[@]}"; do + echo "- #$num" + done + echo "" +fi + +if [ ${#UNCHANGED[@]} -gt 0 ]; then + echo "## Unchanged since last review (${#UNCHANGED[@]})" + echo "$OPEN_PRS" | jq -r --argjson nums "$(printf '%s\n' "${UNCHANGED[@]}" | jq -R 'tonumber' | jq -s '.')" \ + '.[] | select(.number as $n | $nums | index($n)) | "- #\(.number) \(.title) (@\(.author.login))"' + echo "" +fi + +echo "## Update existing reviews (${#UPDATE[@]})" +if [ ${#UPDATE[@]} -gt 0 ]; then + echo "$OPEN_PRS" | jq -r --argjson nums "$(printf '%s\n' "${UPDATE[@]}" | jq -R 'tonumber' | jq -s '.')" \ + '.[] | select(.number as $n | $nums | index($n)) | "- #\(.number) \(.title) (@\(.author.login))"' +fi +echo "" + +echo "## New reviews needed (${#NEW[@]})" +if [ ${#NEW[@]} -gt 0 ]; then + echo "$OPEN_PRS" | jq -r --argjson nums "$(printf '%s\n' "${NEW[@]}" | jq -R 'tonumber' | jq -s '.')" \ + '.[] | select(.number as $n | $nums | index($n)) | "- #\(.number) \(.title) (@\(.author.login))"' +fi diff --git a/.gitignore b/.gitignore index 2f3e6e8c5..e6948cac6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ docs/_deploy_globals.rst pyinfra-debug.log Makefile + +.prs/