fix(ci): refuse card relations in commit messages at push, and re-scope the PR-time gate to the body - #17745
Merged
Merged
Conversation
…lings `partOfRe` required a literal space on both sides of the word, so `Part-of #N`, `Part of: #N` and `Part-of: #N` — the three spellings every dispatch order lists as forbidden, and the last of which is the git-trailer form — were invisible to every reader of this grammar. `refsRe` carried the same colon gap, and a pinned case asserted it as if it were a decision. The separator is now the one `closingKeywordRe` already ships (an optional colon), and the word accepts a hyphen where it accepts a space. Strict superset: nothing that matched before stops matching. Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK Co-authored-by: Claude <noreply@anthropic.com>
…e PR The rule that no commit message carries a card relation was enforced on the pull request, over its commit list. By then the cheap repair was gone: a new commit on top joins that list, and removing the message is the history rewrite this repo forbids, so two rounds each paid a full redo. - `scripts/check-commit-card-trailers.mjs` judges exactly the commits a push would publish — everything it sees is unpublished, so its remedy is an ordinary reword. It adds no grammar: the extractors are the sweep's, and the trailer-position rule covers a bare reference with no keyword beside it. - `.githooks/pre-push` calls it after the os-regen deferral check, which keeps its behaviour unchanged; the ref lines git writes are now read once and handed to the check whose input they are. - `check:partof-closing-keyword` drops its commit-list rule: the squash message is taken from the PR body now, so a branch commit's trailer never lands. Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK Co-authored-by: Claude <noreply@anthropic.com>
`pnpm check:nul-bytes` refuses a raw ASCII control byte in any tracked file, and two places here carried one: the `git log -z` record separator and the self-test fixture that exercises it. The escape was materialised into the byte by the editor that wrote it, which is exactly the failure the gate names. `String.fromCharCode(0)` cannot be materialised into anything. Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 12, 2026
os-sales
marked this pull request as ready for review
September 12, 2026 02:39
This was referenced Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #17727
The card-trailer rule was enforced on the pull request, over its commit list. By the time it spoke, the cheap repair was gone: a new commit on top JOINS that list, and removing the message is the history rewrite this repository forbids — so two rounds, hours apart, each paid a full redo (a new branch, the diff re-applied, a new PR, the old one closed as superseded). This moves the refusal to the moment the repair is still free, closes the spelling gap that let the forbidden form through, and re-scopes the PR-time gate now that the squash message comes from the body.
Three parts
1. A pre-push refusal —
scripts/check-commit-card-trailers.mjs, called from.githooks/pre-push.It judges exactly the commits a push would publish: the local sha, minus the remote sha the push protocol hands it, minus every remote-tracking ref. Everything it judges is therefore UNPUBLISHED, which is what lets it prescribe an ordinary reword instead of a rewrite — the whole trade the move buys. It adds no grammar: the three relation extractors are the half-state sweep's, imported and read at
markdown: false(a commit message is not markdown, so backticks do not defuse a trailer). A fourth shape is new — a bare#Nin TRAILER position, where a declaration needs no keyword beside it; a reference in ordinary body prose is deliberately left alone..githooks/pre-pushkeeps the os-regen deferral refusal it carried, unchanged in behaviour. The ref lines git writes on stdin used to be drained and discarded; they are now read once and handed to the check whose whole input they are.2. The spelling gap —
partOfReandrefsReinscripts/pm/check-half-states.mjs.Measured before the change, on the commit surface:
Part of #NPart-of #NPart of: #NPart-of: #NRefs #NRefs: #NFixes: #NEvery dispatch order in this repo lists
Part-ofas a forbidden form, so a dev who greps for it before pushing gets 0 and trusts a net with no thread in that square. The separator is now the oneclosingKeywordRealready ships (an optional colon), and the word takes a hyphen where it takes a space — a strict superset, so nothing that matched before stops matching.Refs:is the same colon gap in the sibling relation and is the git-trailer spelling of that declaration; it is fixed in the same edit.Ref,ReferencesandParts ofstay out, as before.One pinned case flipped and was replaced rather than reworded: it asserted that
Refs: #Nwas not the spelling, which pinned the gap rather than a decision.3. The PR-time gate is re-scoped —
check:partof-closing-keyword.Its RULE 2 rested on one premise: the squash message is assembled from the COMMIT MESSAGES. That premise is gone —
squash_merge_commit_messageisPR_BODY(paired withPR_TITLE, the only combination GitHub accepts it in), so every squash, queue merges included, lands the body. RULE 2 and its commit-list input are removed; the gather step and thepull-requests: readscope it needed go with them. RULE 1 and RULE 3 are untouched, the numbering is left alone (the numbers are the order the rules were learned), and the header now says where RULE 2 went and what the move gives up: a hook is registered per clone, so a clone that never ranpnpm installpushes unchecked. That is the ruled trade; CI holds the self-test.Measured, live
Four legs on this branch, against the real hook (
core.hooksPath=.githooks):check:commit-card-trailers: 2 commit message(s) on this push carry no card relation.Part of #17727refused the push, exit 1, namingcommit 1c4b62ca7 ("chore: dark control leg one"),line 3.Part-of: #17727refused, namingcommit 1bb31a97e,line 3. This is the form that passed before this PR.Issue: #17727) refused, namingcommit d18d3e2d2,line 3, as a trailer declaration rather than as a relation.Each dark leg was an empty commit, removed with
git reset --hardto the pushed tip afterwards; the branch tip is unchanged by them.Self-tests
check:commit-card-trailers— 56 cases, 8 batteries with a pinned floor. Includes the three substring shapes that must stay GREEN, each measured against the hand grep this replaces: the stem in ordinary prose (a closed ten-member enum), in an identifier (findClosestMatches), and in an ordinary word (fixture) — a bare-stem grep flags all three, and this must not.check:partof-closing-keyword— 45 cases (from 82: RULE 2's batteries moved out), roster and floor updated in the same edit so the removal cannot read as batteries that stopped running.check:pm-half-states— 3616 cases (from 3606).Acceptance notes
pnpm installhaving run in that clone. Noted, not filed — the ruling chose this trade explicitly, and the alternative (a PR-time read of a commit list whose text no longer lands) is what this PR removes..claude/agents/os-dev.mdgains its one-line pointer in a separate PR: it is a governed surface with its own merge path.Generated by Claude Code