Problem
#248 made /raise-pr derive and verify a Closes #<N> keyword. That rule is prose read by a model, and nothing enforces it. The same step 6 that mandates the keyword also says "Do not follow a fixed template. Decide which sections earn their place" — so a future model, or a future edit to raise-pr.md, can silently drop back to the pre-#248 state. #248's own evidence table shows that regression is invisible until someone audits eight PRs by hand.
Constitution Principle I's carve-out does not end at recording RED/GREEN once:
MUST still make the check repeatable. Where the tool can run in CI, add it there, so the invariant is gated rather than verified once.
Why this was deferred out of #248
#248 argued no proportionate gate existed, on the grounds that "every PR body carries a closing keyword" would false-positive on legitimately issue-less PRs (dependabot bumps, chore/normalise-cs-line-endings). That reasoning was wrong, and is corrected here so it is not cited again: it defeats only the unconditional gate. A gate conditioned on branch shape has no such problem, because the condition is the shape itself rather than a maintained exception list.
Measured over the last 25 merged PRs, the separation is exact:
- All 6 branches matching
^feature/[0-9]+- carry a closing keyword — so the gate never fails one.
- Every non-matching branch is exempt by construction, including
dependabot/* and chore/normalise-cs-line-endings.
chore/234-format-ship-cadence carries a keyword without matching, which is a false negative — the safe direction.
Zero false positives, no allowlist to maintain.
Suggested shape
A step in the existing .github/workflows/dotnet.yml (already triggered on pull_request), roughly 20 lines against the ~20 lines of prose it guards — a ~1:1 ratio, so feedback_guard_outgrows_the_fix does not fire:
- Parse
^feature/([0-9]+)- from github.head_ref; exit 0 when it does not match.
- Reuse the rule
raise-pr.md step 6 already establishes: gh issue view <N> --json state,url and exit 0 unless the url is exactly this repo's /issues/<N> path — that rejects a PR at that number and an issue in another repo.
- Require a closing keyword for
#<N> in the PR body, with a documented No-close: #<N> — <reason> escape for a branch that only partly implements its issue.
- Add
types: [opened, edited, reopened, synchronize] to the trigger, or re-fetch the body with gh pr view, so a later body edit is re-checked.
Open decision
The No-close: escape hatch is new repo policy, not a mechanical detail — it decides how a partial-implementation branch opts out of a hard gate. Worth settling before implementing.
Deliberately not a .claude/hooks/ gate: the *.tests.sh matrix that hooks/README.md mandates would put this at ~250-400 lines (~15:1), and that form should be refused. Hooks also fail open and can lock out the tooling that would fix them; a CI step fails loudly with no lockout risk.
Related
Problem
#248 made
/raise-prderive and verify aCloses #<N>keyword. That rule is prose read by a model, and nothing enforces it. The same step 6 that mandates the keyword also says "Do not follow a fixed template. Decide which sections earn their place" — so a future model, or a future edit toraise-pr.md, can silently drop back to the pre-#248 state. #248's own evidence table shows that regression is invisible until someone audits eight PRs by hand.Constitution Principle I's carve-out does not end at recording RED/GREEN once:
Why this was deferred out of #248
#248 argued no proportionate gate existed, on the grounds that "every PR body carries a closing keyword" would false-positive on legitimately issue-less PRs (dependabot bumps,
chore/normalise-cs-line-endings). That reasoning was wrong, and is corrected here so it is not cited again: it defeats only the unconditional gate. A gate conditioned on branch shape has no such problem, because the condition is the shape itself rather than a maintained exception list.Measured over the last 25 merged PRs, the separation is exact:
^feature/[0-9]+-carry a closing keyword — so the gate never fails one.dependabot/*andchore/normalise-cs-line-endings.chore/234-format-ship-cadencecarries a keyword without matching, which is a false negative — the safe direction.Zero false positives, no allowlist to maintain.
Suggested shape
A step in the existing
.github/workflows/dotnet.yml(already triggered onpull_request), roughly 20 lines against the ~20 lines of prose it guards — a ~1:1 ratio, sofeedback_guard_outgrows_the_fixdoes not fire:^feature/([0-9]+)-fromgithub.head_ref; exit 0 when it does not match.raise-pr.mdstep 6 already establishes:gh issue view <N> --json state,urland exit 0 unless the url is exactly this repo's/issues/<N>path — that rejects a PR at that number and an issue in another repo.#<N>in the PR body, with a documentedNo-close: #<N> — <reason>escape for a branch that only partly implements its issue.types: [opened, edited, reopened, synchronize]to the trigger, or re-fetch the body withgh pr view, so a later body edit is re-checked.Open decision
The
No-close:escape hatch is new repo policy, not a mechanical detail — it decides how a partial-implementation branch opts out of a hard gate. Worth settling before implementing.Deliberately not a
.claude/hooks/gate: the*.tests.shmatrix thathooks/README.mdmandates would put this at ~250-400 lines (~15:1), and that form should be refused. Hooks also fail open and can lock out the tooling that would fix them; a CI step fails loudly with no lockout risk.Related