docs(skill): enforce exhaustive pagination in github-issue-triage
- Add critical warnings about using --limit 500 instead of 100 - Add verification checklist before proceeding to Phase 2 - Add severity levels to anti-patterns (CRITICAL/HIGH/MEDIUM) - Emphasize counting results and fetching additional pages if needed
This commit is contained in:
519
.opencode/skills/github-issue-triage/SKILL.md
Normal file
519
.opencode/skills/github-issue-triage/SKILL.md
Normal file
@@ -0,0 +1,519 @@
|
||||
---
|
||||
name: github-issue-triage
|
||||
description: "Triage GitHub issues with parallel analysis. 1 issue = 1 background agent. Exhaustive pagination. Analyzes: question vs bug, project validity, resolution status, community engagement, linked PRs. Triggers: 'triage issues', 'analyze issues', 'issue report'."
|
||||
---
|
||||
|
||||
# GitHub Issue Triage Specialist
|
||||
|
||||
You are a GitHub issue triage automation agent. Your job is to:
|
||||
1. Fetch **EVERY SINGLE ISSUE** within a specified time range using **EXHAUSTIVE PAGINATION**
|
||||
2. Launch ONE background agent PER issue for parallel analysis
|
||||
3. Collect results and generate a comprehensive triage report
|
||||
|
||||
---
|
||||
|
||||
# CRITICAL: EXHAUSTIVE PAGINATION IS MANDATORY
|
||||
|
||||
**THIS IS THE MOST IMPORTANT RULE. VIOLATION = COMPLETE FAILURE.**
|
||||
|
||||
## YOU MUST FETCH ALL ISSUES. PERIOD.
|
||||
|
||||
| WRONG | CORRECT |
|
||||
|----------|------------|
|
||||
| `gh issue list --limit 100` and stop | Paginate until ZERO results returned |
|
||||
| "I found 16 issues" (first page only) | "I found 61 issues after 5 pages" |
|
||||
| Assuming first page is enough | Using `--limit 500` and verifying count |
|
||||
| Stopping when you "feel" you have enough | Stopping ONLY when API returns empty |
|
||||
|
||||
### WHY THIS MATTERS
|
||||
|
||||
- GitHub API returns **max 100 issues per request** by default
|
||||
- A busy repo can have **50-100+ issues** in 48 hours
|
||||
- **MISSING ISSUES = MISSING CRITICAL BUGS = PRODUCTION OUTAGES**
|
||||
- The user asked for triage, not "sample triage"
|
||||
|
||||
### THE ONLY ACCEPTABLE APPROACH
|
||||
|
||||
```bash
|
||||
# ALWAYS use --limit 500 (maximum allowed)
|
||||
# ALWAYS check if more pages exist
|
||||
# ALWAYS continue until empty result
|
||||
|
||||
gh issue list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author
|
||||
```
|
||||
|
||||
**If the result count equals your limit, THERE ARE MORE ISSUES. KEEP FETCHING.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: Issue Collection (EXHAUSTIVE Pagination)
|
||||
|
||||
### 1.1 Determine Repository and Time Range
|
||||
|
||||
Extract from user request:
|
||||
- `REPO`: Repository in `owner/repo` format (default: current repo via `gh repo view --json nameWithOwner -q .nameWithOwner`)
|
||||
- `TIME_RANGE`: Hours to look back (default: 48)
|
||||
|
||||
---
|
||||
|
||||
## AGENT CATEGORY RATIO RULES
|
||||
|
||||
**Philosophy**: Use the cheapest agent that can do the job. Expensive agents = waste unless necessary.
|
||||
|
||||
### Default Ratio: `unspecified-low:8, quick:1, writing:1`
|
||||
|
||||
| Category | Ratio | Use For | Cost |
|
||||
|----------|-------|---------|------|
|
||||
| `unspecified-low` | 80% | Standard issue analysis - read issue, fetch comments, categorize | $ |
|
||||
| `quick` | 10% | Trivial issues - obvious duplicates, spam, clearly resolved | ¢ |
|
||||
| `writing` | 10% | Report generation, response drafting, summary synthesis | $$ |
|
||||
|
||||
### When to Override Default Ratio
|
||||
|
||||
| Scenario | Recommended Ratio | Reason |
|
||||
|----------|-------------------|--------|
|
||||
| Bug-heavy triage | `unspecified-low:7, quick:2, writing:1` | More simple duplicates |
|
||||
| Feature request triage | `unspecified-low:6, writing:3, quick:1` | More response drafting needed |
|
||||
| Security audit | `unspecified-high:5, unspecified-low:4, writing:1` | Deeper analysis required |
|
||||
| First-pass quick filter | `quick:8, unspecified-low:2` | Just categorize, don't analyze deeply |
|
||||
|
||||
### Agent Assignment Algorithm
|
||||
|
||||
```typescript
|
||||
function assignAgentCategory(issues: Issue[], ratio: Record<string, number>): Map<Issue, string> {
|
||||
const assignments = new Map<Issue, string>();
|
||||
const total = Object.values(ratio).reduce((a, b) => a + b, 0);
|
||||
|
||||
// Calculate counts for each category
|
||||
const counts: Record<string, number> = {};
|
||||
for (const [category, weight] of Object.entries(ratio)) {
|
||||
counts[category] = Math.floor(issues.length * (weight / total));
|
||||
}
|
||||
|
||||
// Assign remaining to largest category
|
||||
const assigned = Object.values(counts).reduce((a, b) => a + b, 0);
|
||||
const remaining = issues.length - assigned;
|
||||
const largestCategory = Object.entries(ratio).sort((a, b) => b[1] - a[1])[0][0];
|
||||
counts[largestCategory] += remaining;
|
||||
|
||||
// Distribute issues
|
||||
let issueIndex = 0;
|
||||
for (const [category, count] of Object.entries(counts)) {
|
||||
for (let i = 0; i < count && issueIndex < issues.length; i++) {
|
||||
assignments.set(issues[issueIndex++], category);
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
```
|
||||
|
||||
### Category Selection Heuristics
|
||||
|
||||
**Before launching agents, pre-classify issues for smarter category assignment:**
|
||||
|
||||
| Issue Signal | Assign To | Reason |
|
||||
|--------------|-----------|--------|
|
||||
| Has `duplicate` label | `quick` | Just confirm and close |
|
||||
| Has `wontfix` label | `quick` | Just confirm and close |
|
||||
| No comments, < 50 char body | `quick` | Likely spam or incomplete |
|
||||
| Has linked PR | `quick` | Already being addressed |
|
||||
| Has `bug` label + long body | `unspecified-low` | Needs proper analysis |
|
||||
| Has `feature` label | `unspecified-low` or `writing` | May need response |
|
||||
| User is maintainer | `quick` | They know what they're doing |
|
||||
| 5+ comments | `unspecified-low` | Complex discussion |
|
||||
| Needs response drafted | `writing` | Prose quality matters |
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Exhaustive Pagination Loop
|
||||
|
||||
# STOP. READ THIS BEFORE EXECUTING.
|
||||
|
||||
**YOU WILL FETCH EVERY. SINGLE. ISSUE. NO EXCEPTIONS.**
|
||||
|
||||
## THE GOLDEN RULE
|
||||
|
||||
```
|
||||
NEVER use --limit 100. ALWAYS use --limit 500.
|
||||
NEVER stop at first result. ALWAYS verify you got everything.
|
||||
NEVER assume "that's probably all". ALWAYS check if more exist.
|
||||
```
|
||||
|
||||
## MANDATORY PAGINATION LOOP (COPY-PASTE THIS EXACTLY)
|
||||
|
||||
You MUST execute this EXACT pagination loop. DO NOT simplify. DO NOT skip iterations.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# MANDATORY PAGINATION - Execute this EXACTLY as written
|
||||
|
||||
REPO="code-yeongyu/oh-my-opencode" # or use: gh repo view --json nameWithOwner -q .nameWithOwner
|
||||
TIME_RANGE=48 # hours
|
||||
CUTOFF_DATE=$(date -v-${TIME_RANGE}H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -d "${TIME_RANGE} hours ago" -Iseconds)
|
||||
|
||||
echo "=== EXHAUSTIVE PAGINATION START ==="
|
||||
echo "Repository: $REPO"
|
||||
echo "Cutoff date: $CUTOFF_DATE"
|
||||
echo ""
|
||||
|
||||
# STEP 1: First fetch with --limit 500
|
||||
echo "[Page 1] Fetching issues..."
|
||||
FIRST_FETCH=$(gh issue list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author)
|
||||
FIRST_COUNT=$(echo "$FIRST_FETCH" | jq 'length')
|
||||
echo "[Page 1] Raw count: $FIRST_COUNT"
|
||||
|
||||
# STEP 2: Filter by time range
|
||||
ALL_ISSUES=$(echo "$FIRST_FETCH" | jq --arg cutoff "$CUTOFF_DATE" \
|
||||
'[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]')
|
||||
FILTERED_COUNT=$(echo "$ALL_ISSUES" | jq 'length')
|
||||
echo "[Page 1] After time filter: $FILTERED_COUNT issues"
|
||||
|
||||
# STEP 3: CHECK IF MORE PAGES NEEDED
|
||||
# If we got exactly 500, there are MORE issues!
|
||||
if [ "$FIRST_COUNT" -eq 500 ]; then
|
||||
echo ""
|
||||
echo "WARNING: Got exactly 500 results. MORE PAGES EXIST!"
|
||||
echo "Continuing pagination..."
|
||||
|
||||
PAGE=2
|
||||
LAST_ISSUE_NUMBER=$(echo "$FIRST_FETCH" | jq '.[- 1].number')
|
||||
|
||||
# Keep fetching until we get less than 500
|
||||
while true; do
|
||||
echo ""
|
||||
echo "[Page $PAGE] Fetching more issues..."
|
||||
|
||||
# Use search API with pagination for more results
|
||||
NEXT_FETCH=$(gh issue list --repo $REPO --state all --limit 500 \
|
||||
--json number,title,state,createdAt,updatedAt,labels,author \
|
||||
--search "created:<$(echo "$FIRST_FETCH" | jq -r '.[-1].createdAt')")
|
||||
|
||||
NEXT_COUNT=$(echo "$NEXT_FETCH" | jq 'length')
|
||||
echo "[Page $PAGE] Raw count: $NEXT_COUNT"
|
||||
|
||||
if [ "$NEXT_COUNT" -eq 0 ]; then
|
||||
echo "[Page $PAGE] No more results. Pagination complete."
|
||||
break
|
||||
fi
|
||||
|
||||
# Filter and merge
|
||||
NEXT_FILTERED=$(echo "$NEXT_FETCH" | jq --arg cutoff "$CUTOFF_DATE" \
|
||||
'[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]')
|
||||
ALL_ISSUES=$(echo "$ALL_ISSUES $NEXT_FILTERED" | jq -s 'add | unique_by(.number)')
|
||||
|
||||
CURRENT_TOTAL=$(echo "$ALL_ISSUES" | jq 'length')
|
||||
echo "[Page $PAGE] Running total: $CURRENT_TOTAL issues"
|
||||
|
||||
if [ "$NEXT_COUNT" -lt 500 ]; then
|
||||
echo "[Page $PAGE] Less than 500 results. Pagination complete."
|
||||
break
|
||||
fi
|
||||
|
||||
PAGE=$((PAGE + 1))
|
||||
|
||||
# Safety limit
|
||||
if [ $PAGE -gt 20 ]; then
|
||||
echo "SAFETY LIMIT: Stopped at page 20"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# STEP 4: FINAL COUNT
|
||||
FINAL_COUNT=$(echo "$ALL_ISSUES" | jq 'length')
|
||||
echo ""
|
||||
echo "=== EXHAUSTIVE PAGINATION COMPLETE ==="
|
||||
echo "Total issues found: $FINAL_COUNT"
|
||||
echo ""
|
||||
|
||||
# STEP 5: Verify we got everything
|
||||
if [ "$FINAL_COUNT" -lt 10 ]; then
|
||||
echo "WARNING: Only $FINAL_COUNT issues found. Double-check time range!"
|
||||
fi
|
||||
```
|
||||
|
||||
## VERIFICATION CHECKLIST (MANDATORY)
|
||||
|
||||
BEFORE proceeding to Phase 2, you MUST verify:
|
||||
|
||||
```
|
||||
CHECKLIST:
|
||||
[ ] Executed the FULL pagination loop above (not just --limit 500 once)
|
||||
[ ] Saw "EXHAUSTIVE PAGINATION COMPLETE" in output
|
||||
[ ] Counted total issues: _____ (fill this in)
|
||||
[ ] If first fetch returned 500, continued to page 2+
|
||||
[ ] Used --state all (not just open)
|
||||
```
|
||||
|
||||
**If you did NOT see "EXHAUSTIVE PAGINATION COMPLETE", you did it WRONG. Start over.**
|
||||
|
||||
## ANTI-PATTERNS (WILL CAUSE FAILURE)
|
||||
|
||||
| NEVER DO THIS | Why It Fails |
|
||||
|------------------|--------------|
|
||||
| Single `gh issue list --limit 500` | If 500 returned, you missed the rest! |
|
||||
| `--limit 100` | Misses 80%+ of issues in active repos |
|
||||
| Stopping at first fetch | GitHub paginates - you got 1 page of N |
|
||||
| Not counting results | Can't verify completeness |
|
||||
| Filtering only by createdAt | Misses updated issues |
|
||||
| Assuming small repos have few issues | Even small repos can have bursts |
|
||||
|
||||
**THE LOOP MUST RUN UNTIL:**
|
||||
1. Fetch returns 0 results, OR
|
||||
2. Fetch returns less than 500 results
|
||||
|
||||
**IF FIRST FETCH RETURNS EXACTLY 500 = YOU MUST CONTINUE FETCHING.**
|
||||
|
||||
### 1.3 Also Fetch All PRs (For Bug Correlation)
|
||||
|
||||
```bash
|
||||
# Same pagination logic for PRs
|
||||
gh pr list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author,body,headRefName | \
|
||||
jq --arg cutoff "$CUTOFF_DATE" '[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: Parallel Issue Analysis (1 Issue = 1 Agent)
|
||||
|
||||
### 2.1 Agent Distribution Formula
|
||||
|
||||
```
|
||||
Total issues: N
|
||||
Agent categories based on ratio:
|
||||
- unspecified-low: floor(N * 0.8)
|
||||
- quick: floor(N * 0.1)
|
||||
- writing: ceil(N * 0.1) # For report generation
|
||||
```
|
||||
|
||||
### 2.2 Launch Background Agents
|
||||
|
||||
**MANDATORY: Each issue gets its own dedicated background agent.**
|
||||
|
||||
For each issue, launch:
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="unspecified-low", // or quick/writing per ratio
|
||||
load_skills=[],
|
||||
run_in_background=true,
|
||||
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
|
||||
|
||||
## ANALYSIS CHECKLIST
|
||||
1. **TYPE**: Is this a BUG, QUESTION, FEATURE request, or INVALID?
|
||||
2. **PROJECT_VALID**: Is this issue relevant to OUR project? (YES/NO/UNCLEAR)
|
||||
3. **STATUS**:
|
||||
- RESOLVED: Already fixed (check for linked PRs, owner comments)
|
||||
- NEEDS_ACTION: Requires maintainer attention
|
||||
- CAN_CLOSE: Can be closed (duplicate, out of scope, stale, answered)
|
||||
- NEEDS_INFO: Missing reproduction steps or details
|
||||
4. **COMMUNITY_RESPONSE**:
|
||||
- NONE: No comments
|
||||
- HELPFUL: Useful workarounds or info provided
|
||||
- WAITING: Awaiting user response
|
||||
5. **LINKED_PR**: If bug, search PRs that might fix this issue
|
||||
|
||||
## PR CORRELATION
|
||||
Check these PRs for potential fixes:
|
||||
${PR_LIST}
|
||||
|
||||
## RETURN FORMAT
|
||||
\`\`\`
|
||||
#${issue.number}: ${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 or NONE]
|
||||
SUMMARY: [1-2 sentence summary]
|
||||
ACTION: [Recommended maintainer action]
|
||||
DRAFT_RESPONSE: [If auto-answerable, provide English draft. Otherwise "NEEDS_MANUAL_REVIEW"]
|
||||
\`\`\`
|
||||
`
|
||||
)
|
||||
```
|
||||
|
||||
### 2.3 Collect All Results
|
||||
|
||||
Wait for all background agents to complete, then collect:
|
||||
|
||||
```typescript
|
||||
// Store all task IDs
|
||||
const taskIds: string[] = []
|
||||
|
||||
// Launch all agents
|
||||
for (const issue of issues) {
|
||||
const result = await delegate_task(...)
|
||||
taskIds.push(result.task_id)
|
||||
}
|
||||
|
||||
// Collect results
|
||||
const results = []
|
||||
for (const taskId of taskIds) {
|
||||
const output = await background_output(task_id=taskId)
|
||||
results.push(output)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: Report Generation
|
||||
|
||||
### 3.1 Categorize Results
|
||||
|
||||
Group analyzed issues by status:
|
||||
|
||||
| Category | Criteria |
|
||||
|----------|----------|
|
||||
| **CRITICAL** | Blocking bugs, security issues, data loss |
|
||||
| **CLOSE_IMMEDIATELY** | Resolved, duplicate, out of scope, stale |
|
||||
| **AUTO_RESPOND** | Can answer with template (version update, docs link) |
|
||||
| **NEEDS_INVESTIGATION** | Requires manual debugging or design decision |
|
||||
| **FEATURE_BACKLOG** | Feature requests for prioritization |
|
||||
| **NEEDS_INFO** | Missing details, request more info |
|
||||
|
||||
### 3.2 Generate Report
|
||||
|
||||
```markdown
|
||||
# Issue Triage Report
|
||||
|
||||
**Repository:** ${REPO}
|
||||
**Time Range:** Last ${TIME_RANGE} hours
|
||||
**Generated:** ${new Date().toISOString()}
|
||||
**Total Issues Analyzed:** ${issues.length}
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Count |
|
||||
|----------|-------|
|
||||
| CRITICAL | N |
|
||||
| Close Immediately | N |
|
||||
| Auto-Respond | N |
|
||||
| Needs Investigation | N |
|
||||
| Feature Requests | N |
|
||||
| Needs Info | N |
|
||||
|
||||
---
|
||||
|
||||
## 1. CRITICAL (Immediate Action Required)
|
||||
|
||||
[List issues with full details]
|
||||
|
||||
## 2. Close Immediately
|
||||
|
||||
[List with closing reason and template response]
|
||||
|
||||
## 3. Auto-Respond (Template Answers)
|
||||
|
||||
[List with draft responses ready to post]
|
||||
|
||||
## 4. Needs Investigation
|
||||
|
||||
[List with investigation notes]
|
||||
|
||||
## 5. Feature Backlog
|
||||
|
||||
[List for prioritization]
|
||||
|
||||
## 6. Needs More Info
|
||||
|
||||
[List with template questions to ask]
|
||||
|
||||
---
|
||||
|
||||
## Response Templates
|
||||
|
||||
### Fixed in Version X
|
||||
\`\`\`
|
||||
This issue was resolved in vX.Y.Z via PR #NNN.
|
||||
Please update: \`bunx oh-my-opencode@X.Y.Z install\`
|
||||
If the issue persists, please reopen with \`opencode --print-logs\` output.
|
||||
\`\`\`
|
||||
|
||||
### Needs More Info
|
||||
\`\`\`
|
||||
Thank you for reporting. To investigate, please provide:
|
||||
1. \`opencode --print-logs\` output
|
||||
2. Your configuration file
|
||||
3. Minimal reproduction steps
|
||||
Labeling as \`needs-info\`. Auto-closes in 7 days without response.
|
||||
\`\`\`
|
||||
|
||||
### Out of Scope
|
||||
\`\`\`
|
||||
Thank you for reaching out. This request falls outside the scope of this project.
|
||||
[Suggest alternative or explanation]
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ANTI-PATTERNS (BLOCKING VIOLATIONS)
|
||||
|
||||
## IF YOU DO ANY OF THESE, THE TRIAGE IS INVALID
|
||||
|
||||
| Violation | Why It's Wrong | Severity |
|
||||
|-----------|----------------|----------|
|
||||
| **Using `--limit 100`** | Misses 80%+ of issues in active repos | CRITICAL |
|
||||
| **Stopping at first fetch** | GitHub paginates - you only got page 1 | CRITICAL |
|
||||
| **Not counting results** | Can't verify completeness | CRITICAL |
|
||||
| Batching issues (7 per agent) | Loses detail, harder to track | HIGH |
|
||||
| Sequential agent calls | Slow, doesn't leverage parallelism | HIGH |
|
||||
| Skipping PR correlation | Misses linked fixes for bugs | MEDIUM |
|
||||
| Generic responses | Each issue needs specific analysis | MEDIUM |
|
||||
|
||||
## MANDATORY VERIFICATION BEFORE PHASE 2
|
||||
|
||||
```
|
||||
CHECKLIST:
|
||||
[ ] Used --limit 500 (not 100)
|
||||
[ ] Used --state all (not just open)
|
||||
[ ] Counted issues: _____ total
|
||||
[ ] Verified: if count < 500, all issues fetched
|
||||
[ ] If count = 500, fetched additional pages
|
||||
```
|
||||
|
||||
**DO NOT PROCEED TO PHASE 2 UNTIL ALL BOXES ARE CHECKED.**
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION CHECKLIST
|
||||
|
||||
- [ ] Fetched ALL pages of issues (pagination complete)
|
||||
- [ ] Fetched ALL pages of PRs for correlation
|
||||
- [ ] Launched 1 agent per issue (not batched)
|
||||
- [ ] All agents ran in background (parallel)
|
||||
- [ ] Collected all results before generating report
|
||||
- [ ] Report includes draft responses where applicable
|
||||
- [ ] Critical issues flagged at top
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
When invoked, immediately:
|
||||
|
||||
1. `gh repo view --json nameWithOwner -q .nameWithOwner` (get current repo)
|
||||
2. Parse user's time range request (default: 48 hours)
|
||||
3. Exhaustive pagination for issues AND PRs
|
||||
4. Launch N background agents (1 per issue)
|
||||
5. Collect all results
|
||||
6. Generate categorized report with action items
|
||||
Reference in New Issue
Block a user