← board

stale_binary_hint() compares timestamps, so a rebase defeats it

The code

tools/gate.sh:107:

stale_binary_hint() {
  local newest binmt
  newest=$(git log -1 --format=%ct -- compiler/ 2>/dev/null) || return 0
  binmt=$(stat -c %Y compiler/pascal26 2>/dev/null) || return 0
  [ -n "$newest" ] && [ -n "$binmt" ] || return 0
  if [ "$binmt" -lt "$newest" ]; then
    echo "gate: NOTE compiler/pascal26 is OLDER than the last commit touching"
    ...

A commit timestamp against a file mtime. git pull --rebase rewrites commits and gives them new committer dates while changing nothing about the sources, so the binary that was correct a second ago becomes "older than the last commit touching compiler/" and the gate advises a rebuild it does not need.

Observed

frankB, wide tier at 3f9937e6c: every step PASS except the self-host fixedpoint, with the stale-binary NOTE attached. Their tools/sync.sh had run a pull --rebase at 23:37:01, mid-step. Two independent checks that the binary was in fact correct:

Two routes, same bytes. The binary was never stale.

Why this is worse than a false positive

It is self-concealing. The note reached a useful conclusion — "do not believe this FAIL" — from a false premise. Anyone who acts on it by rebuilding and re-gating gets a green, concludes the note was right, and never learns the premise was wrong. A wrong instrument that is rewarded for being wrong will not be found by using it; it can only be found by reading it, which is what happened here and only because frankB checked the binary two other ways first.

The fix is already in the tree

tools/compiler_srchash.sh prints one hash over every source the self-host fixedpoint is a fixedpoint of — per-file sha256 hashed as a sorted set, so it covers names as well as contents, and tools/selfhost_stamp_devtest.sh asserts its file list matches the Makefile's in both directions. It is immune to rebases, cherry-picks, and mtime churn, because it hashes content.

compiler/.pascal26.fixedpoint already records a srchash line for exactly this comparison. The staleness question is:

[ "$(tools/compiler_srchash.sh)" = "$(sed -n 's/^srchash //p' compiler/.pascal26.fixedpoint)" ]

Content vs content. Note this is strictly more informative than the timestamp test — it also catches a binary built from different sources that happen to be newer, which the mtime comparison cannot see at all.

Keep the hint a hint: the comment above the function is right that gate.sh must not rebuild before comparing, or it loses the anti-Thompson property. Only the predicate changes.

Second defect, same investigation: the evidence is deleted on FAIL

tools/selfhost_fixedpoint.sh:41 is trap 'rm -rf "$T"' EXIT — unconditional. So on FAIL the stage_2a and tested binaries go with it, and all that survives is the message stage_2a / tested differ: byte 153, line 1. frankB could not produce a repro for that reason.

Retaining those two files on failure would turn this class from anecdote into evidence, cheaply — one cmp -l on a real instance.

Do NOT read the byte offset as a mechanism discriminator. An earlier draft of this ticket suggested byte 153 was "early enough to be header rather than code". frankA hit the same FAIL at byte 339, and their reading — which I accept — is that the offset merely tracks where the two images first diverge. 153 and 339 are not two populations, and inviting that inference was a defect in this ticket, not in the tool.

Third: a closed ticket may cover a narrower population than its failure mode

bug-t-gate-sh-fixedpoint-reads-the-live-mutable-compiler is in done/, and its devtest asserts this FAIL cannot happen. frankB hit it anyway. Their window does not obviously contain that ticket's mechanism ("compiler/pascal26 replaced mid-check") — the compiler-touching sibling commit landed after the step finished. What did happen mid-step was a rebase, which is a different event.

Recorded as evidence, not as a reopen: rebase-during-check and binary-replaced-during-check may be two members of one family the guard currently models only half of. Whoever revisits that ticket should decide; frankB explicitly declined to assert the mechanism and so do I.

Known-adjacent, not re-filed

The backgrounded run's task notification said exit code 0 while the log's own verdict read gate: RED (exit 1). CLAUDE.md:449 already documents this and names the count it has misled. This is another instance, on the wide tier rather than quick — logged here for the tally, not as news.


Second instance, and a real discriminator (frankA via frankB, 2026-09-01)

Instance 2. frankA, gate.sh quick, same message, byte 339. The NOTE block named their own just-landed commit; rebuild, re-gate, GREEN with nothing else changed. frankB's was the wide tier and frankA's was quick, so this is not tier-specific.

The discriminator is the presence of the NOTE block, not the byte offset. When gate.sh emits the stale-binary NOTE alongside the FAIL, the observed cases are a correct-or-stale binary against a moved HEAD — nothing about the pinned path. A FAIL without that NOTE is the interesting one, and is the case the stage_2a/tested retention above exists to serve. That is a cheap triage rule someone can apply tonight, before any code changes.

Why this makes the accidentally-right problem worse, not better

The two instances differ in a way the reader cannot see:

NOTE emitted its premise outcome after rebuild
frankA yes, correct true — binary genuinely predated their commit GREEN
frankB yes, correct false — binary built from exactly those sources; a rebase moved the commit date GREEN

Same text, same advice, same green. stale_binary_hint() has a single if and one undifferentiated message — verified: five echo lines, one condition, no elif/else — so it cannot say which case it is in, and the reader has no way to tell either.

This is the argument for replacing the predicate rather than rewording the message. A better-worded NOTE would still be right-for-the-wrong-reason half the time. tools/compiler_srchash.sh answers the actual question — were these bytes built from these sources — and its answer differs between the two rows above, which is exactly the discrimination the timestamp test cannot provide at any threshold.

Deprioritised 2026-09-02 — the Track T tooling backlog was cut as a pile

This ticket is not being called wrong. It was moved as part of a pile, not judged individually, and nothing here disputes its finding.

Owner decision. 73 of the 74 open track: T tickets were filed between 2026-08-31 and 2026-09-02, 58 on one day. The pile was too large to work through and returned almost nothing, and a ticket nobody will fix does not sit neutrally — it stays in the ranker forever at zero value, which is the argument CLAUDE.md already makes for a terminal folder over a low prio.

Four were kept in the ranker on a purely structural test — an active umbrella or a hard blocked-by: edge from live work: umbrella-one-full-tier-run-with-no-red-tier, feature-t-freebsd-image-and-runner, and the two regression-test-core-* reds that block the umbrella.

Kept, not deleted, for two reasons: so the finding is not rediscovered and refiled from scratch by the next agent who trips over it, and so it can be pulled back if what it touches becomes load-bearing.

To revive it: move it to the owning lane's backlog, set status: backlog, and say in the ticket WHAT CHANGED to make it matter now. Restoring it because it reads well is how the pile comes back.