Filed by the domain:engine execution PM seat, from the #16224 round (PR #17252).
⛔ Filed unassigned and unlabelled, as an observation for triage. Grade deliberately not asserted, domain deliberately not asserted.
The gap
check-partof-closing-keyword.mjs RULE 2 forbids a card-relation trailer in any commit message — 「No commit on the pull request may carry a card-relation trailer at all: no closing keyword, no Part-of and no Refs bound to any card number. The body is the only carrier of the relation.」 The same contract is stated to implementers at .claude/agents/os-dev.md:283: 「卡片关系只在 PR 正文声明一次:commit ⛔ 不带卡片 trailer」.
The rule is enforced at PR time by a check that cannot repair what it catches. By the time the guard runs, the commits are pushed. The only edit that removes a trailer from a pushed commit message is an amend/rebase/force-push — which this repository forbids absolutely:
AGENTS.md:470 (Prime Directive): 「Never git push --force / --force-with-lease, and never push main.」
.claude/agents/os-dev.md:68: 「⛔ 永不编辑 content/docs/releases/、force-push、推 main、合并任何东西。」
⇒ A dev that writes the trailer has produced a state that is red and unrepairable at the same time. The check is advisory (partof-closing-keyword-guard.yml:27-30 — no merge_group: trigger, deliberately out of the required-context registry), so the PR still lands; the trailer lands with it.
Measured on PR #17252
All three commits carried it:
ab046d7c25 "Red on this commit, by construction. Part of #16224."
a05c0449e9 "Part of #16224."
179657d5f7 "Part of #16224."
node scripts/check-partof-closing-keyword.mjs ⇒ exit 1, on a real finding. The implementer found it itself and declared it unprompted rather than leaving it for a reviewer — the process worked at every step except the one that would have prevented it.
Why the usual escape does not apply here
The guard's own workflow documents a no-push repair for its OTHER rules: 「rewording the sentence fires a fresh event carrying the fresh body and the check goes green with no push and no re-run」 (partof-closing-keyword-guard.yml:21-26). That works because RULE 1 and RULE 3 read the body, which is editable in place. RULE 2 reads commit messages, which are not. The edited trigger buys RULE 2 nothing.
Nor can it be discharged at the merge button: this repo lands through the merge queue, and a queue merge does not let a lander retype the assembled squash message.
What a fix might look like — options only, not a recommendation
Whoever picks this up should expect to choose, not to implement a foregone conclusion:
- Move the enforcement earlier — a
commit-msg hook in .claude/hooks/ refusing the trailer at write time, where the repair is free. Cost: hooks are a governed surface, so it is not a seat-landable change.
- Make the check's failure text state the accepted outcome, since the state is unrepairable by design. Today it fails without saying that the only lawful response is to land with the residue, which invites exactly the force-push the repo forbids — this seat itself briefly instructed a dev to do that before re-reading the rules.
- Decide the residue is acceptable and downgrade RULE 2 to report-only, on the argument that
Part of moves no card and the body remains the operative carrier.
- Do nothing — a legitimate answer if the trailer's cost is judged lower than any of the above. If so this closes
not_planned with that reasoning recorded.
Not to be conflated with
RULE 1 (a body that says both Part of #N and closes #N) and RULE 3 (a closing keyword inside a negating sentence) are body-scoped, repairable in place, and working as intended. This card is about RULE 2 only.
Generated by Claude Code
Filed by the
domain:engineexecution PM seat, from the #16224 round (PR #17252).⛔ Filed unassigned and unlabelled, as an observation for triage. Grade deliberately not asserted, domain deliberately not asserted.
The gap
check-partof-closing-keyword.mjsRULE 2 forbids a card-relation trailer in any commit message — 「No commit on the pull request may carry a card-relation trailer at all: no closing keyword, no Part-of and no Refs bound to any card number. The body is the only carrier of the relation.」 The same contract is stated to implementers at.claude/agents/os-dev.md:283: 「卡片关系只在 PR 正文声明一次:commit ⛔ 不带卡片 trailer」.The rule is enforced at PR time by a check that cannot repair what it catches. By the time the guard runs, the commits are pushed. The only edit that removes a trailer from a pushed commit message is an amend/rebase/force-push — which this repository forbids absolutely:
AGENTS.md:470(Prime Directive): 「Nevergit push --force/--force-with-lease, and never pushmain.」.claude/agents/os-dev.md:68: 「⛔ 永不编辑content/docs/releases/、force-push、推main、合并任何东西。」⇒ A dev that writes the trailer has produced a state that is red and unrepairable at the same time. The check is advisory (
partof-closing-keyword-guard.yml:27-30— nomerge_group:trigger, deliberately out of the required-context registry), so the PR still lands; the trailer lands with it.Measured on PR #17252
All three commits carried it:
node scripts/check-partof-closing-keyword.mjs⇒ exit 1, on a real finding. The implementer found it itself and declared it unprompted rather than leaving it for a reviewer — the process worked at every step except the one that would have prevented it.Why the usual escape does not apply here
The guard's own workflow documents a no-push repair for its OTHER rules: 「rewording the sentence fires a fresh event carrying the fresh body and the check goes green with no push and no re-run」 (
partof-closing-keyword-guard.yml:21-26). That works because RULE 1 and RULE 3 read the body, which is editable in place. RULE 2 reads commit messages, which are not. Theeditedtrigger buys RULE 2 nothing.Nor can it be discharged at the merge button: this repo lands through the merge queue, and a queue merge does not let a lander retype the assembled squash message.
What a fix might look like — options only, not a recommendation
Whoever picks this up should expect to choose, not to implement a foregone conclusion:
commit-msghook in.claude/hooks/refusing the trailer at write time, where the repair is free. Cost: hooks are a governed surface, so it is not a seat-landable change.Part ofmoves no card and the body remains the operative carrier.not_plannedwith that reasoning recorded.Not to be conflated with
RULE 1 (a body that says both
Part of #Nand closes#N) and RULE 3 (a closing keyword inside a negating sentence) are body-scoped, repairable in place, and working as intended. This card is about RULE 2 only.Generated by Claude Code