refactor: unify github-issue-triage + github-pr-triage into single github-triage skill
Replace two separate triage skills with one unified skill using 'free' category for all subagents. Action-oriented: auto-answer questions, analyze bugs, merge safe PRs. All items tracked via TaskCreate, [sisyphus-bot] comment prefix.
This commit is contained in:
@@ -1,489 +0,0 @@
|
||||
---
|
||||
name: github-issue-triage
|
||||
description: "Triage GitHub issues with streaming analysis. CRITICAL: 1 issue = 1 background task. Processes each issue as independent background task with immediate real-time streaming results. Triggers: 'triage issues', 'analyze issues', 'issue report'."
|
||||
---
|
||||
|
||||
# GitHub Issue Triage Specialist (Streaming Architecture)
|
||||
|
||||
You are a GitHub issue triage automation agent. Your job is to:
|
||||
1. Fetch **EVERY SINGLE ISSUE** within time range using **EXHAUSTIVE PAGINATION**
|
||||
2. **LAUNCH 1 BACKGROUND TASK PER ISSUE** - Each issue gets its own dedicated agent
|
||||
3. **STREAM RESULTS IN REAL-TIME** - As each background task completes, immediately report results
|
||||
4. Collect results and generate a **FINAL COMPREHENSIVE REPORT** at the end
|
||||
|
||||
---
|
||||
|
||||
# CRITICAL ARCHITECTURE: 1 ISSUE = 1 BACKGROUND TASK
|
||||
|
||||
## THIS IS NON-NEGOTIABLE
|
||||
|
||||
**EACH ISSUE MUST BE PROCESSED AS A SEPARATE BACKGROUND TASK**
|
||||
|
||||
| Aspect | Rule |
|
||||
|--------|------|
|
||||
| **Task Granularity** | 1 Issue = Exactly 1 `task()` call |
|
||||
| **Execution Mode** | `run_in_background=true` (Each issue runs independently) |
|
||||
| **Result Handling** | `background_output()` to collect results as they complete |
|
||||
| **Reporting** | IMMEDIATE streaming when each task finishes |
|
||||
|
||||
### WHY 1 ISSUE = 1 BACKGROUND TASK MATTERS
|
||||
|
||||
- **ISOLATION**: Each issue analysis is independent - failures don't cascade
|
||||
- **PARALLELISM**: Multiple issues analyzed concurrently for speed
|
||||
- **GRANULARITY**: Fine-grained control and monitoring per issue
|
||||
- **RESILIENCE**: If one issue analysis fails, others continue
|
||||
- **STREAMING**: Results flow in as soon as each task completes
|
||||
|
||||
---
|
||||
|
||||
# CRITICAL: STREAMING ARCHITECTURE
|
||||
|
||||
**PROCESS ISSUES WITH REAL-TIME STREAMING - NOT BATCHED**
|
||||
|
||||
| WRONG | CORRECT |
|
||||
|----------|------------|
|
||||
| Fetch all → Wait for all agents → Report all at once | Fetch all → Launch 1 task per issue (background) → Stream results as each completes → Next |
|
||||
| "Processing 50 issues... (wait 5 min) ...here are all results" | "Issue #123 analysis complete... [RESULT] Issue #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
|
||||
|
||||
```typescript
|
||||
// CORRECT: Launch all as background tasks, stream results
|
||||
const taskIds = []
|
||||
|
||||
// Category ratio: unspecified-low : writing : quick = 1:2:1
|
||||
// Every 4 issues: 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 issue
|
||||
for (let i = 0; i < allIssues.length; i++) {
|
||||
const issue = allIssues[i]
|
||||
const category = getCategory(i)
|
||||
|
||||
const taskId = await task(
|
||||
category=category,
|
||||
load_skills=[],
|
||||
run_in_background=true, // ← CRITICAL: Each issue is independent background task
|
||||
prompt=`Analyze issue #${issue.number}...`
|
||||
)
|
||||
taskIds.push({ issue: issue.number, taskId, category })
|
||||
console.log(`🚀 Launched background task for Issue #${issue.number} (${category})`)
|
||||
}
|
||||
|
||||
// PHASE 2: Stream results as they complete
|
||||
console.log(`\n📊 Streaming results for ${taskIds.length} issues...`)
|
||||
|
||||
const completed = new Set()
|
||||
while (completed.size < taskIds.length) {
|
||||
for (const { issue, taskId } of taskIds) {
|
||||
if (completed.has(issue)) continue
|
||||
|
||||
// Check if this specific issue's task is done
|
||||
const result = await background_output(task_id=taskId, block=false)
|
||||
|
||||
if (result && result.output) {
|
||||
// STREAMING: Report immediately as each task completes
|
||||
const analysis = parseAnalysis(result.output)
|
||||
reportRealtime(analysis)
|
||||
completed.add(issue)
|
||||
|
||||
console.log(`\n✅ Issue #${issue} 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
|
||||
- **Critical issues flagged early** - maintainer can act on urgent bugs 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.**
|
||||
|
||||
```typescript
|
||||
// Create todos immediately
|
||||
todowrite([
|
||||
{ id: "1", content: "Fetch all issues with exhaustive pagination", status: "in_progress", priority: "high" },
|
||||
{ id: "2", content: "Fetch PRs for bug correlation", status: "pending", priority: "high" },
|
||||
{ id: "3", content: "Launch 1 background task per issue (1 issue = 1 task)", status: "pending", priority: "high" },
|
||||
{ id: "4", content: "Stream-process results as each task completes", status: "pending", priority: "high" },
|
||||
{ id: "5", content: "Generate final comprehensive report", status: "pending", priority: "high" }
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# PHASE 1: Issue Collection (EXHAUSTIVE Pagination)
|
||||
|
||||
### 1.1 Use Bundled Script (MANDATORY)
|
||||
|
||||
```bash
|
||||
# Default: last 48 hours
|
||||
./scripts/gh_fetch.py issues --hours 48 --output json
|
||||
|
||||
# Custom time range
|
||||
./scripts/gh_fetch.py issues --hours 72 --output json
|
||||
```
|
||||
|
||||
### 1.2 Fallback: Manual Pagination
|
||||
|
||||
```bash
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
TIME_RANGE=48
|
||||
CUTOFF_DATE=$(date -v-${TIME_RANGE}H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -d "${TIME_RANGE} hours ago" -Iseconds)
|
||||
|
||||
gh issue list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author | \
|
||||
jq --arg cutoff "$CUTOFF_DATE" '[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]'
|
||||
# Continue pagination if 500 returned...
|
||||
```
|
||||
|
||||
**AFTER Phase 1:** Update todo status.
|
||||
|
||||
---
|
||||
|
||||
# PHASE 2: PR Collection (For Bug Correlation)
|
||||
|
||||
```bash
|
||||
./scripts/gh_fetch.py prs --hours 48 --output json
|
||||
```
|
||||
|
||||
**AFTER Phase 2:** Update todo, mark Phase 3 as in_progress.
|
||||
|
||||
---
|
||||
|
||||
# PHASE 3: LAUNCH 1 BACKGROUND TASK PER ISSUE
|
||||
|
||||
## THE 1-ISSUE-1-TASK PATTERN (MANDATORY)
|
||||
|
||||
**CRITICAL: DO NOT BATCH MULTIPLE ISSUES INTO ONE TASK**
|
||||
|
||||
```typescript
|
||||
// Collection for tracking
|
||||
const taskMap = new Map() // issueNumber -> taskId
|
||||
|
||||
// Category ratio: unspecified-low : writing : quick = 1:2:1
|
||||
// Every 4 issues: 1 unspecified-low, 2 writing, 1 quick
|
||||
function getCategory(index, issue) {
|
||||
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 issue
|
||||
for (let i = 0; i < allIssues.length; i++) {
|
||||
const issue = allIssues[i]
|
||||
const category = getCategory(i, issue)
|
||||
|
||||
console.log(`🚀 Launching background task for Issue #${issue.number} (${category})...`)
|
||||
|
||||
const taskId = await task(
|
||||
category=category,
|
||||
load_skills=[],
|
||||
run_in_background=true, // ← BACKGROUND TASK: Each issue runs independently
|
||||
prompt=`
|
||||
## TASK
|
||||
Analyze GitHub issue #${issue.number} for ${REPO}.
|
||||
|
||||
## ISSUE DATA
|
||||
- Number: #${issue.number}
|
||||
- Title: ${issue.title}
|
||||
- State: ${issue.state}
|
||||
- Author: ${issue.author.login}
|
||||
- Created: ${issue.createdAt}
|
||||
- Updated: ${issue.updatedAt}
|
||||
- Labels: ${issue.labels.map(l => l.name).join(', ')}
|
||||
|
||||
## ISSUE BODY
|
||||
${issue.body}
|
||||
|
||||
## FETCH COMMENTS
|
||||
Use: gh issue view ${issue.number} --repo ${REPO} --json comments
|
||||
|
||||
## PR CORRELATION (Check these for fixes)
|
||||
${PR_LIST.slice(0, 10).map(pr => `- PR #${pr.number}: ${pr.title}`).join('\n')}
|
||||
|
||||
## ANALYSIS CHECKLIST
|
||||
1. **TYPE**: BUG | QUESTION | FEATURE | INVALID
|
||||
2. **PROJECT_VALID**: Is this relevant to OUR project? (YES/NO/UNCLEAR)
|
||||
3. **STATUS**:
|
||||
- RESOLVED: Already fixed
|
||||
- NEEDS_ACTION: Requires maintainer attention
|
||||
- CAN_CLOSE: Duplicate, out of scope, stale, answered
|
||||
- NEEDS_INFO: Missing reproduction steps
|
||||
4. **COMMUNITY_RESPONSE**: NONE | HELPFUL | WAITING
|
||||
5. **LINKED_PR**: PR # that might fix this (or NONE)
|
||||
6. **CRITICAL**: Is this a blocking bug/security issue? (YES/NO)
|
||||
|
||||
## RETURN FORMAT (STRICT)
|
||||
\`\`\`
|
||||
ISSUE: #${issue.number}
|
||||
TITLE: ${issue.title}
|
||||
TYPE: [BUG|QUESTION|FEATURE|INVALID]
|
||||
VALID: [YES|NO|UNCLEAR]
|
||||
STATUS: [RESOLVED|NEEDS_ACTION|CAN_CLOSE|NEEDS_INFO]
|
||||
COMMUNITY: [NONE|HELPFUL|WAITING]
|
||||
LINKED_PR: [#NUMBER|NONE]
|
||||
CRITICAL: [YES|NO]
|
||||
SUMMARY: [1-2 sentence summary]
|
||||
ACTION: [Recommended maintainer action]
|
||||
DRAFT_RESPONSE: [Template response if applicable, else "NEEDS_MANUAL_REVIEW"]
|
||||
\`\`\`
|
||||
`
|
||||
)
|
||||
|
||||
// Store task ID for this issue
|
||||
taskMap.set(issue.number, taskId)
|
||||
}
|
||||
|
||||
console.log(`\n✅ Launched ${taskMap.size} background tasks (1 per issue)`)
|
||||
```
|
||||
|
||||
**AFTER Phase 3:** Update todo, mark Phase 4 as in_progress.
|
||||
|
||||
---
|
||||
|
||||
# PHASE 4: STREAM RESULTS AS EACH TASK COMPLETES
|
||||
|
||||
## REAL-TIME STREAMING COLLECTION
|
||||
|
||||
```typescript
|
||||
const results = []
|
||||
const critical = []
|
||||
const closeImmediately = []
|
||||
const autoRespond = []
|
||||
const needsInvestigation = []
|
||||
const featureBacklog = []
|
||||
const needsInfo = []
|
||||
|
||||
const completedIssues = new Set()
|
||||
const totalIssues = taskMap.size
|
||||
|
||||
console.log(`\n📊 Streaming results for ${totalIssues} issues...`)
|
||||
|
||||
// Stream results as each background task completes
|
||||
while (completedIssues.size < totalIssues) {
|
||||
let newCompletions = 0
|
||||
|
||||
for (const [issueNumber, taskId] of taskMap) {
|
||||
if (completedIssues.has(issueNumber)) 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)
|
||||
completedIssues.add(issueNumber)
|
||||
newCompletions++
|
||||
|
||||
// REAL-TIME STREAMING REPORT
|
||||
console.log(`\n🔄 Issue #${issueNumber}: ${analysis.TITLE.substring(0, 60)}...`)
|
||||
|
||||
// Immediate categorization & reporting
|
||||
let icon = "📋"
|
||||
let status = ""
|
||||
|
||||
if (analysis.CRITICAL === 'YES') {
|
||||
critical.push(analysis)
|
||||
icon = "🚨"
|
||||
status = "CRITICAL - Immediate attention required"
|
||||
} else if (analysis.STATUS === 'CAN_CLOSE') {
|
||||
closeImmediately.push(analysis)
|
||||
icon = "⚠️"
|
||||
status = "Can be closed"
|
||||
} else if (analysis.STATUS === 'RESOLVED') {
|
||||
closeImmediately.push(analysis)
|
||||
icon = "✅"
|
||||
status = "Resolved - can close"
|
||||
} else if (analysis.DRAFT_RESPONSE !== 'NEEDS_MANUAL_REVIEW') {
|
||||
autoRespond.push(analysis)
|
||||
icon = "💬"
|
||||
status = "Auto-response available"
|
||||
} else if (analysis.TYPE === 'FEATURE') {
|
||||
featureBacklog.push(analysis)
|
||||
icon = "💡"
|
||||
status = "Feature request"
|
||||
} else if (analysis.STATUS === 'NEEDS_INFO') {
|
||||
needsInfo.push(analysis)
|
||||
icon = "❓"
|
||||
status = "Needs more info"
|
||||
} else if (analysis.TYPE === 'BUG') {
|
||||
needsInvestigation.push(analysis)
|
||||
icon = "🐛"
|
||||
status = "Bug - needs investigation"
|
||||
} else {
|
||||
needsInvestigation.push(analysis)
|
||||
icon = "👀"
|
||||
status = "Needs investigation"
|
||||
}
|
||||
|
||||
console.log(` ${icon} ${status}`)
|
||||
console.log(` 📊 Action: ${analysis.ACTION}`)
|
||||
|
||||
// Progress update every 5 completions
|
||||
if (completedIssues.size % 5 === 0) {
|
||||
console.log(`\n📈 PROGRESS: ${completedIssues.size}/${totalIssues} issues analyzed`)
|
||||
console.log(` Critical: ${critical.length} | Close: ${closeImmediately.length} | Auto-Reply: ${autoRespond.length} | Investigate: ${needsInvestigation.length} | Features: ${featureBacklog.length} | Needs Info: ${needsInfo.length}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no new completions, wait briefly before checking again
|
||||
if (newCompletions === 0 && completedIssues.size < totalIssues) {
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ All ${totalIssues} issues analyzed`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# PHASE 5: FINAL COMPREHENSIVE REPORT
|
||||
|
||||
**GENERATE THIS AT THE VERY END - AFTER ALL PROCESSING**
|
||||
|
||||
```markdown
|
||||
# Issue Triage Report - ${REPO}
|
||||
|
||||
**Time Range:** Last ${TIME_RANGE} hours
|
||||
**Generated:** ${new Date().toISOString()}
|
||||
**Total Issues Analyzed:** ${results.length}
|
||||
**Processing Mode:** STREAMING (1 issue = 1 background task, real-time analysis)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Summary
|
||||
|
||||
| Category | Count | Priority |
|
||||
|----------|-------|----------|
|
||||
| 🚨 CRITICAL | ${critical.length} | IMMEDIATE |
|
||||
| ⚠️ Close Immediately | ${closeImmediately.length} | Today |
|
||||
| 💬 Auto-Respond | ${autoRespond.length} | Today |
|
||||
| 🐛 Needs Investigation | ${needsInvestigation.length} | This Week |
|
||||
| 💡 Feature Backlog | ${featureBacklog.length} | Backlog |
|
||||
| ❓ Needs Info | ${needsInfo.length} | Awaiting User |
|
||||
|
||||
---
|
||||
|
||||
## 🚨 CRITICAL (Immediate Action Required)
|
||||
|
||||
${critical.map(i => `| #${i.ISSUE} | ${i.TITLE.substring(0, 50)}... | ${i.TYPE} |`).join('\n')}
|
||||
|
||||
**Action:** These require immediate maintainer attention.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Close Immediately
|
||||
|
||||
${closeImmediately.map(i => `| #${i.ISSUE} | ${i.TITLE.substring(0, 50)}... | ${i.STATUS} |`).join('\n')}
|
||||
|
||||
---
|
||||
|
||||
## 💬 Auto-Respond (Template Ready)
|
||||
|
||||
${autoRespond.map(i => `| #${i.ISSUE} | ${i.TITLE.substring(0, 40)}... |`).join('\n')}
|
||||
|
||||
**Draft Responses:**
|
||||
${autoRespond.map(i => `### #${i.ISSUE}\n${i.DRAFT_RESPONSE}\n`).join('\n---\n')}
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Needs Investigation
|
||||
|
||||
${needsInvestigation.map(i => `| #${i.ISSUE} | ${i.TITLE.substring(0, 50)}... | ${i.TYPE} |`).join('\n')}
|
||||
|
||||
---
|
||||
|
||||
## 💡 Feature Backlog
|
||||
|
||||
${featureBacklog.map(i => `| #${i.ISSUE} | ${i.TITLE.substring(0, 50)}... |`).join('\n')}
|
||||
|
||||
---
|
||||
|
||||
## ❓ Needs More Info
|
||||
|
||||
${needsInfo.map(i => `| #${i.ISSUE} | ${i.TITLE.substring(0, 50)}... |`).join('\n')}
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Immediate Actions
|
||||
|
||||
1. **CRITICAL:** ${critical.length} issues need immediate attention
|
||||
2. **CLOSE:** ${closeImmediately.length} issues can be closed now
|
||||
3. **REPLY:** ${autoRespond.length} issues have draft responses ready
|
||||
4. **INVESTIGATE:** ${needsInvestigation.length} bugs need debugging
|
||||
|
||||
---
|
||||
|
||||
## Processing Log
|
||||
|
||||
${results.map((r, i) => `${i+1}. #${r.ISSUE}: ${r.TYPE} (${r.CRITICAL === 'YES' ? 'CRITICAL' : r.STATUS})`).join('\n')}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL ANTI-PATTERNS (BLOCKING VIOLATIONS)
|
||||
|
||||
| Violation | Why It's Wrong | Severity |
|
||||
|-----------|----------------|----------|
|
||||
| **Batch multiple issues in one task** | Violates 1 issue = 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 issues with exhaustive pagination
|
||||
- [ ] Fetched PRs for correlation
|
||||
- [ ] **LAUNCHED**: 1 background task per issue (`run_in_background=true`)
|
||||
- [ ] **STREAMED**: Results via `background_output()` as each task completes
|
||||
- [ ] Showed live progress every 5 issues
|
||||
- [ ] Real-time categorization visible to user
|
||||
- [ ] Critical issues flagged immediately
|
||||
- [ ] **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. Parse time range (default: 48 hours)
|
||||
4. Exhaustive pagination for issues
|
||||
5. Exhaustive pagination for PRs
|
||||
6. **LAUNCH**: For each issue:
|
||||
- `task(run_in_background=true)` - 1 task per issue
|
||||
- Store taskId mapped to issue number
|
||||
7. **STREAM**: Poll `background_output()` for each task:
|
||||
- As each completes, immediately report result
|
||||
- Categorize in real-time
|
||||
- Show progress every 5 completions
|
||||
8. **GENERATE FINAL COMPREHENSIVE REPORT**
|
||||
@@ -1,484 +0,0 @@
|
||||
---
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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.**
|
||||
|
||||
```typescript
|
||||
// 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)
|
||||
|
||||
```bash
|
||||
./scripts/gh_fetch.py prs --output json
|
||||
```
|
||||
|
||||
### 1.2 Fallback: Manual Pagination
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
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)**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```markdown
|
||||
# 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**
|
||||
@@ -1,373 +0,0 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "typer>=0.12.0",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
GitHub Issues/PRs Fetcher with Exhaustive Pagination.
|
||||
|
||||
Fetches ALL issues and/or PRs from a GitHub repository using gh CLI.
|
||||
Implements proper pagination to ensure no items are missed.
|
||||
|
||||
Usage:
|
||||
./gh_fetch.py issues # Fetch all issues
|
||||
./gh_fetch.py prs # Fetch all PRs
|
||||
./gh_fetch.py all # Fetch both issues and PRs
|
||||
./gh_fetch.py issues --hours 48 # Issues from last 48 hours
|
||||
./gh_fetch.py prs --state open # Only open PRs
|
||||
./gh_fetch.py all --repo owner/repo # Specify repository
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, TaskID
|
||||
from rich.table import Table
|
||||
|
||||
app = typer.Typer(
|
||||
name="gh_fetch",
|
||||
help="Fetch GitHub issues/PRs with exhaustive pagination.",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
console = Console()
|
||||
|
||||
BATCH_SIZE = 500 # Maximum allowed by GitHub API
|
||||
|
||||
|
||||
class ItemState(str, Enum):
|
||||
ALL = "all"
|
||||
OPEN = "open"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class OutputFormat(str, Enum):
|
||||
JSON = "json"
|
||||
TABLE = "table"
|
||||
COUNT = "count"
|
||||
|
||||
|
||||
async def run_gh_command(args: list[str]) -> tuple[str, str, int]:
|
||||
"""Run gh CLI command asynchronously."""
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"gh",
|
||||
*args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
return stdout.decode(), stderr.decode(), proc.returncode or 0
|
||||
|
||||
|
||||
async def get_current_repo() -> str:
|
||||
"""Get the current repository from gh CLI."""
|
||||
stdout, stderr, code = await run_gh_command(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"])
|
||||
if code != 0:
|
||||
console.print(f"[red]Error getting current repo: {stderr}[/red]")
|
||||
raise typer.Exit(1)
|
||||
return stdout.strip()
|
||||
|
||||
|
||||
async def fetch_items_page(
|
||||
repo: str,
|
||||
item_type: str, # "issue" or "pr"
|
||||
state: str,
|
||||
limit: int,
|
||||
search_filter: str = "",
|
||||
) -> list[dict]:
|
||||
"""Fetch a single page of issues or PRs."""
|
||||
cmd = [
|
||||
item_type,
|
||||
"list",
|
||||
"--repo",
|
||||
repo,
|
||||
"--state",
|
||||
state,
|
||||
"--limit",
|
||||
str(limit),
|
||||
"--json",
|
||||
"number,title,state,createdAt,updatedAt,labels,author,body",
|
||||
]
|
||||
if search_filter:
|
||||
cmd.extend(["--search", search_filter])
|
||||
|
||||
stdout, stderr, code = await run_gh_command(cmd)
|
||||
if code != 0:
|
||||
console.print(f"[red]Error fetching {item_type}s: {stderr}[/red]")
|
||||
return []
|
||||
|
||||
try:
|
||||
return json.loads(stdout) if stdout.strip() else []
|
||||
except json.JSONDecodeError:
|
||||
console.print(f"[red]Error parsing {item_type} response[/red]")
|
||||
return []
|
||||
|
||||
|
||||
async def fetch_all_items(
|
||||
repo: str,
|
||||
item_type: str,
|
||||
state: str,
|
||||
hours: int | None,
|
||||
progress: Progress,
|
||||
task_id: TaskID,
|
||||
) -> list[dict]:
|
||||
"""Fetch ALL items with exhaustive pagination."""
|
||||
all_items: list[dict] = []
|
||||
page = 1
|
||||
|
||||
# First fetch
|
||||
progress.update(task_id, description=f"[cyan]Fetching {item_type}s page {page}...")
|
||||
items = await fetch_items_page(repo, item_type, state, BATCH_SIZE)
|
||||
fetched_count = len(items)
|
||||
all_items.extend(items)
|
||||
|
||||
console.print(f"[dim]Page {page}: fetched {fetched_count} {item_type}s[/dim]")
|
||||
|
||||
# Continue pagination if we got exactly BATCH_SIZE (more pages exist)
|
||||
while fetched_count == BATCH_SIZE:
|
||||
page += 1
|
||||
progress.update(task_id, description=f"[cyan]Fetching {item_type}s page {page}...")
|
||||
|
||||
# Use created date of last item to paginate
|
||||
last_created = all_items[-1].get("createdAt", "")
|
||||
if not last_created:
|
||||
break
|
||||
|
||||
search_filter = f"created:<{last_created}"
|
||||
items = await fetch_items_page(repo, item_type, state, BATCH_SIZE, search_filter)
|
||||
fetched_count = len(items)
|
||||
|
||||
if fetched_count == 0:
|
||||
break
|
||||
|
||||
# Deduplicate by number
|
||||
existing_numbers = {item["number"] for item in all_items}
|
||||
new_items = [item for item in items if item["number"] not in existing_numbers]
|
||||
all_items.extend(new_items)
|
||||
|
||||
console.print(
|
||||
f"[dim]Page {page}: fetched {fetched_count}, added {len(new_items)} new (total: {len(all_items)})[/dim]"
|
||||
)
|
||||
|
||||
# Safety limit
|
||||
if page > 20:
|
||||
console.print("[yellow]Safety limit reached (20 pages)[/yellow]")
|
||||
break
|
||||
|
||||
# Filter by time if specified
|
||||
if hours is not None:
|
||||
cutoff = datetime.now(UTC) - timedelta(hours=hours)
|
||||
cutoff_str = cutoff.isoformat()
|
||||
|
||||
original_count = len(all_items)
|
||||
all_items = [
|
||||
item
|
||||
for item in all_items
|
||||
if item.get("createdAt", "") >= cutoff_str or item.get("updatedAt", "") >= cutoff_str
|
||||
]
|
||||
filtered_count = original_count - len(all_items)
|
||||
if filtered_count > 0:
|
||||
console.print(f"[dim]Filtered out {filtered_count} items older than {hours} hours[/dim]")
|
||||
|
||||
return all_items
|
||||
|
||||
|
||||
def display_table(items: list[dict], item_type: str) -> None:
|
||||
"""Display items in a Rich table."""
|
||||
table = Table(title=f"{item_type.upper()}s ({len(items)} total)")
|
||||
table.add_column("#", style="cyan", width=6)
|
||||
table.add_column("Title", style="white", max_width=50)
|
||||
table.add_column("State", style="green", width=8)
|
||||
table.add_column("Author", style="yellow", width=15)
|
||||
table.add_column("Labels", style="magenta", max_width=30)
|
||||
table.add_column("Updated", style="dim", width=12)
|
||||
|
||||
for item in items[:50]: # Show first 50
|
||||
labels = ", ".join(label.get("name", "") for label in item.get("labels", []))
|
||||
updated = item.get("updatedAt", "")[:10]
|
||||
author = item.get("author", {}).get("login", "unknown")
|
||||
|
||||
table.add_row(
|
||||
str(item.get("number", "")),
|
||||
(item.get("title", "")[:47] + "...") if len(item.get("title", "")) > 50 else item.get("title", ""),
|
||||
item.get("state", ""),
|
||||
author,
|
||||
(labels[:27] + "...") if len(labels) > 30 else labels,
|
||||
updated,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
if len(items) > 50:
|
||||
console.print(f"[dim]... and {len(items) - 50} more items[/dim]")
|
||||
|
||||
|
||||
@app.command()
|
||||
def issues(
|
||||
repo: Annotated[str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")] = None,
|
||||
state: Annotated[ItemState, typer.Option("--state", "-s", help="Issue state filter")] = ItemState.ALL,
|
||||
hours: Annotated[
|
||||
int | None,
|
||||
typer.Option("--hours", "-h", help="Only issues from last N hours (created or updated)"),
|
||||
] = None,
|
||||
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
||||
) -> None:
|
||||
"""Fetch all issues with exhaustive pagination."""
|
||||
|
||||
async def async_main() -> None:
|
||||
target_repo = repo or await get_current_repo()
|
||||
|
||||
console.print(f"""
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
[cyan]Repository:[/cyan] {target_repo}
|
||||
[cyan]State:[/cyan] {state.value}
|
||||
[cyan]Time filter:[/cyan] {f"Last {hours} hours" if hours else "All time"}
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
""")
|
||||
|
||||
with Progress(console=console) as progress:
|
||||
task: TaskID = progress.add_task("[cyan]Fetching issues...", total=None)
|
||||
|
||||
items = await fetch_all_items(target_repo, "issue", state.value, hours, progress, task)
|
||||
|
||||
progress.update(task, description="[green]Complete!", completed=100, total=100)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓ Found {len(items)} issues[/green]",
|
||||
title="[green]Pagination Complete[/green]",
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
|
||||
if output == OutputFormat.JSON:
|
||||
console.print(json.dumps(items, indent=2, ensure_ascii=False))
|
||||
elif output == OutputFormat.TABLE:
|
||||
display_table(items, "issue")
|
||||
else: # COUNT
|
||||
console.print(f"Total issues: {len(items)}")
|
||||
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
@app.command()
|
||||
def prs(
|
||||
repo: Annotated[str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")] = None,
|
||||
state: Annotated[ItemState, typer.Option("--state", "-s", help="PR state filter")] = ItemState.OPEN,
|
||||
hours: Annotated[
|
||||
int | None,
|
||||
typer.Option("--hours", "-h", help="Only PRs from last N hours (created or updated)"),
|
||||
] = None,
|
||||
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
||||
) -> None:
|
||||
"""Fetch all PRs with exhaustive pagination."""
|
||||
|
||||
async def async_main() -> None:
|
||||
target_repo = repo or await get_current_repo()
|
||||
|
||||
console.print(f"""
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
[cyan]Repository:[/cyan] {target_repo}
|
||||
[cyan]State:[/cyan] {state.value}
|
||||
[cyan]Time filter:[/cyan] {f"Last {hours} hours" if hours else "All time"}
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
""")
|
||||
|
||||
with Progress(console=console) as progress:
|
||||
task: TaskID = progress.add_task("[cyan]Fetching PRs...", total=None)
|
||||
|
||||
items = await fetch_all_items(target_repo, "pr", state.value, hours, progress, task)
|
||||
|
||||
progress.update(task, description="[green]Complete!", completed=100, total=100)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓ Found {len(items)} PRs[/green]",
|
||||
title="[green]Pagination Complete[/green]",
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
|
||||
if output == OutputFormat.JSON:
|
||||
console.print(json.dumps(items, indent=2, ensure_ascii=False))
|
||||
elif output == OutputFormat.TABLE:
|
||||
display_table(items, "pr")
|
||||
else: # COUNT
|
||||
console.print(f"Total PRs: {len(items)}")
|
||||
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
@app.command(name="all")
|
||||
def fetch_all(
|
||||
repo: Annotated[str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")] = None,
|
||||
state: Annotated[ItemState, typer.Option("--state", "-s", help="State filter")] = ItemState.ALL,
|
||||
hours: Annotated[
|
||||
int | None,
|
||||
typer.Option("--hours", "-h", help="Only items from last N hours (created or updated)"),
|
||||
] = None,
|
||||
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
||||
) -> None:
|
||||
"""Fetch all issues AND PRs with exhaustive pagination."""
|
||||
|
||||
async def async_main() -> None:
|
||||
target_repo = repo or await get_current_repo()
|
||||
|
||||
console.print(f"""
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
[cyan]Repository:[/cyan] {target_repo}
|
||||
[cyan]State:[/cyan] {state.value}
|
||||
[cyan]Time filter:[/cyan] {f"Last {hours} hours" if hours else "All time"}
|
||||
[cyan]Fetching:[/cyan] Issues AND PRs
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
""")
|
||||
|
||||
with Progress(console=console) as progress:
|
||||
issues_task: TaskID = progress.add_task("[cyan]Fetching issues...", total=None)
|
||||
prs_task: TaskID = progress.add_task("[cyan]Fetching PRs...", total=None)
|
||||
|
||||
# Fetch in parallel
|
||||
issues_items, prs_items = await asyncio.gather(
|
||||
fetch_all_items(target_repo, "issue", state.value, hours, progress, issues_task),
|
||||
fetch_all_items(target_repo, "pr", state.value, hours, progress, prs_task),
|
||||
)
|
||||
|
||||
progress.update(
|
||||
issues_task,
|
||||
description="[green]Issues complete!",
|
||||
completed=100,
|
||||
total=100,
|
||||
)
|
||||
progress.update(prs_task, description="[green]PRs complete!", completed=100, total=100)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓ Found {len(issues_items)} issues and {len(prs_items)} PRs[/green]",
|
||||
title="[green]Pagination Complete[/green]",
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
|
||||
if output == OutputFormat.JSON:
|
||||
result = {"issues": issues_items, "prs": prs_items}
|
||||
console.print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
elif output == OutputFormat.TABLE:
|
||||
display_table(issues_items, "issue")
|
||||
console.print("")
|
||||
display_table(prs_items, "pr")
|
||||
else: # COUNT
|
||||
console.print(f"Total issues: {len(issues_items)}")
|
||||
console.print(f"Total PRs: {len(prs_items)}")
|
||||
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
482
.opencode/skills/github-triage/SKILL.md
Normal file
482
.opencode/skills/github-triage/SKILL.md
Normal file
@@ -0,0 +1,482 @@
|
||||
---
|
||||
name: github-triage
|
||||
description: "Unified GitHub triage for issues AND PRs. 1 item = 1 background task (category: free). Issues: answer questions from codebase, analyze bugs. PRs: review bugfixes, merge safe ones. All parallel, all background. Triggers: 'triage', 'triage issues', 'triage PRs', 'github triage'."
|
||||
---
|
||||
|
||||
# GitHub Triage — Unified Issue & PR Processor
|
||||
|
||||
<role>
|
||||
You are a GitHub triage orchestrator. You fetch all open issues and PRs, classify each one, then spawn exactly 1 background subagent per item using `category="free"`. Each subagent analyzes its item, takes action (comment/close/merge/report), and records results via TaskCreate.
|
||||
</role>
|
||||
|
||||
---
|
||||
|
||||
## ARCHITECTURE
|
||||
|
||||
```
|
||||
1 issue or PR = 1 TaskCreate = 1 task(category="free", run_in_background=true)
|
||||
```
|
||||
|
||||
| Rule | Value |
|
||||
|------|-------|
|
||||
| Category for ALL subagents | `free` |
|
||||
| Execution mode | `run_in_background=true` |
|
||||
| Parallelism | ALL items launched simultaneously |
|
||||
| Result tracking | Each subagent calls `TaskCreate` with its findings |
|
||||
| Result collection | `background_output()` polling loop |
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: FETCH ALL OPEN ITEMS
|
||||
|
||||
<fetch>
|
||||
Run these commands to collect data. Use the bundled script if available, otherwise fall back to gh CLI.
|
||||
|
||||
```bash
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
# Issues: all open
|
||||
gh issue list --repo $REPO --state open --limit 500 \
|
||||
--json number,title,state,createdAt,updatedAt,labels,author,body,comments
|
||||
|
||||
# PRs: all open
|
||||
gh pr list --repo $REPO --state open --limit 500 \
|
||||
--json number,title,state,createdAt,updatedAt,labels,author,body,headRefName,baseRefName,isDraft,mergeable,reviewDecision,statusCheckRollup
|
||||
```
|
||||
|
||||
If either returns exactly 500 results, paginate using `--search "created:<LAST_CREATED_AT"` until exhausted.
|
||||
</fetch>
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: CLASSIFY EACH ITEM
|
||||
|
||||
For each item, determine its type based on title, labels, and body content:
|
||||
|
||||
<classification>
|
||||
|
||||
### Issues
|
||||
|
||||
| Type | Detection | Action Path |
|
||||
|------|-----------|-------------|
|
||||
| `ISSUE_QUESTION` | Title contains `[Question]`, `[Discussion]`, `?`, or body is asking "how to" / "why does" / "is it possible" | SUBAGENT_ISSUE_QUESTION |
|
||||
| `ISSUE_BUG` | Title contains `[Bug]`, `Bug:`, body describes unexpected behavior, error messages, stack traces | SUBAGENT_ISSUE_BUG |
|
||||
| `ISSUE_FEATURE` | Title contains `[Feature]`, `[RFE]`, `[Enhancement]`, `Feature Request`, `Proposal` | SUBAGENT_ISSUE_FEATURE |
|
||||
| `ISSUE_OTHER` | Anything else | SUBAGENT_ISSUE_OTHER |
|
||||
|
||||
### PRs
|
||||
|
||||
| Type | Detection | Action Path |
|
||||
|------|-----------|-------------|
|
||||
| `PR_BUGFIX` | Title starts with `fix`, `fix:`, `fix(`, branch contains `fix/`, `bugfix/`, or labels include `bug` | SUBAGENT_PR_BUGFIX |
|
||||
| `PR_OTHER` | Everything else (feat, refactor, docs, chore, etc.) | SUBAGENT_PR_OTHER |
|
||||
|
||||
</classification>
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: SPAWN 1 BACKGROUND TASK PER ITEM
|
||||
|
||||
For EVERY item, create a TaskCreate entry first, then spawn a background task.
|
||||
|
||||
```
|
||||
For each item:
|
||||
1. TaskCreate(subject="Triage: #{number} {title}")
|
||||
2. task(category="free", run_in_background=true, load_skills=[], prompt=SUBAGENT_PROMPT)
|
||||
3. Store mapping: item_number -> { task_id, background_task_id }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SUBAGENT PROMPT TEMPLATES
|
||||
|
||||
Each subagent gets an explicit, step-by-step prompt. Free models are limited — leave NOTHING implicit.
|
||||
|
||||
---
|
||||
|
||||
### SUBAGENT_ISSUE_QUESTION
|
||||
|
||||
<issue_question_prompt>
|
||||
|
||||
```
|
||||
You are a GitHub issue responder for the repository {REPO}.
|
||||
|
||||
ITEM:
|
||||
- Issue #{number}: {title}
|
||||
- Author: {author}
|
||||
- Body: {body}
|
||||
- Comments: {comments_summary}
|
||||
|
||||
YOUR JOB:
|
||||
1. Read the issue carefully. Understand what the user is asking.
|
||||
2. Search the codebase to find the answer. Use Grep and Read tools.
|
||||
- Search for relevant file names, function names, config keys mentioned in the issue.
|
||||
- Read the files you find to understand how the feature works.
|
||||
3. Decide: Can you answer this clearly and accurately from the codebase?
|
||||
|
||||
IF YES (you found a clear, accurate answer):
|
||||
Step A: Write a helpful comment. The comment MUST:
|
||||
- Start with exactly: [sisyphus-bot]
|
||||
- Be warm, friendly, and thorough
|
||||
- Include specific file paths and code references
|
||||
- Include code snippets or config examples if helpful
|
||||
- End with "Feel free to reopen if this doesn't resolve your question!"
|
||||
Step B: Post the comment:
|
||||
gh issue comment {number} --repo {REPO} --body "YOUR_COMMENT"
|
||||
Step C: Close the issue:
|
||||
gh issue close {number} --repo {REPO}
|
||||
Step D: Report back with this EXACT format:
|
||||
ACTION: ANSWERED_AND_CLOSED
|
||||
COMMENT_POSTED: yes
|
||||
SUMMARY: [1-2 sentence summary of your answer]
|
||||
|
||||
IF NO (not enough info in codebase, or answer is uncertain):
|
||||
Report back with:
|
||||
ACTION: NEEDS_MANUAL_ATTENTION
|
||||
REASON: [why you couldn't answer — be specific]
|
||||
PARTIAL_FINDINGS: [what you DID find, if anything]
|
||||
|
||||
RULES:
|
||||
- NEVER guess. Only answer if the codebase clearly supports your answer.
|
||||
- NEVER make up file paths or function names.
|
||||
- The [sisyphus-bot] prefix is MANDATORY on every comment you post.
|
||||
- Be genuinely helpful — imagine you're a senior maintainer who cares about the community.
|
||||
```
|
||||
|
||||
</issue_question_prompt>
|
||||
|
||||
---
|
||||
|
||||
### SUBAGENT_ISSUE_BUG
|
||||
|
||||
<issue_bug_prompt>
|
||||
|
||||
```
|
||||
You are a GitHub bug analyzer for the repository {REPO}.
|
||||
|
||||
ITEM:
|
||||
- Issue #{number}: {title}
|
||||
- Author: {author}
|
||||
- Body: {body}
|
||||
- Comments: {comments_summary}
|
||||
|
||||
YOUR JOB:
|
||||
1. Read the issue carefully. Understand the reported bug:
|
||||
- What behavior does the user expect?
|
||||
- What behavior do they actually see?
|
||||
- What steps reproduce it?
|
||||
2. Search the codebase for the relevant code. Use Grep and Read tools.
|
||||
- Find the files/functions mentioned or related to the bug.
|
||||
- Read them carefully and trace the logic.
|
||||
3. Determine one of three outcomes:
|
||||
|
||||
OUTCOME A — CONFIRMED BUG (you found the problematic code):
|
||||
Step 1: Post a comment on the issue. The comment MUST:
|
||||
- Start with exactly: [sisyphus-bot]
|
||||
- Apologize sincerely for the inconvenience ("We're sorry you ran into this issue.")
|
||||
- Briefly acknowledge what the bug is
|
||||
- Say "We've identified the root cause and will work on a fix."
|
||||
- Do NOT reveal internal implementation details unnecessarily
|
||||
Step 2: Post the comment:
|
||||
gh issue comment {number} --repo {REPO} --body "YOUR_COMMENT"
|
||||
Step 3: Report back with:
|
||||
ACTION: CONFIRMED_BUG
|
||||
ROOT_CAUSE: [which file, which function, what goes wrong]
|
||||
FIX_APPROACH: [how to fix it — be specific: "In {file}, line ~{N}, change X to Y because Z"]
|
||||
SEVERITY: [LOW|MEDIUM|HIGH|CRITICAL]
|
||||
AFFECTED_FILES: [list of files that need changes]
|
||||
|
||||
OUTCOME B — NOT A BUG (user misunderstanding, provably correct behavior):
|
||||
ONLY choose this if you can RIGOROUSLY PROVE the behavior is correct.
|
||||
Step 1: Post a comment. The comment MUST:
|
||||
- Start with exactly: [sisyphus-bot]
|
||||
- Be kind and empathetic — never condescending
|
||||
- Explain clearly WHY the current behavior is correct
|
||||
- Include specific code references or documentation links
|
||||
- Offer a workaround or alternative if possible
|
||||
- End with "Please let us know if you have further questions!"
|
||||
Step 2: Post the comment:
|
||||
gh issue comment {number} --repo {REPO} --body "YOUR_COMMENT"
|
||||
Step 3: DO NOT close the issue. Let the user or maintainer decide.
|
||||
Step 4: Report back with:
|
||||
ACTION: NOT_A_BUG
|
||||
EXPLANATION: [why this is correct behavior]
|
||||
PROOF: [specific code reference proving it]
|
||||
|
||||
OUTCOME C — UNCLEAR (can't determine from codebase alone):
|
||||
Report back with:
|
||||
ACTION: NEEDS_INVESTIGATION
|
||||
FINDINGS: [what you found so far]
|
||||
BLOCKERS: [what's preventing you from determining the cause]
|
||||
SUGGESTED_NEXT_STEPS: [what a human should look at]
|
||||
|
||||
RULES:
|
||||
- NEVER guess at root causes. Only report CONFIRMED_BUG if you found the exact problematic code.
|
||||
- NEVER close bug issues yourself. Only comment.
|
||||
- For OUTCOME B (not a bug): you MUST have rigorous proof. If there's ANY doubt, choose OUTCOME C instead.
|
||||
- The [sisyphus-bot] prefix is MANDATORY on every comment.
|
||||
- When apologizing, be genuine. The user took time to report this.
|
||||
```
|
||||
|
||||
</issue_bug_prompt>
|
||||
|
||||
---
|
||||
|
||||
### SUBAGENT_ISSUE_FEATURE
|
||||
|
||||
<issue_feature_prompt>
|
||||
|
||||
```
|
||||
You are a GitHub feature request analyzer for the repository {REPO}.
|
||||
|
||||
ITEM:
|
||||
- Issue #{number}: {title}
|
||||
- Author: {author}
|
||||
- Body: {body}
|
||||
- Comments: {comments_summary}
|
||||
|
||||
YOUR JOB:
|
||||
1. Read the feature request.
|
||||
2. Search the codebase to check if this feature already exists (partially or fully).
|
||||
3. Assess feasibility and alignment with the project.
|
||||
|
||||
Report back with:
|
||||
ACTION: FEATURE_ASSESSED
|
||||
ALREADY_EXISTS: [YES_FULLY | YES_PARTIALLY | NO]
|
||||
IF_EXISTS: [where in the codebase, how to use it]
|
||||
FEASIBILITY: [EASY | MODERATE | HARD | ARCHITECTURAL_CHANGE]
|
||||
RELEVANT_FILES: [files that would need changes]
|
||||
NOTES: [any observations about implementation approach]
|
||||
|
||||
If the feature already fully exists:
|
||||
Post a comment (prefix: [sisyphus-bot]) explaining how to use the existing feature with examples.
|
||||
gh issue comment {number} --repo {REPO} --body "YOUR_COMMENT"
|
||||
|
||||
RULES:
|
||||
- Do NOT close feature requests.
|
||||
- The [sisyphus-bot] prefix is MANDATORY on any comment.
|
||||
```
|
||||
|
||||
</issue_feature_prompt>
|
||||
|
||||
---
|
||||
|
||||
### SUBAGENT_ISSUE_OTHER
|
||||
|
||||
<issue_other_prompt>
|
||||
|
||||
```
|
||||
You are a GitHub issue analyzer for the repository {REPO}.
|
||||
|
||||
ITEM:
|
||||
- Issue #{number}: {title}
|
||||
- Author: {author}
|
||||
- Body: {body}
|
||||
- Comments: {comments_summary}
|
||||
|
||||
YOUR JOB:
|
||||
Quickly assess this issue and report:
|
||||
ACTION: ASSESSED
|
||||
TYPE_GUESS: [QUESTION | BUG | FEATURE | DISCUSSION | META | STALE]
|
||||
SUMMARY: [1-2 sentence summary]
|
||||
NEEDS_ATTENTION: [YES | NO]
|
||||
SUGGESTED_LABEL: [if any]
|
||||
|
||||
Do NOT post comments. Do NOT close. Just analyze and report.
|
||||
```
|
||||
|
||||
</issue_other_prompt>
|
||||
|
||||
---
|
||||
|
||||
### SUBAGENT_PR_BUGFIX
|
||||
|
||||
<pr_bugfix_prompt>
|
||||
|
||||
```
|
||||
You are a GitHub PR reviewer for the repository {REPO}.
|
||||
|
||||
ITEM:
|
||||
- PR #{number}: {title}
|
||||
- Author: {author}
|
||||
- Base: {baseRefName}
|
||||
- Head: {headRefName}
|
||||
- Draft: {isDraft}
|
||||
- Mergeable: {mergeable}
|
||||
- Review Decision: {reviewDecision}
|
||||
- CI Status: {statusCheckRollup_summary}
|
||||
- Body: {body}
|
||||
|
||||
YOUR JOB:
|
||||
1. Fetch PR details (DO NOT checkout the branch — read-only analysis):
|
||||
gh pr view {number} --repo {REPO} --json files,reviews,comments,statusCheckRollup,reviewDecision
|
||||
2. Read the changed files list. For each changed file, use `gh api repos/{REPO}/pulls/{number}/files` to see the diff.
|
||||
3. Search the codebase to understand what the PR is fixing and whether the fix is correct.
|
||||
4. Evaluate merge safety:
|
||||
|
||||
MERGE CONDITIONS (ALL must be true for auto-merge):
|
||||
a. CI status checks: ALL passing (no failures, no pending)
|
||||
b. Review decision: APPROVED
|
||||
c. The fix is clearly correct — addresses an obvious, unambiguous bug
|
||||
d. No risky side effects (no architectural changes, no breaking changes)
|
||||
e. Not a draft PR
|
||||
f. Mergeable state is clean (no conflicts)
|
||||
|
||||
IF ALL MERGE CONDITIONS MET:
|
||||
Step 1: Merge the PR:
|
||||
gh pr merge {number} --repo {REPO} --squash --auto
|
||||
Step 2: Report back with:
|
||||
ACTION: MERGED
|
||||
FIX_SUMMARY: [what bug was fixed and how]
|
||||
FILES_CHANGED: [list of files]
|
||||
RISK: NONE
|
||||
|
||||
IF ANY CONDITION NOT MET:
|
||||
Report back with:
|
||||
ACTION: NEEDS_HUMAN_DECISION
|
||||
FIX_SUMMARY: [what the PR does]
|
||||
WHAT_IT_FIXES: [the bug or issue it addresses]
|
||||
CI_STATUS: [PASS | FAIL | PENDING — list any failures]
|
||||
REVIEW_STATUS: [APPROVED | CHANGES_REQUESTED | PENDING | NONE]
|
||||
MISSING: [what's preventing auto-merge — be specific]
|
||||
RISK_ASSESSMENT: [what could go wrong]
|
||||
AMBIGUOUS_PARTS: [anything that needs human judgment]
|
||||
RECOMMENDED_ACTION: [what the maintainer should do]
|
||||
|
||||
ABSOLUTE RULES:
|
||||
- NEVER run `git checkout`, `git fetch`, `git pull`, or `git switch`. READ-ONLY via gh CLI and API.
|
||||
- NEVER checkout the PR branch. NEVER. Use `gh api` and `gh pr view` only.
|
||||
- Only merge if you are 100% certain ALL conditions are met. When in doubt, report instead.
|
||||
- The [sisyphus-bot] prefix is MANDATORY on any comment you post.
|
||||
```
|
||||
|
||||
</pr_bugfix_prompt>
|
||||
|
||||
---
|
||||
|
||||
### SUBAGENT_PR_OTHER
|
||||
|
||||
<pr_other_prompt>
|
||||
|
||||
```
|
||||
You are a GitHub PR reviewer for the repository {REPO}.
|
||||
|
||||
ITEM:
|
||||
- PR #{number}: {title}
|
||||
- Author: {author}
|
||||
- Base: {baseRefName}
|
||||
- Head: {headRefName}
|
||||
- Draft: {isDraft}
|
||||
- Mergeable: {mergeable}
|
||||
- Review Decision: {reviewDecision}
|
||||
- CI Status: {statusCheckRollup_summary}
|
||||
- Body: {body}
|
||||
|
||||
YOUR JOB:
|
||||
1. Fetch PR details (READ-ONLY — no checkout):
|
||||
gh pr view {number} --repo {REPO} --json files,reviews,comments,statusCheckRollup,reviewDecision
|
||||
2. Read the changed files via `gh api repos/{REPO}/pulls/{number}/files`.
|
||||
3. Assess the PR and report:
|
||||
|
||||
ACTION: PR_ASSESSED
|
||||
TYPE: [FEATURE | REFACTOR | DOCS | CHORE | TEST | OTHER]
|
||||
SUMMARY: [what this PR does in 2-3 sentences]
|
||||
CI_STATUS: [PASS | FAIL | PENDING]
|
||||
REVIEW_STATUS: [APPROVED | CHANGES_REQUESTED | PENDING | NONE]
|
||||
FILES_CHANGED: [count and key files]
|
||||
RISK_LEVEL: [LOW | MEDIUM | HIGH]
|
||||
ALIGNMENT: [does this fit the project direction? YES | NO | UNCLEAR]
|
||||
BLOCKERS: [anything preventing merge]
|
||||
RECOMMENDED_ACTION: [MERGE | REQUEST_CHANGES | NEEDS_REVIEW | CLOSE | WAIT]
|
||||
NOTES: [any observations for the maintainer]
|
||||
|
||||
ABSOLUTE RULES:
|
||||
- NEVER run `git checkout`, `git fetch`, `git pull`, or `git switch`. READ-ONLY.
|
||||
- NEVER checkout the PR branch. Use `gh api` and `gh pr view` only.
|
||||
- Do NOT merge non-bugfix PRs automatically. Report only.
|
||||
```
|
||||
|
||||
</pr_other_prompt>
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: COLLECT RESULTS & UPDATE TASKS
|
||||
|
||||
<collection>
|
||||
Poll `background_output()` for each spawned task. As each completes:
|
||||
|
||||
1. Parse the subagent's report.
|
||||
2. Update the corresponding TaskCreate entry:
|
||||
- `TaskUpdate(id=task_id, status="completed", description=FULL_REPORT_TEXT)`
|
||||
3. Stream the result to the user immediately — do not wait for all to finish.
|
||||
|
||||
Track counters:
|
||||
- issues_answered (commented + closed)
|
||||
- bugs_confirmed
|
||||
- bugs_not_a_bug
|
||||
- prs_merged
|
||||
- prs_needs_decision
|
||||
- features_assessed
|
||||
</collection>
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5: FINAL SUMMARY
|
||||
|
||||
After all background tasks complete, produce a summary:
|
||||
|
||||
```markdown
|
||||
# GitHub Triage Report — {REPO}
|
||||
|
||||
**Date:** {date}
|
||||
**Items Processed:** {total}
|
||||
|
||||
## Issues ({issue_count})
|
||||
| Action | Count |
|
||||
|--------|-------|
|
||||
| Answered & Closed | {issues_answered} |
|
||||
| Bug Confirmed | {bugs_confirmed} |
|
||||
| Not A Bug (explained) | {bugs_not_a_bug} |
|
||||
| Feature Assessed | {features_assessed} |
|
||||
| Needs Manual Attention | {needs_manual} |
|
||||
|
||||
## PRs ({pr_count})
|
||||
| Action | Count |
|
||||
|--------|-------|
|
||||
| Auto-Merged (safe bugfix) | {prs_merged} |
|
||||
| Needs Human Decision | {prs_needs_decision} |
|
||||
| Assessed (non-bugfix) | {prs_assessed} |
|
||||
|
||||
## Items Requiring Your Attention
|
||||
[List each item that needs human decision with its report summary]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
| Violation | Severity |
|
||||
|-----------|----------|
|
||||
| Using any category other than `free` | CRITICAL |
|
||||
| Batching multiple items into one task | CRITICAL |
|
||||
| Using `run_in_background=false` | CRITICAL |
|
||||
| Subagent running `git checkout` on a PR branch | CRITICAL |
|
||||
| Posting comment without `[sisyphus-bot]` prefix | CRITICAL |
|
||||
| Merging a PR that doesn't meet ALL 6 conditions | CRITICAL |
|
||||
| Closing a bug issue (only comment, never close bugs) | HIGH |
|
||||
| Guessing at answers without codebase evidence | HIGH |
|
||||
| Not recording results via TaskCreate/TaskUpdate | HIGH |
|
||||
|
||||
---
|
||||
|
||||
## QUICK START
|
||||
|
||||
When invoked:
|
||||
|
||||
1. `TaskCreate` for the overall triage job
|
||||
2. Fetch all open issues + PRs via gh CLI (paginate if needed)
|
||||
3. Classify each item (ISSUE_QUESTION, ISSUE_BUG, ISSUE_FEATURE, PR_BUGFIX, etc.)
|
||||
4. For EACH item: `TaskCreate` + `task(category="free", run_in_background=true, load_skills=[], prompt=...)`
|
||||
5. Poll `background_output()` — stream results as they arrive
|
||||
6. `TaskUpdate` each task with the subagent's findings
|
||||
7. Produce final summary report
|
||||
@@ -69,7 +69,9 @@ async def run_gh_command(args: list[str]) -> tuple[str, str, int]:
|
||||
|
||||
async def get_current_repo() -> str:
|
||||
"""Get the current repository from gh CLI."""
|
||||
stdout, stderr, code = await run_gh_command(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"])
|
||||
stdout, stderr, code = await run_gh_command(
|
||||
["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]
|
||||
)
|
||||
if code != 0:
|
||||
console.print(f"[red]Error getting current repo: {stderr}[/red]")
|
||||
raise typer.Exit(1)
|
||||
@@ -123,7 +125,6 @@ async def fetch_all_items(
|
||||
all_items: list[dict] = []
|
||||
page = 1
|
||||
|
||||
# First fetch
|
||||
progress.update(task_id, description=f"[cyan]Fetching {item_type}s page {page}...")
|
||||
items = await fetch_items_page(repo, item_type, state, BATCH_SIZE)
|
||||
fetched_count = len(items)
|
||||
@@ -131,24 +132,25 @@ async def fetch_all_items(
|
||||
|
||||
console.print(f"[dim]Page {page}: fetched {fetched_count} {item_type}s[/dim]")
|
||||
|
||||
# Continue pagination if we got exactly BATCH_SIZE (more pages exist)
|
||||
while fetched_count == BATCH_SIZE:
|
||||
page += 1
|
||||
progress.update(task_id, description=f"[cyan]Fetching {item_type}s page {page}...")
|
||||
progress.update(
|
||||
task_id, description=f"[cyan]Fetching {item_type}s page {page}..."
|
||||
)
|
||||
|
||||
# Use created date of last item to paginate
|
||||
last_created = all_items[-1].get("createdAt", "")
|
||||
if not last_created:
|
||||
break
|
||||
|
||||
search_filter = f"created:<{last_created}"
|
||||
items = await fetch_items_page(repo, item_type, state, BATCH_SIZE, search_filter)
|
||||
items = await fetch_items_page(
|
||||
repo, item_type, state, BATCH_SIZE, search_filter
|
||||
)
|
||||
fetched_count = len(items)
|
||||
|
||||
if fetched_count == 0:
|
||||
break
|
||||
|
||||
# Deduplicate by number
|
||||
existing_numbers = {item["number"] for item in all_items}
|
||||
new_items = [item for item in items if item["number"] not in existing_numbers]
|
||||
all_items.extend(new_items)
|
||||
@@ -157,12 +159,10 @@ async def fetch_all_items(
|
||||
f"[dim]Page {page}: fetched {fetched_count}, added {len(new_items)} new (total: {len(all_items)})[/dim]"
|
||||
)
|
||||
|
||||
# Safety limit
|
||||
if page > 20:
|
||||
console.print("[yellow]Safety limit reached (20 pages)[/yellow]")
|
||||
break
|
||||
|
||||
# Filter by time if specified
|
||||
if hours is not None:
|
||||
cutoff = datetime.now(UTC) - timedelta(hours=hours)
|
||||
cutoff_str = cutoff.isoformat()
|
||||
@@ -171,11 +171,14 @@ async def fetch_all_items(
|
||||
all_items = [
|
||||
item
|
||||
for item in all_items
|
||||
if item.get("createdAt", "") >= cutoff_str or item.get("updatedAt", "") >= cutoff_str
|
||||
if item.get("createdAt", "") >= cutoff_str
|
||||
or item.get("updatedAt", "") >= cutoff_str
|
||||
]
|
||||
filtered_count = original_count - len(all_items)
|
||||
if filtered_count > 0:
|
||||
console.print(f"[dim]Filtered out {filtered_count} items older than {hours} hours[/dim]")
|
||||
console.print(
|
||||
f"[dim]Filtered out {filtered_count} items older than {hours} hours[/dim]"
|
||||
)
|
||||
|
||||
return all_items
|
||||
|
||||
@@ -190,14 +193,16 @@ def display_table(items: list[dict], item_type: str) -> None:
|
||||
table.add_column("Labels", style="magenta", max_width=30)
|
||||
table.add_column("Updated", style="dim", width=12)
|
||||
|
||||
for item in items[:50]: # Show first 50
|
||||
for item in items[:50]:
|
||||
labels = ", ".join(label.get("name", "") for label in item.get("labels", []))
|
||||
updated = item.get("updatedAt", "")[:10]
|
||||
author = item.get("author", {}).get("login", "unknown")
|
||||
|
||||
table.add_row(
|
||||
str(item.get("number", "")),
|
||||
(item.get("title", "")[:47] + "...") if len(item.get("title", "")) > 50 else item.get("title", ""),
|
||||
(item.get("title", "")[:47] + "...")
|
||||
if len(item.get("title", "")) > 50
|
||||
else item.get("title", ""),
|
||||
item.get("state", ""),
|
||||
author,
|
||||
(labels[:27] + "...") if len(labels) > 30 else labels,
|
||||
@@ -211,13 +216,21 @@ def display_table(items: list[dict], item_type: str) -> None:
|
||||
|
||||
@app.command()
|
||||
def issues(
|
||||
repo: Annotated[str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")] = None,
|
||||
state: Annotated[ItemState, typer.Option("--state", "-s", help="Issue state filter")] = ItemState.ALL,
|
||||
repo: Annotated[
|
||||
str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")
|
||||
] = None,
|
||||
state: Annotated[
|
||||
ItemState, typer.Option("--state", "-s", help="Issue state filter")
|
||||
] = ItemState.ALL,
|
||||
hours: Annotated[
|
||||
int | None,
|
||||
typer.Option("--hours", "-h", help="Only issues from last N hours (created or updated)"),
|
||||
typer.Option(
|
||||
"--hours", "-h", help="Only issues from last N hours (created or updated)"
|
||||
),
|
||||
] = None,
|
||||
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
||||
output: Annotated[
|
||||
OutputFormat, typer.Option("--output", "-o", help="Output format")
|
||||
] = OutputFormat.TABLE,
|
||||
) -> None:
|
||||
"""Fetch all issues with exhaustive pagination."""
|
||||
|
||||
@@ -225,33 +238,29 @@ def issues(
|
||||
target_repo = repo or await get_current_repo()
|
||||
|
||||
console.print(f"""
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
[cyan]Repository:[/cyan] {target_repo}
|
||||
[cyan]State:[/cyan] {state.value}
|
||||
[cyan]Time filter:[/cyan] {f"Last {hours} hours" if hours else "All time"}
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
""")
|
||||
|
||||
with Progress(console=console) as progress:
|
||||
task: TaskID = progress.add_task("[cyan]Fetching issues...", total=None)
|
||||
|
||||
items = await fetch_all_items(target_repo, "issue", state.value, hours, progress, task)
|
||||
|
||||
progress.update(task, description="[green]Complete!", completed=100, total=100)
|
||||
items = await fetch_all_items(
|
||||
target_repo, "issue", state.value, hours, progress, task
|
||||
)
|
||||
progress.update(
|
||||
task, description="[green]Complete!", completed=100, total=100
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓ Found {len(items)} issues[/green]",
|
||||
title="[green]Pagination Complete[/green]",
|
||||
border_style="green",
|
||||
)
|
||||
Panel(f"[green]Found {len(items)} issues[/green]", border_style="green")
|
||||
)
|
||||
|
||||
if output == OutputFormat.JSON:
|
||||
console.print(json.dumps(items, indent=2, ensure_ascii=False))
|
||||
elif output == OutputFormat.TABLE:
|
||||
display_table(items, "issue")
|
||||
else: # COUNT
|
||||
else:
|
||||
console.print(f"Total issues: {len(items)}")
|
||||
|
||||
asyncio.run(async_main())
|
||||
@@ -259,13 +268,21 @@ def issues(
|
||||
|
||||
@app.command()
|
||||
def prs(
|
||||
repo: Annotated[str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")] = None,
|
||||
state: Annotated[ItemState, typer.Option("--state", "-s", help="PR state filter")] = ItemState.OPEN,
|
||||
repo: Annotated[
|
||||
str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")
|
||||
] = None,
|
||||
state: Annotated[
|
||||
ItemState, typer.Option("--state", "-s", help="PR state filter")
|
||||
] = ItemState.OPEN,
|
||||
hours: Annotated[
|
||||
int | None,
|
||||
typer.Option("--hours", "-h", help="Only PRs from last N hours (created or updated)"),
|
||||
typer.Option(
|
||||
"--hours", "-h", help="Only PRs from last N hours (created or updated)"
|
||||
),
|
||||
] = None,
|
||||
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
||||
output: Annotated[
|
||||
OutputFormat, typer.Option("--output", "-o", help="Output format")
|
||||
] = OutputFormat.TABLE,
|
||||
) -> None:
|
||||
"""Fetch all PRs with exhaustive pagination."""
|
||||
|
||||
@@ -273,33 +290,29 @@ def prs(
|
||||
target_repo = repo or await get_current_repo()
|
||||
|
||||
console.print(f"""
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
[cyan]Repository:[/cyan] {target_repo}
|
||||
[cyan]State:[/cyan] {state.value}
|
||||
[cyan]Time filter:[/cyan] {f"Last {hours} hours" if hours else "All time"}
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
""")
|
||||
|
||||
with Progress(console=console) as progress:
|
||||
task: TaskID = progress.add_task("[cyan]Fetching PRs...", total=None)
|
||||
|
||||
items = await fetch_all_items(target_repo, "pr", state.value, hours, progress, task)
|
||||
|
||||
progress.update(task, description="[green]Complete!", completed=100, total=100)
|
||||
items = await fetch_all_items(
|
||||
target_repo, "pr", state.value, hours, progress, task
|
||||
)
|
||||
progress.update(
|
||||
task, description="[green]Complete!", completed=100, total=100
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓ Found {len(items)} PRs[/green]",
|
||||
title="[green]Pagination Complete[/green]",
|
||||
border_style="green",
|
||||
)
|
||||
Panel(f"[green]Found {len(items)} PRs[/green]", border_style="green")
|
||||
)
|
||||
|
||||
if output == OutputFormat.JSON:
|
||||
console.print(json.dumps(items, indent=2, ensure_ascii=False))
|
||||
elif output == OutputFormat.TABLE:
|
||||
display_table(items, "pr")
|
||||
else: # COUNT
|
||||
else:
|
||||
console.print(f"Total PRs: {len(items)}")
|
||||
|
||||
asyncio.run(async_main())
|
||||
@@ -307,13 +320,21 @@ def prs(
|
||||
|
||||
@app.command(name="all")
|
||||
def fetch_all(
|
||||
repo: Annotated[str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")] = None,
|
||||
state: Annotated[ItemState, typer.Option("--state", "-s", help="State filter")] = ItemState.ALL,
|
||||
repo: Annotated[
|
||||
str | None, typer.Option("--repo", "-r", help="Repository (owner/repo)")
|
||||
] = None,
|
||||
state: Annotated[
|
||||
ItemState, typer.Option("--state", "-s", help="State filter")
|
||||
] = ItemState.ALL,
|
||||
hours: Annotated[
|
||||
int | None,
|
||||
typer.Option("--hours", "-h", help="Only items from last N hours (created or updated)"),
|
||||
typer.Option(
|
||||
"--hours", "-h", help="Only items from last N hours (created or updated)"
|
||||
),
|
||||
] = None,
|
||||
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
||||
output: Annotated[
|
||||
OutputFormat, typer.Option("--output", "-o", help="Output format")
|
||||
] = OutputFormat.TABLE,
|
||||
) -> None:
|
||||
"""Fetch all issues AND PRs with exhaustive pagination."""
|
||||
|
||||
@@ -321,22 +342,25 @@ def fetch_all(
|
||||
target_repo = repo or await get_current_repo()
|
||||
|
||||
console.print(f"""
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
[cyan]Repository:[/cyan] {target_repo}
|
||||
[cyan]State:[/cyan] {state.value}
|
||||
[cyan]Time filter:[/cyan] {f"Last {hours} hours" if hours else "All time"}
|
||||
[cyan]Fetching:[/cyan] Issues AND PRs
|
||||
[cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/cyan]
|
||||
""")
|
||||
|
||||
with Progress(console=console) as progress:
|
||||
issues_task: TaskID = progress.add_task("[cyan]Fetching issues...", total=None)
|
||||
issues_task: TaskID = progress.add_task(
|
||||
"[cyan]Fetching issues...", total=None
|
||||
)
|
||||
prs_task: TaskID = progress.add_task("[cyan]Fetching PRs...", total=None)
|
||||
|
||||
# Fetch in parallel
|
||||
issues_items, prs_items = await asyncio.gather(
|
||||
fetch_all_items(target_repo, "issue", state.value, hours, progress, issues_task),
|
||||
fetch_all_items(target_repo, "pr", state.value, hours, progress, prs_task),
|
||||
fetch_all_items(
|
||||
target_repo, "issue", state.value, hours, progress, issues_task
|
||||
),
|
||||
fetch_all_items(
|
||||
target_repo, "pr", state.value, hours, progress, prs_task
|
||||
),
|
||||
)
|
||||
|
||||
progress.update(
|
||||
@@ -345,12 +369,13 @@ def fetch_all(
|
||||
completed=100,
|
||||
total=100,
|
||||
)
|
||||
progress.update(prs_task, description="[green]PRs complete!", completed=100, total=100)
|
||||
progress.update(
|
||||
prs_task, description="[green]PRs complete!", completed=100, total=100
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓ Found {len(issues_items)} issues and {len(prs_items)} PRs[/green]",
|
||||
title="[green]Pagination Complete[/green]",
|
||||
f"[green]Found {len(issues_items)} issues and {len(prs_items)} PRs[/green]",
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
@@ -362,7 +387,7 @@ def fetch_all(
|
||||
display_table(issues_items, "issue")
|
||||
console.print("")
|
||||
display_table(prs_items, "pr")
|
||||
else: # COUNT
|
||||
else:
|
||||
console.print(f"Total issues: {len(issues_items)}")
|
||||
console.print(f"Total PRs: {len(prs_items)}")
|
||||
|
||||
Reference in New Issue
Block a user