github-issue-triage
code-yeongyu/oh-my-opencode
Triage GitHub issues with streaming analysis—one background task per issue for real-time results.
What is github-issue-triage?
Automates GitHub issue triage by launching independent background tasks for each issue and streaming results in real-time as they complete. Use this when you need to analyze multiple issues concurrently without waiting for batch processing to finish.
- Fetches all issues within a specified time range using exhaustive pagination
- Launches one dedicated background task per issue for parallel processing
- Streams analysis results in real-time as each task completes
- Correlates issues with pull requests to identify fixes
- Categorizes issues by priority (unspecified-low, writing, quick) in a 1:2:1 ratio
- Generates a final comprehensive triage report
How to install github-issue-triage
npx skills add https://github.com/code-yeongyu/oh-my-opencode --skill github-issue-triage- GitHub CLI (gh) installed and authenticated
- Access to the target repository
- Python 3.x for bundled gh_fetch.py script (optional; manual pagination available as fallback)
How to use github-issue-triage
- 1.Trigger the skill with 'triage issues', 'analyze issues', or 'issue report'
- 2.Specify the time range (default: last 48 hours) or use custom hours parameter
- 3.The skill creates a TODO list tracking five phases: issue fetching, PR fetching, background task launch, streaming, and final report
- 4.Monitor real-time streaming output as each issue's background task completes
- 5.Review the final comprehensive report with categorized issues and recommended actions
Use cases
- Rapidly triage a backlog of 50+ GitHub issues while seeing results stream in real-time
- Identify critical bugs early by processing issues in parallel and reporting urgent findings immediately
- Correlate open issues with recent PRs to flag already-fixed problems
- Analyze issues across multiple time ranges (48h, 72h, custom) without manual pagination
- Generate structured triage reports with issue categorization and recommended actions
- Repository maintainers managing high-volume issue queues
- Open-source project leads needing rapid triage workflows
- DevOps engineers automating issue management pipelines
- Contributors analyzing project health and issue patterns
github-issue-triage FAQ
One task per issue enables true parallelism, isolation (failures don't cascade), and streaming results. This avoids the bottleneck of sequential processing and lets you see critical issues flagged immediately rather than waiting for all analysis to finish.
Processing time depends on issue count and complexity. Background tasks run concurrently, so 50 issues typically complete faster than sequential analysis. Results stream in as each task finishes, so you see progress immediately rather than waiting for a final batch.
Since each issue runs as an independent background task, a failure on one issue does not affect others. The skill continues processing remaining issues and reports which analyses succeeded or failed in the final report.
Yes. The bundled gh_fetch.py script accepts --hours parameter (default 48). You can also specify custom time ranges, and the skill uses exhaustive pagination to ensure no issues are missed.
The comprehensive report categorizes issues by type (BUG, QUESTION, FEATURE, INVALID), project relevance, status (RESOLVED, NEEDS_ACTION, CAN_CLOSE, NEEDS_INFO), and correlates issues with related PRs to identify already-fixed problems.
Full instructions (SKILL.md)
Source of truth, from code-yeongyu/oh-my-opencode.
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:
- Fetch EVERY SINGLE ISSUE within time range using EXHAUSTIVE PAGINATION
- LAUNCH 1 BACKGROUND TASK PER ISSUE - Each issue gets its own dedicated agent
- STREAM RESULTS IN REAL-TIME - As each background task completes, immediately report results
- 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
// 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.
// 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)
# 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
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)
./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
// 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
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
# 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:
- CREATE TODOS
gh repo view --json nameWithOwner -q .nameWithOwner- Parse time range (default: 48 hours)
- Exhaustive pagination for issues
- Exhaustive pagination for PRs
- LAUNCH: For each issue:
task(run_in_background=true)- 1 task per issue- Store taskId mapped to issue number
- STREAM: Poll
background_output()for each task:- As each completes, immediately report result
- Categorize in real-time
- Show progress every 5 completions
- GENERATE FINAL COMPREHENSIVE REPORT
Related skills
More from code-yeongyu/oh-my-opencode and the wider catalog.

github-triage
Read-only GitHub issue and PR analyzer that spawns background tasks for evidence-backed triage reports.
build-tam
Build a Total Addressable Market list by sourcing accounts and contacts from Crustdata, Dropleads, and PDL.
clay-to-deepline
Convert Clay table configurations into local Deepline scripts with extraction, documentation, and parity validation.
deepline-ads-audiences
Use this skill when building, enriching, auditing, or uploading B2B paid ads audiences to Google Customer Match, Meta/Facebook Custom Audiences, or LinkedIn Matched Audiences. Triggers on phrases like '/deepline-ads-audience', '/deepline-ads-audiences', 'upload this audience', 'create custom audiences', 'personal email hashes', 'increase Facebook match rate', 'Google ads audience', 'Meta audience', 'FB audience', or any workflow that turns CRM/customer/contact data into paid ads upload lists. Skip for outbound prospecting sequences, cold email, or pure campaign copywriting.
deepline-analytics
Query Snowflake warehouse and semantic layer for RevOps, pipeline, revenue, and customer analytics.
deepline-feedback
Send feedback and bug reports to Deepline with session transcript and environment details.