Rules¶
This is the full rule catalogue behind AGENTS.md. The short file tells an agent
what to do; this file records what is checked, how, and why, so that a rule is never "because it
says so". Every finding carries a rule id: grep it here, run the command named in the last
column, read the finding with file:line.
One principle outranks the list: a false PASS is worse than a false FAIL. A rule that
examined zero objects reports coverage.skipped; a check that could not run exits 2, not 0.
Enforcement map¶
| Rule id prefix | Command that produces it | Module |
|---|---|---|
doc.*, md.*, privacy.*, agents.md-length |
ghagent doc --root . |
tools/ghalib/mdcheck.py |
repo.*, readme.*, changelog.*, agents.*, audit.* |
ghagent audit . |
tools/ghalib/audit.py |
changelog.* |
ghagent changelog check |
tools/ghalib/changelog.py |
assets.* |
ghagent assets check |
tools/ghalib/assets.py |
wiki.* |
ghagent wiki build, ghagent wiki verify |
tools/ghalib/wiki.py |
meta.* |
ghagent meta check, ghagent meta apply, ghagent audit --repo |
tools/ghalib/meta.py |
install.*, verify.*, uninstall.* |
ghagent install, verify-install, uninstall |
tools/ghalib/install.py |
Levels: error fails the command (exit 1), warn is reported but does not change the exit
code, info is a note. ghagent doc accepts --ignore <ids>; .ghagent.json accepts the same
list as ignore_rules, and audit applies that list to its embedded document check.
Configuration (.ghagent.json)¶
| Key | Effect |
|---|---|
owner_repo |
the default <owner>/<name> for meta, changelog and wiki init |
readme, agents, changelog, docs_dir, wiki_dir, wiki_manifest, ci_workflow |
where the files live; an absent file sets no trap |
require_files |
files this repository insists on; each missing one is a repo.required-file error |
ignore_rules |
rule ids switched off for the whole pack — doc, audit and privacy all honour it |
exclude_dirs |
extra directories the documentation walk must not enter (site, _site, public) |
safe_email_domains |
role domains of this repository; the built-in list holds only RFC examples and GitHub noreply addresses |
topic_min, topic_max, description_max |
metadata thresholds |
changelog_style, language |
the contract variant and the language of the localised copies |
Nothing in this file names a person: a repository that wants its own domain recognised adds it here, and no project inherits somebody else's whitelist.
Text hygiene¶
| Rule id | Level | What it checks |
|---|---|---|
doc.utf8 |
error | the file is valid UTF-8; a decoding failure is a finding, not a crash |
doc.unreadable |
error | the file can be read at all |
doc.bom |
error | the file does not start with a UTF-8 BOM |
doc.crlf |
error | the file uses LF, not CRLF |
doc.final-newline |
warn | the file ends with a newline |
doc.tab-indent |
warn | indentation uses spaces; a tab-indented block is reported |
doc.line-length |
info | a prose line is longer than 200 characters (tables and URLs are exempt) |
md.hard-break |
warn | a trailing double space (markdown hard break) — use a blank line instead |
doc.no-text-files |
error | the tree contains at least one text file; an empty tree is not success |
coverage.skipped |
info | a rule examined nothing on this tree and says so |
House style wraps prose near 100 characters; the automated limit is deliberately looser, because a hard limit invites unnaturally short lines rather than readable ones.
Markdown structure¶
| Rule id | Level | What it checks |
|---|---|---|
md.h1 |
error | exactly one H1; a template (.github/, a template filename, or the <!-- ghagent: template --> marker) and a wiki partial (_Sidebar.md, _Footer.md, _Header.md) may start at H2 and are reported as info |
md.heading-skip |
warn | no heading level is skipped (H2 → H4) |
md.fence |
error | every code fence is closed; a fence without a language tag is a warning |
md.table |
error | a table's separator and every row have as many cells as its header |
md.toc |
error | a listed TOC anchor is produced by a heading; an H2 missing from an existing TOC is a warning |
md.alert |
warn | a > [!TYPE] alert uses one of NOTE, TIP, IMPORTANT, WARNING, CAUTION |
Links and anchors¶
| Rule id | Level | What it checks |
|---|---|---|
md.link-local |
error | a relative link target exists in the tree; inside a wiki checkout a page name resolves to Page.md (see below) |
md.link-anchor |
error | an anchor exists in the target file or the current document, compared case-insensitively |
md.link-dir |
warn | a link points to a directory instead of a file (info when it ends with a slash) |
md.image-alt |
warn | every image has non-empty alt text |
md.image-http |
warn | an image is not loaded over plain http:// (mixed content is blocked) |
Anchors follow GitHub: the heading is lowercased, punctuation is dropped, spaces become hyphens,
an em dash disappears and leaves a double hyphen, duplicates gain -1, -2. Cyrillic headings
work; #Установка is written exactly as GitHub renders it.
Wiki mode is detected, never assumed¶
A GitHub wiki links its own pages by name: [Architecture](Architecture) is Architecture.md.
The same link in an ordinary repository is a 404, so the resolution cannot be a default. The
checker switches it on per directory: the presence of _Sidebar.md or _Footer.md proves a wiki
checkout, and the configured wiki_dir (wiki/ by default) proves wiki sources, whose sidebar is
generated only at build time. There Page, Page#anchor and names with dots
(Migration-3.x-to-4.0) resolve.
Everywhere else the link stays an error. _Sidebar.md, _Footer.md and _Header.md are also
exempt from md.h1: the wiki renders them as navigation on every page, not as documents.
Why this matters. On a 24-page wiki the checker reported 121 errors, 119 of them on correct links. The only way to keep the gate green was
--ignore md.link-local,md.h1, which also switched off the two rules that find real breakage.
Privacy¶
| Rule id | Level | What it checks |
|---|---|---|
privacy.secret |
error | GitHub tokens (ghp_, github_pat_), AWS keys (AKIA…), private key blocks, Slack tokens, literal bearer values |
privacy.abs-path |
error | absolute machine paths: a drive letter with a Users directory, or a Unix home directory |
privacy.email |
warn | a personal e-mail address; users.noreply.github.com, example.com, example.org, example.net and localhost are allowed, and a repository adds its own role domains with safe_email_domains |
Placeholders instead of values: <token>, <owner>/<repo>, example.com,
user@users.noreply.github.com, <path>. The privacy scan runs inside doc on every file it
examines, and ghagent privacy runs it over the whole tree without the documentation rules.
What the walk does not enter¶
doc and privacy use one walker, so they cannot disagree about the same tree. It skips build
output (node_modules, vendor, dist, build, target, …) and every directory whose name
starts with a dot — that is tool state: .git, .idea, .ruff_cache, and the scratch space of a
coding agent (.swarm, .claude, .dsh, .cursor). Two dot-directories are published surfaces
and stay: .github/ and .well-known/. A project adds its own build directory with exclude_dirs
in .ghagent.json instead of teaching the pack the name of its generator.
The reason is measured: six privacy.abs-path errors came from plan files in an agent scratch
directory — machine paths that are never published, reported against a repository that was clean.
AGENTS contract¶
AGENTS.md at the repository root is the entry point an agent reads first. The audit checks it
for the four properties that make it usable, and the document checker caps its length.
| Check | Level | Requirement |
|---|---|---|
agents.present |
pass | AGENTS.md exists at the root of the examined tree (the item is created only when the file was found; an absent file is repo.agents) |
agents.commands |
warn | contains copy-paste commands (a code fence or inline code) |
agents.verify |
warn | contains an explicit verification step |
agents.traps |
warn | contains explicit prohibitions ("do not", "trap") |
agents.md-length |
warn | the file is at most 120 lines; above that it stops being a map |
Structure the checks expect: one H1, a purpose paragraph, a TOC, a command table, the
non-negotiable rules, a verification block, a "do not" list and links to the detailed documents.
agents.md-length counts len(text.splitlines()), so a final newline is not an extra line, and
it looks only at the root AGENTS.md — not at AGENTS.ru.md, not at a nested copy.
The reason for the cap is cost: an agent pays for every token of this file on every request.
Detail belongs in this file, WIKI.md and ARCHITECTURE.md.
README contract¶
| Check | Level | Requirement |
|---|---|---|
readme.h1 |
error | exactly one H1, kept under 60 characters (a longer title is a warning) |
readme.summary |
error | one prose paragraph directly after the title: what it is, for whom |
readme.badges |
error/warn | 3–8 badges; fewer than three is a warning, none is an error |
readme.toc |
warn | a table of contents is present; the contract wants it once the file passes roughly 200 lines |
readme.install |
error | an installation section (Installation, Install, Установка, Getting started, …) |
readme.usage |
error | a usage section (Usage, Quick start, Быстрый старт, Examples, …) |
readme.license |
error | a licence section |
readme.code-fence |
warn | every code fence has a language tag |
readme.absolute-link |
info | an absolute link to a .md file; a relative link renders in the tree and in the wiki mirror |
readme.emoji, readme.superlative |
warn | no emoji or unverifiable superlative in the summary line |
Two conventions a reviewer enforces because they cannot be computed: every table of numbers names the measurement source in a column, and the summary states a capability a reader can check. "Fast" and "powerful" are not measurements; a self-test count without the command that produced it is a claim, not a number.
Audit items¶
ghagent audit scores weighted items instead of printing a single verdict. A required file
contributes repo.<name> at its severity: readme, license, changelog, contributing,
security are errors; code-of-conduct, support, agents, gitattributes, editorconfig,
gitignore, pr-template, issue-forms, ci, codeowners, dependabot are warnings;
citation is info. Files demanded by require_files in .ghagent.json are repo.required-file
(errors).
The score counts a pass at full weight, a warn at half, a fail at zero, and excludes skipped items
from both sides; audit.score warns below 80% and errors below 60%, and audit.no-items errors
when nothing was examined at all. The other audit items are named in their contract sections:
readme.*, changelog.*, agents.*, plus assets.banner, assets.social-preview and
wiki-source. wiki-source is skipped, not failed, when the tree has no wiki/ directory: a
repository may keep its wiki in its own git repository, which is what GitHub does by default, and
neither choice is a defect.
CHANGELOG contract¶
Keep a Changelog 2.0.0 with semantic versions, ISO dates, newest first, [Unreleased] on top and
comparison links at the bottom. Six change types are recognised in English and Russian:
Added, Changed, Deprecated, Removed, Fixed, Security (Добавлено, Изменено,
Устарело, Удалено, Исправлено, Безопасность).
# Changelog
All notable changes to this project are documented here. The format is
[Keep a Changelog](https://keepachangelog.com/en/2.0.0/) 2.0.0, and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.2.0] - 2026-10-30
### Added
- Something a user can see.
[Unreleased]: https://github.com/owner/repo/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
| Rule id | Level | What it checks |
|---|---|---|
changelog.missing |
error | the file exists |
changelog.h1, changelog.h1-position |
error, warn | one H1, on line 1 |
changelog.format-link, changelog.semver-link |
warn | the preamble names Keep a Changelog and Semantic Versioning |
changelog.versions |
error | at least one ## [x.y.z] - YYYY-MM-DD section |
changelog.unreleased |
warn | an [Unreleased] section exists |
changelog.duplicate |
error | a version appears once |
changelog.date-missing, changelog.date-format, changelog.date-future |
error, error, warn | every released version has an ISO date, and not in the future |
changelog.order |
error | versions are ordered newest first, by date and by SemVer |
changelog.empty-version |
error | a version has no content at all |
changelog.prose-version |
warn | a version is described in prose, with no ### subsections (empty-version when there is nothing at all) |
changelog.types, changelog.type-order |
warn | known change types, in the canonical order; a grouping subtitle is dropped before the lookup (### Added — template language is Added) |
changelog.link-refs |
warn | [x.y.z] headings without link references at the bottom; the references are optional in Keep a Changelog, so the finding is a warning |
changelog.version, changelog.write, changelog.post-check |
error | release refuses an invalid version, an unverifiable rewrite, or a changelog that fails its own check after writing |
changelog.dry-run |
info | release without --write shows what would change |
changelog.suggest, changelog.suggest-empty, git.log |
info, warn, error | suggest drafts entries from conventional commits; a human writes the entry |
changelog release 1.2.0 --write promotes [Unreleased], inserts the dated heading and rewrites
the comparison links; it verifies the result before and after writing. changelog notes 1.2.0
prints the release body, link references stripped. The template is
reference/changelog-template.md.
Wiki contract¶
The wiki is a separate repository with a flat namespace; the full workflow is in WIKI.md. The rules that decide whether a build is publishable:
| Rule id | Level | What it checks |
|---|---|---|
wiki.source |
error | wiki/ exists |
wiki.manifest |
error in build, warn in audit |
wiki/wiki.json exists |
wiki.page-missing, wiki.page-unlisted |
error | manifest and disk list the same pages; an unlisted page is invisible |
wiki.home |
error | Home.md exists |
wiki.image-relative |
error | every image is absolute; the wiki cannot see the project tree |
wiki.link-broken, wiki.link-outside |
error | a .md link resolves to an existing page or repository file, inside the repository |
wiki.md-suffix |
error | an internal wiki link carries no .md |
wiki.link-target |
error | a link points at a page in the built tree |
wiki.required |
error | Home.md, _Sidebar.md, _Footer.md are in the built tree |
wiki.page-orphan |
warn | a page is reachable from the sidebar |
wiki.sidebar-target |
error | every sidebar link resolves |
wiki.footer-relative |
warn | the footer uses absolute links only |
wiki.remote-missing |
error | the wiki remote exists; GitHub creates it on the first page saved in the web interface |
wiki.build-missing |
error | verify found sources but no build: run wiki build (or wiki publish --push, which builds first) |
wiki.not-applicable |
info | neither sources nor a build: nothing to verify, exit 0 — a wiki kept in its own repository is checked in that checkout |
wiki.kept, wiki.dry-run, wiki.no-change |
info | publication is a dry run without --push; unmatched remote pages are kept unless --prune |
wiki verify resolves its target in a fixed order: an explicit --out, then a --wiki-dir that
already looks like a wiki (Home.md, _Sidebar.md or _Footer.md present), then the build under
.ghagent/wiki. The header of the report names the directory that was actually read, so
--root <repo> --wiki-dir <clone> verifies the clone and says so.
Manifest sample (the only ordering authority):
{
"repo": "owner/name",
"branch": "main",
"title": "Project name",
"footer": "MIT",
"groups": [
{"title": "Start", "pages": ["Home.md", "01-Install.md"]},
{"title": "Reference", "pages": ["02-API.md"]}
]
}
Copyable starting points: reference/wiki-manifest-template.json and reference/wiki-home-template.md.
Images and assets¶
| Rule id | Level | What it checks |
|---|---|---|
assets.no-readme |
error | README.md exists, so a reference can be examined |
assets.readme-banner |
error | the README references at least one image |
assets.missing-file |
error | a referenced image is committed |
assets.alt |
warn | every image has alt text |
assets.insecure-url |
warn | an image is not loaded over plain http:// |
assets.social-preview |
warn | a file named social-preview.* exists under assets/ |
assets.social-size |
error | the preview is at most 1 MB |
assets.social-dimensions |
error/warn | the preview is at least 640×320; 1280×640 is the recommended size (reported as info) |
assets.svg-social |
warn | the preview is a PNG, JPG or GIF; an SVG renders in a README but is rejected as a social preview |
assets.no-pillow |
warn | assets render produced the SVG but no PNG |
assets.truncated |
warn | the title or the tagline does not fit the card and would be cut; the finding names the limit |
assets.no-command |
info | the banner carries no command line (--command fills the lower band) |
assets.dry-run, assets.written |
info | render without --write writes nothing |
assets.upload-hint |
info | the repository copy is not the social preview until it is uploaded in Settings → Social preview |
The chip row describes what the project is: declared keywords first, the top languages only as a
fallback. Emoji from a README H1 are stripped — the font set has no colour glyphs, so they land
as a box.
assets render is the generic generator: it reads the repository and draws a card that any project
can use. This repository's own card is a designed one, and it is still reproducible:
python tools/make_social_preview.py --write renders both assets/social-preview.png and its SVG
twin from a single layout description, so the two files cannot drift apart. The terminal panel in
it repeats output the tool actually prints, which is the only reason the picture can be trusted.
Metadata¶
| Rule id | Level | What it checks |
|---|---|---|
meta.empty |
error | the description is not empty |
meta.hard-limit, meta.length |
error, warn | the description fits the API limit of 350 characters and reads well under 200 |
meta.prefix |
warn | the description does not start with "a tool", "a simple", "this is", "the repo", "tool for" |
meta.superlative, meta.emoji, meta.shape |
warn/warn/info | no unverifiable superlative, no emoji, at least one clause |
meta.topics-count |
warn/error | 8–20 topics; more than 20 is an error because the API stores 20 |
meta.topics-form |
error | topics are lowercase and hyphenated ([a-z0-9]+(-[a-z0-9]+)*) |
meta.topic-year, meta.topic-duplicate |
warn | no year suffix, no duplicates |
meta.topics-put, meta.verify, meta.patch, meta.refused |
error (warn when only the stored topic list differs) | a write lands and reads back; validation errors block a write |
meta.remote |
error | meta check --repo owner/name could not read the repository through gh api; an unread remote is reported as such, never as an empty description |
meta.dry-run, meta.no-change |
info | apply without --write changes nothing |
meta.description, meta.description-length, meta.description-form, meta.homepage, meta.license, meta.releases, meta.community-health, wiki.enabled |
audit item levels | remote metadata read through gh api in audit --repo |
PUT /repos/{owner}/{repo}/topics replaces the whole list, so meta apply merges with the
existing set unless --replace-topics is given, then re-reads the response instead of trusting
the call.
Install contract¶
install copies the pack into an agent skill root and writes .install.json with a sha256 per
file; verify-install answers the only question that matters later: is this the copy that passed
the tests.
| Rule id | Level | What it checks |
|---|---|---|
install.source, install.empty |
error | the source is a pack checkout and contains files to copy |
install.dry-run, install.done |
info | without --write nothing is copied; afterwards the report counts the files |
verify.manifest, verify.empty |
error | the directory carries an install manifest listing files |
verify.missing, verify.changed |
error | every installed file is present and unchanged |
verify.extra, verify.ok |
info | a file outside the manifest; a clean comparison |
uninstall.refused |
error | removal refuses a directory that this pack did not install |
Import paths and profiles are in IMPORT.md; the wheel carries the CLI only, so
install, verify-install, uninstall, snippet and selftest are source-tree commands.
Platform traps¶
Rows are ordered by the symptom a reader hits first. Full platform context is in WIKI.md and PROCEDURES.md.
| Symptom | Trap | Action |
|---|---|---|
remote: Repository not found on a wiki push |
the wiki repository is not created by a push | save the first page in the web interface, then publish |
| a page exists but wiki search never shows it | wikis are barely indexed | only wikis with 500+ stars and public editing disabled are indexed; use GitHub Pages for discovery |
| no wiki on a private repository | a private wiki needs a paid plan | do not promise it |
a link to docs/page fails |
the wiki is a flat namespace | every page is a filename in the wiki root |
| an image does not render in the wiki | a relative image cannot work across repositories | commit it to the project and use an absolute raw URL |
| a wiki link opens a raw file or a 404 | .md in a wiki link |
drop the extension |
#Установка is not found |
Cyrillic anchors | lower case, punctuation dropped, spaces become hyphens |
| a heading anchor has a double hyphen | an em dash in a heading | the dash is removed; two spaces become two hyphens |
| topics you did not touch disappear | PUT /topics replaces the list |
merge with the existing set, then re-read |
| some topics vanish after a write | more than 20 topics | the API stores 20 |
422 Your current plan does not support GitHub Pages |
Pages for a private repository | the free plan does not allow it |
| 404 right after enabling Pages | Pages builds are asynchronous | poll the build, then the URL |
base64 with line breaks in gh api --jq .content |
the contents API returns encoded content | -H 'Accept: application/vnd.github.raw' |
| the social preview is rejected or blank | an SVG as a social preview | PNG, JPG or GIF only |
| an anchor that "should" be lowercase is not | the platform's own example contradicts the capital-Greek rule | compare anchors case-insensitively |
Non-negotiable rules and their reasons¶
The nine rules in AGENTS.md are not style preferences; each one has a failure it prevents.
| Rule | Why it exists | Enforced by |
|---|---|---|
| Only measured numbers | one unverifiable figure discredits every other figure in the document | review; readme.superlative, readme.emoji, meta.* |
| No secrets, machine paths or personal addresses | published history is permanent, and a leaked token has to be rotated | privacy.secret, privacy.abs-path, privacy.email |
| One H1, a language tag on every fence | parsers, search snippets and syntax highlighting depend on it | md.h1, md.fence, readme.code-fence |
| Flat wiki namespace, absolute images, sidebar and footer | the wiki is a separate repository with no directories and no access to the project tree | wiki.md-suffix, wiki.image-relative, wiki.required |
| Publication only on an explicit flag | a pushed wiki page is public at once and cannot be recalled | wiki.dry-run, wiki.kept, --prune without a default |
| Keep a Changelog 2.0.0 | users and release tooling read the changelog, not the commit log | changelog.* |
| AGENTS.md under 120 lines | an agent pays for every line on every request; detail is fetched only when needed | agents.md-length |
| A false PASS is worse than a false FAIL | a green check that examined nothing hides the defect it was hired to find | coverage.skipped, audit.no-items |
| English canonical, owner-signed commits | one canonical text prevents silent drift; an agent signature misattributes responsibility | review; translation parity |
House rules a reviewer enforces¶
- English is canonical;
NAME.ru.mdmirrorsNAME.mdin the same commit. A lagging translation is a defect, not a backlog item. - One logical change per pull request, conventional commit subject, motivation in the body.
- Commits are signed by the repository owner, never by the agent or a bot identity.
- No runtime dependencies; Pillow stays an optional extra and only affects PNG output.
- Documentation changes with the code: a changed flag, exit code or rule id updates this file and AGENTS.md in the same change.
- A rule without a failing fixture and a decoy is rejected; see ARCHITECTURE.md and ../CONTRIBUTING.md.