The defect
plugins/source-control/hooks/pr-linkage-validator.sh is the shared validator core sourced by both local PR-body pre-checks:
pr-linkage-mcp-gate.sh (PreToolUse on the GitHub MCP PR tools)
pr-body-linkage-gate.sh (the Bash-surface sibling)
It validates two things: a native closing keyword (or a no-issue marker), and a present, non-empty ## Related section.
The upstream reusable it mirrors — melodic-software/ci-workflows/.github/workflows/pr-issue-linkage.yml@v0.14.2, pinned in .github/workflows/pr-issue-linkage.yml — requires five: the closing keyword plus four non-empty contract sections. That workflow's own header states it:
Validates the PR body carries a native closing keyword … and four non-empty contract sections — ## Summary, ## Fix, ## Verification, ## Related
So the local gates under-enforce by three sections: ## Summary, ## Fix, ## Verification.
Why this matters more than a missing check
The validator exists specifically to keep the surfaces in sync. Its own call site says so:
the annotated functions live in pr-linkage-validator.sh, sourced so a drift fix against the upstream ci-workflows validator lands on every surface at once.
That property is the thing that has broken. Worse, the local gate's failure message is authoritative in tone — it prints the full remedy:
Add to the body:
Closes #<issue> (or the literal line: No linked issue)
## Related
- <links, or N/A>
An author who follows that remedy exactly still fails CI. The gate does not merely miss a case; it tells you what a passing body looks like, and is wrong.
Reproduction (observed, not constructed)
Opening #3205 with a body carrying No linked issue, ## Summary, ## Verification, and ## Related — but no ## Fix:
-
pr-linkage-mcp-gate.sh allowed the create_pull_request call.
-
CI pr-issue-linkage / pr-issue-linkage failed (job 97243150413):
##[error]Missing a "## Fix" section. State the concrete change and how it addresses the problem.
-
Adding ## Fix and re-editing the body turned the check green, with no code change — confirming the section list, not something else, was the discriminator.
Grep confirming the gap is in the shared core rather than one caller:
$ grep -n 'Summary\|Fix\|Verification\|Related' plugins/source-control/hooks/pr-linkage-validator.sh
101:# Content of the first `## Related` section on stdout; returns 1 when there is
110:related_section() {
149: if related=$(related_section "$body"); then
150: [[ -n "$related" ]] || LINKAGE_PROBLEMS+=('The "## Related" section is empty.')
152: LINKAGE_PROBLEMS+=('Missing a "## Related" section.')
155: LINKAGE_PROBLEMS+=('Missing a native closing keyword (Closes/Fixes/Resolves #N) and no "No linked issue" marker.')
No ## Summary, ## Fix or ## Verification handling exists anywhere in the file.
The fix
Generalize related_section() into a section lookup over a required-section roster, and check all four. related_section()'s heading-level logic is already correct and reusable — a heading at the same level or higher closes a section, so a nested ### is content, not a terminator — so this is mostly parameterizing an existing helper rather than writing new parsing.
Update the blocked-message remedy to print all four sections, so following it produces a body CI accepts.
Worth deciding while here
The roster is currently duplicated in prose across the workflow header, the validator, and the hook header, and drifted silently once already. Consider whether the roster should be stated in one place the other surfaces cite, rather than restated in three — and whether the upstream pin should be checked for other requirements added between the version the validator was written against and v0.14.2. This issue does not assume the answer; the minimum fix above stands on its own.
Acceptance criteria
Related
The defect
plugins/source-control/hooks/pr-linkage-validator.shis the shared validator core sourced by both local PR-body pre-checks:pr-linkage-mcp-gate.sh(PreToolUse on the GitHub MCP PR tools)pr-body-linkage-gate.sh(the Bash-surface sibling)It validates two things: a native closing keyword (or a no-issue marker), and a present, non-empty
## Relatedsection.The upstream reusable it mirrors —
melodic-software/ci-workflows/.github/workflows/pr-issue-linkage.yml@v0.14.2, pinned in.github/workflows/pr-issue-linkage.yml— requires five: the closing keyword plus four non-empty contract sections. That workflow's own header states it:So the local gates under-enforce by three sections:
## Summary,## Fix,## Verification.Why this matters more than a missing check
The validator exists specifically to keep the surfaces in sync. Its own call site says so:
That property is the thing that has broken. Worse, the local gate's failure message is authoritative in tone — it prints the full remedy:
An author who follows that remedy exactly still fails CI. The gate does not merely miss a case; it tells you what a passing body looks like, and is wrong.
Reproduction (observed, not constructed)
Opening #3205 with a body carrying
No linked issue,## Summary,## Verification, and## Related— but no## Fix:pr-linkage-mcp-gate.shallowed thecreate_pull_requestcall.CI
pr-issue-linkage / pr-issue-linkagefailed (job 97243150413):Adding
## Fixand re-editing the body turned the check green, with no code change — confirming the section list, not something else, was the discriminator.Grep confirming the gap is in the shared core rather than one caller:
No
## Summary,## Fixor## Verificationhandling exists anywhere in the file.The fix
Generalize
related_section()into a section lookup over a required-section roster, and check all four.related_section()'s heading-level logic is already correct and reusable — a heading at the same level or higher closes a section, so a nested###is content, not a terminator — so this is mostly parameterizing an existing helper rather than writing new parsing.Update the blocked-message remedy to print all four sections, so following it produces a body CI accepts.
Worth deciding while here
The roster is currently duplicated in prose across the workflow header, the validator, and the hook header, and drifted silently once already. Consider whether the roster should be stated in one place the other surfaces cite, rather than restated in three — and whether the upstream pin should be checked for other requirements added between the version the validator was written against and v0.14.2. This issue does not assume the answer; the minimum fix above stands on its own.
Acceptance criteria
## Summary,## Fix,## Verificationand## Relatedfor presence AND non-emptiness, using the existing heading-level semantics.LINKAGE_PROBLEMS, so the author sees every problem in one pass rather than one per retry.###subsection inside each required section as content, not a terminator.pr-linkage-mcp-gate.shandpr-body-linkage-gate.sh— pick the change up from the shared core with no per-surface duplication.update_pull_request-without-bodypassthrough are unchanged.Related
pr-issue-linkagefailure is the reproduction..github/workflows/pr-issue-linkage.yml— pins the upstream reusable at v0.14.2 and states the four-section requirement.