fix(ci): gate policy check survives an API outage; merge master; drop NUL separators

Three fixes on the strict PR usefulness gate (#3698), plus the merge that
brings in the policy it enforces.

1. The mechanical policy check now outlives the model. The
   ANTHROPIC_API_KEY guard used to sit above detectPolicyMisses, so a PR
   with no intent paragraph and no screenshot got a green NEUTRAL skip
   whenever the key was absent or Anthropic was down — "wait for a 500"
   was a documented way past the one hard requirement. The #3745 branch
   now sits above the key guard and the spend guard: a policy miss is
   close-lane + the friendly fix-it comment + exit 1 with no API
   dependency at all. A compliant PR that hits a missing key or a dead
   API keeps the round-1 NEUTRAL behavior unchanged (loud comment,
   ::warning::, exit 0, stale gate:* labels cleared) — and the NEUTRAL
   comment now says plainly that the *usefulness verdict* did not run,
   while still reporting the title check and mechanical red flags it was
   able to compute without a model.

2. Merged origin/master, which carries #3745's CONTRIBUTING.md section
   and .github/pull_request_template.md. No conflicts: this branch never
   touched VERSION / package.json / CHANGELOG.md, so master's 0.42.72.1
   carried through untouched — the feature branch adds no version bump.
   The test's inlined pull_request_template fallback (only needed while
   the branch predated the merge) is gone; it now reads the real file, so
   growing the template's own prose past the 40-word bar fails here
   instead of silently letting an untouched template through. The
   CONTRIBUTING_URL deep link is pinned against a GitHub-style slug of
   every heading in the merged CONTRIBUTING.md, with the slugger itself
   pinned so it cannot "pass" against an anchor GitHub never generates.

3. hashInputs joined its three fields with literal NUL bytes, which made
   grep treat the whole of scripts/pr-gate.mjs as binary — any future
   grep-based CI guard over that file would have matched nothing and
   passed silently. Replaced with JSON.stringify of the tuple: still
   unforgeable (each field is quoted and escaped), still stable by
   construction, and printable. `grep -c hashInputs scripts/pr-gate.mjs`
   now returns 2 instead of nothing. Existing sticky-comment state hashes
   are invalidated once, costing one re-verdict per open PR.

Tests: 95 pass / 0 fail in test/pr-gate-workflow.test.ts. The no-API-key
policy-miss case was verified to fail against the pre-fix ordering.
Verified live against the Anthropic API: HTTP 200, strict JSON, all seven
required keys, merge-lane on a compliant fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-04 10:46:09 +08:00
committed by Sina Matian
co-authored by Claude Fable 5
parent 94662eb1e1
commit 4a00c31b12
3 changed files with 158 additions and 58 deletions
+9 -5
View File
@@ -14,11 +14,15 @@ name: PR Gate
# every ${{ }} is env-bound; run: scripts use plain env vars.
# - Only the issues API is used (comments + labels), so issues:write is the
# single write grant; the checkout drops its credentials.
# - If ANTHROPIC_API_KEY is missing or the API is unreachable, the script
# NEUTRAL-skips loudly (sticky comment + warning annotation, exit 0) and
# CLEARS any stale gate:* label — never a silent green, never a red X for a
# missing secret, never a stale verdict. A model REFUSAL is not a skip: it
# routes to needs-maintainer so refusing is not a way to dodge the gate.
# - The mechanical CONTRIBUTING.md #3745 check (intent paragraph + screenshot)
# runs BEFORE any API dependency, so a PR missing either still lands in
# close-lane during an Anthropic outage — an outage is not a way through.
# - If ANTHROPIC_API_KEY is missing or the API is unreachable on an otherwise
# compliant PR, the script NEUTRAL-skips loudly (sticky comment + warning
# annotation, exit 0) and CLEARS any stale gate:* label — never a silent
# green, never a red X for a missing secret, never a stale verdict. A model
# REFUSAL is not a skip: it routes to needs-maintainer so refusing is not a
# way to dodge the gate.
# Pinned by test/pr-gate-workflow.test.ts.
on: