@parker-brown-family/craft-commit
v0.2.0
Published
Lint git commit messages for the narrative doctrine: a subject sentence and three why-focused paragraphs, no Conventional-Commits prefix, no 50/72 cap, no AI attribution. Written for the agent who reads the history next.
Maintainers
Readme
craft-commit
A deterministic, LLM-free commit-message linter that enforces a narrative doctrine — so the agent reconstructing your repository six months from now can read why it is the shape it is, not just what changed.
No LLM calls. No network. Pure parser. Exit code is the contract.
What it checks
A conforming message, in the default narrative style:
A subject line that is a sentence saying what happened
The state this commit selected — what the repository now is, in plain terms.
The problem underlying it — what was wrong, missing, or about to go wrong.
This is the paragraph that survives; a diff shows the change and never the reason.
How it was addressed — the approach taken, the numbers and files that mattered,
and what was deliberately not done.- The subject is a sentence, not a tag: no
feat:/fix:prefix, no 50/72 cap, no trailing period. - A non-trivial body is three blank-line-separated paragraphs.
- A trivial commit (a typo, a rename, a lockfile) is a subject line and nothing more.
- No commit credits an AI as author or contributor.
The tool guarantees the form. It cannot judge the substance — whether the second paragraph truly states the problem, whether a rejected alternative is named, whether the prose reads well. Three empty-but-well-shaped paragraphs pass the shell. The words are still yours to get right; this catches only the structure a machine can see.
Why this, and not Conventional Commits
The 50/72-character subject convention was a Linux-kernel norm popularized in
2008 (tpope)
for git log --oneline on an 80-column terminal. The reader is no longer on
that terminal. It is a web UI that soft-wraps, or an agent with no column budget
at all — so hard-wrapping at 72 mangles the one artifact that outlives every
Slack thread and ticket a change references, and the 50-char subject cap
truncates exactly the sentence that carries the meaning.
Conventional Commits (feat: / fix: prefixes) optimizes for changelog
automation, and there is a live backlash that it makes the type prominent when
the why is what matters
(Evans, "Stop Using Conventional Commits";
HN;
Lobsters). The
enduring advice underneath the debate is older and unanimous: use the body to
explain what and why, not how
(Beams).
The agent-era case is sharper still. Stetsenko's Lore (arXiv:2603.15566) names
the Decision Shadow: the reasoning behind a change — the constraints, the
rejected alternatives — that the diff discards, turning working code into legacy
code the moment it merges. A message that says only refactor: clean up utils
transmits almost nothing to the next agent. A narrative that names the problem
and what was deliberately not done transmits the shadow.
This doctrine is machine-global for its author, recorded in
~/.config/agents/AGENTS.md. craft-commit is its enforcer.
What a dumb linter can and cannot enforce
Enforced, deterministically (no LLM):
- no Conventional-Commits prefix on the subject;
- subject shape — capitalized, no trailing period, a soft length warning only;
- a blank line after the subject;
- three blank-line-separated paragraphs for a non-trivial body;
- a body that is present when the commit is not trivial;
- trailer hygiene — no
Co-authored-by:/Generated withline crediting an AI.
Left to review or an LLM pass, because a parser cannot see it: whether the paragraphs carry the right content, whether the numbers and files that mattered are cited, whether a departure from a plan is explained. The tool says so rather than pretending, because a check that rewards filler is worse than no check.
gitlint is the conceptual kin here — it lints prose structure. commitlint is not — its whole model assumes the prefix grammar this doctrine drops.
Install & use
a) Agentic — integrate craft-commit into an agent harness
Primary use case. Paste this block into your agent's rules file —
CLAUDE.md, AGENTS.md, .cursorrules, whatever your harness reads on
startup:
## Commit messages — narrative doctrine (enforced by craft-commit)
Before running `git commit`, write the message to a file and lint it:
npx -y @parker-brown-family/craft-commit <path-to-msg-file>
If the exit code is non-zero, rewrite the message until it passes. Every
non-trivial commit is:
1. A subject line that is a sentence — no feat:/fix: prefix, no length cap,
no trailing period.
2. A blank line.
3. Three paragraphs: the state this commit selected; the problem underlying
it; how it was addressed, and what was deliberately not done.
A trivial commit (typo, rename, lockfile) is a subject line and nothing more.
Never credit an AI as author or contributor. Never pass --lenient to bypass.Then install the hook in every repo you want enforced:
cd your-repo
npx -y @parker-brown-family/craft-commit install-hookFrom that point the agent's commits are machine-checked on every git commit.
Failed messages print the specific issues and the agent self-corrects without
revisiting this README.
b) Conventional — you're a human dev (maybe with light agent assist)
npm install --save-dev @parker-brown-family/craft-commit
npx craft-commit install-hookNow git commit rejects any message that doesn't satisfy the doctrine. To
bypass temporarily (e.g. during a git revert), run with --no-verify.
Reference
CLI
craft-commit <path-to-commit-msg-file> # exit 0 valid, 1 invalid, 2 error
craft-commit # read from stdin
craft-commit --labeled <file> # enforce the earlier labeled form
craft-commit --sections A,B,C <file> # labeled form with custom labels
craft-commit --json <file> # machine output
craft-commit --lenient <file> # downgrade errors to warnings
craft-commit --no-color <file> # disable ANSI color
craft-commit install-hook [path] # drop .git/hooks/commit-msg
craft-commit --help # usage summaryLibrary
import { validate, formatHuman } from "@parker-brown-family/craft-commit";
const result = validate(fs.readFileSync(".git/COMMIT_EDITMSG", "utf8"));
if (!result.valid) {
console.error(formatHuman(result));
process.exit(1);
}Options
| Option | Default | Effect |
|---|---|---|
| style | "narrative" | "narrative" or "labeled" |
| maxSubjectLength | 100 narrative / 72 labeled | over it warns (narrative) or errors (labeled) |
| minParagraphs | 3 | narrative: paragraphs a non-trivial body must have |
| minParagraphWords | 3 | a shorter paragraph/section body warns |
| lenient | false | downgrade all errors to warnings |
| requiredSections | ["Selected State", "Underlying Problem", "How Addressed"] | labeled labels; passing it selects labeled style |
| allowAiAttribution | false | permit AI-crediting trailers (don't) |
Issue codes
| Code | Severity | Meaning |
|---|---|---|
| subject.missing | error | commit has no subject line |
| subject.conventional_prefix | error | subject uses a feat: / fix: prefix |
| subject.too_long | warning (narrative) / error (labeled) | subject over the cap |
| subject.trailing_period | warning | subject ends with . |
| subject.lowercase_start | warning | subject begins lowercase |
| body.missing_blank_after_subject | error | no blank line between subject and body |
| body.too_few_paragraphs | error | non-trivial body has fewer than minParagraphs |
| paragraph.too_short | warning | a narrative paragraph is thin |
| trailer.ai_attribution | error | commit credits an AI as author/contributor |
| section.missing / section.empty / section.too_short / section.out_of_order | error / error / warning / error | labeled-style section checks |
Provenance
The doctrine is recorded in ~/.config/agents/AGENTS.md
("Commit messages are narrative, and written for the agent who reads them
next"). This linter is a standalone enforcer — no external dependency, no
coupling beyond the pattern itself.
License
MIT
