pr-triage
Fetch CodeRabbit and manual reviewer comments from Azure DevOps PRs and generate an interactive HTML report with severity/status breakdowns. Does NOT interactively loop through comments — triaging is
Files6
View on GitHubSKILL.mdplugins/thiran/skills/pr-triage/SKILL.md
GitHubEXECUTABLE WORKFLOW: pr-triage
Start here: Execute this workflow immediately when skill is invoked.
Status Logic (Simplified)
Status = External Assignment (set outside pr-triage):
Pending— Initial state when comment is fetched; not yet reviewedResolved— Comment has been reviewed and addressed (typically set by pr-judge after Apply/Fix)Unresolved— Comment reviewed but marked as unresolved/deferred (typically after Discard on a valid finding)Skipped— Comment intentionally skipped (not triaged)
Separate Data (stored in Technical Info JSON):
pr_judge_assessment— What pr-judge concluded: valid / false_positive / partially_valid / uncertainuser_decision— What the user decided: accept_fix / write_rebuttal / ignore / none
Key Point: pr-triage generates reports with all comments at Pending initially. After triaging a comment with /pr-judge, that skill optionally updates the report by ID with the Status and Verdict you chose. pr-triage itself never modifies status after creation.
Step 0: Check Prerequisites
Before starting, verify all required tools are available:
Python 3.7+
python --version
Should output: Python 3.7.x or higher
If Python not found:
❌ Python 3.7+ is required but not found.
Install Python from: https://www.python.org/downloads/
Then run: python --version to verify
After installation, run /pr-triage again.
Exit if Python not available.
Step 1: New or Refresh Existing Report
Ask user with AskUserQuestion (single choice, required):
What would you like to do?
Options:
- A) New Report - Fetch fresh comments and generate a new report
- B) Refresh Existing - Merge fresh comments into an existing report, preserving statuses from prior triage
Store choice in: mode
If mode = New: Continue to Step 2
If mode = Refresh: Go to Step 1a
Step 1a: Load Existing Report (Refresh Mode Only)
Ask user to provide the HTML report path:
Enter the HTML report path to refresh:
Example: C:\Repos\docs\reports\pr-triage\1898-pr-triage-20260708-120000.html
Process:
- Run extraction script (do NOT print output to Claude):
python ${CLAUDE_SKILL_DIR}/scripts/extract_report_data.py "<html_path>" > /tmp/pr_triage_extract.json 2>&1
Check exit code:
- Exit 0: Success → Read
/tmp/pr_triage_extract.json - Exit 1: Error → Read error message, show user-friendly message
- Exit 0: Success → Read
Parse extracted data to get
pr_urlandpr_number:
{
"success": true,
"pr_number": "1898",
"pr_url": "https://...",
"all_comments_count": 122
}
Store in workflow state:
pr_url= from extracted datapr_number= from extracted datareport_path= the user-provided path (read-only — used in Step 4 to merge prior triage data; refresh never writes back to this exact file)report_dir=os.path.dirname(report_path)(the Step 6 output target — refresh writes a new timestamped file here, same directory as the original, so prior reports are never overwritten)
Show user confirmation:
✓ Report loaded successfully
PR: {pr_number}
URL: {pr_url}
Total comments: {all_comments_count}
- Skip Step 2 — Go directly to Step 3
If file not found or extraction fails:
❌ {error_message}
Options:
A) Try another file path
B) Start a New Report instead
Re-prompt or allow user to switch to New mode.
Step 2: Get PR URL from User (New Mode Only)
Ask the user to provide the full Azure DevOps PR URL using AskUserQuestion.
Prompt:
Enter your Azure DevOps PR URL:
Example: https://dev.azure.com/org/project/_git/repo/pullrequest/1898
(Copy from your browser address bar)
Wait for user input. Parse to extract:
pr_number(from/pullrequest/{number})org(from/dev.azure.com/{org}/)project(from/{org}/{project}/)repo(from_git/{repo}/)
If parsing fails: Show error:
❌ Invalid Azure DevOps URL format.
Expected format:
https://dev.azure.com/{org}/{project}/_git/{repo}/pullrequest/{pr_number}
Copy your PR URL from the browser address bar and try again.
Re-prompt user.
Step 3: Check for Azure DevOps PAT Token
Check in this order (priority):
- Environment variable:
ADO_PAT - User-scoped config:
~/.claude/config.json→ado_patfield - Repo-scoped config:
./.claude/config.json→ado_patfield (repo root) - If none found → Go to Step 3a
If found: Store as pat_token and continue to Step 4
Step 3a: Prompt User for PAT Token Setup
If PAT not found, ask via AskUserQuestion:
Azure DevOps Personal Access Token (PAT) not found.
Choose how to proceed:
A) Set environment variable ADO_PAT
B) Create .claude/config.json in home directory
C) Exit (set up PAT and run again)
⚠️ SECURITY: Never paste your PAT token here. Set it locally on your machine.
Based on selection:
A): Tell user:
export ADO_PAT="your-azure-devops-pat"(in terminal, on their machine)- Provide link: Azure DevOps → Settings → Personal Access Tokens
- Ask them to set it in terminal, then type "done"
- Re-check for token
B): Show template:
~/.claude/config.jsonwith{ "ado_pat": "your-token-here" }- Provide link: Azure DevOps → Settings → Personal Access Tokens
- Ask them to create file locally, then type "done"
- Re-check for token
C): Exit with message: "Set up PAT locally and run
/pr-triageagain"
Do NOT proceed until user confirms they've set PAT locally.
Step 4: Fetch Comments from Azure DevOps
Run Python script silently (do NOT print output to Claude):
New Mode:
python ${CLAUDE_SKILL_DIR}/scripts/fetch_coderabbit_comments.py "<pr_url>" > /tmp/pr_triage_comments.json 2>&1
Refresh Mode:
python ${CLAUDE_SKILL_DIR}/scripts/fetch_coderabbit_comments.py "<pr_url>" "<report_path>" > /tmp/pr_triage_comments.json 2>&1
The optional 2nd argument tells the script to preserve and merge with existing comments from the report.
Refresh Mode Merge Behavior
In Refresh mode, the script preserves all prior triage work:
Fresh comments (in current fetch):
- Updated with latest severity/description from Azure DevOps
- Status/verdict from prior triage is preserved by comment ID
- New comments get Pending status
Old comments (in prior report but not in fresh fetch):
- Appended to the result with all prior status/verdict intact
- Usually resolved/archived comments that no longer appear in current fetch
- Prevents loss of prior triage work
Result: The final report will contain all comments from both fresh fetch and existing report.
Output file: Refresh mode always writes a new report file (fresh timestamp, same directory as the original — see Step 6) rather than overwriting report_path. The original file is left untouched, so a history of reports accumulates over successive refreshes and no prior triage state can ever be lost to a bad fetch.
Process:
- Run script with output redirected to temporary file
- Check exit code:
- Exit 0: Success → Read
/tmp/pr_triage_comments.json - Exit 1: Error → Read error message from file, show user-friendly error, and STOP — do not proceed to Step 6
- Exit 0: Success → Read
⚠️ CRITICAL: /tmp/pr_triage_comments.json is a fixed path that may still hold output from a previous run (e.g. from an earlier successful fetch, or a different PR). A non-zero exit code means this run's fetch never wrote fresh data — the file's contents are stale even though the path exists and parses as valid JSON. Never fall through to Step 6 on a non-zero exit just because a file happens to be sitting at that path; always check the exit code first, every time.
Expected structure (read from file, NOT shown to Claude):
{
"success": true,
"pr_number": "1898",
"pr_url": "https://...",
"all_comments": [
{
"id": 25197,
"file": "src/utils.ts",
"line": 45,
"severity": "Critical",
"description": "...",
"status": "Resolved",
"pr_judge_assessment": "valid",
"user_decision": "accept_fix"
}
]
}
(merge_warning and generated_at are included only when applicable — see below — they may be entirely absent from the JSON rather than present as null.)
If merge_warning is present (Refresh mode only):
Show: ⚠️ Refresh: Could not merge prior comments — {merge_warning}. Using fresh comments only as fallback.
If no comments found: Message: "No CodeRabbit comments found for this PR." Exit gracefully.
If fetch fails: Show the error message and offer to retry or exit. Do not proceed to Step 6.
⚠️ SECURITY: Script output is read directly from file. Do NOT echo or print it to Claude.
Step 5: Get Report Output Path (New Mode Only)
Ask user where to save the report using AskUserQuestion.
Prompt:
Where should the report be saved?
Examples:
C:\Repos\docs\reports\pr-triage
/home/user/reports
./reports
(Press Enter to use default: C:\Repos\docs\reports\pr-triage)
Store the path as report_dir in workflow state.
Validate path:
- If directory doesn't exist, create it:
mkdir -p {report_dir} - If creation fails, show error and re-prompt
Default: C:\Repos\docs\reports\pr-triage
Step 6: Generate/Regenerate HTML Report (File-Based)
⚠️ CRITICAL: Use file path instead of inline JSON to avoid Windows command-line limits (32KB max per argument). Large PRs with 20+ comments produce JSON payloads >100KB, causing silent failures.
6a: Call Report Generator with File Path
Use Python to write JSON to a temp file, then pass the file path to the script:
Both New and Refresh Modes:
import json
import subprocess
import sys
import os
# Read comments from Step 4 output
with open('/tmp/pr_triage_comments.json', 'r') as f:
comments_data = json.load(f)
pr_number = str(comments_data.get('pr_number', 'unknown'))
# Always pass a directory (not a .html path) so a new timestamped file is generated —
# in both New mode (report_dir chosen in Step 5) and Refresh mode (report_dir derived
# from the original report_path in Step 1a). This is what makes refresh non-destructive.
output_path = '<report_dir>'
# Call the script with file path instead of inline JSON
cmd = [
sys.executable,
'${CLAUDE_SKILL_DIR}/scripts/generate_report_react.py',
pr_number,
'/tmp/pr_triage_comments.json', # Pass file path, not JSON string
output_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
# Check result
if result.returncode == 0:
output = json.loads(result.stdout)
report_path = output.get('report_path')
size_kb = output.get('size_kb', 0)
comment_count = output.get('comments_count', 0)
else:
error_msg = result.stderr
# Handle error - see Step 6b below
6b: Script Support for File Paths
The generate_report_react.py script has been updated to detect file paths:
- If argv[2] is a file path (ends with
.json), it reads from the file - If argv[2] is inline JSON, it parses as JSON (backward compatible)
- No changes needed to existing callers
6c: Error Handling with Fallback
If generation fails, attempt Direct Python Import (bypass subprocess):
import sys
import os
import json
from datetime import datetime
# Load directly without subprocess
sys.path.insert(0, '${CLAUDE_SKILL_DIR}/scripts')
from generate_report_react import generate_react_report
with open('/tmp/pr_triage_comments.json', 'r') as f:
comments_data = json.load(f)
results = {
"pr_number": comments_data['pr_number'],
"pr_url": comments_data.get('pr_url', ''),
"all_comments": comments_data.get('all_comments', []),
"comments": []
}
# output_path here is always a directory (see 6a) — this bypasses main()'s CLI
# entrypoint, which is what normally turns a directory into a fresh timestamped
# filename, so that resolution must be replicated here to keep refresh non-destructive.
report_dir = output_path
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
report_path = os.path.join(report_dir, f"{results['pr_number']}-pr-triage-{timestamp}.html")
try:
html = generate_react_report(
str(results['pr_number']),
results,
report_path
)
# Write HTML to file
os.makedirs(report_dir, exist_ok=True)
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html)
except Exception as e:
show_error(f"Report generation failed: {str(e)}")
return False
Show user on success:
✓ React-based report generated successfully!
Report: {report_path}
Size: {size_kb} KB
Comments: {comment_count}
How to triage:
1. Open the report in your browser
2. For each comment, run: /pr-judge <comment text>
3. When pr-judge asks, provide report path + comment ID to sync status back
4. Report updates in real-time as you triage
Store report_path in workflow state.
Step 7: Complete
Show final message:
✓ pr-triage report ready!
Report: {report_path}
How to triage:
1. Open the report in your browser
2. For each comment you want to evaluate, run: /pr-judge <comment text>
3. When pr-judge finishes, it will ask if you want to update this report
4. Say Yes, provide the report path + comment ID, and select Status/Verdict
5. The report updates in real-time — refresh your browser to see changes
If this was a Refresh, this report is a new file — the original report you refreshed from was left untouched. Use this new path for subsequent /pr-judge updates and future refreshes.
The report's client-side filters (Status, Severity) are always available for narrowing the table, and every column header is clickable to sort (default: ID ascending).
Error Handling
At any step, if error occurs:
Show error message with:
- What went wrong
- Suggested fix
- Ask if user wants to retry or exit
Examples:
- "Failed to fetch PR comments" → Check PAT token permissions and PR URL validity
- "Report output directory cannot be created" → Check path permissions
- "Invalid JSON in report" → Report may be corrupted; try a different path or start fresh
References
For more details, see:
references/config.example.json— Configuration template