What happened
src/vouch/pr_bot.py's CORE_GLOBS protects core files (proposals.py,
storage.py, audit.py, server.py, http_server.py, cli.py,
pr_bot.py itself, .github/**, etc.) by exact path match — a change to
any of them must come from the owner, enforced by two workflows:
.github/workflows/trust-gate.yml fails an untrusted-author PR that
touches a core path.
.github/workflows/auto-merge.yml / pr-verify.yml never arm native
auto-merge for a PR classified core — should_arm_automerge() hard-blocks
on is_core.
Both build the changed-file list the same way:
gh pr view "$PR" --repo "$REPO" --json files --jq '.files[].path' > changed.txt
gh pr view --json files is backed by the GraphQL PullRequestFile type,
which carries only path / additions / deletions — no previous-filename
field. When GitHub detects a rename (default similarity threshold, same as
plain git mv), only the new path shows up in this list. _match() in
pr_bot.py does exact string equality against CORE_GLOBS:
def _match(path: str, glob: str) -> bool:
g = glob.lstrip("/")
if g.endswith("/**"):
prefix = g[:-3]
return path == prefix or path.startswith(prefix + "/")
return path == g
So git mv src/vouch/http_server.py src/vouch/web_server.py (plus any edit
inside it) makes the changed-file list contain only web_server.py —
http_server.py never appears — and classify() reports is_core: False.
trust-gate.yml then reports "untrusted author, no core paths touched —
ok" for a PR that substantively rewrote a core file, and if the owner later
applies the auto-merge label (a routine glance, given the bot exists to
avoid re-diffing every PR by hand), the PR is eligible to auto-merge as
klass=code instead of being permanently blocked as klass=core.
What you expected
A PR that renames a CORE_GLOBS path — with or without further edits —
should still classify as core: trust-gate.yml should flag it for an
untrusted author, and should_arm_automerge() should refuse to arm.
Reproduction
from vouch import pr_bot
# what `gh pr view --json files --jq '.files[].path'` produces for a
# GitHub-detected rename of a CORE_GLOBS path:
changed = ["src/vouch/web_server.py"]
print(pr_bot.classify(changed))
# {'is_core': False, 'is_ui': False, 'is_code': True} <-- should be is_core: True
print(pr_bot.klass(changed))
# 'code' <-- what auto-merge.yml's `classify --print-klass` step emits
import subprocess, sys
f = "changed.txt"
open(f, "w").write("src/vouch/web_server.py\n")
r = subprocess.run([sys.executable, "-m", "vouch.pr_bot", "core-touched",
"--files-file", f])
print(r.returncode) # 1 == "not core" == what trust-gate.yml checks;
# a real `http_server.py` rename produces exactly this.
I confirmed independently (reading the cli/cli Go source backing gh)
that gh pr view --json files has no previousFilename/previous_filename
field anywhere in the PullRequestFile shape — this isn't a hypothetical,
it's the exact command all three workflows (trust-gate.yml,
auto-merge.yml, comment-command.yml) run.
Environment
- vouch version:
test branch @ current HEAD (post-1.3.0, pr-bot merged this week)
- Python version: 3.11+ (workflow pins 3.12)
- OS: n/a — this is CI-workflow / GitHub Actions logic, not the CLI
- Host: n/a
.vouch/ state
Not applicable — this bug is in the repo's own CI trust-gate, not in a KB.
Anything else
pr_bot.py's own docstring states the intent plainly: "the review-gate
core: writes here are the north star... a PR touching any of these needs
the owner's review and is never merged by automation." The rename blind
spot breaks that guarantee for the one detection mechanism (_match)
that's supposed to enforce it, in the newest and most trust-sensitive
module in the repo.
Suggested fix: switch the file-list source from gh pr view --json files
(GraphQL-backed, no rename tracking) to the REST files endpoint
(gh api repos/{owner}/{repo}/pulls/{number}/files --paginate), which does
populate previous_filename on renamed entries, and feed both the new and
previous filename into classify/core-touched so a rename can never drop
a core path off the radar.
What happened
src/vouch/pr_bot.py'sCORE_GLOBSprotects core files (proposals.py,storage.py,audit.py,server.py,http_server.py,cli.py,pr_bot.pyitself,.github/**, etc.) by exact path match — a change toany of them must come from the owner, enforced by two workflows:
.github/workflows/trust-gate.ymlfails an untrusted-author PR thattouches a core path.
.github/workflows/auto-merge.yml/pr-verify.ymlnever arm nativeauto-merge for a PR classified
core—should_arm_automerge()hard-blockson
is_core.Both build the changed-file list the same way:
gh pr view --json filesis backed by the GraphQLPullRequestFiletype,which carries only
path/additions/deletions— no previous-filenamefield. When GitHub detects a rename (default similarity threshold, same as
plain
git mv), only the new path shows up in this list._match()inpr_bot.pydoes exact string equality againstCORE_GLOBS:So
git mv src/vouch/http_server.py src/vouch/web_server.py(plus any editinside it) makes the changed-file list contain only
web_server.py—http_server.pynever appears — andclassify()reportsis_core: False.trust-gate.ymlthen reports "untrusted author, no core paths touched —ok" for a PR that substantively rewrote a core file, and if the owner later
applies the
auto-mergelabel (a routine glance, given the bot exists toavoid re-diffing every PR by hand), the PR is eligible to auto-merge as
klass=codeinstead of being permanently blocked asklass=core.What you expected
A PR that renames a
CORE_GLOBSpath — with or without further edits —should still classify as
core:trust-gate.ymlshould flag it for anuntrusted author, and
should_arm_automerge()should refuse to arm.Reproduction
I confirmed independently (reading the
cli/cliGo source backinggh)that
gh pr view --json fileshas nopreviousFilename/previous_filenamefield anywhere in the
PullRequestFileshape — this isn't a hypothetical,it's the exact command all three workflows (
trust-gate.yml,auto-merge.yml,comment-command.yml) run.Environment
testbranch @ current HEAD (post-1.3.0, pr-bot merged this week).vouch/stateNot applicable — this bug is in the repo's own CI trust-gate, not in a KB.
Anything else
pr_bot.py's own docstring states the intent plainly: "the review-gatecore: writes here are the north star... a PR touching any of these needs
the owner's review and is never merged by automation." The rename blind
spot breaks that guarantee for the one detection mechanism (
_match)that's supposed to enforce it, in the newest and most trust-sensitive
module in the repo.
Suggested fix: switch the file-list source from
gh pr view --json files(GraphQL-backed, no rename tracking) to the REST files endpoint
(
gh api repos/{owner}/{repo}/pulls/{number}/files --paginate), which doespopulate
previous_filenameon renamed entries, and feed both the new andprevious filename into
classify/core-touchedso a rename can never dropa core path off the radar.