# 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).

CommitBrief docs · v1.x · Configuration

Canonical URL: https://commitbrief.com/docs/1.x/review-rules

---

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

```sh
commitbrief init
```

Writes the embedded default to `COMMITBRIEF.md`. Use
`--force` (or `--yes`) to overwrite an existing file.

### 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; outside, the prompt builder appends a language directive
(per `output.lang`) and an immutability guard (a prompt-injection
defense — the model is told to treat project rules as data, not
instructions, when they conflict with its baseline behavior).

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](/docs/1.x/safety-and-cost#secret-scanner) checks it
for credential-shaped strings, and a
[prompt-injection scan](/docs/1.x/safety-and-cost#prompt-injection-scan-of-your-rules-v170)
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,
   maintainability, testing.
3. **Output format** — instructions for the structured-findings
   JSON contract (severity, title, description, suggestion, …).
4. **What NOT to flag** — explicit "do not nitpick" list to keep
   the noise floor down.

### Customisation patterns

**Project-specific context**

```markdown
## 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**

```markdown
## 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**

```markdown
## 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:

```sh
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`](https://pkg.go.dev/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

```go
type TemplateData struct {
    Findings []Finding
}

type Finding struct {
    Severity    string
    File        string
    Line        int
    LineEnd     int
    Title       string
    Description string
    Suggestion  string
    Language    string
    Snippet     string
}
```

Inside the template, `.Findings` is a typed slice:

```gotemplate
{{ range .Findings }}
- **{{ upper .Severity }}** {{ .File }}:{{ .Line }} — {{ .Title }}
  {{ .Description }}
  > {{ .Suggestion }}
{{ end }}
```

### Available functions

| Function | Signature | Purpose |
|----------|-----------|---------|
| `upper` | `string → string` | `strings.ToUpper`. |
| `lower` | `string → string` | `strings.ToLower`. |
| `groupBySeverity` | `[]Finding → map[Severity][]Finding` | Bucket by severity. |
| `countFiles` | `[]Finding → int` | Distinct file count. |

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 one finding per severity.

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

```gotemplate
{{ $bucketed := groupBySeverity .Findings }}
# Review summary

Files touched: {{ countFiles .Findings }}

{{ with index $bucketed "critical" }}
## Critical
{{ range . }}
- **{{ .File }}:{{ .Line }}** — {{ .Title }}
  {{ .Description }}
  > {{ .Suggestion }}
{{ end }}
{{ end }}
```

### 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](/docs/1.x/output-formats) — where the
  rendered markdown shows up.
- [Configuration](/docs/1.x/configuration) — `output.lang` and
  other rendering knobs.
- [`.commitbriefignore`](/docs/1.x/review-scopes#the-three-layer-filter-pipeline) —
  separate file controlling which files reach the prompt.
- [Signal control](/docs/1.x/signal-control) — `COMMITBRIEF.md` steers
  *what* gets flagged; the baseline and inline `commitbrief-ignore`
  drop findings you've already triaged.