Skip to content

fix(ci): refuse card relations in commit messages at push, and re-scope the PR-time gate to the body - #17745

Merged
os-sales merged 4 commits into
mainfrom
claude/issue-17727-card-trailers-pre-push
Sep 12, 2026
Merged

fix(ci): refuse card relations in commit messages at push, and re-scope the PR-time gate to the body#17745
os-sales merged 4 commits into
mainfrom
claude/issue-17727-card-trailers-pre-push

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

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 #N in TRAILER position, where a declaration needs no keyword beside it; a reference in ordinary body prose is deliberately left alone.

.githooks/pre-push keeps 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 — partOfRe and refsRe in scripts/pm/check-half-states.mjs.
Measured before the change, on the commit surface:

spelling before after
Part of #N HIT HIT
Part-of #N MISS HIT
Part of: #N MISS HIT
Part-of: #N MISS HIT
Refs #N HIT HIT
Refs: #N MISS HIT
Fixes: #N HIT HIT

Every dispatch order in this repo lists Part-of as 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 one closingKeywordRe already 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, References and Parts of stay out, as before.

One pinned case flipped and was replaced rather than reworded: it asserted that Refs: #N was 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_message is PR_BODY (paired with PR_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 the pull-requests: read scope 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 ran pnpm install pushes 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):

  • LIT — the two real commits of this PR pushed clean: check:commit-card-trailers: 2 commit message(s) on this push carry no card relation.
  • DARK — a commit carrying Part of #17727 refused the push, exit 1, naming commit 1c4b62ca7 ("chore: dark control leg one"), line 3.
  • DARK — the colon spelling Part-of: #17727 refused, naming commit 1bb31a97e, line 3. This is the form that passed before this PR.
  • DARK — a bare reference in trailer position (Issue: #17727) refused, naming commit d18d3e2d2, line 3, as a trailer declaration rather than as a relation.

Each dark leg was an empty commit, removed with git reset --hard to 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

  • The residual this PR does not close: a hook is per-clone and reports to nobody, so enforcement depends on pnpm install having 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.md gains its one-line pointer in a separate PR: it is a governed surface with its own merge path.

Generated by Claude Code

…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>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026
@github-actions github-actions Bot added size/xl ci/cd dependencies Pull requests that update a dependency file labels Sep 12, 2026
`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>
@os-sales
os-sales marked this pull request as ready for review September 12, 2026 02:39
@os-sales
os-sales added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 5d66e38 Sep 12, 2026
37 checks passed
@os-sales
os-sales deleted the claude/issue-17727-card-trailers-pre-push branch September 12, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants