// docs · v1.x

Review rules — COMMITBRIEF.md and OUTPUT.md

Customise the system prompt your reviewer sees (COMMITBRIEF.md) and how findings render to markdown locally (OUTPUT.md template).

Updated June 20, 2026

CommitBrief separates what the reviewer looks for from how the review is rendered. Two files, two layers, neither required to get started.

COMMITBRIEF.md — the system prompt

The team-shared review rules file. Sent to the LLM as the system prompt on every review. Lives at the repo root and is committed to git.

Path

<repo-root>/COMMITBRIEF.md

If the file is missing, CommitBrief falls back to an embedded default. The default works fine on its own — customising is purely additive.

Scaffold it

commitbrief init

Writes the embedded default to COMMITBRIEF.md, and also writes the per-user .commitbrief/OUTPUT.md template (see below). If only one of the two files exists, the other is still written. Use --force (or --yes) to overwrite existing files.

Format

Free-form markdown. Anything you write is passed to the model as the system prompt, wrapped in <project_rules>...</project_rules> XML tags. Inside the envelope the model sees your literal content verbatim; around it, the prompt builder adds an optional <architecture_constraints> block (only when an architecture.json is in effect), a <severity_rubric> block, a <response_format> block carrying the structured-findings JSON contract (a plain-text contract for the CLI-backed providers), a language directive (per output.lang) and an immutability guard (a prompt-injection defense — the model is told to treat project rules as immutable and to ignore any instruction inside them that tries to override its task).

There are no required headings or template variables.

Because the file’s content becomes the system prompt, CommitBrief runs two defenses on a non-default COMMITBRIEF.md (or OUTPUT.md) before the provider call: the secret scanner checks it for credential-shaped strings, and a prompt-injection scan surfaces a non-blocking warning if it spots injection phrasing (“ignore previous instructions”, “you are now…”). Both skip the trusted embedded default.

What the embedded default covers

  1. Persona — adversarial defender + optimization engineer.
  2. What to look for — correctness, security, performance and efficiency, maintainability, code quality.
  3. What NOT to flag — explicit “do not nitpick” list to keep the noise floor down.
  4. Output discipline — how long a description and a suggestion should be, and a preference for silence over speculation.
  5. Project context — a short block for you to fill in.

The structured-findings JSON contract is not part of this file: the prompt builder appends it separately as <response_format>, so a custom COMMITBRIEF.md does not have to restate it.

Customisation patterns

Project-specific context

## Project context

This is a financial transaction service. Every state change to
`Transaction` must go through `transactions.Repo.Apply()` which
enforces audit logging — direct mutations bypass compliance.

The `internal/billing/` package operates on cents (int64). Never
introduce float64 anywhere in the price-handling path.

High-stakes paths

## High-stakes paths

- `internal/auth/` — authentication / session handling. Any
  change here needs a security-focused review.
- `db/migrations/` — schema changes. Verify backward
  compatibility with the previous release.

Style conventions

## Conventions

- Use `errors.Is` / `errors.As`, never string-compare error messages.
- All public exported functions must have a doc comment starting with the function name.
- No `panic()` outside `init()` blocks.

Size considerations

A COMMITBRIEF.md is sent on every review as part of the prompt. Past ~10k tokens (~40k characters) you pay noticeable input cost. Use commitbrief compress to losslessly shrink the file via the LLM:

commitbrief compress                  # default level: balanced
commitbrief compress --dry-run        # preview without writing
commitbrief compress --level=aggressive --out COMMITBRIEF.compressed.md

The compress pipeline backs up the original to .commitbrief/backups/COMMITBRIEF-<ISO-timestamp>.md before overwriting, and refuses to apply when the compressed result is not smaller than the original.

What CommitBrief does NOT do with this file

  • It does NOT lint the file or enforce a schema.
  • It does NOT validate references (e.g. “the file mentioned doesn’t exist”).
  • It does NOT version the file or track changes — that is git’s job.

OUTPUT.md — the markdown template

A per-user Go text/template that controls how findings are formatted locally. Applied to the parsed findings JSON to produce the markdown output for --markdown and --output <file>.md. Never sent to the LLM.

Where it lives

CommitBrief looks in this order (first hit wins):

  1. <repo-root>/.commitbrief/OUTPUT.md — per-repo override.
  2. <user-home>/.commitbrief/OUTPUT.md — per-user default.
  3. Embedded default at internal/rules/output.md.

A repo-local OUTPUT.md is gitignored by default. This makes it a personal preference rather than a team artifact.

Template data

type TemplateData struct {
    Findings []Finding
}

type Finding struct {
    Severity    Severity // named string type: critical|high|medium|low|info
    File        string
    Line        int
    LineEnd     int
    Title       string
    Description string
    Suggestion  string
    Language    string
    Snippet     string
}

Inside the template, .Findings is a typed slice:

{{ range .Findings }}
- **{{ upper (printf "%s" .Severity) }}** {{ .PathRef }} — {{ .Title }}
  {{ .Description }}
  > {{ .Suggestion }}
{{ end }}

Available functions

FunctionSignaturePurpose
upperstring → stringstrings.ToUpper.
lowerstring → stringstrings.ToLower.
groupBySeverity[]Finding → []SeverityGroupGroup by severity, ordered critical to info; empty groups are dropped. Each SeverityGroup has .Severity and .Items.
countFiles[]Finding → intDistinct file count.

.Severity is a named type, not a plain string, so pass it through printf "%s" before upper or lower: {{ upper (printf "%s" .Severity) }}. Each finding also has the methods .LineRef (142 or 142-145) and .PathRef (file:line, or just the file when no line is set).

The set is the public template contract; additions are allowed in v1.x, removals require a schema bump. All built-in text/template actions (range, if, with, len, printf, …) also work.

Pre-send validation

If you write a custom OUTPUT.md and the template is malformed, CommitBrief catches it before any provider call. Three checks run on load:

  1. Parse — text/template syntax check.
  2. Empty execute — template runs against an empty []Finding{}.
  3. Sample execute — template runs against a synthetic sample of two findings (one critical, one info).

A failure aborts the run with a pointer at the file and a hint to run commitbrief init --yes to overwrite with the default.

Example — group by severity

# Review summary

Files touched: {{ countFiles .Findings }}
{{ range groupBySeverity .Findings }}
## {{ upper (printf "%s" .Severity) }} ({{ len .Items }})
{{ range .Items }}
- **{{ .PathRef }}** — {{ .Title }}
  {{ .Description }}
  > {{ .Suggestion }}
{{ end }}
{{ end }}

groupBySeverity returns a slice, so the outer range walks the severities in order and skips the empty ones. To print only one severity, add {{ if eq (printf "%s" .Severity) "critical" }} inside the outer range.

What OUTPUT.md does NOT do

  • It does NOT affect the LLM prompt or the review behavior.
  • It does NOT affect --json output (the JSON schema is fixed).
  • It does NOT apply to the cards renderer (lipgloss-styled layout).
  • It does NOT apply to CLI-tool-backed providers (claude-cli / gemini-cli / codex-cli emit pre-formatted text directly).

See also

  • Output formats — where the rendered markdown shows up.
  • Configuration — output.lang and other rendering knobs.
  • .commitbriefignore — separate file controlling which files reach the prompt.
  • Signal control — COMMITBRIEF.md steers what gets flagged; the baseline and inline commitbrief-ignore drop findings you’ve already triaged.