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

SKILL.md

GitHub

EXECUTABLE 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 reviewed
  • Resolved — 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 / uncertain
  • user_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:

  1. 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
  1. Check exit code:

    • Exit 0: Success → Read /tmp/pr_triage_extract.json
    • Exit 1: Error → Read error message, show user-friendly message
  2. Parse extracted data to get pr_url and pr_number:

{
  "success": true,
  "pr_number": "1898",
  "pr_url": "https://...",
  "all_comments_count": 122
}
  1. Store in workflow state:

    • pr_url = from extracted data
    • pr_number = from extracted data
    • report_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)
  2. Show user confirmation:

✓ Report loaded successfully
  PR: {pr_number}
  URL: {pr_url}
  Total comments: {all_comments_count}
  1. 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):

  1. Environment variable: ADO_PAT
  2. User-scoped config: ~/.claude/config.json → ado_pat field
  3. Repo-scoped config: ./.claude/config.json → ado_pat field (repo root)
  4. 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.json with { "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-triage again"

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:

  1. 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
  2. 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:

  1. Run script with output redirected to temporary file
  2. 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

⚠️ 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:

  1. What went wrong
  2. Suggested fix
  3. 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