todo: propose a standard for commit messages merged into main - #1557
Conversation
Squash-only merges (no rebase/fast-forward, no merge commits), PR title in the changelog-entry topic style, squash body from the PR description ending in a mandatory Changelog: section, and vX.Y.Z release tags — fixed now so the history the changelog-from-git-history investigation would read accumulates in a parseable format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | fb68943 | Commit Preview URL Branch Preview URL |
Aug 14 2026, 07:08 PM |
GitHub's rebase merge is not a literal fast-forward: it rewrites committer and SHAs but keeps each branch commit's own message, which is the actual reason it defeats a changelog generator (no PR number, no reviewed body). Also state explicitly that squash never lands branch commits and that only a direct push can truly fast-forward main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
Pre-merge: a required status check on pull_request (including the edited trigger, so fixing the title or body re-runs it) lints the PR title and the Changelog: section; the linter is a self-hosted fjs/ci module reusing the planned changelog Markdown parser. Post-merge: an audit job on push to main backstops the one hole GitHub leaves open — editing the squash message in the merge dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
Enforcement (PR-lint required check, post-merge audit) starts only after the format is adopted into AGENTS.md §8 — the linter enforces the documented rule, not the proposal. The standard keeps the format, settings, and tagging tasks and points to the new file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
The gap between AGENTS.md adoption and machine enforcement is deliberate: format mistakes found during hand use are documentation edits, not linter changes plus a rule migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Reviewed at 5db378e960820ec639f89e5add36b97649d885f5, merge-base cb1fcdc457ecb5ba3a20d994e4ba4e0c31d9577e — which is also current origin/main, so the branch is not behind.
Docs-only, confirmed from the merge-base diff rather than the title: three files, all todo/*.md, +179/-0, no code touched. Per AGENTS.md §8.3 no changelog entry is owed, and the description's Changelog: none matches. The build and test gates would be identical to main by construction, so I did not run them.
Since a proposal document's value is its factual accuracy, I re-derived every claim it makes about this repo's history against origin/main. Most reproduce. Two do not.
1. "every commit on main today already has this shape" does not reproduce
todo/commit-message-standard.md line 35, closing the squash-merge bullet:
A squash merge never lands branch commits: it always creates one new commit, parented on the
maintip, titled<PR title> (#NNN)— every commit onmaintoday already has this shape.
The mechanism claim before the dash is right. The evidence clause after it is not, under either ruler:
$ git rev-list --count origin/main 1881
$ git log --format='%s' origin/main | grep -vcE '\(#[0-9]+\)$' 560
$ git rev-list --count --first-parent origin/main 1803
$ git log --first-parent --format='%s' origin/main | grep -vcE '\(#[0-9]+\)$'
486
$ git rev-list --count --merges origin/main 15
So 486 of 1803 first-parent commits (560 of 1881 overall) lack the (#NNN) suffix, and main carries 15 merge commits, so it is not linear today either. The newest counterexamples are recent, not ancient:
7b979e74 2026-08-03 0.41.0
830a0c5e 2026-08-02 Fix spelling in comment about optimization
7b979e74 is 11 days old and is a release commit that landed with no (#NNN) — i.e. by direct push, not a squash-merged PR. That is a mild tension with the same document's release-title rule, which is otherwise well supported: 0.45.0 (#1555), 0.44.0 (#1513), 0.43.1 (#1448), 0.43.0 (#1443), 0.42.0 (#1441) all do go through PRs with a bare-version title.
What is true is the narrower statement:
$ git log --first-parent -100 --format='%s' origin/main | grep -vcE '\(#[0-9]+\)$'
0
The last 100 first-parent commits are uniform. So "every commit from the last hundred merges already has this shape" holds; "every commit on main today" does not. Worth narrowing, because the claim is also in slight tension with the Problem section's own premise — the argument for acting now is precisely that pre-standardization history is not uniformly parseable.
2. The "body ends with the Changelog: section" rule is violated by this PR's own description
commit-message-standard.md requires the Changelog: section to be "the last section of the body", and commit-message-enforcement.md specifies the linter as failing "unless the title matches the format and the body ends with a Changelog: section".
The repo's established PR-description shape puts an automated trailer after the prose. Of the last 25 merged PRs, 14 have a Generated with [Claude Code] / Claude-Session: / Co-Authored-By: trailer at the end of the body — including #1556, #1555, #1554, #1553, #1547, #1546, #1545. And this PR's own description ends:
Changelog: none
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
So the linter as specified would fail #1557 itself, and roughly half of recent PRs. The fix is small — require the body to contain a Changelog: section and allow a trailer block after it (or define the section as the last section before any trailer, the way git interpret-trailers treats a trailer block) — but "ends with" is the wording the enforcement issue hands to whoever writes the workflow, so it is worth fixing in the proposal rather than in the linter.
Observation, not a defect: the ≤ 72-character title rule
The rule is proposed for the future, so it is not a factual error, but the adoption cost is larger than it may look:
$ git log --first-parent --format='%s' origin/main | awk 'length($0)>72' | wc -l 219
$ git log --first-parent -200 --format='%s' origin/main | awk 'length($0)>72' | wc -l 38
219 of 1803 first-parent titles exceed 72 characters including the (#NNN), and 38 of the last 200 — about 19% of current practice, e.g. Migration close-out: drop the .js ignore, browser-native browser-testing plan, @import sweep (#1545) at 100 characters and Exact emitted declarations: keep every @module header, remove the last elided any (#1526) at 89. Fine as a deliberate tightening; just noting that a required status check would start rejecting titles at that rate on day one.
What checked out
- Tags. "Releases are not tagged (
git tagis empty)" — confirmed, 0 local tags andgit ls-remote --tags originreturns 0. - The body premise. Recent squash bodies really are GitHub's default concatenation:
cb1fcdc4,5efbe375,8804e783,4fbf0b71all begin* <branch commit title>. - The quoted noise strings are real, not illustrative inventions:
Address reviewmatches 82 lines andDrop stray blank line1 line acrossgit log --format=%B origin/main. - Every referenced path exists and every link resolves:
todo/changelog-from-git-history.md,todo/changelog-website.md,todo/commit-message-enforcement.md,changelog/README.md,changelog/unreleased/,fjs/ci/, and AGENTS.md, which does have a §8 with §8.3 and §8.4. Nopath:linecitations to range-check. - The cross-reference is precise.
changelog-from-git-history.md's "design 1" is in fact1. **Commit-message extraction.**(line 27), so "itsChangelog:section is design 1's input" is right. - "no PR link" in entries matches
changelog/README.mdas it stands after #1552/#1555: "New entries … contain no links: the file name is the pull-request number". - Conventions.
**Priority:**/**Status:**headers match the siblingtodo/changelog-*.mdfiles; P2/P3 versus the investigation's P4 is consistent.todo/README.mdis a placement guide, not an index, so no listing was owed. - Link check.
bin/linkcheck.mjsgives 129 broken links in both trees and the sets are byte-identical (diffempty) againstorigin/mainatcb1fcdc4. The three new files strand nothing and add nothing.
Not verified
The claim that commit-metadata rulesets "require an Enterprise plan" is a GitHub plan-tier detail I cannot check from the tree; I did not confirm or contradict it. The pull_request edited trigger and Require linear history branch protection both exist as described.
Both findings are wording-level and neither blocks the direction, which I think is sound — fixing the format before the history accumulates is the right ordering. Commenting rather than approving because item 2 would be inherited by the linter as a spec bug, and item 1 is an existential claim about the repo that a reader would reasonably take as verified.
The uniform-shape claim now states what git history supports: the last hundred first-parent commits, not all of main (486 of 1803 lack the suffix, 15 are merges, 0.41.0 was direct-pushed on 2026-08-03 — the Problem section's own point). The Changelog: section is the last section before an optional trailer block, not the end of the body — about half of recent PR bodies end with a trailer, so 'ends with' would have made the linter reject them. Also note the 72-char title limit is a tightening: 38 of the last 200 titles exceed it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
|
Both findings fixed in fb68943 — all your numbers reproduced after unshallowing the clone (1803 first-parent, 486 without the suffix, 15 merges,
Also folded in your observation: the 72-char title rule now says it is a deliberate tightening, with the 38-of-200 figure. Generated by Claude Code |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Re-reviewed at fb689437f426cd6aa11d3e671640bcc4792ec06d (previously commented at 5db378e9), against origin/main = cb1fcdc457ecb5ba3a20d994e4ba4e0c31d9577e. Both findings from the last round are addressed. I recomputed every number from git log rather than reading the new wording.
Finding 1 — "every commit on main today already has this shape" — closed
The claim is now scoped, and each of its parts reproduces exactly on origin/main:
| claim in the PR | measured |
|---|---|
| "the last hundred first-parent commits already have this shape" | git log --first-parent -100 | grep -vcE '\(#[0-9]+\)$' → 0 |
"486 of 1803 first-parent commits lack the (#NNN) suffix" |
486 of 1803 |
| "15 are merge commits" | --first-parent --merges → 15 |
"release 0.41.0 landed by direct push as recently as 2026-08-03" |
7b979e74, dated 2026-08-03, single parent b835248d, and commits/7b979e74/pulls returns 0 associated PRs |
For completeness: across the last 200 first-parent commits there are exactly 2 counterexamples — that 0.41.0 push and Fix spelling in comment about optimization (830a0c5e, 2026-08-02). So "the last hundred" is the tightest true bound, and it is the one stated. Whole-history figures are unchanged since my last round (main has not advanced past #1554), so nothing drifted.
Finding 2 — the rule rejecting its own proposal PR — closed
The spec now reads "the last section of the body before an optional trailer block" in commit-message-standard.md, with the matching phrasing in commit-message-enforcement.md. Checked against this PR's actual description: it ends Changelog: none, then a blank line, then 🤖 Generated with [Claude Code](…) and a session URL — which is precisely the trailer block the reworded rule exempts. The proposal now passes its own linter.
One correction to my own last round, and to the PR's figure. I previously said "14 of the last 25 merged PRs end with such a trailer". That undercounted — it was matching only the Co-Authored-By: / 🤖 Generated with shape and missing the bare https://claude.ai/code/session_… and _Generated by [Claude Code](…)_ forms. Recounting the last 25 merged PRs (#1532–#1556) by their final non-empty body line: 25 of 25 end with a trailer, all of them one of those forms. So the PR's "about half of recent PR bodies end with one" is itself an understatement — it is currently universal. That does not change the design (the exemption covers 100% as well as it covers 50%), but if the number is going to appear in a todo that a future linter is written against, "essentially every recent PR body" would be the accurate phrasing.
Title cap — now stated, and exact
"38 of the last 200 titles exceed it today" reproduces exactly: awk 'length($0)>72' over the last 200 first-parent subjects → 38. Over all 1803 first-parent commits it is 219. Calling the ≤72 cap "a deliberate tightening, not current practice" is the right framing, and my earlier observation is satisfied.
Everything else
Docs-only — three files under todo/, no code touched, so per §8.3 no CHANGELOG entry is needed and correctly none is added. linkcheck broken-link sets are byte-identical to origin/main, so the two new files and the new references from todo/changelog-from-git-history.md strand nothing. No Rust, no generator, no public surface.
Still explicitly unverified, as last round: the "commit-metadata rulesets require Enterprise" plan-tier claim in the enforcement doc. That is a GitHub billing-tier fact I cannot check from here, and the doc's fallback plan does not depend on it.
One cosmetic nit: todo/commit-message-enforcement.md:29 came out 94 characters wide —
when the title or description is fixed — no push needed to re-green. Branch protection marks
— which is the only line over 80 in either changed file; the reflow just missed it.
Approving.
Adds two todo files and links them from
todo/changelog-from-git-history.md.todo/commit-message-standard.md(P2, proposed) — the format for commits merged intomain. The changelog-from-git-history investigation (P4) can only read history that exists — every PR merged before the message format is standardized adds a commit a future generator cannot parse, so the format must be fixed now:(#NNN), no reviewed body, so the PR becomes invisible to a generator) and merge commits (non-linear graph). Squash never lands branch commits — it always creates one new commit titled<PR title> (#NNN); no true fast-forward is possible through the GitHub UI, only through a direct push, which branch protection closes.<topic>: <short description>, ≤ 72 chars including GitHub's appended(#NNN); a release PR's title is the bare version.Changelog:section holding exactly the entry list items — orChangelog: none— with**BREAKING CHANGES:**marked as in the files.vX.Y.Z(the repo currently has zero tags) so a generator has factual release boundaries.todo/commit-message-enforcement.md(P3, open) — machine-checking the format before merge: a PR-lint workflow (pull_requestwith theeditedtrigger, self-hostedfjs/cimodule) marked as a required status check, plus a post-merge audit onpushtomainfor the one hole GitHub leaves open (editing the squash message in the merge dialog). Deliberately a separate issue: it starts only after the format is adopted into AGENTS.md §8 — the linter enforces the documented rule, not the proposal.Note: the repository merge-method settings and branch protection are admin settings, not repository files — they need to be flipped in the GitHub UI.
Changelog: none
🤖 Generated with Claude Code
https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