STK-1 — a records PR can merge into a branch that is not main, report merged, and silently deliver nothing: it happened to panel run 42's entire output tonight. Nothing checks a PR's destination — and because the auto-merge workflow reads the GUARD from that same unchecked base ref, AMG-4's invariant rests on it too
status: open · raised panel run 42 (2026-10-21, ~23:45Z, from its own failure) · class: G2 loudness — a success report that is not one (NORTHSTAR §1 asset 1: "nothing fails silently") · judgement: YES · effort ~0.1 pd · horizon: decidable immediately; live for every routine that opens a PR
Plain-language summary. Tonight a routine's whole night of work was
reported as successfully merged and in fact went nowhere. Nothing noticed. The
work was recoverable because the routine happened to re-read main twenty
minutes later for an unrelated reason; had it not, four queue records — one of
them a trading-safety finding — would simply not exist, with a green
"merged" against them.
What happened, CONFIRMED-BY-EXECUTION
Panel run 42 opened #261 with its base set to research-queue/panel-run-41,
deliberately, so that run 41's LAB-1 leg 3 and run 42's leg 4 (sequential
appends to the same file) would not conflict. Then:
- #257 was squash-merged to
mainasa5d38ea. A squash merge creates a new commit; the branch's own tip never becomes an ancestor ofmain. - The branch was not deleted, so GitHub had no reason to retarget #261. Retargeting happens on base-branch deletion, not on base-branch merge.
- #261 was then merged — into
research-queue/panel-run-41(09b5369), and GitHub emittedpull_request.closedwithoutcome: merged. - That branch is not an ancestor of
main:
git merge-base --is-ancestor origin/research-queue/panel-run-41 origin/main # NO
git cat-file -e origin/main:research/queue/open/dlv-1-...md # ABSENT
git cat-file -e origin/research-queue/panel-run-41:research/queue/open/dlv-1-...md # present
All four of run 42's files — LAB-1 leg 4, DLV-1, A4S-1, SHR-1 — were stranded, along with a correction commit that withdrew a claim #265 had falsified. Main was left carrying #264's commit message crediting "panel run 42 as LAB-1 leg 4" while the row contained no leg 4.
Why this is the system's and not only the author's
Half of it is mine: do not stack on a base that will be squash-merged. That is a lesson, and it is recorded here.
The other half is that nothing looks at the destination.
scripts/automerge_guard.py grades the diff — which paths changed, whether
anything touches research/queue/approved/, whether a queue move is
sanctioned. Its only base-shaped input is --queue-before, "path to the
base version of the queue file" (:289) — the file's pre-image, not the PR's
base branch. So a records PR aimed anywhere at all passes the same gate, and
the only signal the author gets is the word merged. Every routine in the shop
opens PRs; any of them can be handed a false success this way, and the
observable consequence is indistinguishable from the work never having been
done.
And the sharper half, which I did not see on the first pass. The
workflow does have the destination in hand — .github/workflows/auto-merge-records.yml:62
binds BASE: ${{ github.event.pull_request.base.ref }} — and it uses it as a
trust anchor rather than checking it:
if ! git show "origin/$BASE:scripts/automerge_guard.py" > /tmp/automerge_guard_base.py # :99
verdict=$(python3 /tmp/automerge_guard_base.py --files "$files" ...) # :105
That is AMG-4's fix, and the workflow header states its reasoning (:22-23):
"the guard that grades a PR is read from the BASE ref, so a PR's own rewrite of the guard never grades that PR."
That invariant holds only if the base ref is trusted, and nothing enforces
that it is. $BASE is never compared to the default branch. For #261 the
base was research-queue/panel-run-41, so the guard that graded run 42's PR
was that branch's copy of the guard — not main's. A PR based on a branch its
author can push is therefore graded by a guard its author can write, which is
precisely the key AMG-4 was closing, reopened through the base-ref door.
Labelled INFERRED-STATICALLY, and deliberately not executed. Confirming it
would mean pushing a deliberately permissive guard to a branch and opening a
PR against it, which is exactly the act the deny set exists to prevent. I read
the workflow instead and am reporting the shape. Whoever fixes this should
verify it in a scratch repo, not here. Note it is not reachable for a PR
whose base is main, which is every PR anyone intended to open — the exposure
arrives with the mistake, which is why the one-line base check closes both
halves at once.
It is also the second false success of the same evening: the auto-merge check
was red for an unrelated account-level reason, which trains a reader to
discount that signal exactly when it would have mattered.
Remedy, ~0.1 pd
The guard already runs on every records PR and already knows how to refuse. Add the destination to what it grades:
- assert
$BASEis the default branch, and refuse otherwise with a message naming the actual base. The variable is already bound at:62; this is one comparison. It closes both halves — the stranded-merge half and the guard-provenance half — because both are consequences of an untrusted base. - in the routine prompts /
ops/conventions, one line: base a records PR onmain; where two runs would conflict on the same file, resolve it by mergingmainin, not by stacking.
Neither touches trading, config, a pinned rule or frozen text.
Pre-registered kill criterion
It dies if the workflow or the guard is shown to compare the PR's base against
the default branch anywhere in a refuse path — it does not today: the four
base hits in scripts/automerge_guard.py are basename and the
--queue-before file pre-image, and the workflow's $BASE is only ever an
argument to git show. It also dies if the repo rules that stacked records PRs
are a wanted pattern and some other mechanism reports a merge whose content
did not reach main. In that second case the ruling carries a corollary: name
that mechanism, because tonight nothing did.
The guard-provenance half dies if the workflow is shown to resolve the guard
from the default branch rather than from $BASE — on today's text it does not.
It does not die because the content was recovered. It was recovered by luck — a re-read for an unrelated reason — and a defect whose detection depends on an unrelated coincidence is undetected.
Slot arithmetic, stated honestly
main's open/ is 13 before this PR; DLV-1 and STK-1 take it to 15 against
a cap of 12. That is 3 over, and triage's mechanical overflow rule applies at
the 2026-10-26 sweep. I am not choosing the evictions — the ranking is
triage's, and the pre-declared one (DGX-1) still rests on a rationale #249
falsified. For what it is worth: this row is cheap, mechanical, and in the
class NORTHSTAR puts at P0; DLV-1 bites every Friday the auto-merge gate is
dark. Both would survive a ranking I did not perform.
Prior art checked
grep -rniE "base.ref|stacked|base branch|non-main|retarget" across all four
research/queue/ dirs → nothing. The nearest rows are a different surface:
amg-4-guard-graded-by-own-copy and amg-5-sanctioned-move-fail-open (both
built) concern what the guard grades within the diff, not where the PR is
aimed. rpl-1-replace-path-bypasses-gate (dismissed) is a path-matching
question inside the diff. No overlap.