Examples
Copy-paste integrations for the four places this filter lives in real projects: a GitHub PR webhook, an Octokit fetch, a GitHub Action step, and a one-shot CI script.
GitHub PR webhook handler
Map GitHub's pull_request.synchronize payload into FileInput. The status field is the one that needs translating —
everything else maps 1:1.
import { classifyPrFiles, type ChangeType } from '@prcompass/pr-triage-filter';
import type { Octokit } from '@octokit/rest';
const STATUS_TO_CHANGE_TYPE: Record<string, ChangeType> = {
added: 'added',
modified: 'modified',
removed: 'deleted',
renamed: 'renamed',
changed: 'modified',
copied: 'added'
};
export async function triagePr(
octokit: Octokit,
owner: string,
repo: string,
pull_number: number
) {
const files = await octokit.paginate(octokit.rest.pulls.listFiles, {
owner, repo, pull_number, per_page: 100
});
const { verdicts } = classifyPrFiles({
files: files.map((f) => ({
path: f.filename,
previousPath: f.previous_filename,
changeType: STATUS_TO_CHANGE_TYPE[f.status] ?? 'modified',
additions: f.additions,
deletions: f.deletions,
patch: f.patch
}))
});
return verdicts;
}
Bucket the verdicts before passing to the next tier
The whole point of triage is to shrink the input set passed to a more expensive analyzer. Group by verdict and forward only what survives.
const { verdicts } = classifyPrFiles({ files });
const skipped = verdicts.filter((v) => v.verdict === 'skip');
const skimmed = verdicts.filter((v) => v.verdict === 'skim');
const reviewable = verdicts.filter((v) => v.verdict === 'review-candidate');
console.log(`triage: ${skipped.length} skip / ${skimmed.length} skim / ${reviewable.length} review`);
console.log(`saved ${Math.round((skipped.length / verdicts.length) * 100)}% of files`);
// Pass only review-candidates to the LLM tier.
await runTier2(reviewable.map((v) => v.path));
GitHub Action — comment a triage summary
A minimal action that runs the filter on every PR sync and posts a collapsed summary as a sticky comment. Useful as a "what did the bot hide from me" sanity check during rollout.
# .github/workflows/triage.yml
name: PR triage
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm install --no-save @prcompass/pr-triage-filter @octokit/rest
- run: node .github/scripts/triage.mjs
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
Branch on a stable rule ID
ruleId is part of the public contract — pattern-matching on
it is safe across minor versions. reason is not — it's
reworded between versions. Use the right field for the right job.
// OK — ruleId is a stable identifier.
for (const v of verdicts) {
if (v.ruleId === 'lockfile' || v.ruleId === 'generated-path') {
metrics.increment('triage.skipped.generated');
}
}
// BAD — reason is human-readable, not UI-stable.
if (v.reason === 'Package lockfile — content is auto-generated') {
// …will silently break when the wording changes.
}
Local diff — using the @prcompass/cli wrapper
The companion CLI runs this filter (alongside the rest of the deterministic pipeline) over a local git diff. No GitHub round-trip needed during dev.
npx @prcompass/cli analyze --repo . --diff main..HEAD --format human
# JSON for piping into another tool:
npx @prcompass/cli analyze --repo . --diff HEAD~1 | jq '.triage.verdicts'
Ready to integrate?
The whole API is one function. Two minutes to install, an afternoon to be sure of the verdicts on your repo's PRs.