- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
323 lines
12 KiB
Bash
Executable File
323 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Extract review comments from GitHub PR with combinable filters
|
|
# Usage: ./extract-review-comments.sh OWNER/REPO PR_NUMBER [--reviewer LOGIN_ID] [FLAGS]
|
|
|
|
set -e
|
|
set -o pipefail
|
|
|
|
# Check dependencies early
|
|
if ! command -v gh >/dev/null 2>&1; then
|
|
echo "❌ Error: GitHub CLI (gh) is required but not installed" >&2
|
|
exit 1
|
|
fi
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "❌ Error: jq is required but not installed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Function to show usage
|
|
show_usage() {
|
|
echo "Usage: $0 OWNER/REPO PR_NUMBER [--reviewer LOGIN_ID] [FLAGS]"
|
|
echo ""
|
|
echo "OPTIONS:"
|
|
echo " --reviewer LOGIN_ID Filter comments by specific reviewer (e.g., coderabbitai[bot], rahulkarajgikar)"
|
|
echo " --limit NUMBER Maximum number of comments to display (default: all)"
|
|
echo " --offset NUMBER Number of comments to skip (default: 0, for pagination)"
|
|
echo ""
|
|
echo "FLAGS (can be combined):"
|
|
echo " --latest-only Latest review by timestamp"
|
|
echo " --latest-actionable Latest review with substantial feedback (has top-level summary)"
|
|
echo " --unresolved-only Only unresolved comments"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 truffle-ai/dexto 293 # All comments from all reviewers"
|
|
echo " $0 truffle-ai/dexto 293 --reviewer coderabbitai[bot] # All CodeRabbit comments"
|
|
echo " $0 truffle-ai/dexto 293 --reviewer rahulkarajgikar --latest-actionable # Latest actionable human review"
|
|
echo " $0 truffle-ai/dexto 293 --reviewer coderabbitai[bot] --unresolved-only # Unresolved CodeRabbit comments"
|
|
echo " $0 truffle-ai/dexto 293 --latest-actionable --unresolved-only # Unresolved from any latest actionable review"
|
|
echo ""
|
|
echo "Pagination examples:"
|
|
echo " $0 truffle-ai/dexto 293 --unresolved-only --limit 10 # First 10 comments"
|
|
echo " $0 truffle-ai/dexto 293 --unresolved-only --limit 10 --offset 10 # Next 10 comments (page 2)"
|
|
echo " $0 truffle-ai/dexto 293 --unresolved-only --limit 5 --offset 20 # Comments 21-25"
|
|
}
|
|
|
|
if [ $# -lt 2 ]; then
|
|
show_usage
|
|
exit 1
|
|
fi
|
|
|
|
REPO="$1"
|
|
PR_NUMBER="$2"
|
|
shift 2 # Remove first two args, leaving only flags
|
|
|
|
# Parse flags
|
|
LATEST_ONLY=false
|
|
LATEST_ACTIONABLE=false
|
|
UNRESOLVED_ONLY=false
|
|
REVIEWER=""
|
|
LIMIT=""
|
|
OFFSET="0"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--reviewer)
|
|
if [ -z "$2" ] || [[ "$2" == --* || "$2" == -* ]]; then
|
|
echo "❌ Error: --reviewer requires a login ID"
|
|
exit 1
|
|
fi
|
|
REVIEWER="$2"
|
|
shift 2
|
|
;;
|
|
--latest-only)
|
|
LATEST_ONLY=true
|
|
shift
|
|
;;
|
|
--latest-actionable)
|
|
LATEST_ACTIONABLE=true
|
|
shift
|
|
;;
|
|
--unresolved-only)
|
|
UNRESOLVED_ONLY=true
|
|
shift
|
|
;;
|
|
--limit)
|
|
if [ -z "$2" ]; then
|
|
echo "❌ Error: --limit requires a number"
|
|
exit 1
|
|
fi
|
|
if ! [[ "$2" =~ ^[0-9]+$ ]] || [ "$2" -le 0 ]; then
|
|
echo "❌ Error: --limit must be a positive integer"
|
|
exit 1
|
|
fi
|
|
LIMIT="$2"
|
|
shift 2
|
|
;;
|
|
--offset)
|
|
if [ -z "$2" ]; then
|
|
echo "❌ Error: --offset requires a number"
|
|
exit 1
|
|
fi
|
|
if ! [[ "$2" =~ ^[0-9]+$ ]]; then
|
|
echo "❌ Error: --offset must be a non-negative integer"
|
|
exit 1
|
|
fi
|
|
OFFSET="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
show_usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown flag: $1"
|
|
show_usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Validate conflicting flags
|
|
if [ "$LATEST_ONLY" = true ] && [ "$LATEST_ACTIONABLE" = true ]; then
|
|
echo "❌ Error: Cannot use both --latest-only and --latest-actionable"
|
|
exit 1
|
|
fi
|
|
|
|
# Extract owner and repo name
|
|
IFS='/' read -r OWNER REPO_NAME <<< "$REPO"
|
|
|
|
# Build display text based on reviewer filter
|
|
if [ -n "$REVIEWER" ]; then
|
|
echo "🤖 Extracting $REVIEWER comments from $REPO PR #$PR_NUMBER"
|
|
BASE_DESC="$REVIEWER comments"
|
|
else
|
|
echo "🤖 Extracting review comments from $REPO PR #$PR_NUMBER"
|
|
BASE_DESC="review comments"
|
|
fi
|
|
|
|
# Build mode description
|
|
MODE_DESC="All $BASE_DESC"
|
|
if [ "$LATEST_ONLY" = true ]; then
|
|
MODE_DESC="Latest review (by timestamp)"
|
|
elif [ "$LATEST_ACTIONABLE" = true ]; then
|
|
MODE_DESC="Latest actionable review"
|
|
fi
|
|
|
|
if [ "$UNRESOLVED_ONLY" = true ]; then
|
|
if [ "$LATEST_ONLY" = true ] || [ "$LATEST_ACTIONABLE" = true ]; then
|
|
MODE_DESC="$MODE_DESC - unresolved only"
|
|
else
|
|
MODE_DESC="All unresolved $BASE_DESC"
|
|
fi
|
|
fi
|
|
|
|
# Add pagination info to mode description
|
|
if [ -n "$LIMIT" ]; then
|
|
if [ "$OFFSET" != "0" ]; then
|
|
MODE_DESC="$MODE_DESC (showing $LIMIT comments starting from #$((OFFSET + 1)))"
|
|
else
|
|
MODE_DESC="$MODE_DESC (showing first $LIMIT comments)"
|
|
fi
|
|
fi
|
|
|
|
echo "📋 Mode: $MODE_DESC"
|
|
echo "=================================================================="
|
|
|
|
# Step 1: Determine the scope (which review(s) to look at)
|
|
TARGET_REVIEW_ID=""
|
|
|
|
if [ "$LATEST_ONLY" = true ]; then
|
|
# Get the most recent review by timestamp
|
|
if [ -n "$REVIEWER" ]; then
|
|
# Filter by specific reviewer, then sort to most recent
|
|
TARGET_REVIEW_ID=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \
|
|
| jq -r --arg reviewer "$REVIEWER" '[.[] | select(.user.login == $reviewer)] | sort_by(.submitted_at // .created_at // .id) | last | .id')
|
|
REVIEWER_DESC=" from $REVIEWER"
|
|
else
|
|
# Any reviewer, sort to most recent
|
|
TARGET_REVIEW_ID=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \
|
|
| jq -r '[.[]] | sort_by(.submitted_at // .created_at // .id) | last | .id')
|
|
REVIEWER_DESC=""
|
|
fi
|
|
|
|
if [ -z "$TARGET_REVIEW_ID" ] || [ "$TARGET_REVIEW_ID" = "null" ]; then
|
|
echo "❌ No reviews found${REVIEWER_DESC} for this PR"
|
|
exit 1
|
|
fi
|
|
echo "🔍 Latest review ID: $TARGET_REVIEW_ID"
|
|
|
|
elif [ "$LATEST_ACTIONABLE" = true ]; then
|
|
# Get the most recent review with a body (top-level summary = actionable review)
|
|
if [ -n "$REVIEWER" ]; then
|
|
# Filter by specific reviewer, then sort to most recent actionable
|
|
TARGET_REVIEW_ID=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \
|
|
| jq -r --arg reviewer "$REVIEWER" '[.[] | select(.user.login == $reviewer and .body != null and .body != "")] | sort_by(.submitted_at // .created_at // .id) | last | .id')
|
|
REVIEWER_DESC=" from $REVIEWER"
|
|
else
|
|
# Any reviewer, most recent actionable
|
|
TARGET_REVIEW_ID=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \
|
|
| jq -r '[.[] | select(.body != null and .body != "")] | sort_by(.submitted_at // .created_at // .id) | last | .id')
|
|
REVIEWER_DESC=""
|
|
fi
|
|
|
|
if [ -z "$TARGET_REVIEW_ID" ] || [ "$TARGET_REVIEW_ID" = "null" ]; then
|
|
echo "❌ No actionable reviews found${REVIEWER_DESC} for this PR"
|
|
exit 1
|
|
fi
|
|
echo "🔍 Latest actionable review ID: $TARGET_REVIEW_ID"
|
|
fi
|
|
|
|
# Step 2: Get the base set of comments based on scope
|
|
if [ -n "$TARGET_REVIEW_ID" ]; then
|
|
# Get comments from specific review (already filtered by reviewer if specified)
|
|
BASE_COMMENTS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --paginate \
|
|
| jq -s --arg review_id "$TARGET_REVIEW_ID" '[ .[] | .[] | select(.pull_request_review_id == ($review_id | tonumber)) ]')
|
|
else
|
|
# Get all comments, optionally filtered by reviewer
|
|
if [ -n "$REVIEWER" ]; then
|
|
BASE_COMMENTS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --paginate \
|
|
| jq -s --arg reviewer "$REVIEWER" '[ .[] | .[] | select(.user.login == $reviewer) ]')
|
|
else
|
|
BASE_COMMENTS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --paginate \
|
|
| jq -s '[ .[] | .[] | select(.user.login) ]') # All comments from any reviewer
|
|
fi
|
|
fi
|
|
|
|
# Step 3: Apply unresolved filter if requested
|
|
if [ "$UNRESOLVED_ONLY" = true ]; then
|
|
# We need to cross-reference with GraphQL data for resolution status
|
|
echo "🔄 Checking resolution status..."
|
|
|
|
# Get unresolved thread data from GraphQL
|
|
UNRESOLVED_THREADS=$(gh api graphql -f query='
|
|
query($owner: String!, $repo: String!, $number: Int!) {
|
|
repository(owner: $owner, name: $repo) {
|
|
pullRequest(number: $number) {
|
|
reviewThreads(first: 100) {
|
|
nodes {
|
|
id
|
|
isResolved
|
|
comments(first: 100) {
|
|
nodes {
|
|
id
|
|
databaseId
|
|
author { login }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}' \
|
|
-f owner="$OWNER" \
|
|
-f repo="$REPO_NAME" \
|
|
-F number="$PR_NUMBER" \
|
|
| jq '[.data.repository.pullRequest.reviewThreads.nodes[] |
|
|
select(.isResolved == false) |
|
|
.comments.nodes[] | .databaseId]')
|
|
|
|
# Filter BASE_COMMENTS to only include unresolved comment IDs
|
|
FILTERED_COMMENTS=$(echo "$BASE_COMMENTS" | jq --argjson unresolved_ids "$UNRESOLVED_THREADS" '
|
|
[.[] | select(.id as $id | $unresolved_ids | index($id))]')
|
|
else
|
|
FILTERED_COMMENTS="$BASE_COMMENTS"
|
|
fi
|
|
|
|
# Step 4: Sort comments by file path and line number
|
|
SORTED_COMMENTS=$(echo "$FILTERED_COMMENTS" | jq 'sort_by([.path, .line // 0])')
|
|
|
|
# Step 5: Apply pagination if specified
|
|
if [ -n "$LIMIT" ]; then
|
|
PAGINATED_COMMENTS=$(echo "$SORTED_COMMENTS" | jq --argjson limit "$LIMIT" --argjson offset "$OFFSET" '
|
|
.[$offset:$offset + $limit]')
|
|
else
|
|
PAGINATED_COMMENTS="$SORTED_COMMENTS"
|
|
fi
|
|
|
|
# Step 6: Count and display results
|
|
TOTAL_COUNT=$(echo "$SORTED_COMMENTS" | jq length)
|
|
DISPLAYED_COUNT=$(echo "$PAGINATED_COMMENTS" | jq length)
|
|
|
|
if [ "$TOTAL_COUNT" -eq 0 ]; then
|
|
echo "📊 No comments found matching the specified criteria"
|
|
echo ""
|
|
echo "✅ Done! Use 'gh pr view $PR_NUMBER --repo $REPO --web' to view the PR in browser"
|
|
exit 0
|
|
fi
|
|
|
|
# Display the comments with GitHub links
|
|
echo "$PAGINATED_COMMENTS" | jq -r --arg repo "$REPO" --arg pr "$PR_NUMBER" '.[] |
|
|
"📄 " + .path + ":" + (.line | tostring) + "\n" +
|
|
"🆔 Comment ID: " + (.id | tostring) + "\n" +
|
|
"🔗 GitHub Link: https://github.com/" + $repo + "/pull/" + $pr + "#discussion_r" + (.id | tostring) + "\n" +
|
|
"📅 Created: " + .created_at + "\n" +
|
|
"👍 Reactions: " + (.reactions.total_count | tostring) +
|
|
(if .pull_request_review_id then "\n🔗 Review ID: " + (.pull_request_review_id | tostring) else "" end) +
|
|
"\n---\n" + .body + "\n" +
|
|
"=================================================================="
|
|
'
|
|
|
|
echo ""
|
|
if [ -n "$LIMIT" ]; then
|
|
TOTAL_PAGES=$(( (TOTAL_COUNT + LIMIT - 1) / LIMIT ))
|
|
CURRENT_PAGE=$(( OFFSET / LIMIT + 1 ))
|
|
echo "📊 Summary: Showing $DISPLAYED_COUNT of $TOTAL_COUNT total comments (Page $CURRENT_PAGE of $TOTAL_PAGES)"
|
|
|
|
# Show pagination hints
|
|
if [ $CURRENT_PAGE -gt 1 ]; then
|
|
PREV_OFFSET=$((OFFSET - LIMIT))
|
|
if [ $PREV_OFFSET -lt 0 ]; then PREV_OFFSET=0; fi
|
|
echo "⬅️ Previous page: $0 $REPO $PR_NUMBER $(echo "$@" | sed "s/--offset [0-9]*//" | sed "s/$/ --offset $PREV_OFFSET/")"
|
|
fi
|
|
|
|
if [ $CURRENT_PAGE -lt $TOTAL_PAGES ]; then
|
|
NEXT_OFFSET=$((OFFSET + LIMIT))
|
|
echo "➡️ Next page: $0 $REPO $PR_NUMBER $(echo "$@" | sed "s/--offset [0-9]*//" | sed "s/$/ --offset $NEXT_OFFSET/")"
|
|
fi
|
|
else
|
|
echo "📊 Summary: Found $TOTAL_COUNT comments matching criteria (sorted by file/line)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "✅ Done! Use 'gh pr view $PR_NUMBER --repo $REPO --web' to view the PR in browser" |