guard
guard is a required check on every PR. It reads the PR through the GitHub API, checks it against .github/guard.toml and the issue the PR closes, and never checks out or runs code from the PR.
Sources: .github/guard.toml, .github/scripts/guard.py, .github/workflows/guard.yml.
Workflow
Section titled “Workflow”| Item | Value |
|---|---|
| Trigger | pull_request_target, types opened, edited, synchronize, reopened, ready_for_review |
| Code that runs | the workflow and guard.py as they are on the base branch, with guard.toml from there; PR code is never checked out |
| Permissions | contents: read, pull-requests: read, issues: read |
| Job name | guard; branch protection requires a check of this name |
| Python | 3.12 on ubuntu-latest; standard library only |
| Environment | GH_TOKEN, PR_NUMBER, REPO |
| Exit | 0 when every check passes; 1 on any failure, or when the PR can’t be read through the API |
| Output | one guard: <message> line per failure, and a ## guard section in the job summary |
guard.toml
Section titled “guard.toml”| Key | Type | CVSM’s value | Meaning |
|---|---|---|---|
layout |
globs | README.md, AGENTS.md, .gitignore, .agents/**, .claude/**, .codex/**, .github/**, docs/**, scripts/**, tests/** |
Where files may exist. Adopters add their own directories and root files (src/**, configs/**, pyproject.toml, lock files, the data manifest). |
markdown |
globs | README.md, AGENTS.md, docs/**, .agents/skills/**, .github/** |
Where .md files may exist. |
forbidden_names |
names | TODO, NOTES, PLAN, PROGRESS |
File names refused anywhere, in any case and with any extension. |
protected |
globs | AGENTS.md, .agents/**, .codex/**, .claude/**, .github/** |
Paths a PR may change only when its issue’s Scope names them explicitly. Adopters add eval and data paths. merge.py reads this list too. |
types |
names | feat, fix, refactor, test, exp, docs, chore |
Allowed PR title types. |
max_body_lines |
integer | 40 |
Longest allowed PR body. |
test_paths |
globs | tests/** |
Paths left out of max_diff_lines. |
max_diff_lines |
integer | unset (commented out, example 400) |
Optional. Most changed lines (additions plus deletions) outside test_paths. |
Task #39 adds LICENSE, the Apache-2.0 licence at the repo root, to CVSM’s own layout.
Glob syntax
Section titled “Glob syntax”| Token | Matches |
|---|---|
**/ |
zero or more whole directories |
** |
anything, / included |
* |
anything except / |
? |
one character except / |
| anything else | itself, literally ([ included) |
A glob must match the whole path.
Checks
Section titled “Checks”guard runs every check and reports every failure, in this order.
1. Closing reference
Section titled “1. Closing reference”- The PR body closes exactly one issue. Closing keywords are
close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved, any case, with an optional colon, followed by#N,owner/repo#Nor an issue URL. - References to other repos are ignored, and a repeated reference counts once.
2. The issue
Section titled “2. The issue”- It exists and can be read.
- It is open.
- It has the
codelabel. - Its
### Scopesection is not empty. - No Scope entry starts with a wildcard (
*or?).
3. Each changed file
Section titled “3. Each changed file”For every file except removed ones:
- its path matches
layout; - if it ends in
.md, its path matchesmarkdown; - its base name before the first dot, upper-cased, is not in
forbidden_names.
For every file, removed ones included, and for the old path of a rename:
- the path matches a Scope entry;
- if it matches a
protectedglob, some Scope entry both matches the path and begins with that glob’s literal prefix. A broad entry such assrc/**therefore never unlocks.github/**; the Scope has to name.github/...itself.
4. Title
Section titled “4. Title”The PR title matches <type>(#N): <summary>: type from types, N the closed issue, and a summary of 1 to 72 characters that starts with a non-space.
5. Body
Section titled “5. Body”- The body contains the lines
## Changes,## Acceptanceand## Out of scope. - It has at most
max_body_lineslines.
6. Diff size (optional)
Section titled “6. Diff size (optional)”When max_diff_lines is set, the changed lines outside test_paths stay within it.
Run it locally
Section titled “Run it locally”From the repo root, logged in to gh:
REPO=owner/repo PR_NUMBER=15 python .github/scripts/guard.pyIt reads .github/guard.toml from the current directory, so run it from a checkout of main to get CI’s result.