pr-brief
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
PR description gate and skill: brief, Input→Functions→Output diagrams, review guide
pr-brief
PR descriptions a reviewer can read: a brief, a flow diagram, a reading order.
Website · Install · Convention over configuration · Themes · The gate
Large pull requests are hard to review because nobody knows where to start. pr-brief writes the description for you, and a gate keeps every PR that way. It is a Claude Code plugin, a GitHub Action and a Go CLI. The PR title is never touched. Only the description changes.
Install
Requirements
pr-brief calls two other pieces of software. Claude Code does not install them for you. Install them first, or the skill stops and tells you what is missing. It never switches style on its own.
| Needs | What it is | Install | Required by style |
|---|---|---|---|
| asd-ste100 | A skill: the ASD-STE100 writing rules | git clone https://github.com/danyuchn/asd-ste100-skill ~/.claude/skills/asd-ste100 |
ste, ste+iceberg (the default) |
| iceberg | A plugin: the Hemingway-style editor (iceberg:edit) |
/plugin marketplace add gagoar/gago-plugins, then /plugin install iceberg@gago-plugins |
iceberg, ste+iceberg (the default) |
To need only one of them, change the style: /pr-brief config style iceberg needs only iceberg, and ste needs only asd-ste100. The CI check and the CLI need neither.
The plugin
/plugin marketplace add gagoar/gago-plugins
/plugin install pr-brief@gago-plugins
Or straight from this repo: /plugin marketplace add gagoar/pr-brief, then /plugin install pr-brief@pr-brief.
Then, on any branch, run /pr-brief. To rewrite a PR that exists, run /pr-brief improve 123. It saves the old description first and asks before it writes.
What you get
- Brief. One paragraph. What the system does differently now, and the effect. No file names.
- Change map. One Input → Functions → Output diagram for each flow, drawn by the tool in a theme. The Input and Output boxes show the word and a number (
Input 1,Output 1); a References table explains them, so the diagram stays small. Function boxes show names only, with no parentheses. A Legend line directly under the diagram names every colour it uses, in one row. - Review guide. What changed, a ranked table of the files that carry logic and risk, and where to start reading. Each file in that table is a link to its diff in the PR.
A rewrite never loses a Jira or Linear ticket. /pr-brief improve reads every ticket in the old description (links, bare keys such as ABC-123, and words such as Closes ENG-45) and in the branch name, and keeps them in a **Tickets:** line above the markers. It refuses to write if one would be lost. For a new PR, pr-brief tickets does the same from the branch name. The gate warns when a ticket in the branch name is missing from the description. What counts as a ticket is the tickets.pattern setting. The default is a guess at keys such as ABC-123. It can match a word such as lodash-4, so name your projects: pr-brief config set tickets.pattern "(PAY|OPS|ENG)-[0-9]+" --scope repo. The pattern may also match Azure DevOps work items (AB#[0-9]+) or GitHub issues (#[0-9]+).
%%{init: {"theme":"base","flowchart":{"curve":"step","nodeSpacing":28,"rankSpacing":48,"diagramPadding":40},"themeVariables":{"darkMode":true,"background":"#0d1117","fontFamily":"Inter, Helvetica, Arial","fontSize":"13px","dropShadow":"none","primaryColor":"#14181e","primaryTextColor":"#e6edf3","primaryBorderColor":"#383d43","nodeTextColor":"#e6edf3","textColor":"#e6edf3","mainBkg":"#14181e","nodeBorder":"#383d43","lineColor":"#3d444d","clusterBkg":"#0d1117","clusterBorder":"#383d43","titleColor":"#9198a1","edgeLabelBackground":"#0d1117"}}}%%
graph LR
subgraph IN["Input"]
I1(["Input 1"])
end
subgraph FN["Functions"]
F1["InviteCommand.Handle"]:::added
F2["!CodeGenerator.Next"]:::riskadded
F3["UserQuery.Get"]:::context
end
subgraph OUT["Output"]
O1[("Output 1")]
O2>"Output 2"]
end
I1 ==> F1
F1 ==> F2
F1 --> F3
F1 ==> O1
F1 ==> O2
F2 ~~~ O1
F2 ~~~ O2
classDef default fill:#14181e,stroke:#383d43,stroke-width:0.75px,color:#e6edf3,font-weight:500
classDef added fill:#12221d,stroke:#3fb950,stroke-width:1px,color:#e6edf3,font-weight:500
classDef modified fill:#211f18,stroke:#d29922,stroke-width:1px,color:#e6edf3,font-weight:500
classDef removed fill:#24171c,stroke:#f85149,stroke-width:1px,stroke-dasharray:4 4,color:#e6edf3,font-weight:500
classDef context fill:#14181e,stroke:#383d43,stroke-width:0.75px,color:#9198a1,font-weight:500
classDef risk fill:#14181e,stroke:#f85149,stroke-width:2px,color:#e6edf3,font-weight:500
classDef riskadded fill:#12221d,stroke:#f85149,stroke-width:2px,color:#e6edf3,font-weight:500
classDef zone fill:#0d1117,stroke:#383d43,stroke-width:1px,color:#9198a1,font-weight:600,font-size:12px
class IN,FN,OUT zone
linkStyle 2 stroke:#3d444d,stroke-width:1px
linkStyle 0,1,3,4 stroke:#3d444d,stroke-width:2px
Legend: 🟩 added · ⬜ unchanged context · 🔴 risk (! and a thick red border)
Green is added, amber is modified, a dashed red border is removed, grey is context, and a thick red border marks risk. The diagram text, style included, comes from pr-brief diagram. The gate recomputes it, so a hand-edited colour fails.
How it works
graph LR
A["/pr-brief"] --> B["pr-brief shape"]
B --> C["a reader agent for each flow"]
C --> D["pr-brief diagram"]
D --> E["prose style: ASD-STE100, iceberg"]
E --> F["pr-brief gate"]
F --> G["gh pr create / edit"]
shape reads the git diff, finds the changed functions, entry points and outputs, follows the call graph, and builds the flows. A reader agent checks each flow against the code. diagram draws it. The prose is edited by the style you chose and linted. The gate checks the whole description. Then the PR is created or updated, after you confirm.
Themes
Four are built in. github-dark is the default. The style follows beautiful-mermaid (MIT, Craft Docs). alucard is Dracula's official light theme. Every theme below is the tool's own output.
%%{init: {"theme":"base","flowchart":{"curve":"step","nodeSpacing":28,"rankSpacing":48,"diagramPadding":40},"themeVariables":{"darkMode":true,"background":"#0d1117","fontFamily":"Inter, Helvetica, Arial","fontSize":"13px","dropShadow":"none","primaryColor":"#14181e","primaryTextColor":"#e6edf3","primaryBorderColor":"#383d43","nodeTextColor":"#e6edf3","textColor":"#e6edf3","mainBkg":"#14181e","nodeBorder":"#383d43","lineColor":"#3d444d","clusterBkg":"#0d1117","clusterBorder":"#383d43","titleColor":"#9198a1","edgeLabelBackground":"#0d1117"}}}%%
graph LR
subgraph IN["Input"]
I1(["Input 1"])
end
subgraph FN["Functions"]
F1["handleInvite"]:::added
F2["checkQuota"]:::modified
F3["sendLegacyMail"]:::removed
F4["UserQuery.Get"]:::context
F5["!CodeGenerator.Next"]:::risk
F6["!saveInvite"]:::riskadded
end
subgraph OUT["Output"]
O1[("Output 1")]
O2>"Output 2"]
end
I1 ==> F1
F1 --> F2
F1 --> F4
F2 --> F5
F1 ==> F6
F1 -.-> F3
F6 ==> O1
F6 ==> O2
F5 ~~~ O1
F5 ~~~ O2
classDef default fill:#14181e,stroke:#383d43,stroke-width:0.75px,color:#e6edf3,font-weight:500
classDef added fill:#12221d,stroke:#3fb950,stroke-width:1px,color:#e6edf3,font-weight:500
classDef modified fill:#211f18,stroke:#d29922,stroke-width:1px,color:#e6edf3,font-weight:500
classDef removed fill:#24171c,stroke:#f85149,stroke-width:1px,stroke-dasharray:4 4,color:#e6edf3,font-weight:500
classDef context fill:#14181e,stroke:#383d43,stroke-width:0.75px,color:#9198a1,font-weight:500
classDef risk fill:#14181e,stroke:#f85149,stroke-width:2px,color:#e6edf3,font-weight:500
classDef riskadded fill:#12221d,stroke:#f85149,stroke-width:2px,color:#e6edf3,font-weight:500
classDef zone fill:#0d1117,stroke:#383d43,stroke-width:1px,color:#9198a1,font-weight:600,font-size:12px
class IN,FN,OUT zone
linkStyle 1,2,3 stroke:#3d444d,stroke-width:1px
linkStyle 0,4,6,7 stroke:#3d444d,stroke-width:2px
linkStyle 5 stroke:#3d444d,stroke-width:1px,stroke-dasharray:4 4
github-light
%%{init: {"theme":"base","flowchart":{"curve":"step","nodeSpacing":28,"rankSpacing":48,"diagramPadding":40},"themeVariables":{"darkMode":false,"background":"#ffffff","fontFamily":"Inter, Helvetica, Arial","fontSize":"13px","dropShadow":"none","primaryColor":"#f8f8f9","primaryTextColor":"#1f2328","primaryBorderColor":"#d2d3d4","nodeTextColor":"#1f2328","textColor":"#1f2328","mainBkg":"#f8f8f9","nodeBorder":"#d2d3d4","lineColor":"#d1d9e0","clusterBkg":"#ffffff","clusterBorder":"#d2d3d4","titleColor":"#59636e","edgeLabelBackground":"#ffffff"}}}%%
graph LR
subgraph IN["Input"]
I1(["Input 1"])
end
subgraph FN["Functions"]
F1["handleInvite"]:::added
F2["checkQuota"]:::modified
F3["sendLegacyMail"]:::removed
F4["UserQuery.Get"]:::context
F5["!CodeGenerator.Next"]:::risk
F6["!saveInvite"]:::riskadded
end
subgraph OUT["Output"]
O1[("Output 1")]
O2>"Output 2"]
end
I1 ==> F1
F1 --> F2
F1 --> F4
F2 --> F5
F1 ==> F6
F1 -.-> F3
F6 ==> O1
F6 ==> O2
F5 ~~~ O1
F5 ~~~ O2
classDef default fill:#f8f8f9,stroke:#d2d3d4,stroke-width:0.75px,color:#1f2328,font-weight:500
classDef added fill:#e8f2eb,stroke:#1a7f37,stroke-width:1px,color:#1f2328,font-weight:500
classDef modified fill:#f5f0e6,stroke:#9a6700,stroke-width:1px,color:#1f2328,font-weight:500
classDef removed fill:#fae9ea,stroke:#cf222e,stroke-width:1px,stroke-dasharray:4 4,color:#1f2328,font-weight:500
classDef context fill:#f8f8f9,stroke:#d2d3d4,stroke-width:0.75px,color:#59636e,font-weight:500
classDef risk fill:#f8f8f9,stroke:#cf222e,stroke-width:2px,color:#1f2328,font-weight:500
classDef riskadded fill:#e8f2eb,stroke:#cf222e,stroke-width:2px,color:#1f2328,font-weight:500
classDef zone fill:#ffffff,stroke:#d2d3d4,stroke-width:1px,color:#59636e,font-weight:600,font-size:12px
class IN,FN,OUT zone
linkStyle 1,2,3 stroke:#d1d9e0,stroke-width:1px
linkStyle 0,4,6,7 stroke:#d1d9e0,stroke-width:2px
linkStyle 5 stroke:#d1d9e0,stroke-width:1px,stroke-dasharray:4 4
dracula
%%{init: {"theme":"base","flowchart":{"curve":"step","nodeSpacing":28,"rankSpacing":48,"diagramPadding":40},"themeVariables":{"darkMode":true,"background":"#282a36","fontFamily":"Inter, Helvetica, Arial","fontSize":"13px","dropShadow":"none","primaryColor":"#2e303c","primaryTextColor":"#f8f8f2","primaryBorderColor":"#52535c","nodeTextColor":"#f8f8f2","textColor":"#f8f8f2","mainBkg":"#2e303c","nodeBorder":"#52535c","lineColor":"#6272a4","clusterBkg":"#282a36","clusterBorder":"#52535c","titleColor":"#6272a4","edgeLabelBackground":"#282a36"}}}%%
graph LR
subgraph IN["Input"]
I1(["Input 1"])
end
subgraph FN["Functions"]
F1["handleInvite"]:::added
F2["checkQuota"]:::modified
F3["sendLegacyMail"]:::removed
F4["UserQuery.Get"]:::context
F5["!CodeGenerator.Next"]:::risk
F6["!saveInvite"]:::riskadded
end
subgraph OUT["Output"]
O1[("Output 1")]
O2>"Output 2"]
end
I1 ==> F1
F1 --> F2
F1 --> F4
F2 --> F5
F1 ==> F6
F1 -.-> F3
F6 ==> O1
F6 ==> O2
F5 ~~~ O1
F5 ~~~ O2
classDef default fill:#2e303c,stroke:#52535c,stroke-width:0.75px,color:#f8f8f2,font-weight:500
classDef added fill:#2c3f3d,stroke:#50fa7b,stroke-width:1px,color:#f8f8f2,font-weight:500
classDef modified fill:#3e383b,stroke:#ffb86c,stroke-width:1px,color:#f8f8f2,font-weight:500
classDef removed fill:#3e2e39,stroke:#ff5555,stroke-width:1px,stroke-dasharray:4 4,color:#f8f8f2,font-weight:500
classDef context fill:#2e303c,stroke:#52535c,stroke-width:0.75px,color:#6272a4,font-weight:500
classDef risk fill:#2e303c,stroke:#ff5555,stroke-width:2px,color:#f8f8f2,font-weight:500
classDef riskadded fill:#2c3f3d,stroke:#ff5555,stroke-width:2px,color:#f8f8f2,font-weight:500
classDef zone fill:#282a36,stroke:#52535c,stroke-width:1px,color:#6272a4,font-weight:600,font-size:12px
class IN,FN,OUT zone
linkStyle 1,2,3 stroke:#6272a4,stroke-width:1px
linkStyle 0,4,6,7 stroke:#6272a4,stroke-width:2px
linkStyle 5 stroke:#6272a4,stroke-width:1px,stroke-dasharray:4 4
alucard
%%{init: {"theme":"base","flowchart":{"curve":"step","nodeSpacing":28,"rankSpacing":48,"diagramPadding":40},"themeVariables":{"darkMode":false,"background":"#fffbeb","fontFamily":"Inter, Helvetica, Arial","fontSize":"13px","dropShadow":"none","primaryColor":"#f8f4e5","primaryTextColor":"#1f1f1f","primaryBorderColor":"#d2cfc2","nodeTextColor":"#1f1f1f","textColor":"#1f1f1f","mainBkg":"#f8f4e5","nodeBorder":"#d2cfc2","lineColor":"#6c664b","clusterBkg":"#fffbeb","clusterBorder":"#d2cfc2","titleColor":"#6c664b","edgeLabelBackground":"#fffbeb"}}}%%
graph LR
subgraph IN["Input"]
I1(["Input 1"])
end
subgraph FN["Functions"]
F1["handleInvite"]:::added
F2["checkQuota"]:::modified
F3["sendLegacyMail"]:::removed
F4["UserQuery.Get"]:::context
F5["!CodeGenerator.Next"]:::risk
F6["!saveInvite"]:::riskadded
end
subgraph OUT["Output"]
O1[("Output 1")]
O2>"Output 2"]
end
I1 ==> F1
F1 --> F2
F1 --> F4
F2 --> F5
F1 ==> F6
F1 -.-> F3
F6 ==> O1
F6 ==> O2
F5 ~~~ O1
F5 ~~~ O2
classDef default fill:#f8f4e5,stroke:#d2cfc2,stroke-width:0.75px,color:#1f1f1f,font-weight:500
classDef added fill:#e8edd4,stroke:#14710a,stroke-width:1px,color:#1f1f1f,font-weight:500
classDef modified fill:#f6ead6,stroke:#a34d14,stroke-width:1px,color:#1f1f1f,font-weight:500
classDef removed fill:#fae8d8,stroke:#cb3a2a,stroke-width:1px,stroke-dasharray:4 4,color:#1f1f1f,font-weight:500
classDef context fill:#f8f4e5,stroke:#d2cfc2,stroke-width:0.75px,color:#6c664b,font-weight:500
classDef risk fill:#f8f4e5,stroke:#cb3a2a,stroke-width:2px,color:#1f1f1f,font-weight:500
classDef riskadded fill:#e8edd4,stroke:#cb3a2a,stroke-width:2px,color:#1f1f1f,font-weight:500
classDef zone fill:#fffbeb,stroke:#d2cfc2,stroke-width:1px,color:#6c664b,font-weight:600,font-size:12px
class IN,FN,OUT zone
linkStyle 1,2,3 stroke:#6c664b,stroke-width:1px
linkStyle 0,4,6,7 stroke:#6c664b,stroke-width:2px
linkStyle 5 stroke:#6c664b,stroke-width:1px,stroke-dasharray:4 4
Your own theme is a JSON file: pr-brief config set diagram.theme ./design/theme.json --scope repo. See the themes page for the format and the rules.
Convention over configuration
There are four settings. Everything else is fixed on purpose: the three parts of a description, at most 9 nodes and 3 diagrams, a gate that is always on. Fixed rules mean every PR reads the same, the gate can be exact, and nobody debates node limits. The convention page lists every rule, what you get in return, and what it costs.
{
"$schema": "https://raw.githubusercontent.com/gagoar/pr-brief/main/schema/pr-brief.schema.json",
"version": 1,
"style": "ste+iceberg",
"improve": { "previous": "drop" },
"diagram": { "theme": "github-dark" },
"tickets": { "pattern": "(PAY|OPS|ENG)-[0-9]+" }
}
| Setting | Values | Default |
|---|---|---|
style |
ste+iceberg, ste, iceberg |
ste+iceberg |
improve.previous |
drop replaces the earlier description, comment hides it in the PR |
drop |
diagram.theme |
github-dark, github-light, dracula, alucard, or a path to a theme file |
github-dark |
tickets.pattern |
A regular expression for a ticket key, such as (PAY|OPS)-[0-9]+ or AB#[0-9]+. It decides what improve keeps and what the gate warns about. Jira and Linear links are always read |
A guess at ABC-123 style keys |
Standardize a team with one file at the repo root. It wins over personal settings, and CI reads it from the base branch:
pr-brief init --pr-template --workflow
The gate
pr-brief has two halves. The writer is an AI: the /pr-brief skill in Claude Code reads the change and writes the Brief and the review guide. The gate is fixed code in the pr-brief binary. It makes no model call and no network call. Given a description, it passes or fails, and it lists what to fix. That makes every description that reaches review follow the same shape, whoever or whatever wrote it.
The same check runs in five places: as a Claude Code hook that blocks gh pr create|edit, az repos pr create|update and the MCP PR tools; as a GitHub Action; as an Azure pipeline; as the last job after a CI rewrite; and as pr-brief gate --file body.md. Exit 0 passes, 1 means findings, 2 means a usage or config error.
It checks the text between the markers, and the total length. It lists every finding it can in one run, each with a rule name:
| Rule | What it checks |
|---|---|
length |
At most 65,536 characters on GitHub, 4,000 on Azure DevOps |
skip, markers, previous |
The skip line has a reason. The begin and end markers are present, in order, with a style and a theme. The hidden earlier-description block is well formed |
sections, brief |
The three sections, in order. One paragraph of at most 5 sentences |
diagram, style |
One diagram per flow, at most 3, inside the limits. The chart and its colours equal what the theme requires, to the character. The Legend line sits directly under it |
references |
Every Input n and Output n has a row, and no row is unused |
review |
What changed, Read these first (1 to 7 rows, each file a link into the PR), Review order |
ste |
No hard ASD-STE100 violations (styles ste and ste+iceberg) |
The gate page has the full rules, the usual fix for each, what the gate reads, and how to read a finding.
on:
pull_request:
types: [opened, edited, synchronize, reopened, ready_for_review]
permissions:
contents: read
jobs:
description:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
# The BASE commit, so a PR cannot edit the rules that check it.
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: gagoar/pr-brief@v0
Set up CI
Run /pr-brief ci in Claude Code. It asks where you work (GitHub or Azure DevOps) and where the settings live, then writes the file. Or do it from a shell:
pr-brief init --workflow # GitHub, settings in .pr-brief.json
pr-brief init --workflow --config inline \
--style iceberg --theme dracula # GitHub, settings written in the workflow
pr-brief init --workflow --host azure-devops # Azure Pipelines, .azuredevops/pr-brief.yml
The host comes from git remote get-url origin. --host overrides it. The check needs no secret and no personal access token. On GitHub the job needs contents: read. On Azure Pipelines the job reads the PR with its own System.AccessToken, and you add the pipeline as a Build Validation policy on the target branch.
Where the settings live:
.pr-brief.json(the default). CI reads it from the base branch, so a PR cannot loosen its own rules.- In the workflow: the Action inputs
styleandtheme(the pipeline variablesPR_BRIEF_STYLEandPR_BRIEF_THEMEon Azure). They apply only when the base branch has no.pr-brief.json; the file wins, and the check prints a notice when it overrides an input. A theme here is a built-in name. Anyone who can edit the workflow in a PR can change these values, so use the file when that matters.
Let CI rewrite the description
The gate only checks. On GitHub you can also let the Claude Code GitHub Action write the description when a PR opens:
pr-brief init --rewrite --auth federation
The workflow has two jobs. rewrite runs the pr-brief skill in CI mode (no questions, only the description changes). description is the gate. It reads the live description after the rewrite, and it is the job to make required. Your team chooses how the job signs in to Claude, so --auth has no default: federation, bedrock and vertex use GitHub OIDC with no stored secret, and api-key uses an ANTHROPIC_API_KEY secret. The rewrite job stays off until those credentials exist, and the gate runs either way. It runs only for PRs from the same repository. This workflow is not tested against a live repository yet. Azure DevOps has no rewrite job. See Rewriting in CI.
Length
GitHub accepts 65,536 characters in a description. Azure DevOps accepts 4,000. The check counts characters, not bytes, and picks the limit from the host: the pipeline, the PR tool that opens the PR, or the git remote. An unknown host gets the GitHub limit. Write for the smaller limit when the same text must work on both. pr-brief gate --host azure-devops and pr-brief body improve --host dev.azure.com set it by hand.
To skip one PR, put a visible line in its description: > pr-brief skipped: <reason>. The gate is a quality control, not a security boundary: see SECURITY.md.
What the hook sees
The hook receives every Bash command and every PR tool call that Claude Code is about to run. It runs on your machine. It matches patterns in the command text and never runs that text. It reads only files the command itself names, such as --body-file. It makes no network calls, keeps no log and sends nothing. It denies only PR create and edit commands whose description fails the gate, and it lets a call through if its own input is broken. It adds about 16 ms to a Bash call.
Limits
pr-brief shapeis regex-based, not a compiler. It finds functions in Go, C#, Java, TypeScript, JavaScript and Python, and reads Terraform, Bicep and GitHub workflow files. It skips a call when two functions could match. A reader agent checks each draft, but read the diagram before you trust it.- The diagram styles are checked on GitHub (Mermaid 11.17.2, light and dark). Azure DevOps is not verified for the init line, the invisible layout links or per-edge styling.
- The GitHub Action does not run in Azure Pipelines.
pr-brief init --workflow --host azure-devopswrites a pipeline that runspr-brief gate --stdin. That pipeline is not tested against a live Azure DevOps organisation yet. The 4,000-character limit is the documented Azure DevOps value; check it against your organisation. - The ASD-STE100 lint is a Go port of the upstream linter, tested to match its output byte for byte. It checks the structural rules, not the full approved dictionary.
More questions: the FAQ.
Documentation
Website · Reference · Contributing · Security · Changelog and releases
Credits
The diagram themes use the colours, the colour-mixing rule and the style constants of beautiful-mermaid by Craft Docs (MIT), and the Dracula colours (MIT). The prose lint is a port of ste-lint.py by Dustin Yuchen Teng (MIT). ASD-STE100 is a specification owned by ASD; this project does not redistribute it. See THIRD_PARTY_NOTICES.md.
License
MIT
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found