Skip to content

backlog: invert BACKLOG #1269 off a file that cannot answer it, and file #1349 with its own severity already retracted - #554

Merged
wshallwshall merged 9 commits into
mainfrom
ledger/1269-inversion-and-1349-rescue-refs
Aug 23, 2026
Merged

backlog: invert BACKLOG #1269 off a file that cannot answer it, and file #1349 with its own severity already retracted#554
wshallwshall merged 9 commits into
mainfrom
ledger/1269-inversion-and-1349-rescue-refs

Conversation

@wshallwshall

@wshallwshall wshallwshall commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Five ledger commits from the Dispatcher, cherry-picked onto current main with authorship preserved. docs/BACKLOG.md only.

BACKLOG #1269 prescribed the defect it was filed to fix

The row said to read seat-tick.last. seat-tick.ps1 states in its own source that that file cannot answer "when did THIS seat last get a tick", because a later run overwrites it. It answers WHY NOT, never WHEN or WHO. state.json answers both.

Builder 2 built the code half against the same inversion, independently, and it is PR 557. Neither was derived from the other.

BACKLOG #1349 is filed, amended three times, and the last amendment retracts its own severity

The row now lands already corrected rather than landing and being corrected on main. That sequence is deliberate, and it is why this PR was held and force-pushed twice while it was open.

What the last commit changes: P1 to P2, cluster from data-safety to naming, severity from "unrecoverable loss of committed work" to "a wrong recovery decision", the writer-defect argument recorded as withdrawn with its reason, and the 22 short tags relabelled measured fact rather than measured defect.

Two premises fell, and neither was an instrument failure

The first was mine. I told the author a branch was "gone from every worktree, so the tag is the only thing standing between that work and nothing". The first clause is true; the second is false, and my own output two lines earlier had printed the branch alive:

refs/heads/claude/builder-1-160cec                            b2149fcc   alive
refs/remotes/private/rescuetags/.../claude/builder-1-160cec   b2149fcc   holds the tip
refs/tags/rescue/2026-08-21/builder-1-160cec-local            b68c0b4d   75 behind

A branch can exist as a ref while being checked out nowhere. The tool answered correctly, printed the answer, and the prose said the opposite. No control catches that; only re-reading your own output against your own sentence does.

The second is sharper and belongs to the cleanup seat. They had argued that 374 of 730 tags holding their tip shows tags are meant to track heads. They withdrew it: 374 is also exactly what snapshots of mostly-dormant branches look like. The measurement cannot distinguish "tags track heads" from "most branches have not moved since their snapshot" — a correlation read as intent.

What survives as measured fact

22 tags short of their branch tip. The 167-file, 23,410-insertion gap on the named instance, re-derived independently. The 15 percent disagreement between the two namespaces across 110 branches. And that all 730 dated tags are uncomparable by name, because the leaf is a date-scoped label rather than a branch name.

The remedy in the row is a naming rule, not a repair: quote the full refname, never "the rescue tag".

33b291ea  refs/heads/claude/builder-1-1091-credential-rule                 the branch
33b291ea  refs/remotes/private/rescuetags/auto/.../builder-1-1091-...      tracks it
99650504  refs/tags/rescue/auto/.../builder-1-1091-...                     13 behind

git tag -l cannot see a remotes ref, and an update by push reflog entry cannot exist on a tag. Each instrument is structurally blind to the other's object, which is how two seats produced confident, correct, mutually contradictory reports within an hour.

Counts are deliberately not reconciled

Several namespace slices appear without being forced into one figure. Collapsing them would manufacture exactly the false precision the item is about.

Ledger check

parse_items against main as control: 356/260 to 357/261, 0 duplicate banner fields on both sides. Added 1349. Lost none.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

@wshallwshall
wshallwshall force-pushed the ledger/1269-inversion-and-1349-rescue-refs branch from 1bf2442 to f5606fb Compare August 23, 2026 19:22
@wshallwshall wshallwshall changed the title backlog: invert BACKLOG #1269 off a file that cannot answer it, and file #1349 on rescue-ref staleness backlog: invert BACKLOG #1269 off a file that cannot answer it Aug 23, 2026
@wshallwshall
wshallwshall force-pushed the ledger/1269-inversion-and-1349-rescue-refs branch from f5606fb to 9c0bd81 Compare August 23, 2026 19:24
@wshallwshall wshallwshall changed the title backlog: invert BACKLOG #1269 off a file that cannot answer it backlog: invert BACKLOG #1269 off a file that cannot answer it, and file #1349 with its own severity already retracted Aug 23, 2026
@wshallwshall
wshallwshall force-pushed the ledger/1269-inversion-and-1349-rescue-refs branch 2 times, most recently from 5b0009f to 429f80e Compare August 23, 2026 19:44
wshallwshall and others added 9 commits August 23, 2026 14:52
…fect it was filed to fix

The row directed the alarm to read seat-tick.last. seat-tick.ps1 states verbatim, at its
state-file declaration, that this file cannot answer "when did THIS seat last actually get a
tick", because a run reporting COLD for a seat overwrites the run in which that seat was SENT
-- and that the throttle, needing the same question, was given seat-tick.state.json for it.

#1269 is titled "the obvious implementation reads healthy at the moment it should fire". An
alarm reading seat-tick.last goes healthy on the overwrite. The row specified its own subject.

Inverted: state.json, keyed by absolute worktree path with unix-second values, answers WHEN
and WHO. seat-tick.last answers only WHY NOT, for the exclusion decision. The tick-identity
dedupe, exclusion rule, pinned absolute path, throttle-age consistency check and the
discriminating test are unchanged and still required -- the file assignment moved, not the job.

Also recorded: the seat name is NOT unique in seat-tick.last. Measured today, steward, lander
and dispatcher each appear twice in one live line, once STALE(no-live-session) and once
SENT:<id>, so a first-match scan gets STALE for all three. That is the same first-match trap
as the fleet roster, in the file this alarm was told to parse, and it is the third instance of
one shape in one day.

The emitted vocabulary grew after the row was written -- STALE(no-live-session) at :654 and
the (roster-blind) suffix at :767 are absent from its exclusion list, which is incomplete
rather than wrong. And the pin-the-path rule is right with stale evidence: its two named
decoys are gone, three exist today under a live lane's scratchpad, one in a directory called
ticktest, so newest-wins would land in a live lane's test fixture.

Measured by the building lane, ruled by the dispatcher, authored here under the 2026-08-13
ruling that builders do not write ledger content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…be checked where it matters

P1. A rescue ref whose name contains a branch name can dereference to an ancestor of that
branch with nothing reporting it. Confirmed instance, re-derived independently by two seats to
the file and the insertion: one dated rescue tag is a strict ancestor of the branch it names,
75 commits short, 167 files and 23410 insertions, holding zero commits that branch lacks. That
branch is gone from every worktree, so the ref is the only thing between that work and nothing.

Capturing the tip is plainly the contract: 374 of 730 tag-scheme refs hold it exactly, so the
short ones are broken and the remedy is in whatever writes them. Measured defect count is 22. A
further 32 read as diverged and are recorded UNVERIFIED, because that matcher pairs on the
longest branch name in the tag name and may mis-pair. The 302 naming no local branch are not a
defect -- a deleted branch is the case rescue refs exist for.

A second scheme was censused separately and does not cover the first: of 248 auto-scheme refs,
114 name a live branch, 103 hold the tip, 10 diverged, 0 purely stale. That zero must not be
read as refuting the tag-scheme finding.

The structural finding is worse than any count. A rescue ref can only be verified against a
branch that still exists, and 436 refs name a branch that is gone -- precisely the ones a rescue
ref is for. The measurement is possible exactly where it does not matter and impossible exactly
where it does, so no census of this kind can bound the risk.

Ref names are deliberately not quoted. The forbidden-content gate blocked the first attempt on
three internal worktree slugs and it was right: those do not belong in a public ledger, and the
finding is the shape and the counts rather than the identifiers. Not allowlisted, removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… must not be generalised together

Two seats measured opposite objects and both said "rescue tags". Dated tags are true one-time
snapshots with nothing re-taking them, so staleness is permanent. Auto refs are updated by
push -- one ref's reflog shows six updates -- so staleness there is transient lag.

A seat reported an auto ref 13 commits behind. That reading was correct and transient: it sits
at that ref's reflog @{4} with four pushes after it, and the mechanism caught up. The confirmed
instance in this row is a DATED tag, which is the half where staleness is permanent and
therefore the half worth alerting on.

I had written the amendment as "every ref is a snapshot and nothing re-takes it", citing the
auto-ref reading as evidence. That is false for the auto scheme and I reverted it before
committing. The correction arrived while the file was still dirty.

The structural problem is worse than the row first stated: all 730 dated tags are uncomparable
by name, because the leaf is a date-scoped label rather than a branch name. Zero of them can be
censused the way the auto scheme was, and the confirmed instance was checkable only because the
branch it belonged to was already known.

Instrument note: these are annotated tags, 4 of the 730. rev-parse returns the tag object, not
the commit; use rev-parse ^{commit} or %(*objectname). diff, merge-base and rev-list dereference
silently, so ancestry and diffstat were unaffected, but a tag-object sha published into a log
will not resolve. One such sha was published and corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gree one time in seven

For one branch, one clone holds both a refs/tags/rescue/auto/... tag 13 commits behind and a
refs/remotes/private/rescuetags/auto/... ref holding the head exactly. Both readings correct.
The names differ by namespace alone.

Over 110 branches carrying both and still alive: the tag holds the current head 86 times, the
remote ref 99 times, and the two disagree with each other 16 times. So "check your rescue tag"
returns opposite answers about one time in seven, decided only by which ref the reader reaches
for.

The tooling hides each from the other. git tag -l cannot see a remotes ref; a reflog showing
"update by push" cannot exist on a tag. Two seats each verified a real object with an instrument
structurally blind to the other's, then read the other's result as a refutation. That is why
this row has now been amended three times, and each amendment added a measurement rather than
an opinion.

The remedy is a naming rule rather than a repair: quote the full refname, never "the rescue
tag". The counts in the row -- 730, 393, 752, 248 -- are slices of different namespaces and are
deliberately NOT reconciled, because forcing them into one figure would manufacture the false
precision this row is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t is withdrawn

Filed P1 on "unrecoverable loss of committed work". Measured since, on the one examined case:
the branch exists as a ref at b2149fcc and the push-updated namespace holds that tip exactly.
Two dated tags are behind it. Nothing is unrecoverable.

I was told the branch was gone from every WORKTREE and read that as gone. A branch can exist
as a ref without being checked out anywhere, and I built a P1 severity on the difference
without running the one command that separates them. Same shape as reading four RUNNING roster
rows as four live sessions this morning: an absence in one instrument read as an absence.

The writer-defect argument is withdrawn by its author and is now recorded in the row only so it
is not re-derived. It ran: 374 of 730 tags hold the tip, therefore capturing a tip is intended,
therefore the short ones are broken. But 374 is also exactly what snapshots of mostly-dormant
branches look like, so that measurement cannot distinguish "tags track heads" from "most
branches have not moved since their snapshot". A correlation read as intent.

Corrected: tags are snapshots by design and the push-updated namespace is the live tracking
mechanism. Severity is a wrong recovery decision -- reaching for a short ref, or concluding
work is gone when another namespace holds it -- not data loss. P1 to P2, cluster from data
safety to naming.

Surviving as measured fact: 22 tags short of their branch tip, now labelled a fact rather than
a defect; the 167-file 23410-insertion gap on the named instance; the 15 percent disagreement
between namespaces over 110 branches; and that all 730 dated tags are uncomparable by name.
The remedy was already stated correctly in the third amendment and is unchanged: quote the full
refname, never "the rescue tag".

Corrected before landing rather than after. The row is with the lander and held on request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… no longer resolves

#1236: a brief I sent a lane said admin-unlock shipped in 3ab2775. It did not. That commit's
SUBJECT says sole-admin lockout recovery; its CONTENT adds no locked_until line and no unlock
affordance exists anywhere in messagefoundry/, with a 7-hit control proving the search fires.
A commit subject is not a content check -- and the brief came from a verification pass whose
own agent instructions said not to decide from commit messages. The lane caught it by going to
the code before building on it.

Scope was also wrong in the narrowing direction: the brief named service.py:800 alone, and the
refusal is emitted at :797, :2243 and :2608, so a duration fix at one leaves two inconsistent.

Two findings from the lane are recorded because they remove work rather than add it. The
enumeration tension is already spent -- "account locked" is distinct from "invalid credentials"
today, so a duration-bearing refusal refines a shipped signal rather than opening a disclosure
class, and no owner ruling is needed. And set_password clears the lock at store.py:7897, so
recovery exists mechanically; what is missing is an out-of-band affordance, since a locked-out
sole admin cannot authenticate to reach it and 38 subcommands offer no unlock path.

Scope ruled: build the on-demand recovery, do not re-scope to self-expiry. Waiting is a
workaround, not recovery, for an item whose premise is that nobody else exists to unlock you.
The predecessor lane's own note set the boundary -- a re-scope routes back, confirming the
original scope does not -- so this needed no owner.

#1214: the row cited test_documented_bounds_match_the_live_constants, renamed to
test_transcribed_bounds_match_the_live_constants in a5d5a9a. The old citation no longer
resolves. Both names recorded so a reader of either can follow it.

Authored here under the 2026-08-13 ruling; measured by the lanes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n was the error

The row briefly said admin-unlock did not ship in 3ab2775. That correction was wrong and is
replaced. Verified repo-wide with both controls firing: __main__.py:603 registers the
admin-unlock subcommand, :3962 clears the lock via record_login_failure(failed_attempts=0,
locked_until=None), :3964 audits auth.admin_unlocked, and 3ab2775 added 92 lines to that file.

So the on-demand affordance already exists and needs no build. What remains is narrower: a
duration-bearing refusal is absent at all three sites, and lock cycles are uncapped because
_register_failure has no counter. My earlier scope ruling of "build the on-demand recovery" is
therefore satisfied already; the remaining limbs are the duration and the cycle cap.

The row now records how the wrong correction was produced, because the control is the lesson.
Three layers failed in the same direction: a search scoped to auth/service.py returned a zero
that was TRUE FOR THAT FILE and was published as repo-wide, since admin-unlock lives in
__main__.py; the backstop was both filtered and truncated with head -8 so it never reached the
right file; and a control was printed and did pass.

The control confirmed the instrument worked on the file that was searched, not on the claim
that was made. A control that certifies your scope instead of your claim is not a control. A
scoped search whose scope is wrong produces a true zero and a false conclusion, and a control
drawn from the same scope cannot see it.

The #1214 citation fix in the previous commit is unaffected and stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The row said __main__.py:603 registers admin-unlock and named three sites. An independent
repo-wide re-derivation measures :604, and a FOURTH site at :5091 -- the dispatch-table entry
"admin-unlock": _admin_unlock, which is the line that makes the subcommand reachable at all.
Neither changes the conclusion; both are corrected because a line number in a ledger row gets
followed.

Verified here before amending: :604 and :5091 both confirmed by git grep on origin/main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion limb needs an owner

This row said twice that a duration-bearing refusal needs no owner ruling. Wrong both times.
The distinct strings live only in logs and audit. Verified at the boundary with the control
firing: service.py:147's LoginOutcome contract says the error field is for logs/audit and must
never leak the reason to clients; auth_routes.py:259 raises HTTPException(401, "invalid
credentials") as a fixed string whenever not outcome.ok, regardless of outcome.error; and
outcome.error appears zero times in auth_routes.py against a control of 7 for "outcome.".

So a client sees "invalid credentials" whether the account is locked, the password is wrong, or
the user does not exist. There is no enumeration leak today, which inverts the conclusion: a
duration reaching the locked-out operator is a NEW disclosure to an unauthenticated caller --
that the account exists, is locked, and for how long. Owner-level, and routed.

A duration added only to the error field would help nobody, since the operator never sees it.
And MFA and WebAuthn return a bare False with no error channel, so a duration there needs a
different mechanism rather than the same edit three times.

The cycle cap is unaffected and remains buildable.

Root cause of both wrong versions, from the lane that measured and retracted them: error
strings were compared INSIDE service.py and a conclusion published about what a CLIENT sees.
Both times the local measurement was correct and the LAYER was wrong -- the same shape as
searching one file and concluding about the repo, one level up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall
wshallwshall force-pushed the ledger/1269-inversion-and-1349-rescue-refs branch from 429f80e to e6820e6 Compare August 23, 2026 19:52
@wshallwshall
wshallwshall merged commit 8a48f8f into main Aug 23, 2026
39 of 40 checks passed
@wshallwshall
wshallwshall deleted the ledger/1269-inversion-and-1349-rescue-refs branch August 23, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant