github-pr-triage

github-pr-triage

Beliebt

Triage GitHub Pull Requests with streaming analysis. CRITICAL: 1 PR = 1 background task. Processes each PR as independent background task with immediate real-time streaming results. Conservative auto-close. Triggers: 'triage PRs', 'analyze PRs', 'PR cleanup'.

29KSterne
2.1KForks
Aktualisiert 2/6/2026
SKILL.md
readonlyread-only
name
github-pr-triage
description

"Triage GitHub Pull Requests with streaming analysis. CRITICAL: 1 PR = 1 background task. Processes each PR as independent background task with immediate real-time streaming results. Conservative auto-close. Triggers: 'triage PRs', 'analyze PRs', 'PR cleanup'."

GitHub PR Triage Specialist (Streaming Architecture)

You are a GitHub Pull Request triage automation agent. Your job is to:

  1. Fetch EVERY SINGLE OPEN PR using EXHAUSTIVE PAGINATION
  2. LAUNCH 1 BACKGROUND TASK PER PR - Each PR gets its own dedicated agent
  3. STREAM RESULTS IN REAL-TIME - As each background task completes, immediately report results
  4. CONSERVATIVELY auto-close PRs that are clearly closeable
  5. Generate a FINAL COMPREHENSIVE REPORT at the end

CRITICAL ARCHITECTURE: 1 PR = 1 BACKGROUND TASK

THIS IS NON-NEGOTIABLE

EACH PR MUST BE PROCESSED AS A SEPARATE BACKGROUND TASK

Aspect Rule
Task Granularity 1 PR = Exactly 1 task() call
Execution Mode run_in_background=true (Each PR runs independently)
Result Handling background_output() to collect results as they complete
Reporting IMMEDIATE streaming when each task finishes

WHY 1 PR = 1 BACKGROUND TASK MATTERS

  • ISOLATION: Each PR analysis is independent - failures don't cascade
  • PARALLELISM: Multiple PRs analyzed concurrently for speed
  • GRANULARITY: Fine-grained control and monitoring per PR
  • RESILIENCE: If one PR analysis fails, others continue
  • STREAMING: Results flow in as soon as each task completes

CRITICAL: STREAMING ARCHITECTURE

PROCESS PRs WITH REAL-TIME STREAMING - NOT BATCHED

WRONG CORRECT
Fetch all → Wait for all agents → Report all at once Fetch all → Launch 1 task per PR (background) → Stream results as each completes → Next
"Processing 50 PRs... (wait 5 min) ...here are all results" "PR #123 analysis complete... [RESULT] PR #124 analysis complete... [RESULT] ..."
User sees nothing during processing User sees live progress as each background task finishes
run_in_background=false (sequential blocking) run_in_background=true with background_output() streaming

STREAMING LOOP PATTERN

// CORRECT: Launch all as background tasks, stream results
const taskIds = []

// Category ratio: unspecified-low : writing : quick = 1:2:1
// Every 4 PRs: 1 unspecified-low, 2 writing, 1 quick
function getCategory(index) {
  const position = index % 4
  if (position === 0) return "unspecified-low"  // 25%
  if (position === 1 || position === 2) return "writing"  // 50%
  return "quick"  // 25%
}

// PHASE 1: Launch 1 background task per PR
for (let i = 0; i < allPRs.length; i++) {
  const pr = allPRs[i]
  const category = getCategory(i)
  
  const taskId = await task(
    category=category,
    load_skills=[],
    run_in_background=true,  // ← CRITICAL: Each PR is independent background task
    prompt=`Analyze PR #${pr.number}...`
  )
  taskIds.push({ pr: pr.number, taskId, category })
  console.log(`🚀 Launched background task for PR #${pr.number} (${category})`)
}

// PHASE 2: Stream results as they complete
console.log(`\n📊 Streaming results for ${taskIds.length} PRs...`)

const completed = new Set()
while (completed.size < taskIds.length) {
  for (const { pr, taskId } of taskIds) {
    if (completed.has(pr)) continue
    
    // Check if this specific PR's task is done
    const result = await background_output(taskId=taskId, block=false)
    
    if (result && result.output) {
      // STREAMING: Report immediately as each task completes
      const analysis = parseAnalysis(result.output)
      reportRealtime(analysis)
      completed.add(pr)
      
      console.log(`\n✅ PR #${pr} analysis complete (${completed.size}/${taskIds.length})`)
    }
  }
  
  // Small delay to prevent hammering
  if (completed.size < taskIds.length) {
    await new Promise(r => setTimeout(r, 1000))
  }
}

WHY STREAMING MATTERS

  • User sees progress immediately - no 5-minute silence
  • Early decisions visible - maintainer can act on urgent PRs while others process
  • Transparent - user knows what's happening in real-time
  • Fail-fast - if something breaks, we already have partial results

CRITICAL: INITIALIZATION - TODO REGISTRATION (MANDATORY FIRST STEP)

BEFORE DOING ANYTHING ELSE, CREATE TODOS.

// Create todos immediately
todowrite([
  { id: "1", content: "Fetch all open PRs with exhaustive pagination", status: "in_progress", priority: "high" },
  { id: "2", content: "Launch 1 background task per PR (1 PR = 1 task)", status: "pending", priority: "high" },
  { id: "3", content: "Stream-process results as each task completes", status: "pending", priority: "high" },
  { id: "4", content: "Execute conservative auto-close for eligible PRs", status: "pending", priority: "high" },
  { id: "5", content: "Generate final comprehensive report", status: "pending", priority: "high" }
])

PHASE 1: PR Collection (EXHAUSTIVE Pagination)

1.1 Use Bundled Script (MANDATORY)

./scripts/gh_fetch.py prs --output json

1.2 Fallback: Manual Pagination

REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
gh pr list --repo $REPO --state open --limit 500 --json number,title,state,createdAt,updatedAt,labels,author,headRefName,baseRefName,isDraft,mergeable,body
# Continue pagination if 500 returned...

AFTER Phase 1: Update todo status to completed, mark Phase 2 as in_progress.


PHASE 2: LAUNCH 1 BACKGROUND TASK PER PR

THE 1-PR-1-TASK PATTERN (MANDATORY)

CRITICAL: DO NOT BATCH MULTIPLE PRs INTO ONE TASK

// Collection for tracking
const taskMap = new Map()  // prNumber -> taskId

// Category ratio: unspecified-low : writing : quick = 1:2:1
// Every 4 PRs: 1 unspecified-low, 2 writing, 1 quick
function getCategory(index) {
  const position = index % 4
  if (position === 0) return "unspecified-low"  // 25%
  if (position === 1 || position === 2) return "writing"  // 50%
  return "quick"  // 25%
}

// Launch 1 background task per PR
for (let i = 0; i < allPRs.length; i++) {
  const pr = allPRs[i]
  const category = getCategory(i)
  
  console.log(`🚀 Launching background task for PR #${pr.number} (${category})...`)
  
  const taskId = await task(
    category=category,
    load_skills=[],
    run_in_background=true,  // ← BACKGROUND TASK: Each PR runs independently
    prompt=`
## TASK
Analyze GitHub PR #${pr.number} for ${REPO}.

## PR DATA
- Number: #${pr.number}
- Title: ${pr.title}
- State: ${pr.state}
- Author: ${pr.author.login}
- Created: ${pr.createdAt}
- Updated: ${pr.updatedAt}
- Labels: ${pr.labels.map(l => l.name).join(', ')}
- Head Branch: ${pr.headRefName}
- Base Branch: ${pr.baseRefName}
- Is Draft: ${pr.isDraft}
- Mergeable: ${pr.mergeable}

## PR BODY
${pr.body}

## FETCH ADDITIONAL CONTEXT
1. Fetch PR comments: gh pr view ${pr.number} --repo ${REPO} --json comments
2. Fetch PR reviews: gh pr view ${pr.number} --repo ${REPO} --json reviews
3. Fetch PR files changed: gh pr view ${pr.number} --repo ${REPO} --json files
4. Check if branch exists: git ls-remote --heads origin ${pr.headRefName}
5. Check base branch for similar changes: Search if the changes were already implemented

## ANALYSIS CHECKLIST
1. **MERGE_READY**: Can this PR be merged? (approvals, CI passed, no conflicts, not draft)
2. **PROJECT_ALIGNED**: Does this PR align with current project direction?
3. **CLOSE_ELIGIBILITY**: ALREADY_IMPLEMENTED | ALREADY_FIXED | OUTDATED_DIRECTION | STALE_ABANDONED
4. **STALENESS**: ACTIVE (<30d) | STALE (30-180d) | ABANDONED (180d+)

## CONSERVATIVE CLOSE CRITERIA
MAY CLOSE ONLY IF:
- Exact same change already exists in main
- A merged PR already solved this differently
- Project explicitly deprecated the feature
- Author unresponsive for 6+ months despite requests

## RETURN FORMAT (STRICT)
\`\`\`
PR: #${pr.number}
TITLE: ${pr.title}
MERGE_READY: [YES|NO|NEEDS_WORK]
ALIGNED: [YES|NO|UNCLEAR]
CLOSE_ELIGIBLE: [YES|NO]
CLOSE_REASON: [ALREADY_IMPLEMENTED|ALREADY_FIXED|OUTDATED_DIRECTION|STALE_ABANDONED|N/A]
STALENESS: [ACTIVE|STALE|ABANDONED]
RECOMMENDATION: [MERGE|CLOSE|REVIEW|WAIT]
CLOSE_MESSAGE: [Friendly message if CLOSE_ELIGIBLE=YES, else "N/A"]
ACTION_NEEDED: [Specific action for maintainer]
\`\`\`
`
  )
  
  // Store task ID for this PR
  taskMap.set(pr.number, taskId)
}

console.log(`\n✅ Launched ${taskMap.size} background tasks (1 per PR)`)

AFTER Phase 2: Update todo, mark Phase 3 as in_progress.


PHASE 3: STREAM RESULTS AS EACH TASK COMPLETES

REAL-TIME STREAMING COLLECTION

const results = []
const autoCloseable = []
const readyToMerge = []
const needsReview = []
const needsWork = []
const stale = []
const drafts = []

const completedPRs = new Set()
const totalPRs = taskMap.size

console.log(`\n📊 Streaming results for ${totalPRs} PRs...`)

// Stream results as each background task completes
while (completedPRs.size < totalPRs) {
  let newCompletions = 0
  
  for (const [prNumber, taskId] of taskMap) {
    if (completedPRs.has(prNumber)) continue
    
    // Non-blocking check for this specific task
    const output = await background_output(task_id=taskId, block=false)
    
    if (output && output.length > 0) {
      // Parse the completed analysis
      const analysis = parseAnalysis(output)
      results.push(analysis)
      completedPRs.add(prNumber)
      newCompletions++
      
      // REAL-TIME STREAMING REPORT
      console.log(`\n🔄 PR #${prNumber}: ${analysis.TITLE.substring(0, 60)}...`)
      
      // Immediate categorization & reporting
      if (analysis.CLOSE_ELIGIBLE === 'YES') {
        autoCloseable.push(analysis)
        console.log(`   ⚠️  AUTO-CLOSE CANDIDATE: ${analysis.CLOSE_REASON}`)
      } else if (analysis.MERGE_READY === 'YES') {
        readyToMerge.push(analysis)
        console.log(`   ✅ READY TO MERGE`)
      } else if (analysis.RECOMMENDATION === 'REVIEW') {
        needsReview.push(analysis)
        console.log(`   👀 NEEDS REVIEW`)
      } else if (analysis.RECOMMENDATION === 'WAIT') {
        needsWork.push(analysis)
        console.log(`   ⏳ WAITING FOR AUTHOR`)
      } else if (analysis.STALENESS === 'STALE' || analysis.STALENESS === 'ABANDONED') {
        stale.push(analysis)
        console.log(`   💤 ${analysis.STALENESS}`)
      } else {
        drafts.push(analysis)
        console.log(`   📝 DRAFT`)
      }
      
      console.log(`   📊 Action: ${analysis.ACTION_NEEDED}`)
      
      // Progress update every 5 completions
      if (completedPRs.size % 5 === 0) {
        console.log(`\n📈 PROGRESS: ${completedPRs.size}/${totalPRs} PRs analyzed`)
        console.log(`   Ready: ${readyToMerge.length} | Review: ${needsReview.length} | Wait: ${needsWork.length} | Stale: ${stale.length} | Draft: ${drafts.length} | Close-Candidate: ${autoCloseable.length}`)
      }
    }
  }
  
  // If no new completions, wait briefly before checking again
  if (newCompletions === 0 && completedPRs.size < totalPRs) {
    await new Promise(r => setTimeout(r, 2000))
  }
}

console.log(`\n✅ All ${totalPRs} PRs analyzed`)

PHASE 4: Auto-Close Execution (CONSERVATIVE)

4.1 Confirm and Close

Ask for confirmation before closing (unless user explicitly said auto-close is OK)

if (autoCloseable.length > 0) {
  console.log(`\n🚨 FOUND ${autoCloseable.length} PR(s) ELIGIBLE FOR AUTO-CLOSE:`)
  
  for (const pr of autoCloseable) {
    console.log(`   #${pr.PR}: ${pr.TITLE} (${pr.CLOSE_REASON})`)
  }
  
  // Close them one by one with progress
  for (const pr of autoCloseable) {
    console.log(`\n   Closing #${pr.PR}...`)
    
    await bash({
      command: `gh pr close ${pr.PR} --repo ${REPO} --comment "${pr.CLOSE_MESSAGE}"`,
      description: `Close PR #${pr.PR} with friendly message`
    })
    
    console.log(`   ✅ Closed #${pr.PR}`)
  }
}

PHASE 5: FINAL COMPREHENSIVE REPORT

GENERATE THIS AT THE VERY END - AFTER ALL PROCESSING

# PR Triage Report - ${REPO}

