commitlint.config.mjs
// Branch-commit shape for agent sessions (docs/plans/2026-08-29-agent-posting-controls.md).
// Run by .claude/skills/post-commit-message/scripts/check.sh before `git commit` executes.
// Branch commits vanish at squash (the repo setting is BLANK), so the body is a short "why"
// for the reviewer, not a record.
const BODY_MAX_LINES = 6
// Lines after the header, with the separating blank line and trailing blanks removed.
function bodyLines(raw) {
const lines = raw.replace(/\r\n/g, '\n').split('\n').slice(1)
while (lines.length && lines[0].trim() === '') lines.shift()
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop()
return lines
}
const localRules = {
rules: {
'body-max-lines': ({ raw }) => {
const n = bodyLines(raw).length
return [n <= BODY_MAX_LINES, `body has ${n} lines; the cap is ${BODY_MAX_LINES}`]
},
'no-coauthor-trailer': ({ raw }) => [
!/^co-authored-by:/im.test(raw),
'Co-authored-by trailers are never added; remove it'
],
'no-closing-keyword': ({ raw }) => [
!/\b(close[sd]?|fix(e[sd])?|resolve[sd]?)\s*:?\s*#\d+/i.test(raw),
'issue-closing keywords belong on line 1 of the PR body, not in a commit'
]
}
}
export default {
extends: ['@commitlint/config-conventional'],
plugins: [localRules],
rules: {
'header-max-length': [2, 'always', 72],
'scope-empty': [2, 'never'],
'body-max-line-length': [2, 'always', 72],
'body-max-lines': [2, 'always'],
'no-coauthor-trailer': [2, 'always'],
'no-closing-keyword': [2, 'always']
}
}