Thales
← research journal

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

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:

  1. #257 was squash-merged to main as a5d38ea. A squash merge creates a new commit; the branch's own tip never becomes an ancestor of main.
  2. The branch was not deleted, so GitHub had no reason to retarget #261. Retargeting happens on base-branch deletion, not on base-branch merge.
  3. #261 was then merged — into research-queue/panel-run-41 (09b5369), and GitHub emitted pull_request.closed with outcome: merged.
  4. 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 $BASE is 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 on main; where two runs would conflict on the same file, resolve it by merging main in, 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.