Summary
The local PR-body linkage gates accept two linkage states (Closes/Fixes/Resolves #N, or the literal No linked issue / No related issue:). The pr-contract CI step they exist to predict accepts three — the same two plus an explicit non-closing Refs: #N / Relates to: #N marker on its own line — and additionally fails a negated closing reference that the local gates pass.
The gates drifted from CI when melodic-software/ci-workflows#544 (fix(pr-issue-linkage): accept Refs/Relates markers and fail negated closers, merged 2026-09-02) landed. Nothing local followed.
This is a two-directional contract break in one file:
- False positive (blocking). A body carrying
Refs: #N and no closing keyword passes CI and is blocked locally. The author is told to add a closing keyword that would auto-close an issue the PR must not close — the exact hazard ci-workflows#544 added the marker to avoid.
- False negative. A body carrying a negated closer (
does not close #N) passes locally and fails CI, which fails it outright because GitHub's linkage parser is negation-blind and will auto-close the issue on merge anyway.
The gate's own header states mirroring is the point: plugins/source-control/hooks/pr-body-linkage-gate.sh:12 — "WHAT IT ENFORCES — the five requirements of the linkage contract, mirrored" — and :27-38 keys enforcement to the consuming repo's own pr-contract step. So the hook is the side that should change; extending CI further would un-ship #544 for every consumer.
Fix
In plugins/source-control/hooks/pr-linkage-validator.sh (citations at main cf6d15df732ed4a12bbfef10a8be934b5398e8ea):
:151 KEYWORD_ERE and :152 NO_ISSUE_ERE are the only two patterns; :154-158 has_linkage() tests exactly those.
- Add a third, line-anchored
NON_CLOSING_ERE mirroring CI's shape (indent ≤ 3, marker on its own line, optional owner/repo prefix) and OR it into has_linkage at :157. There is no refs/relates pattern anywhere in plugins/source-control/hooks/ today.
- Update the message at
:343 ('Missing a native closing keyword (Closes/Fixes/Resolves #N) and no "No linked issue" marker.') to name the Refs: / Relates to: option, matching CI's own wording at run.sh:515.
- Update the mirrored header text at
pr-body-linkage-gate.sh:21-22 and pr-linkage-mcp-gate.sh:22, and the block remedy at pr-body-linkage-gate.sh:293 (echo " Closes #<issue> (or the literal line: No linked issue)", and its twin at pr-linkage-mcp-gate.sh:296).
- Second half: add the negated-closer check CI runs, or state explicitly in the header that the local gate does not model it. CI's implementation is
run.sh:245-277 (negation_trigger(), the disclaimer-word scan) surfacing at :508-510.
- Cover both shapes in
pr-body-linkage-gate.test.sh so the pair cannot drift again silently.
Verification
Upstream, melodic-software/ci-workflows .github/actions/pr-contract/run.sh, read at tag v0.22.2 (5776760254f8b63cba44e896f51604cb755350d9) and re-read at v0.24.0 (2c1de45aa0e1b1489afb8edfebc12cb3a4fa6ac3) — the linkage logic is byte-identical at both:
:200-203 (header) — "the body must carry a native closing keyword, an explicit non-closing Refs:/Relates to: marker, or a no-issue opt-out".
:284-285 — if (rest ~ /^(refs|relates[ \t]+to):[ \t]*([a-z0-9_.-]+\/[a-z0-9_.-]+)?#[0-9]+[ \t]*$/) { has_non_closing = 1 }
:444 — if (has_non_closing) print "non-closing"
:512-515 — the "Missing a native closing keyword" error fires only when none of closing, non-closing, no-issue is present.
:509 — the negated-closer error, which has no local counterpart.
Local side, plugins/source-control/hooks/ at main cf6d15d — verified byte-identical to the installed plugin cache at source-control 0.55.88, and the same rule is present unchanged back to 0.55.31 (pr-linkage-validator.sh:92-93 there), so this is not a stale-cache artifact.
A concrete repro: a PR body carrying Refs: #1234 on its own line, the four contract sections, and no closing keyword. pr-body-linkage-gate.sh blocks the gh pr create; the pr-contract step accepts it.
Related
Refs: #630
🤖 Generated with Claude Code
https://claude.ai/code/session_01L9DAKHEMJr77LfCskkyit6
Summary
The local PR-body linkage gates accept two linkage states (
Closes/Fixes/Resolves #N, or the literalNo linked issue/No related issue:). Thepr-contractCI step they exist to predict accepts three — the same two plus an explicit non-closingRefs: #N/Relates to: #Nmarker on its own line — and additionally fails a negated closing reference that the local gates pass.The gates drifted from CI when
melodic-software/ci-workflows#544(fix(pr-issue-linkage): accept Refs/Relates markers and fail negated closers, merged 2026-09-02) landed. Nothing local followed.This is a two-directional contract break in one file:
Refs: #Nand no closing keyword passes CI and is blocked locally. The author is told to add a closing keyword that would auto-close an issue the PR must not close — the exact hazard ci-workflows#544 added the marker to avoid.does not close #N) passes locally and fails CI, which fails it outright because GitHub's linkage parser is negation-blind and will auto-close the issue on merge anyway.The gate's own header states mirroring is the point:
plugins/source-control/hooks/pr-body-linkage-gate.sh:12— "WHAT IT ENFORCES — the five requirements of the linkage contract, mirrored" — and:27-38keys enforcement to the consuming repo's ownpr-contractstep. So the hook is the side that should change; extending CI further would un-ship #544 for every consumer.Fix
In
plugins/source-control/hooks/pr-linkage-validator.sh(citations atmaincf6d15df732ed4a12bbfef10a8be934b5398e8ea)::151KEYWORD_EREand:152NO_ISSUE_EREare the only two patterns;:154-158has_linkage()tests exactly those.NON_CLOSING_EREmirroring CI's shape (indent ≤ 3, marker on its own line, optionalowner/repoprefix) and OR it intohas_linkageat:157. There is norefs/relatespattern anywhere inplugins/source-control/hooks/today.:343('Missing a native closing keyword (Closes/Fixes/Resolves #N) and no "No linked issue" marker.') to name theRefs:/Relates to:option, matching CI's own wording atrun.sh:515.pr-body-linkage-gate.sh:21-22andpr-linkage-mcp-gate.sh:22, and the block remedy atpr-body-linkage-gate.sh:293(echo " Closes #<issue> (or the literal line: No linked issue)", and its twin atpr-linkage-mcp-gate.sh:296).run.sh:245-277(negation_trigger(), the disclaimer-word scan) surfacing at:508-510.pr-body-linkage-gate.test.shso the pair cannot drift again silently.Verification
Upstream,
melodic-software/ci-workflows.github/actions/pr-contract/run.sh, read at tagv0.22.2(5776760254f8b63cba44e896f51604cb755350d9) and re-read atv0.24.0(2c1de45aa0e1b1489afb8edfebc12cb3a4fa6ac3) — the linkage logic is byte-identical at both::200-203(header) — "the body must carry a native closing keyword, an explicit non-closingRefs:/Relates to:marker, or a no-issue opt-out".:284-285—if (rest ~ /^(refs|relates[ \t]+to):[ \t]*([a-z0-9_.-]+\/[a-z0-9_.-]+)?#[0-9]+[ \t]*$/) { has_non_closing = 1 }:444—if (has_non_closing) print "non-closing":512-515— the "Missing a native closing keyword" error fires only when none ofclosing,non-closing,no-issueis present.:509— the negated-closer error, which has no local counterpart.Local side,
plugins/source-control/hooks/atmaincf6d15d— verified byte-identical to the installed plugin cache atsource-control0.55.88, and the same rule is present unchanged back to 0.55.31 (pr-linkage-validator.sh:92-93there), so this is not a stale-cache artifact.A concrete repro: a PR body carrying
Refs: #1234on its own line, the four contract sections, and no closing keyword.pr-body-linkage-gate.shblocks thegh pr create; thepr-contractstep accepts it.Related
melodic-software/ci-workflows#544— merged 2026-09-02, added theRefs:/Relates to:acceptance and the negated-closer failure. This is the change the local gates did not follow.Refs #Nfrom the skill's accepted opt-out set") because extending CI "changes shared CI behavior for every consumer". ci-workflows#544 then did extend CI, reversing that premise.melodic-software/provisioning#460item 3.8 (remote-access lane disk-hygiene: skill-frontmatter guard reads CLAUDE_PLUGIN_DATA from env but never receives it -> engine lane fails closed on all platforms #376 close-out). That repo pinspr-contract@2c1de45 # v0.24.0at.github/workflows/ci.yml:504.Refs: #630
🤖 Generated with Claude Code
https://claude.ai/code/session_01L9DAKHEMJr77LfCskkyit6