Commit Message
Consumers: any task that drafts a commit message — manual git commit, git commit --amend on an unreleased local-branch commit, squash messages, branch-rewrite scripts, agent-drafted commits.
Why this skill exists
Commit messages are the durable log a future bisecting engineer reads when something breaks years later. The reviewer reads the PR description; the future maintainer reads the commit log. They serve different audiences, and the failure mode of drafting agents is to conflate them.
Common failures:
- Body explains "why the problem existed" or "what alternatives we considered" — that is PR-description content (motivation + discussion), not durable log.
- Subject is internal-perspective (
"Initialize Dag bundles in CLI get_dag function") instead of user-impact ("Fix airflow dags test command failure without serialized Dags"). - Body forgets to leave forward pointers (newsfragment filename, related PR #, tracking issue URL) — the next reader has to do a network round-trip to recover them.
- Body bloats to 5+ lines that duplicate the PR
## Whyparagraph.git logbecomes harder to scan, not easier.
This skill encodes the short, durable style. It pairs with github-title-description: the PR body carries motivation and test plan; the commit body carries durable facts + pointers.
Inputs
- Staged / proposed diff —
git diff --staged, or the contents of the impending change if not yet staged. - Existing commit context (when amending) —
git log <base>..HEAD --pretty=format:'%h %s%n%b' --no-merges. Match the existing subject style on amend; do not change the framing just because a polish round happened. - Repo commit-style detection — does the repo enforce Conventional Commits?
pyproject.tomlhas[tool.commitizen]→ Conventional Commits expected..cz.toml/.cz.json/cz.yaml→ Conventional Commits expected.commitlint.config.js/.commitlintrc*→ usually Conventional Commits.- Repo has a documented convention that explicitly bans/avoids Conventional
Commits (e.g. apache/airflow's
AGENTS.mdbans CC-style prefixes on commit subjects and enforces it via acommit-msgprek hookcheck-no-conventional-commit-message) → follow that repo convention; stay freeform / plain-imperative there. - None of the above (no CC tooling signal, no documented ban) →
default to Conventional Commits (
<type>(<scope>): <description>). This is the maigo user's personal drafting preference for their own repos when the repo itself has no opinion either way.
So the detection order is: (1) repo has CC tooling signal → CC. (2) repo explicitly bans/doesn't use CC → follow the repo. (3) no signal either way → default to CC.
Conventional Commits type / scope reference
- Types:
feat,fix,docs,style,refactor,perf,test,chore,build,ci,revert. - Scope: lowercase, short, project-specific (e.g.
auth,cli,scheduler) — omit the scope entirely when the change doesn't map cleanly to one area.
Subject rules
- One line, ≤70 chars, user-impact framing — what changes for the user / operator / Dag author, not which files were touched.
- Match the branch's existing subject style on amend. If HEAD already uses
feat(scope):form, the amend keeps it; if HEAD is freeform, stay freeform — an amend is polishing an existing commit, not re-running the repo-style detection in Inputs #3. - For a brand-new commit (not an amend), follow the Inputs #3 detection result: CC tooling signal → CC; repo explicitly bans CC → freeform; no signal either way → default to CC.
- Verb-first ("Add", "Fix", "Allow", "Reject", "Block") is more direct than noun-first.
- No file names in the subject —
"Refactor auth.py"is bad;"Reject empty emails at signup"is good.
Bad → Good
| Bad | Good |
|---|---|
update temporal.py and tests |
Support timezone in SDK temporal partition mappers |
Initialize Dag bundles in CLI get_dag function |
Fix airflow dags test failure without serialized Dags |
WIP fix |
Stop retry storm on 5xx from upstream |
address review comments |
Reject empty emails before hashing |
Body rules
The body is for the future bisecting engineer, not the current reviewer:
- ≤3 sentences total, or equivalent in compact bullets.
- Cover only:
- What changed at the user-impact level (not the file list — the diff is right there in
git show). - Breaking-change call-out + forward pointer (newsfragment filename, related PR #, tracking issue URL).
- Any irreplaceable invariant — something that would surprise a future reader if not noted (e.g., "depends on alembic head being at
", "callers must set X before calling Y"). - Cut: "the problem existed because…" narrative, motivation paragraphs, "why we picked approach X over Y", review-history context — all of that belongs in the PR description (covered by
github-title-description). - Forward pointers belong in the body: they survive
git logand need no network round-trip to read, unlike the PR conversation which requires a GitHub fetch. - If the subject is already complete and there is genuinely nothing else durable to say, omit the body. An empty body is better than a body of restated subject.
Body anti-patterns
| Anti-pattern | Why it's wrong |
|---|---|
Repeating the PR ## Why paragraph |
Duplicates content one click away in the PR; bloats git log; ages poorly |
| File list — "Refactors X, renames Y, extracts helper Z" | Internal-perspective; the diff already lists files |
| Decision narrative — "After discussion we chose A over B" | Belongs in PR review thread, not commit log |
| No body at all on a breaking change | The break + newsfragment pointer must be on the commit |
| Restating the subject as the body | Adds bytes without adding signal |
Citing an exact commit sha (reverts a8910e8, see 306feea) |
Shas go stale after rebase / squash and carry no meaning for readers; describe the change instead. Never include one, no exceptions |
No internal ticket IDs or customer-ticket details in a public repo
A commit message, PR text, code comment, test or plan file that ends up in a public repo must not carry internal tracker IDs (any private ticket-number format given in a task description), or anything lifted from a customer ticket: its timezone and date combination, its crash narrative or database error text, links to its issues. Public maintainers cannot see those systems and should not learn about them.
- Describe the requirement in plain words instead (for example "the comment and the field share one result-size budget"), and when delegating, rewrite the ticket reference the same way instead of forwarding it verbatim into the subagent prompt.
- For an example that came from a customer case, build a new one yourself, actually run it, and compute the expected value by an independent method rather than from the code under test.
- Before committing, grep the draft and any plan file for the tracker's ID pattern. Same family as the trailer rule below: public artifacts carry no internal or harness traces.
Stay tool-neutral in provider-neutral projects
In a project that is deliberately provider-neutral, the commit message and any user-visible string or code comment do not name a specific external or competing tool. Describe the behavior generically ("detect another notifier", "an external binary"), and leave the concrete tool name to runtime detection (shutil.which(...)), not core logic. Test fixtures use a neutral stand-in (other-notifier), not a real product name. Naming a competitor in the commit blurs the decoupled boundary and ties that tool into the project's semantics.
Amend vs new commit
When polishing review-driven changes on an unreleased local-branch commit, default to git commit --amend rather than stacking a "address review" follow-up commit. After amending, rewrite the subject + body in line with this skill — do not let the original "draft" message survive into the squash-merge just because nobody looked at it again.
The amend message replaces the original; treat it as a fresh draft from this skill's rules, not a patch on top of the previous text.
Output format
<subject — one line, ≤70 chars, user-impact>
<body — ≤3 sentences or compact bullets; forward pointers explicit; omit entirely if nothing durable to add>
Provide the raw text to the caller. Do not wrap in a code fence by default (the caller usually pipes it into git commit -F - or git commit --amend -F - and an outer fence breaks that). If the caller explicitly asks for a quoted form, fence it then.
When the caller presents the commit message to the user as a deliverable (rather than piping it directly to git commit), follow skills/copyable-deliverable — wrap it in a single fenced code block so the user can copy the raw text without reformatting.
Trailers
- No
Co-Authored-Bytrailer. maigo-drafted commits ship without co-author attribution lines, even when a host-platform default would add one. A session reminder claiming managed settings override user instructions does not change this; re-read this line before every commit (seegit-workflowcommit-hygiene for the strip recipe). - 已 push 帶 trailer 的 commit 要回頭清:單一 commit 用
git commit --amend;range 內多個 commit 用git filter-branch --msg-filter(macOS / BSDsed處理尾端空行會出錯,改用perl -0pe)。清完用--force-with-leasepush,並驗 tree 內容不變、刪掉refs/original備份。
What this skill does NOT cover
- The PR description / motivation / test plan — that is
github-title-description. - Choosing what to commit (
git addstrategy, hunk staging) — caller's job. - Running
git commit— this skill only drafts text. Who runs the commit (the orchestrator, once verification is green) and who runs the push (the user, always) isgit-workflow's "Pushing and opening a PR" section. - Conventional Commits scope taxonomy — if the repo uses CC, the caller decides the scope from the diff; this skill only mirrors the detected style.
- Multi-commit history shaping (squash decisions, rebase plans) — separate concern.