
Bug Review
- 196 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
bug-review: A skill for development. This provides functionality for development workflows.
Key points
- bug-review
Bug Review by the numbers
- 196 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,033 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill bug-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 196 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use bug-review for development tasks?
Use bug-review for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with bug-review.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use bug-review for development tasks, or when bug-review: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to bug-review: bug-review.
Files
Bug Review v2
Multi-pass PR review agent with 5 parallel review passes, majority voting, independent Opus validation, and resolution rate learning. Posts inline PR comments and optionally generates autofix commits. Tracks whether findings get resolved at merge time and uses that signal to improve future reviews.
When to Apply
- User asks to review a pull request for bugs or correctness issues
- User runs
/bug-review <PR-number-or-URL> - User runs
/bug-review:resolve <PR>to classify resolutions after merge - User runs
/bug-review:reportfor resolution rate statistics - User asks for code review focused on logic errors, edge cases, or security
- User wants to find bugs in a diff or set of changes
Setup
On first run, verify:
ghCLI is installed and authenticated (gh auth status)- Current directory is a git repo with a GitHub remote
jqis installed (for JSON processing)bcis installed (for resolution rate calculations; pre-installed on most systems)
Read config.json for configuration (passes, vote threshold, models, category weights).
Workflow Overview
/bug-review <PR>
|
v
Fetch PR context + gather-context.sh
|
v
5 parallel passes (shuffled diffs, Sonnet) --> Aggregate & vote (3/5 majority)
|
v
Independent Opus validator --> Dedup --> Present findings --> Post + store
|
(later, after merge)
v
/bug-review:resolve <PR> --> Classify resolutions --> Update category weightsCommand: /bug-review <PR>
Step 1: Parse Input & Fetch Context
1. Parse the PR identifier (number, URL, or branch name) 2. Check cache: Look for ${CLAUDE_PLUGIN_DATA}/bug-review/cache/pr-{N}/ — if cache exists for the same head commit, offer to resume from the last checkpoint 3. Run scripts/fetch-pr.sh <pr-identifier> to get PR diff + metadata as JSON 4. Save the diff to a temp file for shuffling 5. Run scripts/gather-context.sh <changed-files-json> to get prioritized context (callers, types, tests, repo rules) 6. Read .bug-review.md from repo root if it exists 7. Save checkpoint: Write context to ${CLAUDE_PLUGIN_DATA}/bug-review/cache/pr-{N}/context.json
Step 2: Run 5 Parallel Review Passes
For each pass (1-5), prepare a shuffled diff:
scripts/shuffle-diff.sh <pass-number> < pr.diff > pass-<N>.diffLaunch 5 Agent subprocesses in parallel. Read review-passes.md for the exact prompt for each pass.
- Pass 1: Logic & Edge Cases (seed 1)
- Pass 2: Security & Data Integrity (seed 2)
- Pass 3: Error Handling & API Contracts (seed 3)
- Pass 4: Concurrency & State (seed 4)
- Pass 5: Data Flow & Contracts (seed 5)
Use model from config.json agent_model (default: "sonnet").
Each agent returns a JSON array of findings.
Save checkpoint: Write all pass results to ${CLAUDE_PLUGIN_DATA}/bug-review/cache/pr-{N}/pass-results.json
Step 3: Aggregate & Vote
1. Collect findings from all 5 passes 2. Group findings by similarity: same file + line within +/-5 + same or related category 3. Count votes per group 4. Keep only findings with 3+ votes (majority of 5, configurable via vote_threshold) 5. Apply category weights from config.json: final_score = votes × severity_weight × category_weight 6. Categories with weight < 0.1 are suppressed entirely 7. Rank by final_score descending
If only 1-2 passes found bugs and the others found none, present findings but note they lack consensus.
Save checkpoint: Write voted findings to cache.
Step 4: Independent Validation (Opus)
Launch a separate Agent using validator_model from config.json (default: "opus").
This agent has NOT seen the review passes. It receives only the voted findings and the original code. Read the Validator section in review-passes.md for the prompt.
For each finding, the validator outputs: {id, verdict: "KEEP"|"DISCARD", confidence, reasoning}
Remove DISCARDed findings. Multiply each finding's score by the validator's confidence.
Compute each finding's final confidence field:
confidence = (votes / total_passes) × validator_confidenceFindings with confidence < 0.5 are shown with a "low confidence" warning.
Save checkpoint: Write validated findings to cache.
Step 5: Dedup Against Prior Reviews
Run scripts/dedup.sh <pr-number> to get existing [bug-review] comments. Match by location proximity (file + line within +/-10) and category — not text similarity.
Step 6: Present Findings to User
Display a table:
| # | Severity | Confidence | File | Line | Title | Votes |
|---|
For each finding, show full description, trigger scenario, suggested fix, and validator reasoning.
Ask the user (using AskUserQuestion with multiSelect):
- Which findings to post as PR comments (default: all)
- Which findings to autofix (default: none)
If no findings survived voting + validation: "No bugs found across 5 review passes. The changes look clean."
Step 7a: Post PR Review
Write approved findings to a temporary JSON file, then run:
scripts/post-review.sh <pr-number> <findings-json-file>Then persist findings for resolution tracking:
scripts/store-findings.sh <pr-number> <findings-json-file> <head-commit-sha>Step 7b: Autofix (User-Selected Findings)
For each finding selected for autofix: 1. Read the file and understand surrounding context 2. Generate a minimal fix (smallest possible change) 3. Apply the fix using the Edit tool 4. Scope check: Run git diff --stat — verify only the finding's file was modified and diff is under 20 lines. If exceeded, revert and warn. 5. Run existing tests if available (npm test, go test ./..., pytest, etc.) 6. If tests pass: commit with fix: {title} [bug-review] 7. If tests fail: revert the fix (git checkout -- <file>) and report to user 8. After all fixes: push to the PR branch
Safety: one commit per fix, run tests between fixes, never force-push, scope-validate every fix.
Command: /bug-review:resolve <PR>
Run after a PR is merged to classify whether findings were resolved.
1. Run scripts/classify-resolutions.sh <pr-number>
- Loads stored findings from
${CLAUDE_PLUGIN_DATA}/bug-review/findings/pr-{N}.json - Checks if PR is merged
- For each finding: diffs code between review commit and merge commit
- Classifies each as RESOLVED, UNRESOLVED, or INCONCLUSIVE
- Updates the stored findings file with resolution data
2. Display resolution summary to user 3. If enough data accumulated (10+ findings, 3+ PRs): run scripts/update-weights.sh to adjust category weights
Command: /bug-review:report
Display resolution rate statistics across all tracked PRs.
Run scripts/resolution-report.sh which outputs:
- Overall resolution rate
- Resolution rate by severity
- Resolution rate by category (sorted worst-first to highlight noisy categories)
- Suppressed categories (weight < 0.1)
Repo-Specific Rules (.bug-review.md)
Teams can create .bug-review.md at their repo root:
## Focus Areas
- Pay special attention to authentication flows
- Check all database queries for SQL injection
## Ignore
- Don't flag issues in generated files (*.generated.ts)
- Ignore style-only concerns
## Invariants
- All API endpoints must check req.user before accessing user data
- Database migrations must be reversible
## Severity Overrides
- Treat any auth bypass as CRITICAL regardless of category defaultHow to Use
Read workflow.md for detailed step-by-step with error handling. Read review-passes.md for all 5 review pass prompts and the validator. Read categories.md for bug categories and learned weights.
Related Skills
- Consider creating a Runbook skill for investigating bugs found by this review
- Consider creating a CI/CD skill to run this review automatically on PR open
name: Track Bug Review Resolutions
# TEMPLATE: This Action requires adaptation for your repository.
#
# Before using:
# 1. Copy this file to your repo's .github/workflows/
# 2. Copy the bug-review scripts/ directory to your repo root (or adjust SCRIPT_DIR below)
# 3. Set FINDINGS_DIR to where your findings are stored
# (default assumes findings are committed to .bug-review/findings/ in the repo)
# 4. Ensure gh CLI is available (actions/checkout provides it)
#
# This Action uses repo-committed findings (not CLAUDE_PLUGIN_DATA, which is local-only).
# To use this, configure store-findings.sh to write to .bug-review/findings/ in the repo
# and commit the findings as part of the review workflow.
#
# Alternative: Skip this Action entirely and run /bug-review:resolve <PR> manually.
on:
pull_request:
types: [closed]
env:
# Adjust these paths for your repository
SCRIPT_DIR: .bug-review/scripts
FINDINGS_DIR: .bug-review/findings
jobs:
resolve:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for stored findings
id: check
run: |
PR=${{ github.event.pull_request.number }}
if [[ -f "${{ env.FINDINGS_DIR }}/pr-${PR}.json" ]]; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No bug-review findings for PR #$PR — skipping."
fi
- name: Classify resolutions
if: steps.check.outputs.found == 'true'
run: |
export CLAUDE_PLUGIN_DATA="$(pwd)/.bug-review"
bash ${{ env.SCRIPT_DIR }}/classify-resolutions.sh ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update category weights
if: steps.check.outputs.found == 'true'
run: |
export CLAUDE_PLUGIN_DATA="$(pwd)/.bug-review"
bash ${{ env.SCRIPT_DIR }}/update-weights.sh ${{ env.SCRIPT_DIR }}/../config.json \
|| echo "Insufficient data for weight update — skipping."
- name: Commit resolution data
if: steps.check.outputs.found == 'true'
run: |
git config user.name "bug-review[bot]"
git config user.email "bug-review[bot]@users.noreply.github.com"
git add .bug-review/findings/ .bug-review/config.json 2>/dev/null || true
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: update bug-review resolution data for PR #${{ github.event.pull_request.number }}"
git push
fi
{
"review_passes": 5,
"vote_threshold": 3,
"max_context_files": 15,
"agent_model": "sonnet",
"validator_model": "opus",
"min_confidence": 0.5,
"category_weights": {
"null-access": 1.0,
"boundary": 1.0,
"race": 1.0,
"resource-leak": 1.0,
"injection": 1.0,
"auth-bypass": 1.0,
"data-loss": 1.0,
"error-swallow": 1.0,
"type-coercion": 1.0,
"stale-ref": 1.0,
"api-contract": 1.0,
"dead-code": 1.0,
"perf": 1.0,
"config": 1.0
},
"_setup_instructions": {
"review_passes": "Number of parallel review passes (default: 5)",
"vote_threshold": "Minimum votes to surface a finding (default: 3 = majority of 5)",
"max_context_files": "Max context files beyond changed files (default: 15)",
"agent_model": "Model for review passes: 'sonnet' (faster/cheaper) or 'opus' (thorough)",
"validator_model": "Model for independent validator: 'opus' (recommended) or 'sonnet'",
"min_confidence": "Findings below this confidence get a 'low confidence' warning (default: 0.5)",
"category_weights": "Learned from resolution rate data. 1.0 = full weight, 0.1 = suppressed. Updated by scripts/update-weights.sh"
}
}
Gotchas
No known gotchas yet. This file is updated as the skill is used and edge cases are discovered.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "if echo \"$TOOL_INPUT\" | grep -qE 'git push.*--force($| )|git push.*-f([^i]|$)|git reset --hard' && ! echo \"$TOOL_INPUT\" | grep -q 'force-with-lease'; then echo 'BLOCKED: Destructive git operation. Bug-review never force-pushes or hard resets.' >&2; exit 1; fi",
"timeout": 5
}]
}
],
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [{
"type": "command",
"command": "CHANGED=$(git diff --stat 2>/dev/null | tail -1); FILES=$(echo \"$CHANGED\" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+'); LINES=$(echo \"$CHANGED\" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+'); if [ \"${FILES:-0}\" -gt 1 ] || [ \"${LINES:-0}\" -gt 20 ]; then echo \"WARNING: Autofix scope exceeded (${FILES:-0} files, ${LINES:-0} insertions). Verify the change is minimal.\" >&2; fi",
"timeout": 5
}]
}
]
}
}
{
"version": "1.0.4",
"organization": "dot-skills",
"technology": "GitHub Pull Requests",
"discipline": "composition",
"type": "automation",
"date": "March 2026",
"abstract": "Multi-pass PR review agent with 5 parallel passes (shuffled diffs for attention diversity), majority voting, independent Opus validation, and resolution rate learning. Tracks whether findings get resolved at merge time and adjusts category weights to improve future precision. Supports repo-specific rules via .bug-review.md, optional autofix with scope validation, and resolution rate reporting.",
"references": [
"https://cursor.com/blog/bugbot-out-of-beta",
"https://cursor.com/blog/building-bugbot",
"https://cursor.com/blog/dynamic-context-discovery"
]
}
Bug Categories
These categories define the types of bugs the review passes look for. Each finding must reference one of these category IDs. Severity defaults can be overridden per-finding based on context, or by repo rules in .bug-review.md.
---
1. Null/Undefined Access (null-access)
Default Severity: CRITICAL Description: Code accesses properties or methods on a value that can be null or undefined without checking first. Causes TypeError crashes at runtime. Examples: user.name when user can be null, optional chaining missing, database query returning null.
2. Off-by-One / Boundary Error (boundary)
Default Severity: CRITICAL Description: Loop bounds, array indexing, string slicing, or range calculations that are off by one. Causes silent data corruption or out-of-bounds access. Examples: for (i = 0; i <= arr.length), str.substring(0, len - 1) missing last char, pagination skipping first item.
3. Race Condition (race)
Default Severity: CRITICAL Description: Two or more operations access shared state concurrently without proper synchronization. Results depend on execution order. Examples: TOCTOU (check-then-act), concurrent map modification, parallel async updates without locks, unprotected shared counters.
4. Resource Leak (resource-leak)
Default Severity: HIGH Description: Resources acquired but never released. Over time, this exhausts system resources (memory, file descriptors, connections). Examples: Unclosed database connections, file handles not closed in error paths, event listeners never removed, timers not cleared.
5. Injection / XSS (injection)
Default Severity: HIGH Description: User-controlled input reaches a sensitive sink (SQL query, HTML output, shell command, file path) without sanitization. Examples: SQL injection via string concatenation, XSS via innerHTML, command injection via child_process, path traversal via user-supplied filename.
6. Auth/Authz Bypass (auth-bypass)
Default Severity: HIGH Description: Missing or incorrect authentication/authorization checks allowing unauthorized access to data or operations. Examples: API endpoint missing auth middleware, role check using wrong field, token validation skipped in error path, IDOR (accessing other users' resources).
7. Data Loss / Corruption (data-loss)
Default Severity: HIGH Description: Operations that silently lose or corrupt data. Often subtle and discovered late. Examples: Overwriting without backup, missing database transactions, truncating data on type conversion, ignoring write errors.
8. Error Swallowing (error-swallow)
Default Severity: MEDIUM Description: Errors caught but not handled, logged, or propagated. Silently hides failures, making debugging impossible. Examples: Empty catch blocks, .catch(() => {}), ignoring callback errors, logging error but continuing as if success.
9. Type Coercion (type-coercion)
Default Severity: MEDIUM Description: Implicit type conversions causing unexpected behavior. Particularly common in JavaScript/TypeScript. Examples: == instead of ===, string + number concatenation, truthy/falsy checks on 0 or empty string, JSON.parse without validation.
10. Stale Closure / Reference (stale-ref)
Default Severity: MEDIUM Description: Closures or references capturing a mutable value that changes after capture, leading to stale data. Examples: React useEffect with missing dependencies, setTimeout capturing loop variable, event handler referencing outdated state.
11. API Contract Violation (api-contract)
Default Severity: MEDIUM Description: Code violates the expected interface of a function, API, or library. May work by accident but breaks on updates. Examples: Wrong argument order, missing required fields, assuming return type without checking, deprecated API usage.
12. Dead Code / Unreachable Path (dead-code)
Default Severity: LOW Description: Logic branches that can never execute due to earlier conditions. Indicates a logic error elsewhere. Examples: if (x && !x), return before code, impossible enum case, overridden method never called.
13. Performance Regression (perf)
Default Severity: LOW Description: Code patterns that significantly degrade performance in hot paths. Only flagged when the regression is substantial. Examples: O(n^2) loop where O(n) is possible, unnecessary re-renders, repeated database queries in a loop, missing index hints.
14. Configuration Error (config)
Default Severity: LOW Description: Incorrect or mismatched configuration values that cause runtime failures or unexpected behavior. Examples: Wrong environment variable name, mismatched API version, incorrect timeout value, hardcoded dev URL in production code.
---
Severity Weights (for ranking)
| Severity | Weight | Meaning |
|---|---|---|
| CRITICAL | 4 | Causes crashes, data loss, or security breaches in production |
| HIGH | 3 | Significant bugs that affect correctness or security |
| MEDIUM | 2 | Real bugs with limited blast radius or workarounds available |
| LOW | 1 | Minor issues, potential future problems |
Learned Category Weights
Each category has a weight (0.0 to 1.0) that adjusts its impact on finding ranking. Weights are initialized to 1.0 and learned from resolution rate data — categories where developers consistently fix the flagged issues get higher weight, categories where findings are ignored get suppressed.
The ranking formula is: final_score = votes × severity_weight × category_weight
Weights are stored in config.json → category_weights and updated by scripts/update-weights.sh after resolution data is collected. Categories with weight below 0.1 are suppressed entirely (findings in those categories are discarded before presentation).
To reset weights: set all values in config.json → category_weights back to 1.0.
The weight learning requires minimum data: 10+ findings across 3+ resolved PRs. Below that threshold, all categories keep their default weight of 1.0.
Review Pass Prompts
This file contains prompts for the 5 parallel review passes and the independent validator. Each pass receives the same PR context but with shuffled diff ordering (via scripts/shuffle-diff.sh) and different focus areas to maximize attention diversity.
Common Context (All Passes)
All passes receive:
- PR diff with file changes (shuffled per pass — different file ordering creates different attention patterns)
- Extended context from
scripts/gather-context.sh(callers, types, tests) - Bug category definitions (from categories.md)
- Repo-specific rules (from .bug-review.md, if exists)
- Category weights from config.json (categories with low resolution rates are deprioritized)
All passes must output a JSON array of findings. Each finding:
{
"file": "src/auth.ts",
"line": 42,
"endLine": 45,
"severity": "CRITICAL",
"category": "null-access",
"title": "Unchecked null return from getUserSession()",
"description": "getUserSession() returns null when session expired, but line 42 accesses .userId without checking. This crashes for any user with an expired session.",
"triggerScenario": "User with expired session token calls /api/profile -> getUserSession returns null -> TypeError: Cannot read property 'userId' of null",
"suggestedFix": "Add null check: if (!session) return res.status(401)"
}If no bugs found, output [].
---
Pass 1: Logic & Edge Cases
Focus: Logic errors, boundary conditions, off-by-one errors, null/undefined handling, incorrect boolean logic, missing edge cases.
Diff ordering: Shuffled with seed 1 via scripts/shuffle-diff.sh 1.
Prompt:
You are a code reviewer focused on logic errors and edge cases.
Review this PR diff and surrounding context. Find bugs that would cause incorrect behavior at runtime.
Focus areas:
- Null/undefined access without guards
- Off-by-one errors in loops, slices, or ranges
- Boolean logic errors (wrong operator, inverted condition, missing case)
- Missing edge cases (empty arrays, zero values, negative numbers, NaN, unicode, concurrent access)
- Incorrect state transitions
- Return value mishandling (ignoring errors, wrong type assumptions)
Rules:
- Only report bugs introduced or exposed by THIS PR's changes
- You MUST provide a concrete trigger scenario for every finding
- If you cannot construct a plausible trigger, do NOT report it
- Do not report style issues, documentation gaps, or TODOs
- Do not report issues the compiler/type checker would catch
- Check surrounding code before reporting — the "bug" may be handled elsewhere
- Review unchanged code only when needed to understand if a change creates a bug
Output: JSON array of findings. Empty array [] if no bugs found.---
Pass 2: Security & Data Integrity
Focus: Security vulnerabilities, data integrity issues, authentication/authorization bypasses, injection attacks, data corruption.
Diff ordering: Shuffled with seed 2 via scripts/shuffle-diff.sh 2.
Prompt:
You are a security-focused code reviewer finding vulnerabilities and data integrity bugs.
Review this PR diff and surrounding context. Find security issues and data corruption bugs.
Focus areas:
- Injection attacks (SQL injection, XSS, command injection, path traversal)
- Authentication/authorization bypasses (missing checks, wrong order of operations)
- Data corruption (race conditions on shared state, missing transactions, partial writes)
- Sensitive data exposure (logging secrets, leaking PII, insecure storage)
- Resource exhaustion (unbounded allocations, missing rate limits, ReDoS)
- Cryptographic misuse (weak algorithms, hardcoded keys, improper randomness)
Rules:
- Only report vulnerabilities introduced or exposed by THIS PR's changes
- Trace attacker-controlled input to the actual sink — do not guess
- Verify existing controls don't already block the attack path
- You MUST provide a concrete trigger/attack scenario for every finding
- If you cannot construct a plausible attack, do NOT report it
- Do not report theoretical risks without code evidence
- Do not report issues already caught by security linters (if configured)
Output: JSON array of findings. Empty array [] if no bugs found.---
Pass 3: Error Handling & API Contracts
Focus: Error handling gaps, API contract violations, resource leaks, type mismatches.
Diff ordering: Shuffled with seed 3 via scripts/shuffle-diff.sh 3.
Prompt:
You are a code reviewer focused on error handling, API contracts, and resource management.
Review this PR diff and surrounding context. Find bugs related to error handling gaps, API misuse, and resource leaks.
Focus areas:
- Swallowed exceptions (catch blocks that ignore errors)
- Missing error propagation (async errors not awaited, callbacks without error params)
- Resource leaks (unclosed file handles, database connections, event listeners, timers)
- API contract violations (wrong argument types, missing required fields, incorrect return types)
- Incorrect async patterns (missing await, unhandled promise rejections)
- Stale closures or references (capturing mutable state in callbacks/effects)
Rules:
- Only report bugs introduced or exposed by THIS PR's changes
- You MUST provide a concrete trigger scenario for every finding
- If you cannot construct a plausible trigger, do NOT report it
- Check if error handling exists elsewhere (middleware, higher-level catch, framework guarantees)
- Do not report defensive programming suggestions — only actual bugs
- Do not report style issues or missing type annotations
Output: JSON array of findings. Empty array [] if no bugs found.---
Pass 4: Concurrency & State
Focus: Shared state mutations, async ordering problems, event loop blocking, deadlocks, cache invalidation.
Diff ordering: Shuffled with seed 4 via scripts/shuffle-diff.sh 4.
Prompt:
You are a code reviewer specialized in concurrency bugs and state management.
Review this PR diff and surrounding context. Find bugs where concurrent or asynchronous operations interact incorrectly with shared state.
Focus areas:
- TOCTOU (time-of-check to time-of-use) where state changes between check and action
- Non-atomic read-modify-write sequences on shared data (balances, counters, flags)
- Event loop blocking (synchronous I/O, CPU-heavy computation on main thread)
- Deadlocks and lock ordering violations
- Cache invalidation bugs (stale data served after mutation, missing cache busting)
- Promise/async ordering assumptions that break under load (parallel requests, retries)
- Shared mutable state across request handlers without isolation
Rules:
- Only report bugs introduced or exposed by THIS PR's changes
- You MUST describe the specific interleaving or ordering that triggers the bug
- If you cannot construct a concrete race/ordering scenario, do NOT report it
- Check if atomic operations, transactions, or locks are already in place
- Do not report theoretical concurrency issues in single-threaded code
Output: JSON array of findings. Empty array [] if no bugs found.---
Pass 5: Data Flow & Contracts
Focus: Data transformation correctness, type narrowing gaps, serialization fidelity, implicit contract violations.
Diff ordering: Shuffled with seed 5 via scripts/shuffle-diff.sh 5.
Prompt:
You are a code reviewer focused on data flow correctness and implicit contracts.
Review this PR diff and surrounding context. Find bugs where data is transformed, serialized, or passed across boundaries incorrectly.
Focus areas:
- Type narrowing gaps (value asserted as type X but can actually be type Y at runtime)
- Serialization round-trip bugs (data lost or corrupted through JSON.parse/stringify, URL encoding, base64)
- Implicit contract violations (function returns different shape than callers expect, optional fields treated as required)
- Numeric precision loss (floating point in currency, integer overflow, string-to-number coercion)
- Encoding mismatches (UTF-8 vs ASCII, URL encoding, HTML entities)
- Schema drift (API response shape changed but consumers not updated)
- Partial object updates that leave state inconsistent
Rules:
- Only report bugs introduced or exposed by THIS PR's changes
- You MUST show the specific data path where information is lost or corrupted
- If you cannot trace the data flow to a concrete failure, do NOT report it
- Check if validation or type guards exist at the boundary
- Do not report type annotation gaps that TypeScript would catch
Output: JSON array of findings. Empty array [] if no bugs found.---
Validator (Independent — Runs After Voting)
The validator is a separate agent using a different model (Opus by default) from the review passes (Sonnet by default). This prevents "grading your own homework" — the validator has not seen the code review before and evaluates each finding from scratch.
Trigger: Runs after Step 3 (Aggregate & Vote), receives only the findings that survived majority voting.
Model: config.json → validator_model (default: "opus")
Prompt:
You are a precision-focused validator. Your job is to REDUCE FALSE POSITIVES.
You are reviewing findings from a multi-pass code review. Each finding was identified by multiple independent reviewers and passed majority voting. Your job is to verify each finding is a real bug, not a false alarm.
For each finding, read the code at the specified location and determine:
KEEP if ALL of these are true:
1. The trigger scenario describes a reachable code path — trace from a real entry point (HTTP handler, event listener, public function) to the bug
2. The code at the specified lines actually exhibits the described issue — not a misread or misunderstanding
3. No existing mechanism already handles this (middleware, framework guarantees, parent try-catch, type system)
4. This is a real correctness/security bug, not a style preference, missing optimization, or defensive programming suggestion
5. The compiler, linter, or type checker would NOT already catch this
DISCARD if ANY of these are true:
1. The trigger scenario requires preconditions that are impossible or extremely unlikely in production
2. The code has been misread — the described issue is not present when you re-read the actual lines
3. An existing safeguard handles this case (check the full call chain, not just the immediate function)
4. It is a style/readability concern, not a bug
5. Static analysis would catch it before runtime
For EACH finding, output:
{
"id": "<finding ID>",
"verdict": "KEEP" or "DISCARD",
"confidence": 0.0 to 1.0,
"reasoning": "One sentence explaining why"
}
Output: JSON array of verdicts. Err on the side of DISCARD — false positives destroy trust faster than missed bugs.---
How Passes Are Launched
The main agent launches all 5 passes as parallel Agent subprocesses, each with a shuffled diff:
# Prepare shuffled diffs (one per pass)
for seed in 1 2 3 4 5:
scripts/shuffle-diff.sh $seed < pr.diff > pass-$seed.diff
# Launch passes in parallel
Agent(prompt=pass1_prompt + pass-1.diff + context, model=config.agent_model)
Agent(prompt=pass2_prompt + pass-2.diff + context, model=config.agent_model)
Agent(prompt=pass3_prompt + pass-3.diff + context, model=config.agent_model)
Agent(prompt=pass4_prompt + pass-4.diff + context, model=config.agent_model)
Agent(prompt=pass5_prompt + pass-5.diff + context, model=config.agent_model)
# After voting aggregation, launch validator
Agent(prompt=validator_prompt + voted_findings + code, model=config.validator_model)Each review pass agent has access to Read, Grep, and Glob tools to pull additional context as needed (dynamic context discovery). The validator also has these tools to verify code at finding locations.
None of these agents have Edit, Write, or Bash access — they are read-only.
Bug Review v2 Workflow — Detailed Reference
Complete step-by-step workflow with error handling, caching, and resolution tracking.
Prerequisites
1. gh CLI installed and authenticated: gh auth status 2. jq installed: jq --version 3. Current directory is a git repo with a GitHub remote 4. Target PR must exist and be open (for review) or merged (for resolve)
/bug-review <PR> — Full Review Flow
1. Parse Input
Accepts: PR number (42), URL (https://github.com/owner/repo/pull/42), or branch name.
Cache check: Before fetching, look for ${CLAUDE_PLUGIN_DATA}/bug-review/cache/pr-{N}/. If cache exists and the PR head commit matches, offer to resume from last checkpoint.
2. Fetch PR Context
Run: bash scripts/fetch-pr.sh <pr-identifier>
Outputs JSON: {number, title, body, baseRefName, headRefName, files[{path, additions, deletions}], additions, deletions, diff}
Save diff to temp file for shuffling.
3. Gather Extended Context
Run: bash scripts/gather-context.sh <changed-files-json> [max-files]
Outputs JSON: {files[{path, relevance, reason}], stats}
Priority order: changed files → callers (5) → type definitions (3) → test files (3) → .bug-review.md
4. Shuffle Diffs & Launch 5 Passes
For each pass 1-5:
scripts/shuffle-diff.sh <pass-number> < pr.diff > pass-<N>.diffLaunch 5 Agent subprocesses in parallel. Each receives its shuffled diff, context, categories, and repo rules.
Error handling:
- If 1-2 agents fail: proceed with remaining passes' findings
- If 3+ agents fail: abort
- If all 5 return empty: "No bugs found" (valid outcome)
5. Aggregate & Vote
1. Flatten findings from all passes 2. Group by: same file + line within ±5 + same/related category 3. Count votes per group 4. Apply category weights: final_score = votes × severity_weight × category_weight 5. Keep findings with votes >= vote_threshold (default: 3) 6. Suppress categories with weight < 0.1 entirely
6. Independent Validation
Launch a separate Opus agent (configurable via validator_model) with the Validator prompt from review-passes.md.
For each finding: verdict (KEEP/DISCARD), confidence (0-1), reasoning.
Compute final confidence: confidence = (votes / total_passes) × validator_confidence
7. Dedup
Run: bash scripts/dedup.sh <pr-number>
Match by file + line proximity (±10) + category — not text similarity.
8. Present & Post
Show findings table with confidence scores. Get user approval.
Post via: bash scripts/post-review.sh <pr-number> <findings-json-file> Store via: bash scripts/store-findings.sh <pr-number> <findings-json-file> <head-commit>
9. Autofix (Optional)
Per finding: apply fix → scope check (1 file, <20 lines) → test → commit → push.
/bug-review:resolve <PR> — Resolution Tracking
Run after PR merge:
scripts/classify-resolutions.sh <pr-number>For each stored finding: diff review commit vs merge commit at the finding's location. Classify as RESOLVED (code changed at location), UNRESOLVED (no change), or INCONCLUSIVE (file changed but not near finding).
Then update weights:
scripts/update-weights.shRequires: 10+ findings across 3+ PRs before adjusting weights.
/bug-review:report — Resolution Statistics
scripts/resolution-report.sh # markdown output
scripts/resolution-report.sh --json # JSON outputShows: overall rate, by severity, by category (worst-first), suppressed categories.
Caching & Resumability
Checkpoints saved to ${CLAUDE_PLUGIN_DATA}/bug-review/cache/pr-{N}/:
context.json— after Step 3pass-results.json— after Step 4voted.json— after Step 5validated.json— after Step 6
Cache is invalidated when the PR head commit changes.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| "No PR found" | Wrong number or closed | gh pr list |
| "gh: not authenticated" | Not logged in | gh auth login |
| All passes empty | Well-written code | Normal |
| Too many false positives | Noisy categories | Run /bug-review:resolve after merges to train weights |
| Review fails to post | No write permission | Check gh token: needs repo scope |
| Autofix scope exceeded | Fix too broad | Fix is warned, not applied |
| "Insufficient data for weights" | <10 findings or <3 PRs | Keep reviewing and resolving |
| Slow execution | Large PR | Reduce max_context_files in config.json |
Dismissing a Posted Review
gh api "repos/{owner}/{repo}/pulls/<PR>/reviews" | \
jq '.[] | select(.body | contains("[bug-review]")) | {id, state}'
gh api "repos/{owner}/{repo}/pulls/<PR>/reviews/<REVIEW_ID>/dismissals" \
--method PUT -f message="Dismissed: incorrect findings"#!/usr/bin/env bash
# classify-resolutions.sh — Classify whether findings were resolved at merge time
# Part of: bug-review
# Purpose: The core of the resolution rate feedback loop. Compares code at
# review time vs merge time to determine if flagged bugs were fixed.
#
# Usage: $0 <pr-number>
# Exit codes: 0 = success, 1 = error, 2 = PR not merged or no stored findings
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <pr-number>" >&2
exit 1
fi
PR_NUMBER="$1"
# Load stored findings
STORE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugin-data}/bug-review/findings"
FINDINGS_FILE="$STORE_DIR/pr-${PR_NUMBER}.json"
if [[ ! -f "$FINDINGS_FILE" ]]; then
echo "Error: No stored findings for PR #$PR_NUMBER" >&2
echo "Hint: Run /bug-review on this PR first, then resolve after merge" >&2
exit 2
fi
# Check if PR is merged
MERGE_INFO=$(gh pr view "$PR_NUMBER" --json merged,mergeCommit 2>/dev/null) || {
echo "Error: Could not fetch PR #$PR_NUMBER" >&2
exit 1
}
IS_MERGED=$(echo "$MERGE_INFO" | jq -r '.merged')
if [[ "$IS_MERGED" != "true" ]]; then
echo "PR #$PR_NUMBER is not merged yet. Resolve after merge." >&2
exit 2
fi
MERGE_COMMIT=$(echo "$MERGE_INFO" | jq -r '.mergeCommit.oid')
REVIEW_COMMIT=$(jq -r '.reviewCommit' "$FINDINGS_FILE")
FINDINGS=$(jq '.findings' "$FINDINGS_FILE")
FINDINGS_COUNT=$(echo "$FINDINGS" | jq 'length')
if [[ "$FINDINGS_COUNT" -eq 0 ]]; then
echo "No findings to classify for PR #$PR_NUMBER"
exit 0
fi
echo "Classifying $FINDINGS_COUNT findings for PR #$PR_NUMBER"
echo " Review commit: $REVIEW_COMMIT"
echo " Merge commit: $MERGE_COMMIT"
RESOLUTIONS="[]"
RESOLVED=0
UNRESOLVED=0
INCONCLUSIVE=0
# For each finding, check if the code changed at the finding's location
for i in $(seq 0 $((FINDINGS_COUNT - 1))); do
FINDING=$(echo "$FINDINGS" | jq ".[$i]")
FILE=$(echo "$FINDING" | jq -r '.file')
LINE=$(echo "$FINDING" | jq -r '.line')
TITLE=$(echo "$FINDING" | jq -r '.title')
echo " [$((i+1))/$FINDINGS_COUNT] $TITLE ($FILE:$LINE)"
# Get the diff for this file between review and merge
FILE_DIFF=$(git diff "$REVIEW_COMMIT".."$MERGE_COMMIT" -- "$FILE" 2>/dev/null || echo "")
if [[ -z "$FILE_DIFF" ]]; then
# No changes to this file between review and merge
STATUS="UNRESOLVED"
CONFIDENCE="0.9"
REASONING="File $FILE was not modified between review and merge"
((UNRESOLVED++))
else
# Check if the specific line range was modified
# Parse full hunk ranges: @@ -old,count +new,count @@ → expand to all lines in range
CHANGED_LINES=$(echo "$FILE_DIFF" | grep -oE '@@ -[0-9]+(,[0-9]+)? \+([0-9]+)(,[0-9]+)? @@' | \
sed -E 's/@@ -[0-9]+(,[0-9]+)? \+([0-9]+),?([0-9]*) @@/\2 \3/' | \
while read -r start count; do
count="${count:-1}"
for ((l=start; l<start+count; l++)); do echo "$l"; done
done || true)
LINE_START=$((LINE - 5))
LINE_END=$((LINE + 10))
[[ $LINE_START -lt 1 ]] && LINE_START=1
LINE_CHANGED=false
while IFS= read -r changed_line; do
[[ -z "$changed_line" ]] && continue
if [[ "$changed_line" -ge "$LINE_START" && "$changed_line" -le "$LINE_END" ]]; then
LINE_CHANGED=true
break
fi
done <<< "$CHANGED_LINES"
if $LINE_CHANGED; then
# Code at the finding's location was modified — likely resolved
STATUS="RESOLVED"
CONFIDENCE="0.75"
REASONING="Code at $FILE:$LINE was modified between review ($REVIEW_COMMIT) and merge ($MERGE_COMMIT)"
((RESOLVED++))
else
# File changed but not at this location — inconclusive
STATUS="INCONCLUSIVE"
CONFIDENCE="0.5"
REASONING="File $FILE was modified but not near line $LINE"
((INCONCLUSIVE++))
fi
fi
echo " → $STATUS (confidence: $CONFIDENCE)"
RESOLUTIONS=$(echo "$RESOLUTIONS" | jq \
--arg id "$(echo "$FINDING" | jq -r '.id // ("F" + ('"$i"' | tostring))')" \
--arg status "$STATUS" \
--arg confidence "$CONFIDENCE" \
--arg reasoning "$REASONING" \
--arg merge_commit "$MERGE_COMMIT" \
--arg detected_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
'. + [{
id: $id,
status: $status,
confidence: ($confidence | tonumber),
reasoning: $reasoning,
mergeCommit: $merge_commit,
detectedAt: $detected_at
}]')
done
# Calculate resolution rate
TOTAL=$((RESOLVED + UNRESOLVED + INCONCLUSIVE))
if [[ $TOTAL -gt 0 ]]; then
RATE=$(echo "scale=1; $RESOLVED * 100 / $TOTAL" | bc)
else
RATE="0.0"
fi
# Update the stored findings with resolutions
jq --argjson resolutions "$RESOLUTIONS" \
--arg rate "$RATE" \
--arg merge "$MERGE_COMMIT" \
'.resolutions = {
mergeCommit: $merge,
analyzedAt: (now | todate),
results: $resolutions,
summary: {
total: ($resolutions | length),
resolved: ([$resolutions[] | select(.status == "RESOLVED")] | length),
unresolved: ([$resolutions[] | select(.status == "UNRESOLVED")] | length),
inconclusive: ([$resolutions[] | select(.status == "INCONCLUSIVE")] | length),
resolutionRate: ($rate | tonumber)
}
}' "$FINDINGS_FILE" > "$FINDINGS_FILE.tmp" && mv "$FINDINGS_FILE.tmp" "$FINDINGS_FILE"
echo ""
echo "Resolution Summary for PR #$PR_NUMBER:"
echo " Resolved: $RESOLVED"
echo " Unresolved: $UNRESOLVED"
echo " Inconclusive: $INCONCLUSIVE"
echo " Resolution rate: ${RATE}%"
#!/usr/bin/env bash
# dedup.sh — Find existing [bug-review] comments on a PR for deduplication
# Part of: bug-review
# Purpose: Extracts prior bug-review findings by location (file + line)
# so the main agent can match by proximity, not text similarity.
#
# Usage: $0 <pr-number>
# Output: JSON array of {id, path, line, category, created_at}
# Exit codes: 0 = success (may return empty array)
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <pr-number>" >&2
exit 1
fi
PR_NUMBER="$1"
# Fetch all review comments (inline comments on diff)
REVIEW_COMMENTS=$(gh api "repos/{owner}/{repo}/pulls/$PR_NUMBER/comments" --paginate 2>/dev/null) || {
echo "[]"
exit 0
}
# Filter for [bug-review] tagged comments and extract location + category
# The category is extracted from the comment body pattern "**SEVERITY**: title"
echo "$REVIEW_COMMENTS" | jq '[
.[] | select(.body | contains("[bug-review]")) |
{
id: .id,
path: .path,
line: (.line // .original_line),
category: (
.body | capture("\\*\\*(?<sev>[A-Z]+)\\*\\*: (?<title>.+?)\\n") |
.title // "unknown"
),
severity: (
.body | capture("\\*\\*(?<sev>[A-Z]+)\\*\\*:") | .sev // "UNKNOWN"
),
created_at: .created_at
}
]'
#!/usr/bin/env bash
# fetch-pr.sh — Fetch PR diff and metadata as JSON
# Part of: bug-review
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <pr-number-or-url>" >&2
echo " Accepts: PR number (42), URL (https://github.com/.../pull/42), or branch name" >&2
exit 1
fi
PR_INPUT="$1"
# Verify gh is authenticated before any gh calls
if ! gh auth status >/dev/null 2>&1; then
echo "Error: gh CLI is not authenticated" >&2
echo "Hint: Run 'gh auth login' to authenticate" >&2
exit 1
fi
# Resolve PR number from various input formats
if [[ "$PR_INPUT" =~ ^https://github\.com/.*/pull/([0-9]+) ]]; then
PR_NUMBER="${BASH_REMATCH[1]}"
elif [[ "$PR_INPUT" =~ ^[0-9]+$ ]]; then
PR_NUMBER="$PR_INPUT"
else
# Try to resolve branch name to PR number
PR_NUMBER=$(gh pr view "$PR_INPUT" --json number --jq '.number' 2>/dev/null) || {
echo "Error: Could not find an open PR for '$PR_INPUT'" >&2
echo "Hint: Check that the PR exists and is open with 'gh pr list'" >&2
exit 1
}
fi
# Fetch PR metadata (use 'files' for file paths, 'changedFiles' is just a count)
PR_META=$(gh pr view "$PR_NUMBER" --json number,title,body,baseRefName,headRefName,files,additions,deletions 2>/dev/null) || {
echo "Error: Could not fetch PR #$PR_NUMBER" >&2
echo "Hint: Make sure the PR exists and you have access to the repository" >&2
exit 1
}
# Fetch PR diff into a temp file (avoids ARG_MAX limits on large PRs)
DIFF_TMP=$(mktemp)
trap 'rm -f "$DIFF_TMP"' EXIT
gh pr diff "$PR_NUMBER" > "$DIFF_TMP" 2>/dev/null || {
echo "Error: Could not fetch diff for PR #$PR_NUMBER" >&2
exit 1
}
# Combine metadata and diff into a single JSON output
echo "$PR_META" | jq --rawfile diff "$DIFF_TMP" '. + {diff: $diff}'
#!/usr/bin/env bash
# gather-context.sh — Priority-based context gathering for code review
# Part of: bug-review
# Purpose: Intelligently gather surrounding code context for review passes.
# Prioritizes callers, types, and tests of modified functions.
#
# Usage: $0 <changed-files-json> [max-files]
# changed-files-json: JSON array of file paths or gh pr files output
# max-files: max context files beyond changed files (default: 15)
#
# Output: JSON with {files: [{path, relevance, reason}]}
# Exit codes: 0 = success, 1 = error
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <changed-files-json> [max-files]" >&2
echo " changed-files-json: JSON file with array of {path} objects or string paths" >&2
exit 1
fi
FILES_JSON="$1"
MAX_FILES="${2:-15}"
BUDGET="$MAX_FILES"
if [[ ! -f "$FILES_JSON" ]]; then
echo "Error: File not found: $FILES_JSON" >&2
exit 1
fi
# Extract file paths from JSON (handles both [{path:"x"}] and ["x"] formats)
CHANGED_FILES=$(jq -r '
if type == "array" then
.[] | if type == "object" then .path else . end
else empty end
' "$FILES_JSON" 2>/dev/null)
if [[ -z "$CHANGED_FILES" ]]; then
echo '{"files":[],"stats":{"changed":0,"callers":0,"types":0,"tests":0,"total":0}}'
exit 0
fi
# Output accumulator
CONTEXT_FILES="[]"
CALLER_COUNT=0
TYPE_COUNT=0
TEST_COUNT=0
# Skip patterns
SKIP_PATTERN="node_modules/|vendor/|\.generated\.|dist/|build/|\.min\.|__pycache__"
# --- Priority 1: Extract modified function names from changed files ---
MODIFIED_FUNCTIONS=""
while IFS= read -r file; do
[[ -f "$file" ]] || continue
# Extract function/method names using common patterns
# Handles: function foo, const foo =, export function foo, def foo, func foo
FUNCS=$(grep -nE '(function\s+\w+|const\s+\w+\s*=\s*(async\s+)?\(|export\s+(async\s+)?function\s+\w+|def\s+\w+|func\s+\w+)' "$file" 2>/dev/null | \
sed -E 's/.*function\s+(\w+).*/\1/; s/.*const\s+(\w+).*/\1/; s/.*def\s+(\w+).*/\1/; s/.*func\s+(\w+).*/\1/' | \
sort -u || true)
if [[ -n "$FUNCS" ]]; then
MODIFIED_FUNCTIONS="$MODIFIED_FUNCTIONS"$'\n'"$FUNCS"
fi
done <<< "$CHANGED_FILES"
MODIFIED_FUNCTIONS=$(echo "$MODIFIED_FUNCTIONS" | sort -u | grep -v '^$' || true)
# --- Priority 2: Find callers of modified functions (up to 5) ---
if [[ -n "$MODIFIED_FUNCTIONS" && $BUDGET -gt 0 ]]; then
CALLER_LIMIT=$((BUDGET < 5 ? BUDGET : 5))
while IFS= read -r func; do
[[ $CALLER_COUNT -ge $CALLER_LIMIT ]] && break
[[ -z "$func" ]] && continue
# Search for callers, excluding changed files themselves and skip patterns
CALLERS=$(grep -rlE "\b${func}\b" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.py" --include="*.go" --include="*.rs" --include="*.java" . 2>/dev/null | \
grep -vE "$SKIP_PATTERN" | \
while IFS= read -r caller; do
# Exclude the changed files themselves
IS_CHANGED=false
while IFS= read -r cf; do
[[ "$caller" == "./$cf" || "$caller" == "$cf" ]] && IS_CHANGED=true
done <<< "$CHANGED_FILES"
$IS_CHANGED || echo "$caller"
done | head -"$CALLER_LIMIT" || true)
while IFS= read -r caller; do
[[ -z "$caller" ]] && continue
[[ $CALLER_COUNT -ge $CALLER_LIMIT ]] && break
CONTEXT_FILES=$(echo "$CONTEXT_FILES" | jq --arg path "$caller" --arg reason "Calls $func" \
'. + [{"path": $path, "relevance": "caller", "reason": $reason}]')
((CALLER_COUNT++))
((BUDGET--))
done <<< "$CALLERS"
done <<< "$MODIFIED_FUNCTIONS"
fi
# --- Priority 3: Find type definitions imported by changed files (up to 3) ---
if [[ $BUDGET -gt 0 ]]; then
TYPE_LIMIT=$((BUDGET < 3 ? BUDGET : 3))
while IFS= read -r file; do
[[ $TYPE_COUNT -ge $TYPE_LIMIT ]] && break
[[ -f "$file" ]] || continue
# Extract import paths (handles: import {X} from './path', from path import X)
IMPORTS=$(grep -oE "(from\s+['\"]\.?\.?/[^'\"]+['\"]|import\s+['\"]\.?\.?/[^'\"]+['\"])" "$file" 2>/dev/null | \
sed -E "s/(from|import)\s+['\"]//; s/['\"]$//" | \
grep -vE "$SKIP_PATTERN" || true)
while IFS= read -r imp; do
[[ -z "$imp" ]] && continue
[[ $TYPE_COUNT -ge $TYPE_LIMIT ]] && break
# Resolve relative import to a file path
DIR=$(dirname "$file")
for ext in "" ".ts" ".tsx" ".js" ".jsx" "/index.ts" "/index.js"; do
RESOLVED="${DIR}/${imp}${ext}"
if [[ -f "$RESOLVED" ]]; then
# Check if it contains type definitions
if grep -qE "(interface\s|type\s|enum\s|class\s)" "$RESOLVED" 2>/dev/null; then
CONTEXT_FILES=$(echo "$CONTEXT_FILES" | jq --arg path "$RESOLVED" --arg reason "Types imported by $file" \
'. + [{"path": $path, "relevance": "type-definition", "reason": $reason}]')
((TYPE_COUNT++))
((BUDGET--))
fi
break
fi
done
done <<< "$IMPORTS"
done <<< "$CHANGED_FILES"
fi
# --- Priority 4: Find test files for changed modules (up to 3) ---
if [[ $BUDGET -gt 0 ]]; then
TEST_LIMIT=$((BUDGET < 3 ? BUDGET : 3))
while IFS= read -r file; do
[[ $TEST_COUNT -ge $TEST_LIMIT ]] && break
[[ -f "$file" ]] || continue
BASENAME=$(basename "$file" | sed -E 's/\.[^.]+$//')
DIR=$(dirname "$file")
# Search for test files matching the changed file name
for pattern in "${DIR}/${BASENAME}.test."* "${DIR}/${BASENAME}.spec."* "${DIR}/__tests__/${BASENAME}."* "${DIR}/${BASENAME}_test."*; do
[[ $TEST_COUNT -ge $TEST_LIMIT ]] && break
for testfile in $pattern; do
[[ -f "$testfile" ]] || continue
CONTEXT_FILES=$(echo "$CONTEXT_FILES" | jq --arg path "$testfile" --arg reason "Tests for $file" \
'. + [{"path": $path, "relevance": "test", "reason": $reason}]')
((TEST_COUNT++))
((BUDGET--))
break
done
done
done <<< "$CHANGED_FILES"
fi
# --- Priority 5: Check for .bug-review.md ---
if [[ -f ".bug-review.md" && $BUDGET -gt 0 ]]; then
CONTEXT_FILES=$(echo "$CONTEXT_FILES" | jq \
'. + [{"path": ".bug-review.md", "relevance": "repo-rules", "reason": "Repository-specific review rules"}]')
((BUDGET--))
fi
# Deduplicate by path
CONTEXT_FILES=$(echo "$CONTEXT_FILES" | jq 'unique_by(.path)')
CHANGED_COUNT=$(echo "$CHANGED_FILES" | wc -l | tr -d ' ')
TOTAL=$(echo "$CONTEXT_FILES" | jq 'length')
# Output final JSON
jq -n \
--argjson files "$CONTEXT_FILES" \
--arg changed "$CHANGED_COUNT" \
--arg callers "$CALLER_COUNT" \
--arg types "$TYPE_COUNT" \
--arg tests "$TEST_COUNT" \
--argjson total "$TOTAL" \
'{
files: $files,
stats: {
changed_files: ($changed | tonumber),
callers_found: ($callers | tonumber),
type_defs_found: ($types | tonumber),
test_files_found: ($tests | tonumber),
total_context_files: $total
}
}'
#!/usr/bin/env bash
# post-review.sh — Post findings as a GitHub PR review with inline comments
# Part of: bug-review
# Exit codes: 0 = review posted, 1 = error, 2 = no findings to post (skipped)
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <pr-number> <findings-json-file>" >&2
echo " findings-json-file: path to JSON file with array of findings" >&2
exit 1
fi
PR_NUMBER="$1"
FINDINGS_FILE="$2"
if [[ ! -f "$FINDINGS_FILE" ]]; then
echo "Error: Findings file not found: $FINDINGS_FILE" >&2
exit 1
fi
FINDINGS_COUNT=$(jq 'length' "$FINDINGS_FILE")
if [[ "$FINDINGS_COUNT" -eq 0 ]]; then
echo "No findings to post. Skipping review."
exit 2
fi
# Get the latest commit SHA on the PR (required for creating review)
COMMIT_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid')
# Build severity summary
CRITICAL_COUNT=$(jq '[.[] | select(.severity == "CRITICAL")] | length' "$FINDINGS_FILE")
HIGH_COUNT=$(jq '[.[] | select(.severity == "HIGH")] | length' "$FINDINGS_FILE")
MEDIUM_COUNT=$(jq '[.[] | select(.severity == "MEDIUM")] | length' "$FINDINGS_FILE")
LOW_COUNT=$(jq '[.[] | select(.severity == "LOW")] | length' "$FINDINGS_FILE")
REVIEW_BODY="## Bug Review Summary
Found **${FINDINGS_COUNT}** issue(s): ${CRITICAL_COUNT} critical, ${HIGH_COUNT} high, ${MEDIUM_COUNT} medium, ${LOW_COUNT} low.
<!-- [bug-review] automated review -->"
# Build inline comments from findings
COMMENTS=$(jq -c '[.[] | {
path: .file,
line: .line,
body: (
"**" + .severity + "**: " + .title + "\n\n" +
.description + "\n\n" +
"**Trigger scenario**: " + .triggerScenario + "\n\n" +
(if .suggestedFix then ("**Suggested fix**: " + .suggestedFix + "\n\n") else "" end) +
"<!-- [bug-review] -->"
)
}]' "$FINDINGS_FILE")
# Create the review via GitHub API
PAYLOAD=$(jq -n \
--arg body "$REVIEW_BODY" \
--arg sha "$COMMIT_SHA" \
--argjson comments "$COMMENTS" \
'{
commit_id: $sha,
body: $body,
event: "COMMENT",
comments: $comments
}')
echo "$PAYLOAD" | gh api "repos/{owner}/{repo}/pulls/$PR_NUMBER/reviews" \
--input - \
--method POST >/dev/null 2>&1 || {
echo "Error: Failed to post review on PR #$PR_NUMBER" >&2
echo "Hint: Check that your gh token has write permission on the repository" >&2
exit 1
}
echo "Posted review with $FINDINGS_COUNT finding(s) on PR #$PR_NUMBER"
#!/usr/bin/env bash
# resolution-report.sh — Aggregate resolution rate statistics
# Part of: bug-review
# Purpose: Generates resolution rate report across all tracked PRs.
# Shows overall rate, by severity, by category, and trends.
#
# Usage: $0 [--json]
# --json: output JSON instead of markdown
# Exit codes: 0 = success, 1 = error, 2 = no data
set -euo pipefail
FORMAT="markdown"
[[ "${1:-}" == "--json" ]] && FORMAT="json"
STORE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugin-data}/bug-review/findings"
if [[ ! -d "$STORE_DIR" ]]; then
echo "No bug-review data found. Run /bug-review on a PR first." >&2
exit 2
fi
# Collect all PR files that have resolutions
RESOLVED_FILES=$(find "$STORE_DIR" -name "pr-*.json" -exec grep -l '"resolutions"' {} + 2>/dev/null || true)
if [[ -z "$RESOLVED_FILES" ]]; then
echo "No resolution data found. Run /bug-review:resolve on a merged PR first." >&2
exit 2
fi
# Aggregate all resolution data
TOTAL_FINDINGS=0
TOTAL_RESOLVED=0
TOTAL_UNRESOLVED=0
TOTAL_INCONCLUSIVE=0
PRS_ANALYZED=0
# Per-severity and per-category accumulators (stored as temp files)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
while IFS= read -r file; do
[[ -f "$file" ]] || continue
HAS_RESOLUTIONS=$(jq 'has("resolutions") and .resolutions != null and .resolutions.results != null' "$file" 2>/dev/null)
[[ "$HAS_RESOLUTIONS" == "true" ]] || continue
((PRS_ANALYZED++))
# Extract findings with their resolutions
jq -c '.findings as $findings | .resolutions.results[] as $res |
($findings | to_entries[] | select(
(.value.id // ("F" + (.key | tostring))) == $res.id
) | .value) as $finding |
{
severity: ($finding.severity // "UNKNOWN"),
category: ($finding.category // "unknown"),
status: $res.status
}' "$file" 2>/dev/null >> "$TMPDIR/all_resolutions.jsonl" || true
# Count per-status
R=$(jq '.resolutions.summary.resolved // 0' "$file")
U=$(jq '.resolutions.summary.unresolved // 0' "$file")
I=$(jq '.resolutions.summary.inconclusive // 0' "$file")
T=$(jq '.resolutions.summary.total // 0' "$file")
TOTAL_FINDINGS=$((TOTAL_FINDINGS + T))
TOTAL_RESOLVED=$((TOTAL_RESOLVED + R))
TOTAL_UNRESOLVED=$((TOTAL_UNRESOLVED + U))
TOTAL_INCONCLUSIVE=$((TOTAL_INCONCLUSIVE + I))
done <<< "$RESOLVED_FILES"
# Calculate overall rate
if [[ $TOTAL_FINDINGS -gt 0 ]]; then
OVERALL_RATE=$(echo "scale=1; $TOTAL_RESOLVED * 100 / $TOTAL_FINDINGS" | bc)
else
OVERALL_RATE="0.0"
fi
# Calculate per-severity rates
SEVERITY_REPORT="[]"
for severity in CRITICAL HIGH MEDIUM LOW; do
SEV_TOTAL=$(grep -c "\"severity\":\"$severity\"" "$TMPDIR/all_resolutions.jsonl" 2>/dev/null || echo "0")
SEV_RESOLVED=$(grep "\"severity\":\"$severity\"" "$TMPDIR/all_resolutions.jsonl" 2>/dev/null | grep -c '"status":"RESOLVED"' || echo "0")
if [[ "$SEV_TOTAL" -gt 0 ]]; then
SEV_RATE=$(echo "scale=1; $SEV_RESOLVED * 100 / $SEV_TOTAL" | bc)
else
SEV_RATE="0.0"
fi
SEVERITY_REPORT=$(echo "$SEVERITY_REPORT" | jq \
--arg sev "$severity" --arg total "$SEV_TOTAL" \
--arg resolved "$SEV_RESOLVED" --arg rate "$SEV_RATE" \
'. + [{"severity": $sev, "total": ($total|tonumber), "resolved": ($resolved|tonumber), "rate": ($rate|tonumber)}]')
done
# Calculate per-category rates
CATEGORY_REPORT="[]"
if [[ -f "$TMPDIR/all_resolutions.jsonl" ]]; then
CATEGORIES=$(jq -r '.category' "$TMPDIR/all_resolutions.jsonl" 2>/dev/null | sort -u || true)
while IFS= read -r cat; do
[[ -z "$cat" ]] && continue
CAT_TOTAL=$(grep -c "\"category\":\"$cat\"" "$TMPDIR/all_resolutions.jsonl" 2>/dev/null || echo "0")
CAT_RESOLVED=$(grep "\"category\":\"$cat\"" "$TMPDIR/all_resolutions.jsonl" 2>/dev/null | grep -c '"status":"RESOLVED"' || echo "0")
if [[ "$CAT_TOTAL" -gt 0 ]]; then
CAT_RATE=$(echo "scale=1; $CAT_RESOLVED * 100 / $CAT_TOTAL" | bc)
else
CAT_RATE="0.0"
fi
CATEGORY_REPORT=$(echo "$CATEGORY_REPORT" | jq \
--arg cat "$cat" --arg total "$CAT_TOTAL" \
--arg resolved "$CAT_RESOLVED" --arg rate "$CAT_RATE" \
'. + [{"category": $cat, "total": ($total|tonumber), "resolved": ($resolved|tonumber), "rate": ($rate|tonumber)}]')
done <<< "$CATEGORIES"
fi
# Sort categories by rate (ascending — worst performers first)
CATEGORY_REPORT=$(echo "$CATEGORY_REPORT" | jq 'sort_by(.rate)')
if [[ "$FORMAT" == "json" ]]; then
jq -n \
--arg prs "$PRS_ANALYZED" \
--arg total "$TOTAL_FINDINGS" \
--arg resolved "$TOTAL_RESOLVED" \
--arg unresolved "$TOTAL_UNRESOLVED" \
--arg inconclusive "$TOTAL_INCONCLUSIVE" \
--arg rate "$OVERALL_RATE" \
--argjson severity "$SEVERITY_REPORT" \
--argjson category "$CATEGORY_REPORT" \
'{
prs_analyzed: ($prs|tonumber),
overall: {
total: ($total|tonumber),
resolved: ($resolved|tonumber),
unresolved: ($unresolved|tonumber),
inconclusive: ($inconclusive|tonumber),
resolution_rate: ($rate|tonumber)
},
by_severity: $severity,
by_category: $category
}'
else
echo "# Bug Review Resolution Report"
echo ""
echo "**PRs analyzed:** $PRS_ANALYZED"
echo "**Overall resolution rate:** ${OVERALL_RATE}%"
echo ""
echo "| Metric | Count |"
echo "|--------|-------|"
echo "| Total findings | $TOTAL_FINDINGS |"
echo "| Resolved | $TOTAL_RESOLVED |"
echo "| Unresolved | $TOTAL_UNRESOLVED |"
echo "| Inconclusive | $TOTAL_INCONCLUSIVE |"
echo ""
echo "## By Severity"
echo ""
echo "| Severity | Total | Resolved | Rate |"
echo "|----------|-------|----------|------|"
echo "$SEVERITY_REPORT" | jq -r '.[] | "| \(.severity) | \(.total) | \(.resolved) | \(.rate)% |"'
echo ""
echo "## By Category"
echo ""
echo "| Category | Total | Resolved | Rate |"
echo "|----------|-------|----------|------|"
echo "$CATEGORY_REPORT" | jq -r '.[] | "| \(.category) | \(.total) | \(.resolved) | \(.rate)% |"'
fi
#!/usr/bin/env bash
# shuffle-diff.sh — Randomize file ordering in a unified diff
# Part of: bug-review
# Purpose: Each review pass gets a different file ordering to create
# attention diversity. Earlier files in a diff get more careful
# review, so shuffling forces different bugs to the "top."
#
# Usage: $0 <seed> < input.diff > shuffled.diff
# Exit codes: 0 = success, 1 = error, 2 = empty diff
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <seed>" >&2
echo " Reads unified diff from stdin, outputs shuffled diff to stdout" >&2
echo " seed: integer seed for deterministic shuffling (use pass number)" >&2
exit 1
fi
SEED="$1"
# Read entire diff from stdin
DIFF=$(cat)
if [[ -z "$DIFF" ]]; then
echo "Error: Empty diff on stdin" >&2
exit 2
fi
# Split diff into per-file chunks using "diff --git" as delimiter
# Store each chunk in a temp directory as a numbered file
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
CHUNK_NUM=0
CURRENT_CHUNK=""
while IFS= read -r line; do
if [[ "$line" =~ ^diff\ --git ]]; then
# Save previous chunk if exists
if [[ -n "$CURRENT_CHUNK" ]]; then
printf '%s\n' "$CURRENT_CHUNK" > "$TMPDIR/chunk_$(printf '%04d' $CHUNK_NUM)"
((CHUNK_NUM++))
fi
CURRENT_CHUNK="$line"
else
if [[ -n "$CURRENT_CHUNK" ]]; then
CURRENT_CHUNK="$CURRENT_CHUNK"$'\n'"$line"
fi
fi
done <<< "$DIFF"
# Save last chunk
if [[ -n "$CURRENT_CHUNK" ]]; then
printf '%s\n' "$CURRENT_CHUNK" > "$TMPDIR/chunk_$(printf '%04d' $CHUNK_NUM)"
((CHUNK_NUM++))
fi
if [[ $CHUNK_NUM -eq 0 ]]; then
# Not a git diff format — try splitting on "---" lines (plain unified diff)
echo "$DIFF"
exit 0
fi
# Generate a shuffled order using the seed
# Use awk with seeded random to create a permutation
ls "$TMPDIR"/chunk_* 2>/dev/null | awk -v seed="$SEED" '
BEGIN { srand(seed) }
{ print rand() "\t" $0 }
' | sort -k1,1n | cut -f2- | while IFS= read -r chunk_file; do
cat "$chunk_file"
done
#!/usr/bin/env bash
# store-findings.sh — Persist findings to durable storage for resolution tracking
# Part of: bug-review
# Purpose: Save review findings so we can later measure whether they were
# resolved at merge time (the resolution rate feedback loop).
#
# Usage: $0 <pr-number> <findings-json-file> <review-commit>
# Exit codes: 0 = success, 1 = error
set -euo pipefail
if [[ $# -lt 3 ]]; then
echo "Usage: $0 <pr-number> <findings-json-file> <review-commit>" >&2
echo " Stores findings to \${CLAUDE_PLUGIN_DATA}/bug-review/findings/" >&2
exit 1
fi
PR_NUMBER="$1"
FINDINGS_FILE="$2"
REVIEW_COMMIT="$3"
if [[ ! -f "$FINDINGS_FILE" ]]; then
echo "Error: Findings file not found: $FINDINGS_FILE" >&2
exit 1
fi
# Determine storage directory
STORE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugin-data}/bug-review/findings"
mkdir -p "$STORE_DIR"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Get repo info for context
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "unknown")
# Read findings and wrap with metadata
FINDINGS=$(jq '
if type == "array" then .
elif .findings then .findings
else []
end
' "$FINDINGS_FILE")
FINDINGS_COUNT=$(echo "$FINDINGS" | jq 'length')
# Build the stored record
jq -n \
--arg pr "$PR_NUMBER" \
--arg repo "$REPO" \
--arg commit "$REVIEW_COMMIT" \
--arg timestamp "$TIMESTAMP" \
--argjson count "$FINDINGS_COUNT" \
--argjson findings "$FINDINGS" \
'{
pr: ($pr | tonumber),
repo: $repo,
reviewCommit: $commit,
postedAt: $timestamp,
findingsCount: $count,
findings: $findings,
resolutions: null
}' > "$STORE_DIR/pr-${PR_NUMBER}.json"
echo "Stored $FINDINGS_COUNT findings for PR #$PR_NUMBER at $STORE_DIR/pr-${PR_NUMBER}.json"
#!/usr/bin/env bash
# update-weights.sh — Learn category weights from resolution rate data
# Part of: bug-review
# Purpose: The optimization loop. Categories with low resolution rates
# (findings that developers don't fix) are likely false-positive-heavy.
# This script adjusts category_weights in config.json based on actual
# resolution data, so future reviews deprioritize noisy categories.
#
# Usage: $0 [--dry-run] [config-path]
# --dry-run: show the proposed weight changes without modifying config.json
# config-path: path to config.json (default: auto-detect from skill dir)
# Exit codes: 0 = success, 1 = error, 2 = insufficient data
set -euo pipefail
SCRIPT_DIR=$(dirname "$0")
DRY_RUN=0
CONFIG_PATH=""
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
*) CONFIG_PATH="$arg" ;;
esac
done
CONFIG_PATH="${CONFIG_PATH:-$SCRIPT_DIR/../config.json}"
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "Error: config.json not found at $CONFIG_PATH" >&2
exit 1
fi
# Get resolution report as JSON
REPORT=$(bash "$SCRIPT_DIR/resolution-report.sh" --json 2>/dev/null) || {
echo "Error: Could not generate resolution report. Need at least 1 resolved PR." >&2
exit 2
}
TOTAL_FINDINGS=$(echo "$REPORT" | jq '.overall.total')
PRS_ANALYZED=$(echo "$REPORT" | jq '.prs_analyzed')
# Require minimum data before adjusting weights
MIN_FINDINGS=10
MIN_PRS=3
if [[ "$TOTAL_FINDINGS" -lt "$MIN_FINDINGS" || "$PRS_ANALYZED" -lt "$MIN_PRS" ]]; then
echo "Insufficient data for weight learning (need $MIN_FINDINGS+ findings across $MIN_PRS+ PRs)." >&2
echo " Current: $TOTAL_FINDINGS findings across $PRS_ANALYZED PRs" >&2
exit 2
fi
# Compute weights from category resolution rates
# Weight = resolution_rate / 100, clamped to [0.1, 1.0]
# Categories below 30% resolution rate get minimum weight (0.1)
CATEGORY_WEIGHTS=$(echo "$REPORT" | jq '
.by_category | map({
key: .category,
value: (
if .total < 3 then 1.0 # Not enough data, keep default
elif .rate < 30 then 0.1 # Likely false-positive-heavy, suppress
else (.rate / 100) # Scale 0-100% to 0-1.0
end
)
}) | from_entries
')
echo "Category weights computed from $TOTAL_FINDINGS findings across $PRS_ANALYZED PRs:"
echo "$CATEGORY_WEIGHTS" | jq -r 'to_entries[] | " \(.key): \(.value)"'
# Show the change (current -> proposed) so the effect is reviewable before applying.
echo ""
echo "Proposed category_weights change (current -> new):"
jq -n \
--argjson cur "$(jq '.category_weights // {}' "$CONFIG_PATH")" \
--argjson new "$CATEGORY_WEIGHTS" '
(($cur + $new) | keys_unsorted)
| map({ key: ., value: { old: ($cur[.] // "default"), new: ($new[.] // "unchanged") } })
| from_entries' \
| jq -r 'to_entries[] | " \(.key): \(.value.old) -> \(.value.new)"'
if [[ "$DRY_RUN" -eq 1 ]]; then
echo ""
echo "Dry run — no changes written. Re-run without --dry-run to apply."
exit 0
fi
# Back up before overwriting, then update config.json with new weights.
cp "$CONFIG_PATH" "${CONFIG_PATH}.bak"
jq --argjson weights "$CATEGORY_WEIGHTS" '.category_weights = $weights' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" \
&& mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH"
echo ""
echo "Updated $CONFIG_PATH with learned category weights (backup: ${CONFIG_PATH}.bak)."
# Flag suppressed categories
SUPPRESSED=$(echo "$CATEGORY_WEIGHTS" | jq -r 'to_entries[] | select(.value <= 0.1) | .key')
if [[ -n "$SUPPRESSED" ]]; then
echo ""
echo "WARNING: These categories have been suppressed (resolution rate < 30%):"
while IFS= read -r cat; do
RATE=$(echo "$REPORT" | jq -r --arg c "$cat" '.by_category[] | select(.category == $c) | .rate')
echo " - $cat (${RATE}% resolution rate)"
done <<< "$SUPPRESSED"
echo " They will have minimal impact on future review scoring."
fi
#!/usr/bin/env bash
# verify.sh — Verify bug review was posted successfully
# Part of: bug-review
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <pr-number>" >&2
exit 1
fi
PR_NUMBER="$1"
PASS=0
FAIL=0
assert_true() {
local label="$1" condition="$2"
if [[ "$condition" == "true" ]]; then
echo " PASS: $label"
((PASS++))
else
echo " FAIL: $label"
((FAIL++))
fi
}
# Check that at least one [bug-review] review exists on the PR
REVIEWS_RAW=$(gh api "repos/{owner}/{repo}/pulls/$PR_NUMBER/reviews" --paginate 2>&1) || {
echo " ERROR: Could not fetch reviews — API call failed: $REVIEWS_RAW" >&2
exit 1
}
REVIEW_COUNT=$(echo "$REVIEWS_RAW" | jq '[.[] | select(.body | contains("[bug-review]"))] | length')
assert_true "At least one [bug-review] review exists" \
"$([ "$REVIEW_COUNT" -gt 0 ] && echo true || echo false)"
# Check that review comments exist
COMMENTS_RAW=$(gh api "repos/{owner}/{repo}/pulls/$PR_NUMBER/comments" --paginate 2>&1) || {
echo " ERROR: Could not fetch comments — API call failed: $COMMENTS_RAW" >&2
exit 1
}
COMMENT_COUNT=$(echo "$COMMENTS_RAW" | jq '[.[] | select(.body | contains("[bug-review]"))] | length')
assert_true "Review has inline comments" \
"$([ "$COMMENT_COUNT" -gt 0 ] && echo true || echo false)"
echo ""
echo "Results: $PASS passed, $FAIL failed"
echo "Reviews: $REVIEW_COUNT, Inline comments: $COMMENT_COUNT"
[[ $FAIL -eq 0 ]] || exit 1
Related skills
FAQ
What does bug-review do?
bug-review: A skill for development. This provides functionality for development workflows.
When should I use bug-review?
When you need to use bug-review for development tasks, or when bug-review: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
bug-review.