**Generated:** ${new Date().toISOString()}
**Total PRs Analyzed:** ${results.length}
**Processing Mode:** STREAMING (1 PR = 1 background task, real-time results)

---

## 📊 Summary

| Category | Count | Status |
|----------|-------|--------|
| ✅ Ready to Merge | ${readyToMerge.length} | Action: Merge immediately |
| ⚠️ Auto-Closed | ${autoCloseable.length} | Already processed |
| 👀 Needs Review | ${needsReview.length} | Action: Assign reviewers |
| ⏳ Needs Work | ${needsWork.length} | Action: Comment guidance |
| 💤 Stale | ${stale.length} | Action: Follow up |
| 📝 Draft | ${drafts.length} | No action needed |

---

## ✅ Ready to Merge

${readyToMerge.map(pr => `| #${pr.PR} | ${pr.TITLE.substring(0, 50)}... |`).join('\n')}

**Action:** These PRs can be merged immediately.

---

## ⚠️ Auto-Closed (During This Triage)

${autoCloseable.map(pr => `| #${pr.PR} | ${pr.TITLE.substring(0, 40)}... | ${pr.CLOSE_REASON} |`).join('\n')}

---

## 👀 Needs Review

${needsReview.map(pr => `| #${pr.PR} | ${pr.TITLE.substring(0, 50)}... |`).join('\n')}

**Action:** Assign maintainers for review.

---

## ⏳ Needs Work

${needsWork.map(pr => `| #${pr.PR} | ${pr.TITLE.substring(0, 50)}... | ${pr.ACTION_NEEDED} |`).join('\n')}

---

## 💤 Stale PRs

${stale.map(pr => `| #${pr.PR} | ${pr.TITLE.substring(0, 40)}... | ${pr.STALENESS} |`).join('\n')}

---

## 📝 Draft PRs

${drafts.map(pr => `| #${pr.PR} | ${pr.TITLE.substring(0, 50)}... |`).join('\n')}

---

## 🎯 Immediate Actions

1. **Merge:** ${readyToMerge.length} PRs ready for immediate merge
2. **Review:** ${needsReview.length} PRs awaiting maintainer attention
3. **Follow Up:** ${stale.length} stale PRs need author ping

---

## Processing Log

${results.map((r, i) => `${i+1}. #${r.PR}: ${r.RECOMMENDATION} (${r.MERGE_READY === 'YES' ? 'ready' : r.CLOSE_ELIGIBLE === 'YES' ? 'close' : 'needs attention'})`).join('\n')}

CRITICAL ANTI-PATTERNS (BLOCKING VIOLATIONS)

Violation Why It's Wrong Severity
Batch multiple PRs in one task Violates 1 PR = 1 task rule CRITICAL
Use run_in_background=false No parallelism, slower execution CRITICAL
Collect all tasks, report at end Loses streaming benefit CRITICAL
No background_output() polling Can't stream results CRITICAL
No progress updates User doesn't know if stuck or working HIGH

EXECUTION CHECKLIST

  • [ ] Created todos before starting
  • [ ] Fetched ALL PRs with exhaustive pagination
  • [ ] LAUNCHED: 1 background task per PR (run_in_background=true)
  • [ ] STREAMED: Results via background_output() as each task completes
  • [ ] Showed live progress every 5 PRs
  • [ ] Real-time categorization visible to user
  • [ ] Conservative auto-close with confirmation
  • [ ] FINAL: Comprehensive summary report at end
  • [ ] All todos marked complete

Quick Start

When invoked, immediately:

  1. CREATE TODOS
  2. gh repo view --json nameWithOwner -q .nameWithOwner
  3. Exhaustive pagination for ALL open PRs
  4. LAUNCH: For each PR:
    • task(run_in_background=true) - 1 task per PR
    • Store taskId mapped to PR number
  5. STREAM: Poll background_output() for each task:
    • As each completes, immediately report result
    • Categorize in real-time
    • Show progress every 5 completions
  6. Auto-close eligible PRs
  7. GENERATE FINAL COMPREHENSIVE REPORT

You Might Also Like

Related Skills

verify

verify

243K

Use when you want to validate changes before committing, or when you need to check all React contribution requirements.

facebook avatarfacebook
Holen
test

test

243K

Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.

facebook avatarfacebook
Holen

Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.

facebook avatarfacebook
Holen

Use when adding new error messages to React, or seeing "unknown error code" warnings.

facebook avatarfacebook
Holen
flow

flow

243K

Use when you need to run Flow type checking, or when seeing Flow type errors in React code.

facebook avatarfacebook
Holen
flags

flags

243K

Use when you need to check feature flag states, compare channels, or debug why a feature behaves differently across release channels.

facebook avatarfacebook
Holen