Skip to content

fix(source-control): PR-body linkage validator checks 2 of 5 upstream requirements, so both local gates pass bodies CI rejects #3206

Description

@kyle-sexton

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:

  1. pr-linkage-mcp-gate.sh allowed the create_pull_request call.

  2. 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.
    
  3. 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

  • The shared validator checks ## Summary, ## Fix, ## Verification and ## Related for presence AND non-emptiness, using the existing heading-level semantics.
  • Each missing or empty section produces its own distinct entry in LINKAGE_PROBLEMS, so the author sees every problem in one pass rather than one per retry.
  • The blocked-message remedy lists all four sections; an author following it verbatim produces a body the CI gate accepts.
  • A regression test pins the exact fix(docs-hygiene): correct audit-noise's stated negation limitation (0.21.2) #3205 body shape (linkage + Summary + Verification + Related, no Fix) as blocked — it is currently allowed.
  • A regression test pins a nested ### subsection inside each required section as content, not a terminator.
  • Both surfaces — pr-linkage-mcp-gate.sh and pr-body-linkage-gate.sh — pick the change up from the shared core with no per-surface duplication.
  • The no-issue-marker path and the update_pull_request-without-body passthrough are unchanged.

Related

Metadata

Metadata

Assignees

Labels

agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: needs-triageDefault until a priority tier is assigned.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions