Skip to content

auth,store: username identity is case-sensitive and must not depend on store collation (BACKLOG #1268), plus the ledger parse hook - #458

Merged
wshallwshall merged 46 commits into
mainfrom
claude/builder-1-160cec
Aug 22, 2026
Merged

auth,store: username identity is case-sensitive and must not depend on store collation (BACKLOG #1268), plus the ledger parse hook#458
wshallwshall merged 46 commits into
mainfrom
claude/builder-1-160cec

Conversation

@wshallwshall

@wshallwshall wshallwshall commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

HELD. AUTO-MERGE IS OFF, DELIBERATELY. The adversarial pass reported NOT CLEAN. Do not arm
this PR.

ONE INTRODUCED FAIL-OPEN, three INTRODUCED FALSE DENIES, each verified against origin/main
and this head with discriminating controls, and a positive control that denies on both.

THE FAIL-OPEN. A pwsh -Command payload invoked through the Bash tool is ALLOWED here
and DENIED on origin/main. PosixEscapes is keyed on the TOOL NAME, not on the
interpreter the command actually reaches -- and a Bash tool call can invoke pwsh. The outer host
is bash so escapes are honoured; the inner interpreter is pwsh, where a backslash is not an
escape. The recursion pulls out the inner code and scans it under POSIX rules, and the span
straddles. It is the same defect the host flag was added to fix, arriving through the
interpreter instead of through the tool name.
An odd number of backslashes is the trigger;
zero and two both deny on both gates.

THE FALSE DENIES ARE ORDINARY WORK, AND THAT IS THE WORSE HALF. Restoring a backup
directory whose last component is named Git; writing a documentation line that quotes a git
command; quoting a config line in prose. All three are ALLOWED on origin/main and DENIED here.
The directory case comes from the program-path collapse now emitting lowercase -- a
canonicalisation that closed a real fail-open and bought this. Controls discriminate cleanly: a
directory named GitHub allows on both, and the same path unquoted allows on both.

This is why the false-deny lens outranks the bypass count: a gate that blocks ordinary work
gets disabled entirely, which is worse than any single bypass.

THE AUTHOR'S WRITTEN DECISION, given rather than inferred: NONE OF THE FOUR ARE ACCEPTED.
The fail-open is blocking -- a security fix that re-opens its own defect through a neighbouring
door must not land. On the Git directory false deny the author's words are worth keeping:
"it is my lowercasing that did it... I would be accepting a cost I created, on behalf of
everyone who runs the gate, at the end of a session where I have been wrong four times about
this file."
That is a trade with both ends named, declined by the person best placed to take it
and worst placed to judge it.

RECOMMENDED DISPOSITION: REASSIGN, DO NOT MERGE AND DO NOT REVERT. The commits close two
real fail-opens, a case-fold gap and one inherited hole, all independently verified. The
findings are precise, controlled, and carry a named fix direction, and the interpreter-axis fix
is small for whoever holds the rig. The branch needs one more pass, not unwinding. Nothing
is exposed by holding: the gate actually running is main's copy and is unaffected.

UNRELATED AND ALSO OPEN: CI is red on an invalid escape in the escaped-quote test. Real --
it becomes a SyntaxError on a future Python and fails at collection, so the module
disappears and reports as fewer tests rather than as a failure. Do not apply the gate's own
remedy
: it advises making the whole literal raw, and doubling the backslash preserves the
value while making the literal raw changes it
, turning one trailing backslash into two. Odd is
the ALLOW case, even is DENY -- so the raw fix would convert the regression row into a test of
the non-regressing case, and it would go green proving nothing.

Builder 1's lane. Two fixes to the worktree gate's own correctness, plus the ledger and
classification work each one turned out to need.

What is in it

The gate's escape-blind fail-opens (BACKLOG #1229 residual). Span ownership is now decided
once, closing two paths where the gate returned ALLOW on input it should have denied. Two verbs
under one rule, not three -- see the retraction below.

An empty population read as a clean run (BACKLOG #1235). The CLI's default path list is
CWD-relative, so invoked from a directory with no docs/ it reported zero files and zero
allocated item numbers and exited 0. Nothing objected, because both counts agreeing at zero
is what a clean run looks like. It refuses a non-zero exit on an empty population now, and
prints what it scanned and where from.

The sharp edge is how it would have armed: the population floor lived only in a pytest, so
anyone wiring the CLI into CI -- the documented way to satisfy the item -- inherited a gate that
reported clean having scanned nothing. Closing the item is what would have armed it.

The coverage bound was missing from exactly the run a reader acts on. "Not scanned: the
private companion repository" printed only after the hits loop, so an early clean return
skipped it. One named constant on every exit path now, with a test pinning the single definition.

Verification, with its scope stated

tests/test_dangling_citation_check.py 45 passed. Across the test_worktree_gate* modules,
581 passed / 13 skipped. Both arms of each fix mutation-verified with an applied-check per
mutant and a byte-identical restore. ruff and ruff format clean; mypy --strict clean over
267 files.

This is not a green full suite and is not offered as one. The x12 and xml extras are
absent from the authoring environment, so that run does not reproduce CI collection. The
tooling tier -- which is where the changed gate actually runs, and therefore the relevant
regression check -- was still running at authoring time and its result is not in hand here.
CI on this PR is the first place both are answered.

A deliberate behaviour change

The emptiness guard makes the CLI exit non-zero where it previously exited 0. Anything
invoking it from a directory without docs/ will now fail where it passed silently. That is
the fix rather than a regression.

Callers are cleared, and the clearance is a second author's. The original check grepped the
script name, while the gate is wired by manifest path -- so it was one probe, not a
clearance. Both angles have since been run: no workflow or pre-commit hook invokes this script,
no path-shaped route reaches it either, and the changed exit code is pinned by
tests/test_dangling_citation_check.py at lines 373 and 388 -- a suite that IS
manifest-classified and therefore runs on the tooling leg rather than being deselected.

A near-name trap sits here and is documented in the file itself.
scripts/docs/backlog_citation_check.py is CI-wired; scripts/docs/dangling_citation_check.py
is not. A grep for citation_check hits a workflow and the hit belongs to the sibling --
confident, well-formed, and wrong in the direction of believing a change is CI-covered when it is
not, which is the direction that lets a behaviour change ship unexercised. Verified with a
positive control in the same pass: the dangling script returns zero wiring hits while
backlog_citation_check, backlog_status_check, ledger_check and scan_forbidden return 2, 4,
2 and 3 -- so the probe finds wiring where wiring exists and the zero is real.

The test contains an attack construct on purpose

tests/test_worktree_gate_escaped_quote.py contains input of the class the gate exists to
refuse. That is authorised, not an oversight -- reviewed and ruled on, and the reason the
test is meaningful at all: a gate asserted only against benign input cannot demonstrate it
denies hostile input. The construct is named here by class rather than narrated as a
procedure, and the analysis behind it is held privately rather than in this repository.

Four retractions, carried because they are already in flight

  1. worktree add is NOT affected by the escape. The original report named three verbs; the
    third is wrong -- that path ALLOWs with or without the escape, because rule 3b governs
    checkout/switch only. It was removed rather than widening the gate to satisfy it. Left
    uncorrected, this sends a reader to loosen a gate that is behaving correctly. Two seats
    measured this independently in separate trees.

  2. A third defect is fixed here that no item mentions. -replace is case-insensitive by
    default while the rules compare case-sensitively, so a differently-cased executable token
    ALLOWed where the lowercase form DENIED. Found while testing, no escape involved. Fixed by
    canonicalising the token on emit. Named here because a reader diffing this branch against
    the item will not find it there.

  3. An authoring note recorded the gate fix as "not yet built" for roughly twenty minutes
    after it was
    , and another session acted on that line. Corrected at the source.

  4. The escaped-quote suite does not test the property it claimed. Its author wrote that the
    verb set covers more than one rule; measured, both verbs return the same rule message.
    So the suite exercises its own parametrisation rather than the rule-agnostic behaviour it
    advertised. Corrected in place rather than deleted, and deliberately not widened here --
    a companion suite covering four distinct rules, asserting the rule ids the gate itself
    records, is arriving separately, and duplicating it is the redundancy this lane already
    avoided once.

    This is worth stating plainly because it is the shape that let the original defect survive:
    fourteen gate suites were green and none could see the class. A green suite over one rule
    is not evidence about a rule-agnostic fix, and landing one alongside such a fix rebuilds that
    condition a layer up.

Rule-agnostic coverage, from a second author

tests/test_worktree_gate_rule_agnostic_coverage.py covers four distinct rules against a real
git repository -- primary working tree, linked-worktree hijack, shared git config, worktree removal
-- and asserts the rule ids the gate itself recorded in its receipt log, with a per-row
unescaped positive control. Discrimination was demonstrated, not assumed: 5 rows red against the
pre-fix hook and green against the fix.

Asserting the rule id rather than the outcome is the part worth keeping past this PR. "Denied"
and "denied by the rule we think" are different claims
, and only the second catches a fix that
denies for an accidental reason.

One row from the original draft was dropped rather than ported, with its reason recorded: it
pinned a design this branch's fix deliberately rejects. A deleted test row with no stated reason
reads as coverage quietly reduced.

The fix does not protect this machine until someone installs it

tests/test_gate_installed_parity.py fails on a developer box here, and it is correct to fail.
Measured independently: the installed hook at ~/.claude/hooks/worktree_gate.ps1 is
byte-identical to origin/main -- the running gate is the unfixed one. The fix lives in the
checkout, and the gate executes from an installed copy.

That leg goes green in CI, and the green means nothing here. The test skips when
~/.claude/hooks is absent, which is every hosted runner. So CI does not guard installed-vs-source
parity, and its pass must not be read as "the gate is fixed everywhere".

This requires a human after this PR lands. scripts/worktree/install-gate.ps1 refuses when
$env:CLAUDECODE is set -- by design, because a session that can install the gate can also remove
it -- so it must be run from a plain terminal. Until then the fix protects nothing on any box whose
installed copy predates it.

Lander notes

The lane's tips were merged, never force-pushed. Twice the handed-off tip was not a descendant
of the PR head, and taking it would have discarded commits present only on origin -- including
the ADR-index collision resolution and ledger banners. Each merge was verified in the result
rather than trusted for being conflict-free.

tests/tooling_manifest.txt gained the escaped-quote test's registration (lander-authored).
CI required it: the partition drift guard refuses any tests/test_*.py importing no engine
module that is named in neither the manifest nor _STAYS_WITHOUT_IMPORTING. That guard exists
so a new gate test cannot quietly join the engine legs and grow the tier back. All thirteen
sibling test_worktree_gate_*.py files sit in the manifest, so the placement was determined
rather than chosen.

BACKLOG #1229 is not closed by this. This is the residual. Its own ledger closure was never
written, which is a separate gap and is not papered over with a residual banner here.

…he caller's spelling (BACKLOG #1268)

`users.username` was the one identifier column in the SQL Server schema carrying no COLLATE
clause, so it inherited the DATABASE default -- case-INsensitive on a stock install -- while
every sibling identifier column in the same file pinned Latin1_General_100_BIN2 and both other
backends were case-SENSITIVE. `Admin` and `admin` were two accounts on two backends and one
account on the third, under a UNIQUE constraint that reads as if it had settled the question.

That portability defect became a security defect because a second site answered the same
question by a different rule. `_login_local` gated WP-3 bootstrap expiry/supersession
enforcement on a PYTHON comparison against the caller's input, while the lookup one line below
was resolved by the COLUMN'S collation. The two disagree in exactly one direction: `Admin`
fails the Python guard, so retirement never runs, then succeeds at the lookup and returns the
very row the skipped call would have disabled.

MEASURED before the fix, on a lapsed unclaimed bootstrap with the ASVS 6.4.1 credential expiry
disarmed so it could not mask the result:

    login("admin")  -> refused, account disabled      -- the control fired
    login("Admin")  -> ok=True, session issued        -- the control never ran

A lapsed first-run credential logging in because one letter was capitalised. SDS-3.7 exactly: a
compensating control resting on the false premise that the username the gate compared is the
username the store matched. No live exposure -- zero deployments (CLAUDE.md section 0).

Two limbs, and the second does not depend on the first:

  - the column now pins the collation its own file's convention already required;
  - the gate compares the value THE STORE RETURNED, then re-reads by id because retirement may
    have disabled the row. That stays correct under a collation the engine does not control --
    an operator-supplied database, a restored dump, a column altered downstream -- where the
    column fix alone leaves the gate one ALTER COLUMN from being wrong again with nothing
    reporting it.

Cost is one extra lookup on the bootstrap path only; an ordinary login does the single lookup
it always did plus a string compare, so the original guard's stated intent is preserved.

The test file keeps its own retraction: both gate tests PASSED against the unfixed code in
their first form, because they used the supersession arm and `create_local_user` retires the
bootstrap eagerly at service.py:2685 -- the account was already disabled before the login ran.
Supersession can never exercise this defect; only the expiry arm reaches the login path with
retirement still pending.
…entity decision may depend on store collation (BACKLOG #1268)

Records the decision behind the #1268 fix, because the diff shows WHAT and not WHY, and the
why is the half that decays. Two rules:

  1. Usernames are case-sensitive, on every backend.
  2. No identity decision may be delegated to store collation -- compare the value the store
     RETURNED, never the caller's input.

Rule 2 is the load-bearing one and is deliberately independent of rule 1, so the gate stays
correct on a database whose collation the engine does not control.

Rejected alternatives are recorded with their reasons, because each is re-proposable and each
is wrong non-obviously. Case-INsensitive normalisation is the one a reader will reach for
first: it requires a canonicalisation that is not locale-neutral (the Turkish dotless i), so a
wrong fold silently MERGES two accounts -- and a UNIQUE constraint would enforce that merge
rather than catch it. It would also have to hold across three backends and the audit trail,
every one a fresh place for the two rules to diverge again.

Flagged and deliberately NOT decided here: two accounts differing only in case are themselves
a confusability risk, and case-sensitivity preserves it. That is closeable additively by a
registration-time refusal, which is a different control and does not require reopening this
decision. Not built, and not filed as an item by this lane.

Also records what the change does NOT do: the DDL is creation-guarded, so an existing SQL
Server database keeps its original column collation and no re-type is attempted. Zero
deployments means nothing to migrate; it is written down so a later reader does not mistake
the schema-hash bump for a column alteration.
…ort it instead of raising (BACKLOG #1259)

The parse_items conflict refusal shipped and is on main. It protected programmatic readers and the
CI leg. NOTHING called it before a commit, so the item's own failure sentence was still true: a
conflicted docs/BACKLOG.md can be COMMITTED and every ledger gate passes over it.

MEASURED at dd655da against a docs/BACKLOG.md carrying a realistic PROSE-level conflict, with the
file restored byte-identical afterwards:

    ledger gate ....... Passed        backlog_status_check.py ... exit 1
    forbidden-content . Passed
    control characters  Passed
    gitleaks .......... Passed        overall rc 0 -- the commit lands
    bandit ............ Skipped

THE FIRST VERSION OF THAT MEASUREMENT WAS CONFOUNDED AND SAID THE OPPOSITE. Injecting a conflict
that ADDS headings made the ledger gate fail -- on OWNERSHIP, because both sides' numbers read as
unallocated, not on the conflict. That reads as "already covered". A real ledger conflict is
usually two sides editing PROSE, adds no heading, and is the case nothing sees. Testing the
heading case instead would have closed the item as a non-defect.

Two parts:

  1. main() catches the ValueError per source and reports it with the PATH, returning 1. It raised
     from inside scan(), so every caller rendered an uncaught traceback -- which reads as "the
     checker is broken" rather than "your ledger is conflicted", sending an author to the wrong
     file, and the exception carries a LINE number but no path while the checker scans the
     published ledger plus every archive by default. Also removes a real double-parse.
  2. A LOCAL pre-commit hook, scoped to the ledger files.

NOT pre-commit/pre-commit-hooks' merge-conflict check, and the reason is not dependency
squeamishness -- that repo is absent here, so it would be a new third-party source with its own
pinned rev, but the deciding argument is SINGLE SOURCE. parse_items DEFINES what a readable ledger
is, and CLAUDE.md section 11 requires this file be read through it and never a hand-rolled scan. A
generic textual matcher is exactly that second, silently different definition.

Scoped to the ledger files rather than always_run: the checker resolves its own sources, so running
it elsewhere buys nothing and spends the credibility a gate needs on the day it fires.
pass_filenames is false because feeding it a staged path list would narrow the corpus --min-items
exists to protect.

Verified both directions: the conflicted ledger now Fails with the checker's own message naming the
file, and the clean ledger still Passes -- a gate that fires on the healthy case is one everybody
learns to skip. Mutation-verified: repointing the hook entry elsewhere reds the wiring test.

The wiring test asserts on DIRECTIVES with comments stripped, not raw text. Its first draft failed
on this commit's own explanatory prose, which names the alternative it rejects -- a substring check
over a commented config cannot tell a wiring from a note explaining why that wiring was not chosen.

NOT DONE: the item's stale DO-NOT-BUILD banner still stands. A builder may not edit the ledger; it
routes to whoever writes banners.
…-authored banner)

ADR 0165 pairing. The fix and ADR 0169 are the builder lane's work; this banner is the
lander's, because a builder may not author ledger content and a PR that honestly cites its
item is red by construction until someone who may write docs/BACKLOG.md does.

Closes 1268 with both limbs recorded, and states the residual rather than only the fix: the
users DDL is creation-guarded, so an existing SQL Server database keeps its original column
collation. Nothing to migrate -- zero deployments, CLAUDE.md section 0 -- and limb 2 is correct
on such a database anyway, which is why it was built not to depend on limb 1.

parse_items before/after: 313/232/81 -> 313/231/82, the expected 0 items / -1 open / +1 closed
for one closure. Item 1268 declares exactly one status; the open-alphabet glyph was REMOVED
from the original filing line rather than left to coexist with the closing banner.

Built with plumbing (read-tree, update-index, commit-tree) so no working tree was touched --
the branch is checked out in a live sibling worktree. Plumbing bypasses pre-commit, so the
ledger, forbidden-content and backlog-status checks were run by hand against the result.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 20, 2026 13:47
…2's fifth sighting against its own argument

Two body amendments to landed items. No item filed, no item closed, no banner
status changed: parse_items reads 313 items / 232 open / 81 closed before and
after, the expected 0/0/0 delta for an amendment.

#1259 -- the 2026-08-15 banner had INVERTED. It read "DO NOT BUILD -- ALREADY
BUILT AND UNLANDED ON PR #394" and named #394 landing as its own expiry
condition. #394 landed, so the banner then said do-not-build over work that was
only half done. That is the dangerous direction: it removes an item from the
queue so nobody looks again.

The first scope option shipped -- parse_items refuses a source carrying conflict
markers, defined :94, called :132, reached before any parsing. The second did
not, and the item's own failure sentence stayed true: no pre-commit hook called
parse_items, so a conflicted ledger could still be committed with every gate
passing.

Also folded in a measurement the item did not carry, and it is the difference
between a defect and a non-defect here: the SHAPE of the conflict decides
whether anything catches it. A conflict that ADDS HEADINGS is caught
incidentally, by the ownership guard, because both sides' numbers read as
unallocated -- which renders as "already covered". A real ledger conflict is two
sides editing PROSE, adds no heading, and the ownership guard has nothing to
fire on. Measured with a prose-only conflict at dd655da, file restored
byte-identical: every gate passes, overall rc 0, the conflicted ledger commits,
while backlog_status_check.py on the same file exits 1.

#1292 -- a fifth sighting on main's own tip, recorded because it WEAKENS the
argument the fourth entry made rather than confirming it. The ack series is now
13, 14, 24 against a fixed sent=36. The fourth entry's case was that the
shortfall reproduces to within one ack, which is what made "systematic"
persuasive; 24 is a spread, not a reproduction. The verdict is NOT flipped to
starvation -- two clustered points and one outlier is not a distribution. What
changed is that neither reading now has an argument from reproducibility, so the
store-side discriminator is the only thing that settles it.

The same entry records that this sighting is NOT what was redding main. That
run's failing assertion is tests/test_connscale_smoke.py:128, the FD probe.
no_loss passed, and it is computed as agg_sent minus engine_read so acked is not
in it at all. The sent/acked figures are real and incidental -- pytest prints the
record's full repr on any assertion failure, and three sessions attributed the
red to this item because the numbers pattern-matched it. When a sighting names
an item, quote the line with the caret, never the values in the dump.

Amendments to items already on origin/main add no heading, so the ledger gate's
ownership check is never consulted; docs-only, so the claim gate's code-touching
scope does not apply. Every added line verified cp1252-encodable.


Cherry-picked by the lander onto PR #458 under ADR 0165: the dispatcher seat authored these
dispositions and holds no push route, so routing them separately would have left #1259's INVERTED
do-not-build banner standing over work landing in this very PR -- the #60 failure the hygiene gate
exists to prevent. Applied with plumbing; merge-tree with a COMMIT-scoped base returned rc=0.

Correcting one clause of the handoff that offered this commit: it said the change 'touches no file
your branches touch'. It does -- all three commits touch docs/BACKLOG.md, the single-writer file.
The conclusion (it cannot conflict) is right and measured; the stated reason is not. The regions are
disjoint by ITEM, not by file.
…llision

PR #428 landed as 4c28bad adding ADR 0167's index row; this branch adds 0169's. Both are
appended as the LAST line of docs/adr/README.md, so git sees one line changed two ways. Every
other path content-merged cleanly, including docs/BACKLOG.md and auth/service.py, which are
changed on BOTH sides.

RESOLVED AGAINST A COMPUTED TARGET, NOT BY EYE, because an index is exactly where keep-both-sides
is a hope rather than a resolution:

    A = rows at merge-base   157
    B = rows at PR head      158   (adds 0169)
    C = rows at origin/main  158   (adds 0167)
    target = C + (B - A)     159

    result                   159 rows, 0167 present, 0169 present, ZERO duplicate numbers

Built with plumbing; no working tree touched.
# Conflicts:
#	docs/adr/README.md
…its EXIT CODE asks (BACKLOG #1235)

`is_live_shape` was extracted so the exit list and the test stop restating the rule. The branch
that NARRATES the verdict to a human was left behind, still testing `hit.number <= floor` alone
and never consulting `pr_shaped`.

MEASURED at 4c28bad, after the single-definition refactor landed: six hits printed BOTH

    [PR/issue/foreign-repo shaped -- very likely NOT a backlog citation]
    -> ABOVE THE FLOOR (1295) ... This is the live shape.

two contradictory annotations on the SAME hit, two lines apart. All six are genuinely foreign
(a code-server discussion, a Mirth discussion, a pyodbc issue). The exit code passes over them
correctly; only the prose called them live.

THE PART WORTH KEEPING: this survived the very refactor whose subject is single-definition
cleanup. The definitions a tool COMPUTES with got unified; the one it NARRATES with was missed,
because no assertion reads it. A third definition is not caught by the pass that removes the
second.

The test asserts the CONTRADICTION rather than either sentence alone -- in isolation each is
true, since the hit IS above the floor and IS pr-shaped. Only their co-occurrence on one hit is
the defect, so only that can pin it. Paired with a negative control: a genuinely live citation
must still be narrated as the live shape and must still fail the gate, or the fix trades a wrong
answer for no answer.

Mutation-verified: restoring the floor-only branch reds exactly one test, and the suite returns
to green on revert.
…t resolutions of one conflict

Both this seat and the lane resolved the SAME docs/adr/README.md append collision against 4c28bad
independently, neither knowing the other was on it. That is a coordination failure and it is mine:
I pushed to a branch a live session was holding, having told that session the freeze SHA one message
earlier.

The lane's branch carries one thing origin does not -- BACKLOG #1235's annotation fix, making the
citation gate's ANNOTATION ask the same predicate its EXIT CODE asks. Origin carries three the lane
does not: #1268's closing banner and the dispatcher seat's #1259 / #1292 amendments.

Neither side is discardable, so this merges rather than picks. merge-tree rc=0 -- no conflict.

Verified on the result rather than assumed:
  - docs/adr/README.md: 159 rows, 0167 AND 0169 both present, zero duplicate numbers
  - docs/BACKLOG.md blob differs from BOTH parents, which is what a correct three-way merge looks
    like and is the check that catches a silent revert
  - zero conflict markers in any file
wshallwshall added a commit that referenced this pull request Aug 20, 2026
… does not guarantee

`test_worker_kill_reaps_the_whole_process_tree` asserted the LATER of two events at the instant the
EARLIER one landed, and it is currently reding a REQUIRED context: PR #458's ubuntu leg failed at
tests/test_sandbox.py:998, "the grandchild survived the worker kill", and the same test failed on
`main` in run 32206563674.

THE DOCSTRING WAS THE DEFECT, NOT ONLY THE TIMING. It claimed pipe-EOF is equivalent to "grandchild
reaped". It is not, and the two observables are not the same event:
  - pipe EOF fires when the last holder of the write end releases its fds -- at process EXIT;
  - `os.kill(pid, 0)` raises ESRCH only once the pid is REAPED;
  - the grandchild is ORPHANED, because the sandbox worker (its parent) is killed and `proc.wait()`ed
    first, so pytest cannot `waitpid` it and reaping falls to PID 1 or the nearest subreaper,
    asynchronously.
So t_reap is STRICTLY AFTER t_exit == t_EOF, always, and the test asserted the later one with no wait,
poll or deadline between them. It passed only when the reap won a race it was never entitled to win.

TIMING-DEPENDENT AND WRONG ARE NOT ALTERNATIVES HERE -- the ordering is guaranteed by the mechanism.
A sleep or a retry would have made it green while leaving the false equivalence in place for the next
reader to rely on again, which is why the docstring is part of the change rather than a footnote to it.

The two senses of "reap" that collide here are now stated: `_reap_process_tree` reaps in this
codebase's sense -- TERMINATE every process in the tree, which is what the engine guarantees and what
the pipe-EOF assertion genuinely proves -- while POSIX `waitpid` reaping is a different act on a
different schedule, and the test now waits for TERMINATION only. It asks for nothing the engine does
not promise.

VERIFIED: tests/test_sandbox.py -> 25 passed; the false-equivalence sentence is gone (grep returns 0);
ruff 0.15.22 clean; no cp1252-unsafe character introduced.

ITS ADVERSARIAL REVIEW HAD NOT REPORTED WHEN THIS WAS COMMITTED -- committed to protect the work across
a usage-window boundary, with any finding to be fixed forward. Nothing is pushed. The platform split
matters for whoever reads this next: the ordering argument is POSIX-shaped, and this box is Windows, so
the CI red is the behavioural evidence rather than a local reproduction.
…ind fail-opens (BACKLOG #1229 residual)

The gate blanks quoted spans before any rule reads the line. It already learned once that two
ordered regexes cannot express "whichever quote opened first owns the span" -- that was the
straddle #1229 fixed. Two more places were still deciding span boundaries without that rule.

ONE: the scan resolved OWNERSHIP correctly and ignored ESCAPING entirely. In sh a backslashed
quote is an ordinary literal and the command around it RUNS, but the scan read it as an opener,
paired two of them across a live command and deleted the middle. Nothing reached any rule.

    echo \" ; git -C <governed> checkout main ; echo \"     ALLOW
    echo \" ; git -C <governed> reset --hard ; echo \"      ALLOW   (destructive)

RULE-AGNOSTIC, and that is the reason it matters more than a missing case: it is a SCANNER
defect, so it disarms whatever rule sits behind it rather than one verb.

TWO: the quoted-program-path collapse was two ordered regexes run BEFORE the scan, double quotes
first -- the exact shape the scan replaced. It could pair a quote with a distant path token
ACROSS a gated command and rewrite the whole middle to a bare token, verb and arguments gone.

Both are now decided inside the single left-to-right pass, on spans it already owns. The
asymmetry is deliberate and is shell semantics, not an optimisation: a backslash escapes inside
a double-quoted span but NOT inside a single-quoted one, where sh gives it no special meaning.
Treating them alike would swallow the rest of a line from a trailing backslash, which is the
fail-open direction this function exists to avoid.

A THIRD GAP FOUND WHILE TESTING, WITH NO ESCAPE INVOLVED. PowerShell -replace is
case-insensitive by default, so the old collapse accepted every spelling by accident while the
rules downstream compare case-sensitively -- the token survived in a form they skip:

    "...\git.exe" -C <governed> reset --hard    DENY
    "...\GIT.EXE" -C <governed> reset --hard    ALLOW

Now canonicalised on emit, which is the fail-closed direction: it hands the rules a verb to
judge rather than a token they ignore.

ALL PRE-EXISTING, none a regression, and all three survived a dedicated pass over this exact
function. Nothing in the repository could see them: across the 13 existing gate suites, ZERO
carried a backslash-escaped-quote case (positive control: the token appears in four of them,
every hit unrelated).

The new suite carries both arms -- the escape must not hide a command, and a genuine quoted span
must still be blanked so a commit message cannot supply a verb. Without the second, "never blank
anything" would pass. Mutation-verified per arm with an applied-check on each mutant, because a
mutation that silently fails to apply reads exactly like a test that cannot fail: neutering the
escape arm reds the escape case, neutering the collapse reds the suite, dropping the
canonicalisation reds the two case-varied spellings, and the file restores byte-identical.

Verification scope: 581 passed / 13 skipped across every test_worktree_gate* module, ruff and
ruff format clean, mypy strict clean over 267 files. The x12 and xml extras are absent from this
venv, so this does not reproduce CI collection.

A case for `worktree add` was written and REMOVED rather than fixed around. A report said the
escape hid it; the discriminating probe -- same command with and without the escape -- allows
either way, so the escape hides nothing there and the rule-3b resolver says why. Keeping a
failing case built on a false premise would have pressured the next reader to widen a security
gate until it went green.
…ver it

The lane handed me tip b05b48f (7 ahead of main at 13dd27d, by its own
measure). Origin's head was 5627d4a, and b05b48f is NOT a descendant of it:
the two lines had diverged by 4 commits on origin against 5 on the lane.

A force-push of the handed-off tip would have discarded all four, which is why
this is a merge:

  5627d4a  merge of the lane's 22ff4bb into the PR head
  c6070a4  merge origin/main -- ADR index append collision, already resolved
  a663b89  backlog: retire 1259's inverted banner, record 1292's fifth sighting
  92d9002  backlog: close 1268, both limbs (lander-authored banner)

Verified before committing, because a clean merge is not the same claim as a
correct one: the merged tree carries fb1f53c's two files byte-identically to
the fix (scripts/hooks/worktree_gate.ps1 and the escaped-quote test), so
combining the sides did not restore what the fix removes.

Note for the lane: the ADR index did not collide on your merge because c6070a4
had already resolved it on origin, not because this wave stopped colliding.
…d on a clean run (BACKLOG #1235)

Two more ways this gate said "clean" without that meaning anything.

ONE: THE CLI FALSE-CLEANED ON AN EMPTY POPULATION. The default path list is CWD-relative
(`Path("docs").rglob(...)`), not repo-relative. Measured from a directory with no docs/:

    No unresolved backlog citation in 0 file(s).
    Resolved against 0 allocated item numbers (open and closed).
    exit 0

Both counts zero and nothing objecting -- a green that is a statement about the ENVIRONMENT
rather than the subject.

AND IT ARMS BY CLOSING THE ITEM, which is why it is not a hypothetical. The >200-file population
floor lives only in test_the_docs_scan_actually_covers_something, so it guards the pytest arm and
nothing else. Wiring this CLI into .github/ or .pre-commit-config.yaml is the documented way to
satisfy the item's first disjunct -- and doing so inherits a gate that reports clean when it
scanned nothing.

Now refuses non-zero on an empty path list OR an empty allocation, and prints what it scanned and
the directory it scanned from, because the cause is a relative default and the count alone does
not show that. Refusing on an empty allocation too is deliberate: an unreadable ledger resolves
every citation to "no filed item", which fails the other way and would bury a real run in false
positives. Both zeros mean the same thing -- the tool is not looking at this repository.

TWO: THE COVERAGE BOUND WAS ABSENT FROM EXACTLY THE RUN A READER ACTS ON. "Not scanned: the
private companion repository" printed only after the hits loop, so the early clean return skipped
it -- the bound missing at the moment someone concludes "clean", which is the whole population it
exists to qualify. It is in the module docstring, and a docstring is not what a CI log shows.

Now one named constant printed on every exit path. That is this item's own single-definition rule
applied to prose: two copies of a caveat drift, and the copy that goes stale is the one nobody
reads, because it only prints on the path they are not on. A test pins the single definition.

Both arms carry their control -- a populated scan must still reach a verdict rather than trip the
emptiness guard, or the fix is "always refuse"; and the clean path must be the clean path, or the
bound assertion proves nothing. Mutation-verified with an applied-check on each mutant, since a
mutation that silently fails to apply reads exactly like a test that cannot fail: disabling the
emptiness guard reds one test, removing the clean-run bound reds one test, and the file restores
byte-identical.

Verification scope: 45 passed in tests/test_dangling_citation_check.py, ruff and ruff format
clean, mypy strict clean over 267 files. The tooling tier was still running when this was
committed and is NOT claimed green here. x12 and xml extras are absent from this venv, so none of
this reproduces CI collection.
…ift guard demanded

CI red on 1a7a31c: test_every_non_engine_test_is_classified failed with

    assert not ['test_worktree_gate_escaped_quote.py']

The new test imports no engine module, so the partition guard requires it to be named
either in tests/tooling_manifest.txt or in _STAYS_WITHOUT_IMPORTING. That guard exists
precisely so a new worktree-gate test cannot quietly join the engine legs and grow the
tier back, which is what would have happened here.

The manifest is the right side and the choice is not a judgement call: all thirteen
sibling test_worktree_gate_*.py files are listed there and none appear in
_STAYS_WITHOUT_IMPORTING. Placed between _emitter and _git to match the block's order.

Verified before committing: the named file exists at this commit, so the companion check
that every manifest name resolves to a real file stays satisfied, and the name appears
exactly once so the duplicate check does too.

Lander-authored on the builder-1 lane's behalf -- the fix is a mechanical registration
fully determined by the guard's own message and by where its siblings already sit.
One genuinely new commit, 1972575. The lane's previous tip b05b48f is already
fully merged here (measured: zero commits outstanding on that side), so this
brings only the #1235 work and not a re-merge of what landed earlier.

Merged rather than taking the handed-off tip, for the same reason as last time:
origin carried six commits the lane's tip did not, including the escaped-quote
manifest registration that cleared this PR's CI red. Verified in the result --
the merged tree keeps tests/tooling_manifest.txt at 122 lines with the
escaped_quote entry present exactly once, and carries the lane's two files.

CARRIED FORWARD FROM THE LANE, UNVERIFIED AND LABELLED AS SUCH: the tooling tier
was still running when this was committed and its result is not in hand. That
tier is where the changed gate actually runs, so it is the relevant regression
check. What is verified is 45 passed in tests/test_dangling_citation_check.py,
both arms mutation-verified with a byte-identical restore, ruff and ruff format
clean, mypy strict clean over 267 files. That is not a green tier and is not
offered as one.

BEHAVIOUR CHANGE, deliberate: the CLI now exits non-zero where it previously
exited 0 on an empty population. Anything invoking it from a directory without
docs/ will now red where it passed silently. That is the fix, not a regression.
…LOG #1229 residual)

The comment above the parametrisation said the verb set covers more than one rule, because the
defect is rule-agnostic. IT DOES NOT. Measured, which is the only reason I know: both verbs come
back with the SAME rule's message -- "would change the working tree of the SHARED PRIMARY
checkout". Two verbs, one rule.

That matters because the property the suite claims to pin is precisely the one it cannot see. A
scanner defect disarms every rule equally, so coverage that would actually pin it has to exercise
DIFFERENT rules; exercising one rule twice tests the parametrisation, not the property.

And "denied" is a weaker claim than "denied by the rule we think". A suite asserting only that a
deny happened cannot distinguish a rule-agnostic scanner fix from a lucky overlap in one rule's
matching. The stronger form -- reading back the rule ids the gate RECORDS and asserting they are
distinct -- was built independently on another lane, cross-checked at 10 of 11 rows passing
against this gate unchanged, and that is the right home for the coverage. This file should not
grow a second, thinner version of it.

Corrected in place rather than deleted. A reader who saw only a fixed comment could not tell this
suite had ever asserted coverage it does not have, and the gap is real.

NOT fixing the coverage here, deliberately: widening this file would duplicate work that exists
and is better, and the decision about which suite lands is the Dispatcher's and the Lander's.
wshallwshall added a commit that referenced this pull request Aug 20, 2026
…ound both of them require (#464)

* feat(probe): a connscale gap now records WHICH path degraded it, and the smoke test asserts only what it can

The FD probe returned the same all-None sample from THREE different paths and the record could not say
which one fired. That is why `main`'s tip red was mis-attributed by four sessions today: the failing
assertion was `tests/test_connscale_smoke.py:128`, `assert r.fd_count_peak is not None`, and the
record's repr carried `sent=36 acked=24` -- so the attribution followed the NUMBERS in the dump to the
item about sent/acked, rather than the ASSERTION, which was about the probe. The artifact did not carry
the information needed to attribute it.

WHAT CHANGED. `ProbeDegraded` names each cause distinctly -- the subtree walk timing out, erroring
fast, returning zero rows, or returning a snapshot with no root; and the per-PID read timing out,
erroring or parsing nothing. `ProcSample.degraded` carries it, `_gap(cause)` replaces the single shared
`_EMPTY_PROC`, and `_resolve_errored` becomes a property over the recorded cause so a boolean can no
longer be set without saying why. `is_budget_exhausted()` is the discriminator, stated once: a path
that SPENT its budget measures the runner; one that failed without spending it is a broken probe. Those
get opposite readings and they were previously indistinguishable.

THE SMOKE ASSERTION IS NARROWED, NOT RELAXED, AND THE DIFFERENCE IS THE WHOLE POINT. It used to demand
`fd_count_peak is not None` of EVERY record, with no tolerance for a probe that honestly could not
measure -- so a degraded probe red a REQUIRED context as though the ENGINE were at fault. It now asserts
`> 0` only where a value is present AND still requires that AT LEAST ONE record measured something. A
run where the probe never measured at all still fails. Buying a green by deleting the requirement was
the available wrong answer here and it is not what this does.

Twelve cases in a new tests/test_connscale_probe_degradation.py pin each cause to its path, including
the two that must record NO cause on success, and one asserting a resolution that fails without naming
a cause is still given one -- so the enum cannot be bypassed by a future path that forgets to classify.

Does NOT fix the underlying walk timeout, and does not claim to. It makes the next occurrence
attributable, which is what four sessions lacked today.

VERIFIED: tests/test_connscale_probe_degradation.py + tests/test_connscale_smoke.py -> 32 passed;
tests/test_sandbox.py unaffected; ruff 0.15.22 clean on all five files; no cp1252-unsafe character
introduced (census identical against HEAD). harness/ is not in CI's mypy scope
(`mypy messagefoundry messagefoundry_webconsole --exclude 'messagefoundry/tray/'`), and the 3-file
mypy invocation reports the same single package-discovery error at HEAD as with this change, so it is
pre-existing and not introduced here.

ITS ADVERSARIAL REVIEW HAD NOT REPORTED WHEN THIS WAS COMMITTED. Committed anyway to protect the work
across a usage-window boundary, on the explicit understanding that a review finding is fixed FORWARD --
which is exactly what happened to the sibling #1290 commit earlier on this branch, where the review
found a blocking defect and e2ce84ad corrected it before anything left this tree. Nothing here is
pushed.

* fix(test): the orphan-reap assertion asked for an ordering the engine does not guarantee

`test_worker_kill_reaps_the_whole_process_tree` asserted the LATER of two events at the instant the
EARLIER one landed, and it is currently reding a REQUIRED context: PR #458's ubuntu leg failed at
tests/test_sandbox.py:998, "the grandchild survived the worker kill", and the same test failed on
`main` in run 32206563674.

THE DOCSTRING WAS THE DEFECT, NOT ONLY THE TIMING. It claimed pipe-EOF is equivalent to "grandchild
reaped". It is not, and the two observables are not the same event:
  - pipe EOF fires when the last holder of the write end releases its fds -- at process EXIT;
  - `os.kill(pid, 0)` raises ESRCH only once the pid is REAPED;
  - the grandchild is ORPHANED, because the sandbox worker (its parent) is killed and `proc.wait()`ed
    first, so pytest cannot `waitpid` it and reaping falls to PID 1 or the nearest subreaper,
    asynchronously.
So t_reap is STRICTLY AFTER t_exit == t_EOF, always, and the test asserted the later one with no wait,
poll or deadline between them. It passed only when the reap won a race it was never entitled to win.

TIMING-DEPENDENT AND WRONG ARE NOT ALTERNATIVES HERE -- the ordering is guaranteed by the mechanism.
A sleep or a retry would have made it green while leaving the false equivalence in place for the next
reader to rely on again, which is why the docstring is part of the change rather than a footnote to it.

The two senses of "reap" that collide here are now stated: `_reap_process_tree` reaps in this
codebase's sense -- TERMINATE every process in the tree, which is what the engine guarantees and what
the pipe-EOF assertion genuinely proves -- while POSIX `waitpid` reaping is a different act on a
different schedule, and the test now waits for TERMINATION only. It asks for nothing the engine does
not promise.

VERIFIED: tests/test_sandbox.py -> 25 passed; the false-equivalence sentence is gone (grep returns 0);
ruff 0.15.22 clean; no cp1252-unsafe character introduced.

ITS ADVERSARIAL REVIEW HAD NOT REPORTED WHEN THIS WAS COMMITTED -- committed to protect the work across
a usage-window boundary, with any finding to be fixed forward. Nothing is pushed. The platform split
matters for whoever reads this next: the ordering argument is POSIX-shaped, and this box is Windows, so
the CI red is the behavioural evidence rather than a local reproduction.

* fix(test): the run-level FD bound let a real collapse pass, and the POSIX probe called a success blind

Two defects, both found by the adversarial reviews of 937803a9 and 1a514e14, both proven end-to-end by
the reviewers rather than argued. Fixed forward; neither commit had left this tree.

FINDING 1 (CRITICAL) -- THE GREEN MEANT LESS, IN EXACTLY THE WAY THE BRIEF PREDICTED. The one-line
assertion 937803a9 replaced was doing DOUBLE DUTY: besides checking each reading, it was the only thing
guaranteeing `fd_count_monotonic` had two readings in a group to COMPARE. `_monotonic_slo` SKIPS None
readings by design, so with a run-wide `any(measured)` bound it can return ok=True observed='monotonic'
having compared NOTHING. Measured on the real smoke: forcing every step after the first to WALK_TIMEOUT
left one group holding a single reading, PAIRS ACTUALLY COMPARED=0, and the SLO still reported
monotonic -- and a genuine 1000-handle FD collapse passed the same way, while the unforced control
compared 2 pairs and a real 1000->100 drop correctly failed.

So the bound is now at the SLO's GRANULARITY, not the run's: some (sweep_mode, claim_mode) group must
hold two measured readings, which is the same key `_monotonic_slo` groups on (BACKLOG #1101). Grouping
on sweep_mode alone would reintroduce the bug one level up, since two readings split across claim modes
are never compared with each other.

AND THE JUSTIFICATION I SHIPPED FOR THE WEAK BOUND WAS ITSELF WRONG. It claimed the run-wide form was
"the same bound assertion (5) already places on the reload probe". Same SHAPE, wrong ANALOGY:
`reload_seconds` feeds no monotonicity SLO, so one reading there costs nothing, while `fd_count_peak`
feeds one. Borrowing a bound from the one probe where the analogy is safe is how this got through.

FINDING 2 -- THE POSIX PATH CALLED A SUCCESS BLINDNESS. `_pid_running` did `os.kill(pid, 0)` and then
read /proc, two syscalls with a gap. If the reaper retired the pid inside that gap the /proc read
raised FileNotFoundError, `_proc_state` returned None, and the test's blindness guard turned a
SUCCESSFUL reap into a red on Linux -- a new false failure introduced by 1a514e14 on the platform where
the original red actually fires.

A missing /proc/<pid> on a kernel that HAS /proc is a definite answer, "gone", not an inability to
answer. Only a platform with no /proc at all (macOS) genuinely cannot separate a zombie from a runner,
and None is now reserved for that. The check reads the DIRECTORY rather than `sys.platform`, because
the question is whether this kernel exposes the interface, which is what the code actually depends on.

THREE TESTS MOVED WITH THE SEMANTICS, and one is renamed rather than merely edited:
`test_a_spent_budget_gap_is_tolerated_when_another_step_measured` becomes
`..._is_tolerated_beside_a_comparable_pair`, because "another step measured" was never the condition --
two readings split so that no group holds two are never compared. A test whose NAME asserts the wrong
rule is a second source of truth for it.

ADDED test_one_measured_reading_in_a_group_is_not_a_comparison as the regression test for finding 1,
carrying the measurement that proves it: a green resting on an empty comparison is what this file
exists to prevent, and it had just shipped one.

VERIFIED: 31 passed in test_connscale_probe_degradation.py; 27 passed across test_connscale_smoke.py
and test_sandbox.py; the corrected bound rejects the reviewer's failing shape (per-group={...: 1}) and
accepts the control ({...: 2}). ruff 0.15.22 clean. Windows host: the POSIX branch of finding 2 is
argued from the code and the reviewer's WSL2 measurement, not reproduced locally, and I am not claiming
otherwise.
… not this one (BACKLOG #1235)

This file already opens by distinguishing itself from `backlog_citation_check.py` on SEMANTICS --
that gate asks whether a citation names the right ledger FILE, this one asks whether the number
names anything at all. The names differ by one word and the paragraph exists because of it.

They also differ on WIRING, and that is the half that misleads. The sibling is run by
backlog-hygiene.yml and its test is named in ci.yml's docs-lane list. THIS script is run by no
workflow and no pre-commit hook; it reaches CI only through its test, which is manifest-classified
and therefore runs on the tooling leg.

MEASURED WITH A POSITIVE CONTROL IN THE SAME PASS, because a zero without one proves nothing:
`dangling_citation_check` returns ZERO hits across .github/ and .pre-commit-config.yaml, while
`backlog_status_check`, `ledger_check` and `scan_forbidden` return 3, 2 and 3. The probe finds
wiring where wiring exists, so the zero is a real zero.

WHY IT IS WORTH WRITING DOWN RATHER THAN LEAVING TO BE LOOKED UP: a grep for `citation_check` HITS
A WORKFLOW, and the hit belongs to the sibling. The answer is confident, well-formed, and wrong in
the direction of believing a change here is CI-covered when it is not -- which is the direction
that lets a behaviour change ship unexercised.

I asked exactly this question about my own emptiness-guard change one commit ago and reported the
answer as ONE PROBE rather than a clearance, because I had grepped the NAME while the gate is
wired by manifest PATH. The gate-fix seat ran the path angle independently, with the control
above, and cleared it. The clearance is theirs; this paragraph is so the next person does not have
to spend the same round trip -- or, worse, gets the sibling's hit and stops.

Comment-only. No behaviour change.
TEST-ONLY. The fix this rests on is already on this branch; nothing here
changes the hook.

The escape-blind span scan blanks text BEFORE any rule is dispatched, so it
disarms whichever rule would have judged that text. The suite shipped with the
fix parametrises two verbs and both reach the SAME rule, the one guarding the
primary working tree. A rule-agnostic defect with single-rule coverage rebuilds
the condition that let this class live in the first place: fourteen green gate
suites, none able to see it. A later change repairing one rule's own matching
while leaving the scan blind would keep every existing row green.

This adds one arm per rule -- primary working tree, linked worktree hijack,
shared git configuration, worktree removal -- against a real repository,
because three of the four ask git itself what it is looking at and are
unreachable from a synthetic fixture.

IT ASSERTS THE RULE IDS THE GATE ITSELF RECORDED, not merely that a deny
happened. Those are different claims, and only the second catches a fix that
denies for an accidental reason; a suite checking outcomes alone passes when
the right answer arrives by the wrong route. The ids come from the gate's own
receipt log rather than from the deny prose, which is written for a human and
is rewritten whenever a remediation changes. That instrument carries its own
positive control, because a reader whose pattern has drifted returns a partial
set and can satisfy the assertion by accident.

Every offender row runs its bare, unescaped command first and requires it to be
gated. An arm that cannot fail is not evidence, and that is not hypothetical
here: a synthetic fixture reported an ungoverned verb as a fail-open when it
was simply unreachable.

Measured: five rows red against the pre-fix hook and green against the fix on
this branch, while the remaining rows stay green against both.

ONE ROW FROM THE ORIGINAL DRAFT WAS DROPPED RATHER THAN PORTED, said here so a
deletion does not read as coverage quietly reduced. It pinned a different
design for the same function -- refusing to let an escape extend a
double-quoted span -- and this branch deliberately honours the escape there,
which is what a shell does. That row asserted a deny no shell requires, so it
is a statement about the rejected design and not about the gate.

The file is named for what it adds rather than for the defect. The obvious name
sat one character from the suite already beside the fix, which is a trap in a
directory a reader scans by eye.

Classifies itself in tests/tooling_manifest.txt. Without an entry
test_every_non_engine_test_is_classified fails, and the classification is what
decides which CI leg a suite runs on -- an unclassified tooling suite can be
deselected on the engine legs and unreached by the tooling job, leaving it
present, green, and never actually run. That is the condition a gate test can
least afford, and it is the same shape as the defect this suite covers.

The sibling suite's own manifest entry is NOT added here: it is already
upstream, landed separately after CI went red on exactly this check. Recorded
because its absence from this diff is otherwise indistinguishable from not
having noticed -- the same reason the dropped row above carries a reason.

Deliberately not duplicated: the commit-message and unterminated-quote
boundaries are pinned in the quote-straddle suite, and the escape-inside-a-real-
span control and the program-path spellings in the suite beside the fix.

Not an arm, and recorded because it was reported as one: git worktree add
allows with AND without the escape, measured independently by two sessions on
separate trees. It is not evidence about escape handling, and whether it should
be governed at all is a separate question.
@wshallwshall
wshallwshall disabled auto-merge August 20, 2026 19:41
…ix re-opened #1229 on PowerShell (BACKLOG #1229 residual)

MY PREVIOUS COMMIT INTRODUCED A REGRESSION AND THIS IS IT. Honouring a backslash escape inside a
double-quoted span is correct POSIX. But line 999 scans BOTH tool names through ONE matcher --
`$tool -in @("Bash", "PowerShell")` -- and POWERSHELL HAS NO BACKSLASH ESCAPE; its escape is the
backtick. On a PowerShell payload the scan held a span open that PowerShell had already CLOSED,
straddled the live command between it and a later quote, and blanked it.

THAT IS #1229's OWN STRADDLE, RE-CREATED BY THE FIX FOR #1229, ON THE OTHER HOST.

    Write-Output "C:\Temp\" ; git -C <governed> reset --hard ; Write-Output "x"    ALLOW
    ... one fewer backslash (control)                                              DENY
    ... two backslashes (even count)                                               DENY

An ODD count before the closer was the trigger, and the verdict had MOVED against origin/main.

FOUND BY THE SEAT WHOSE DROPPED DESIGN ROW ASSERTED EXACTLY THIS PROPERTY -- a row I argued should
go, and which they dropped agreeing with me. Their commit message called it "a deny no shell
requires". True of sh, FALSE OF POWERSHELL. I made the argument; neither of us checked the second
half; the conservative design was conservative for a reason we did not articulate.

THE FIX: `$PosixEscapes`, threaded from the tool name through Get-ScannableSegments to the scanner,
gating BOTH escape arms. DEFAULT $false, and the direction is the point -- honouring the escape
makes spans LONGER, so it blanks MORE and can hide a command (fail OPEN); refusing it leaves more
text visible to the rules (fail CLOSED). An unrecognised host gets the conservative reading and
only a host known to use backslash escapes opts in.

EVERY TEST ROW IS PINNED TO WHETHER THE COMMAND ACTUALLY EXECUTES ON THAT HOST, measured with an
inert payload that COMPUTES (111*3 -> 333) rather than echoes, so an echo-back could not be
mistaken for a run:

    odd-BS       PowerShell   333 printed, middle RAN            -> DENY
    odd-BS       Bash         "unexpected EOF", nothing parses   -> ALLOW is harmless
    straddle     PowerShell   no 333, middle did NOT run         -> ALLOW is CORRECT
    straddle     Bash         333 printed, middle RAN            -> DENY

The two ALLOW rows are not concessions: denying them would be a FALSE DENY on a command that
cannot run. "Should this deny" is a question about the shell, not about the gate -- so the rows
record the observation rather than a previous verdict, and a gate that denied everything would
fail them.

Verification: 586 passed / 13 skipped across every test_worktree_gate* module, ruff clean. x12 and
xml extras absent from this venv, so this does not reproduce CI collection.

NOT CLAIMED: the same lens reports this one character also disarming rules 3c and 3d. I have not
reproduced those and they are not addressed here.
…suite

The lane based 4d46a2a on 0642792 and reported it as one ahead of 49f425b. It is one
ahead of 0642792: the gate-fix seat's four-rule coverage suite (49f425b) landed on this
branch in between, so the two lines had diverged one against one.

Taking the handed-off tip would have discarded that suite -- the only coverage this PR has
that exercises more than a single rule, and the coverage whose absence let the regression
this very commit fixes go unnoticed. So this is a merge.

Verified in the result rather than trusted for merging cleanly: the merged tree carries
4d46a2a's worktree_gate.ps1 byte-identically (the $PosixEscapes fix, defaulting to false
so an unrecognised host gets the fail-CLOSED reading), the coverage suite file, and the
manifest at 123 lines classifying both new test files.

458 REMAINS HELD AND AUTO-MERGE STAYS OFF. This commit clears the cause the hold was
placed for; it does not clear the hold. The gate-fix seat's refutation phase has not
finished, and two of its findings -- the same character reportedly disarming rule 3c
(shared core.hooksPath) and rule 3d (another session's worktree) -- are unreproduced and
unaddressed here, as this commit's own message states.
…ing about where a span ends (BACKLOG #1229 residual)

SECOND REGRESSION FROM MY OWN ESCAPE FIX, and the host flag could not close this one because the
failing host is BASH -- where the escape is real and honouring it is correct.

The interpreter-argument regex was `[^"]*`, which is escape-BLIND: it stops at the first quote,
including an escaped one. Once the span blanking became escape-AWARE, the two disagreed about
where the argument ends:

    bash -c "bash -c \"git -C <governed> reset --hard\""
    extraction got:   `bash -c \`     truncated at the escaped quote, no verb in it
    blanking removed: the whole span
    result:           nothing reached any rule -> ALLOW

MEASURED: origin/main DENY x3, the escape-aware fix ALLOW x3, control DENY on both. The inner
command really runs -- `bash -c "bash -c \"expr 111 \* 3\""` prints 333, so this is execution and
not a parse curiosity.

ON MAIN THE TWO AGREED BY ACCIDENT, both being escape-blind, which left the verb visible OUTSIDE
the span. My fix removed the accident on one side only. That is the whole defect: not a wrong
rule, a rule applied to one of two places that had to move together.

Fix: the double-quoted extraction becomes escape-aware under the same $PosixEscapes flag, so both
sides share one answer. The SINGLE-quoted arm stays escape-blind, matching Remove-QuotedSpans --
sh gives the backslash no special meaning inside a single-quoted word.

Found by the gate-fix seat, who separated introduced-from-inherited by running every shape against
BOTH gates rather than arguing about it, and who stood down mid-dispatch on the collision guard
rather than overwriting this file while I was editing it.

NOT CONFIRMED AND NOT CLAIMED: the same lens reports rules 3c and 3d disarmed by this character.
My reproduction of those does not reproduce -- my CONTROL allows on origin/main too, which means
my harness is not exercising rule 3c at all, so it is evidence about my construction and not about
the gate. Their measurement stands unrefuted; mine is simply not a test of it. Left open rather
than claimed either way.

Verification: 587 passed / 13 skipped across every test_worktree_gate* module, ruff clean. x12 and
xml extras absent from this venv, so this does not reproduce CI collection.
…ail both ways

TEST-ONLY, and it corrects a weakness in my own earlier commit rather than
anything in the gate.

An adversarial pass asked whether this suite could pass VACUOUSLY. Mutation-
tested standalone, with a per-mutant applied-check, it could not:

    gate mutated to deny every verb against a governed tree   7 passed, 0 failed

It could see a gate that had stopped denying and NOT one that had started
denying everything. Every row it carried agreed that a deny was correct, so a
fix that widened the gate until the offenders passed would have satisfied all of
them.

THE CAUSE WAS A RULE I APPLIED IN THE WRONG PLACE. The earlier draft dropped its
must-ALLOW rows on the grounds that equivalents live in the sibling suites, and
that a load-bearing fact should be stated once and linked. That is right for
PROSE and wrong for a TEST: a suite discriminates only through the assertions it
actually runs, and delegation is not coverage when the file is read, run, or
trusted on its own.

So each rule now has an ordinary use of the same subject that must be ALLOWED --
a commit message for the primary-tree rule, creating a NEW branch for the hijack
rule, a harmless key for the shared-config rule, a read-only listing for the
worktree-removal rule. Deliberately close to each offender: same subject,
differing only in being legitimate.

After the change, measured the same way:

    deny-every-verb mutant                    3 failed   (was 0)
    scanner replaced by the identity function 1 failed   (was not scored)

THE MUTATION HARNESS CARRIES AN APPLIED-CHECK AND IT EARNED IT IMMEDIATELY: it
hashes the file and refuses to score a mutant that did not change it. The
identity mutant did not match its anchor on the first run and was reported as
NOT SCORED rather than as survived -- and separately, an ad-hoc probe written
without that check silently measured an unmutated gate and read as a clean pass.
A mutant that fails to apply is indistinguishable from a test that cannot fail.

ONE MUTANT STILL SURVIVES AND IT IS NOT A GAP. Disabling the escape branch
OUTSIDE a span leaves the in-span rule, which makes the span unterminated, and
an unterminated span is emitted RAW -- so the command stays visible and the gate
still denies. Verified rather than reasoned: the mutated gate was driven against
the offender payload directly and returned DENY. There is nothing for the suite
to see, so passing is correct. A plausible cause that retires a finding without
being tested is how a real gap gets closed on paper.

The decisive check is against a REAL gate rather than a synthetic mutant: this
file still goes five rows red against origin/main's hook and green against the
fix on this branch.
…sed (BACKLOG #1229 residual)

TWO OF MY COMMIT MESSAGES CARRY A FALSE CLAIM AND A COMMIT MESSAGE CANNOT BE AMENDED ONCE PUSHED,
so the correction goes where the next reader will actually be: beside the tests.

4d46a2a and c308cc3 both say rules 3c and 3d are unaddressed and that I did not reproduce them.
THEY ARE CLOSED. Verified COLD by the seat that found them, on a rig predating my fix that I did
not write: all four PowerShell shapes now DENY where the previous fix ALLOWed, 3c (shared
core.hooksPath) and 3d (another session's worktree) included.

MY NON-REPRODUCTION WAS MY HARNESS. My control allowed on origin/main too, which means my shapes
never exercised rule 3c at all. I said so at the time and left it open rather than claiming either
way, which was right -- but the note as written sends the next reader hunting two live defects
that do not exist, and an honest hedge that misdirects is still a defect in the record.

Gating the escape AT THE SCANNER closed all three rules at once. That is what rule-agnostic cuts
both ways means: one character disarmed three rules and one flag re-armed them.

AND MEASURING AGAINST MAIN FOUND SOMETHING I DID NOT SET OUT TO FIX:

    one-level escaped interpreter arg    main ALLOW -> DENY     INHERITED, now closed
    two-level escaped interpreter arg    main DENY  -> DENY     my regression, repaired

The left column is the point. A fix judged only against the branch it repairs cannot tell
"restored" from "improved" -- I would have reported the weaker claim, and the inherited hole would
have stayed open with nobody counting it as anyone's.

LEFT OPEN AND NOT CLAIMED: twelve further shapes the same lens reports as inherited from main.
None introduced here, none fixed here, with the Dispatcher as a disclosure question.
… would neuter this test (BACKLOG #1229 residual)

460's escape-sequence gate flags three invalid escapes in this file. They are real: each becomes a
SyntaxError on a future Python and would fail at COLLECTION, so the module vanishes and a security
regression suite stops running while everything stays green. Exactly the class this file is about.

BUT THE GATE'S PRESCRIBED REMEDY -- "make the whole literal raw" -- IS WRONG HERE AND WOULD HAVE
SILENTLY NEUTERED THE REGRESSION ROW. `_ODD_BS` contains BOTH an invalid `\T` and a valid `\`.
Raw preserves the `\` as TWO backslashes, turning the line's ODD trailing count into an EVEN one
-- and per the measured table ODD is the ALLOW case under test while EVEN DENIES. The remedy
yields a GREEN test of the non-regressing case.

A remedy that turns a failing check into a passing one by changing what is checked is worse than
the warning it silences, and nothing would have reported it.

Caught by the Lander, who measured it by TRANSFORMING THE SOURCE LINE rather than retyping it --
and whose first attempt was itself corrupted by shell escaping, so they redid it programmatically.
I verified the same way: comparing ast.literal_eval before and after, never reading the line.

    whole literal raw       value preserved = FALSE
    escape only the invalid value preserved = TRUE

Applied to all three: `\T`, and two in a docstring (`` \` `` and `\*`) where the same trap
applies, since that docstring also carries valid `\\"` sequences. Rendered text proven identical
for each, so the documentation displays exactly as before.

Verified after: compiles clean under -W error::SyntaxWarning, 16 passed, and the rendered _ODD_BS
still carries an ODD trailing backslash count -- i.e. the row still exercises the ALLOW case it was
written for.

FOR THE 460 AUTHOR, and it is a real defect in the remedy text rather than a nit: "make the whole
literal raw" is unsafe for any literal that ALSO contains a valid backslash escape. Narrow it to
"escape the invalid sequence".
wshallwshall and others added 5 commits August 21, 2026 01:06
… residual)

Get-FlagOwner's docstring said the five shapes that defeated five earlier
program-token candidates were "all five measured to survive" the bounded leftward
scan. That is true of four of them and false of the fifth, and the fifth is the
one a reader needs to know about, because it is the cost this change takes.

The five are `echo hi;cmd /k`, `(cmd /mnt/c`, `cmd /d /Q/C`, an alias, and a
renamed copy of cmd.exe. The first three break ADJACENCY -- every earlier
candidate asked whether the token IMMEDIATELY LEFT of the switch run was a cmd
spelling -- and all three deny under a scan that walks left past options and
switch components. The last two break IDENTITY, and an allowlist cannot close
them: an unknown name gets no recursion.

The $cmdExeFlag note beside it already said this correctly. The function's own
docstring did not, and a completeness claim that is one member too wide is the
liability CLAUDE.md section 11 names.
…LOG #1229 residual)

Remove-QuotedSpans keeps the git token of a closed quoted span so a quoted PROGRAM path
stays visible to the rules. Two changes were layered on top of that and both are withdrawn
here:

  (a) a case-INSENSITIVE emit canonicalised to lowercase, so `GIT.EXE` -- a real Windows
      spelling the case-SENSITIVE rules downstream skip -- would still present a verb;
  (b) Test-GitProgramPosition, added because (a) then read `cp -r "/c/backups/Git" restore`
      as a git command, keeping the token only where the span is dispatched as a program.

WHY, and it is not the false denies. (b) bought those back at the price of two fail-OPENS on
shapes origin/main DENIES, measured over the real hook:

    cmd /c "<...>\Git\bin\git.exe" -C <governed> reset --hard  main=DENY  experiment=ALLOW
    . "<...>\Git\bin\git.exe" -C <governed> reset --hard       main=DENY  experiment=ALLOW
        (the second a PowerShell dot-source, on a PowerShell tool call)

Spending a security gate's DENY to buy a tidier false-deny profile is the wrong direction.
Re-measured after the revert, both rows are DENY again. Get-FlagOwner -- the interpreter
axis, 548cf50 -- is KEPT and untouched; its wrapper set is a different mechanism from (b)'s
and keeps its own coverage in tests/test_worktree_gate_interpreter_flags.py.

The emit is now case-SENSITIVE (-cmatch, no ToLowerInvariant). Not a regression: origin/main
ALLOWs `GIT.EXE` in program position too, by another route -- its collapse regex is
case-insensitive but substitutes $1, preserving a case the rules then fail to recognise.

TESTS RETIRED, each because the behaviour it asserted exists nowhere any more:

  - test_a_wrapper_prefix_does_not_move_a_quoted_git_program_out_of_program_position
    (24 rows) pinned Test-GitProgramPosition's wrapper vocabulary. The function is gone, and
    every row used the GIT.EXE spelling, which measures ALLOW on origin/main as well.

  - test_an_UNLISTED_wrapper_word_is_a_known_open_residual asserted ALLOW for `myrunner` and
    `setarch x86_64` ahead of a quoted lowercase git path. Both measure DENY on origin/main
    and on this tree, so the residual it disclosed does not exist. Its own note says to
    delete the row rather than restore the ALLOW.

  - test_the_argument_position_ALLOW_is_a_RECORDED_WEAKENING_for_the_lowercase_spelling
    asserted ALLOW for `cp -r "/c/backups/git" restore` from primary.parent. It PASSED and
    could not have failed: from that cwd no governed target resolves, so the verdict comes
    from the target resolver and not from the emit. Run from INSIDE the governed repo the
    same line measures DENY on origin/main and on this tree -- the opposite of its claim.

  - test_the_program_path_token_survives_every_spelling lost its `GIT.EXE` and `Git` rows and
    keeps `git` and `git.exe`. The two dropped spellings measure ALLOW on origin/main.

WHAT REPLACED THEM, so neither end of the trade lives only in prose:

  - the argument-position rows now run from INSIDE the governed repo, where the emit is the
    only variable, and each carries a control varying THE LEAF CASE ALONE. Measured 14 of 14,
    both blobs: Title-cased leaf ALLOW, lowercase leaf DENY.
  - the uppercase quoted PROGRAM spelling is pinned as a known-open residual (asserts ALLOW).
  - the lowercase argument-position leaf is pinned as a known-open FALSE DENY (asserts DENY).

Verification: 33-row and 28-row probe matrices driven over the real hook against origin/main
and this tree returned IDENTICAL verdicts on every row -- no weakening and no tightening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…redicate (BACKLOG #1229 residual)

Prose only. Every assertion in tests/test_worktree_gate_hijack.py is unchanged and every row
passed before and after: `cp -r "/c/backups/Git" switch` on line one must not eat a real
`git switch <free branch>` on line two.

What the prose said was that these rows guard "the position predicate" and
Test-GitProgramPosition. That function was reverted, so the comment pointed at nothing and
the next reader would have scored the rows as dead coverage.

What they actually guard is the CASE-SENSITIVITY of the emit in Remove-QuotedSpans, which is
why the leaf axis is `Git` / `Git.exe`. A case-insensitive emit hands line one a git token,
rule 3 records that segment, and rule 3b returns ALLOW on the hijack behind it. So relaxing
the case rule is a fail-OPEN on 3b and not only a false-deny question -- which is precisely
why this coverage lives here rather than beside the false-deny rows in
tests/test_worktree_gate_escaped_quote.py.

Also corrected: "the unconditional emit" now reads "a case-INSENSITIVE emit". The emit IS
unconditional in position after the revert, so the old wording named current behaviour while
meaning the withdrawn experiment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t a comment (BACKLOG #1229 residual)

The program-position experiment was reverted because it moved two shapes from DENY
to ALLOW while origin/main denies both:

    cmd /c "<...>\Git\bin\git.exe" -C <governed> reset --hard    main=DENY  experiment=ALLOW
    . "<...>\Git\bin\git.exe" -C <governed> reset --hard         main=DENY  experiment=ALLOW
        (the second is a PowerShell dot-source, on a PowerShell tool call)

That reason lived only in prose -- the narrative block at the top of the file and two
assertion-failure strings -- so nothing executable would have reported it if the
predicate came back. Measured: restoring the experiment blob reds 16 tests, and every
one of the 16 is a FALSE-DENY row whose own text instructs the reader to delete it.
Followed literally, the suite's stated remedy lands both fail-opens green.

The nearest existing coverage is not this shape: test_worktree_gate_interpreter_flags
asserts DENY for `cmd /c '<whole gated payload>'`, where git sits INSIDE the quoted
span. Here git.exe IS the quoted span and the verb is outside it, which is what the
position predicate changed.

Measured on the real hook, cwd = the governed repo:
    both rows                       main=DENY  fb93c9c=ALLOW  HEAD=DENY
    control, UNGOVERNED target      ALLOW on every blob
The ungoverned control is what stops the row degenerating into "this gate denies any
cmd /c"; the fb93c9c column is the anti-vacuity check -- the test genuinely reds if
the predicate returns, rather than passing against anything.

Docstring says DO NOT delete this row on red, inverting the two neighbouring tripwires,
because a red here is a re-opened hole rather than a closed one.

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

#1305 -- the worktree gate matches git by SPELLING, so a case variant bypasses every rule.
Measured against origin/main driving the real hook as a subprocess: a capitalised spelling of
the program name ALLOWS where the lowercase spelling DENIES, with a benign negative control
allowing on both. Not confined to one rule -- checkout allows too. No quoting, wrapper or
escape involved. Windows resolves the spellings to the same executable so all of them RUN; the
gate's comparison is case-sensitive so only one MATCHES. It is matching a spelling and calling
it a program. Reproduces identically on origin/main and on the unlanded lane branch, so
#1229's residual work neither opened nor closed it.

Filed in the ledger rather than vaulted: this guard is coordination tooling, is not shipped in
the wheel, and is not graded by any ASVS cell. The exhaustive spelling-by-rule matrix is kept
in the episode note rather than here.

#1301 -- a banner citing a sha must cite a commit naming its own item. The #1022/#1259
transposition passed every gate: valid Markdown, unchanged item count, untouched status glyph,
and the misplaced paragraph carried no glyph of its own so parse_items had no second banner to
object to. The signal present throughout: the overwritten banner cited two shas whose subjects
both ended in that item's number; the paragraph replacing it cited a sha naming a different
item. Filed as a PARTIAL control and described as one -- it fires only on banners citing shas.

The forbidden-content gate blocked the first attempt at this commit over a branch slug in
#1305's prose. Removed rather than allowlisted, per the gate's own instruction.

Both appended at the file tail, the lowest-conflict edit against the re-scored main.
parse_items: 314/230/84 before, 316/232/84 after -- the expected +2 open, no other movement.
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

STILL CARRIES GENUINELY UNLANDED WORK -- measured, so this is not stale-by-assumption.

Method: for each changed file, the lines this branch ADDED relative to its merge base 13dd27d, searched against main's copy of the SAME file at aed856b. Dot-prefixed paths read as origin/main:./<path> after the MSYS rewrite silently emptied a comparison file elsewhere tonight and produced a confident wrong verdict.

ABSENT FROM MAIN ENTIRELY (4 files)
docs/adr/0169-username-identity-is-case-sensitive-and-must-not-depend-on-store-collation.md
tests/test_username_identity_collation.py
tests/test_worktree_gate_escaped_quote.py
tests/test_worktree_gate_rule_agnostic_coverage.py

ADDED LINES PRESENT ON MAIN: 7 to 21 percent across every remaining file
scripts/docs/dangling_citation_check.py 10/75 · scripts/hooks/worktree_gate.ps1 8/70
tests/test_dangling_citation_check.py 6/76 · tests/test_backlog_status_check.py 5/50
messagefoundry/auth/service.py 3/20 · messagefoundry/store/sqlserver.py 1/9
docs/adr/README.md 0/1 · tests/tooling_manifest.txt 0/2

Those low percentages are incidental common lines, not partial landings. Contrast #432, closed an hour ago, where 13 of 15 differing files matched at 100 percent and the two outstanding lines both resolved -- that is what landed-by-content looks like, and this is not it.

WHAT BLOCKS IT, and it is not the conflict. ADR 0169 is allocated to the worktree repo-security-review-afbae3, whose session has been INTERRUPTED for roughly 13 hours. ledger_check requires owns("adr", "0169") for an added ADR file, and that key is the WORKTREE PATH, not a live session -- the directory still exists on disk, so the allocation is intact and resolvable, but not from any other seat's worktree.

NOTHING IS AT RISK: the branch is on origin and this PR is open, so the work is stalled rather than endangered. It needs an assignment, not a rescue.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

THE BRANCH BEHIND THIS PR IS DIVERGED, NOT AHEAD, AND THIS PR SHOWS THE SMALLER SIDE. Measured after the Liaison identified this worktree as the predecessor builder-1 seat.

git rev-list --left-right --count origin/claude/builder-1-160cec...claude/builder-1-160cec
origin-only 3    local-only 12

This PR's head is e4c342ce. The local tip is b68c0b4d. Neither is a superset of the other, so a plain push is rejected and a force-push in either direction destroys real commits.

ON ORIGIN AND IN THIS PR, ABSENT LOCALLY (3):
e4c342ce hardened counterpart per gate rule · ebdf0308 merge of the #1229 host-specific escape fix · 49f425b1 escape-blind scan across four rules

LOCAL AND NOT IN THIS PR (12) — eleven #1229 residuals plus a filing:
b68c0b4d files BACKLOG #1305 and #1301, both allocated by that seat and left unwritten · dbd45f2a the revert's two fail-opens get an assertion rather than a comment · c0d6cef8 reverts the program-position emit experiment · 548cf506 recurse from the INTERPRETER not the outer tool name · 3fa2b2ca a quoted git path denies only in PROGRAM position · 69b95d67 escape only the INVALID sequences, because the gate's own remedy would neuter the test · 7979ac60 corrects its own earlier commit messages · plus four more.

CORRECTING MY OWN EARLIER COMMENT ON THIS PR. I measured the four-absent-files and 7-to-21-percent figures against this PR's head, and they are right about that. They understate the branch: the twelve above are outside what I probed, and one of them files two allocated-but-unwritten backlog numbers, which is the same allocated-and-unfiled exposure that justified rescuing #1307/#1308 earlier tonight.

NOTHING IS AT RISK. b68c0b4d is reachable from the private durability remote (1 ref), verified. The exposure is that the work is invisible on origin and that the divergence has no safe one-command resolution — not that it can be lost.

WHOEVER TAKES THIS MUST NOT FORCE-PUSH EITHER WAY. The two sides both contain unique work; the resolution is a merge, and it has to come from the worktree that owns ADR 0169 and the #1305/#1301 allocations, which is the same interrupted seat.

wshallwshall and others added 6 commits August 22, 2026 11:52
Committed from the worktree that OWNS the two allocations, which is what the
ledger gate is for rather than something to route around. BACKLOG #1301 and
#1305 are allocated to this worktree (claude/builder-1-160cec); a merge made
from any other worktree trips ledger_check.owns(), and alloc.ps1 has no
transfer verb. Nothing was renamed, copied or --no-verify'd.

Also brings in origin's three commits, which this branch's local tip lacked.
That side and this one had DIVERGED, so a force-push would have discarded the
four-rule coverage suite -- the third time this lane has met that trap.

Conflicts, each resolved on what the hunk actually is:

  * #1022 -- TOOK MAIN. Both sides retire the same 2026-08-15 do-not-build
    banner; main's pass is later (08-21 vs 08-20) and names the two commits
    PR #394 carried, so this lane's copy is superseded, not lost.

  * #1268 -- TOOK THIS LANE. Main still scores it open because on main both
    limbs do survive; this branch is what ships them. Taking main's re-score
    would have landed the fix under a banner saying it was not built, and
    dropping that banner is what the one-item-one-status rule requires.

  * the new-item block -- TOOK BOTH. A pure insertion collision: main appended
    #1299-#1317, this lane appended #1305 and #1301, no number claimed twice.
    Either side alone silently deletes real filings.

  * docs/adr/README.md -- TOOK BOTH, ordered 0169, 0170, 0171.

backlog_status_check: 568 items, each declaring exactly one status.
worktree_gate.ps1 verified content-identical to this lane's fixed copy after
both merges, so neither merge quietly reverted the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves the one conflicted file, docs/BACKLOG.md, in two hunks. Files nothing
and fixes nothing -- no item's disposition moves here.

HUNK 1, under #1317: took main whole. The scoping clause this branch carried
("That cell's own ruling is a separate question about allowlist width; this item
is the build") is ALREADY the last line of main's side, its sentence completing
past the conflict at "which both readings of the cell now require". The lander's
approved plan said re-attach it; re-attaching would have DUPLICATED it. Asserted
in the resolution script rather than trusted, so the merge fails rather than
silently duplicating if that ever stops holding.

HUNK 2: kept both sides. A pure tail append -- main adds #1318-#1323, #1327,
#1328; this branch adds #1305 and #1301. Neither number is on the other side.

VERIFIED with parse_items, not a hand-rolled scan: 340 items resolved against 338
on main and 332 on HEAD, ZERO lost from either side, added-beyond-main is exactly
[1301, 1305], no duplicate numbers. Negative control main-minus-HEAD returns
[1318..1323], so the set comparison can distinguish and the zeros are not vacuous.
…BACKLOG #1314)

Both SMTP connectors branch three ways on TLS posture. The `not use_tls` and
`not tls_verify` arms each carry an escape gate AND an absolute credential gate.
The verified arm carried a body guard and no credential gate at all -- while
`tls_check_hostname` is read right below it and fed to the context builder. So
`use_tls=true` + `tls_verify=true` + `tls_check_hostname=false` + a username
constructed silently, and `smtp.login()` then ran against any peer whose cert
chained to the anchor, whatever name it was issued to.

Adds a third credential gate to each, matching the wording and the ABSOLUTE
placement of the two beside it. Keyed on no escape, deliberately: in both existing
arms the escape governs the BODY posture and never the credential, and a hop must
not be able to attest its way to a credentialed unverified-name session.

DIRECT HAD NO `else` ARM AT ALL -- the chain fell straight through to the context
build -- so that branch is new rather than extended. The gate is inlined in both
files rather than extracted, which is this cell's stated convention: the verify-off
arms say a shared helper was refused because "a third spelling is how the next bug
gets written".

VERIFIED, and the controls are the point:
- RED FIRST on email: "DID NOT RAISE ValueError" before the gate existed.
- Direct's tests were written AFTER its gate, so they had never been red. Disarmed
  the gate to `if False and ...`: the refusal test failed and a control still
  passed. Restoring it revealed a second thing worth recording -- `git checkout --`
  reverts to HEAD, which never held the fix, so it destroyed the gate instead of
  the mutation. Caught by asserting the restore rather than assuming it.
- NEGATIVE CONTROL both files: `tls_check_hostname=false` with NO username still
  constructs, so the gate is keyed on the credential and does not widen the arms.
- POSITIVE CONTROL both files: a fully verified hop WITH credentials still
  constructs, so the refusal tests are not green by refusing everything.
- 66 passed across both modules; ruff format, ruff check and mypy strict clean.

Not measured: no live handshake was run here. The item already carries one.
Severity is conditional -- zero deployments, so this is what a deploying site
would hit, not an exposure today.
…5 scope 2)

Band B1 item 2 of four. Adds `harden_crl_check(ctx, crl_file)` beside
`harden_verify_flags`, whose docstring says in as many words that it does strict
RFC 5280 path validation and NOT revocation, plus `read_crl_facts` in pki.py as
the sibling of `read_cert_facts` that the item names.

RE-MEASURED THE TRAP TABLE FIRST, because the item says to and forbids inheriting
it. CPython 3.14.6 / OpenSSL 3.5.7, TLS 1.2 pinned, throwaway synthetic PKI:

  arm                              crls  good client   revoked client
  CA only, no flag (SHIPPED)          0  ACCEPTED      ACCEPTED       <- the gap
  cafile= CA + FRESH crl, flag        1  ACCEPTED      REFUSED revoked
  cadata= CA + FRESH crl, flag        0  REFUSED       REFUSED        <- trap 1
  cafile= CA + STALE crl, flag        1  REFUSED       REFUSED        <- trap 2

All four inherited rows reproduce. THE FIFTH ARM IS NOT MEASURED AND I AM NOT
RECORDING IT AS RULED OUT: `capath=` needs a hashed directory and `openssl rehash`
was unavailable here, so what that arm tested was an UNHASHED directory failing,
which is expected and answers nothing. The item's capath question stays open.

Both traps are ASSERTIONS in the helper, not comments:
- loads through `cafile=` only, then requires `cert_store_stats()["crl"] >= 1`.
  `cadata=` sets the flag with zero CRLs loaded and then refuses EVERY client with
  "unable to get certificate CRL", silently. The count is the only thing that
  separates "loaded" from "silently ignored".
- refuses an already-expired CRL at construction, because past nextUpdate OpenSSL
  refuses every client rather than just revoked ones -- a PKI housekeeping lapse
  becomes a total outage whose first symptom is every partner dropping at once.
- refuses a missing file rather than degrading to no revocation checking.

tls_policy.py stays STDLIB-PURE at import time: it declares itself "pure stdlib ssl
helpers" and touches cryptography only in two prose comments, so the parsing lives
in pki.py and both imports here are function-level.

VERIFIED: red-first (ImportError, symbol absent), then 91 passed in the module;
ruff format, ruff check and mypy strict clean.

NOT YET DONE, so this is not the item: scope 1 (the tls_crl_file setting on three
factories + ApiSettings), scope 3 (the posture-keyed fail-closed refusal), scope 4
(the pre-expiry alarm on CertExpiryRunner), and the real-handshake rig. This commit
asserts the FLAG IS SET; it does not yet assert a revoked client is refused, and
those are different claims.
…ed (BACKLOG #1005)

The previous commit set a flag and said so. "The flag is set" and "a revoked
client is refused" are different claims, and only the second is worth having.
The item makes the same point: on the two builders it touches most there is no
real client-cert handshake anywhere, and test_mllp_tls.py asserts
`ctx.verify_mode == ssl.CERT_REQUIRED` and stops.

Three tests, driving an actual mTLS handshake on 127.0.0.1 against a context
built by harden_crl_check. TLS 1.2 is pinned so client auth happens IN the
handshake -- under 1.3 the client certificate arrives after the server has
finished and the refusal surfaces on a later read instead, which is how a
revocation test can look green while proving nothing.

1. A revoked client IS refused, and the assertion reads OpenSSL's reason rather
   than merely that the handshake failed.
2. A good client IS ACCEPTED by the same context. This is the control that
   separates working revocation from a context refusing everyone -- which is
   exactly what trap 1 produces, and what a flag assertion cannot tell apart.
3. WITHOUT the CRL, the revoked client GETS IN. The shipped gap the item is
   filed against, pinned deliberately as a negative control: if this ever starts
   failing, revocation arrived by some other route and the item's premise needs
   re-deriving rather than the test relaxing.

Synthetic throwaway PKI in a module-scoped tmp fixture. No PHI, no network beyond
loopback, nothing persisted.

94 passed in the module; ruff format, ruff check clean.
…G #1005 scope 1)

Band B1 item 1, wired in the SAME commit as the setting on purpose. A setting the
factories accept and nothing reads is a silent no-op, which is the exact shape this
whole item is about -- so it never exists in that state.

SURFACE: `tls_crl_file` on MLLP(), Http() and DICOM(), plus ApiSettings.
tls_client_crl_file beside tls_client_ca_file. Each declaration matches its OWN
local shape -- MLLP's union spans two lines, Http's is one, DICOM's takes EnvRef
across three -- rather than one imposed spelling. There is no separate TOML key
list to edit: connections_file.py:290 states "the factory IS the schema", and
_INBOUND_KEYS allow-lists top-level entry keys, not settings keys.

THE ITEM'S LINE NUMBERS WERE STALE, so every coordinate was re-derived from the
symbol. tls_ca_file resolves in FIVE factories; Email() and Direct() also carry it
and are outbound SMTP, outside this item.

PLACEMENT IS THE LOAD-BEARING PART AND IT DIFFERS FROM ITS SIBLING'S. harden_crl_check
loads into the trust store, so it must follow the CA load, inside the mTLS branch. In
mllp.py and dicom.py harden_verify_flags already sits after the CA. IN api/tls.py IT
SITS BEFORE IT -- so placing the CRL call beside its sibling there would load a CRL
into an empty store, producing a context with the check flag set and zero CRLs, which
refuses EVERY client. That is trap 1, reached by copying a neighbour.

One wiring covers two listeners: http_listener.py calls _mllp_ssl_context, so the
inbound HTTP listener inherits it.

THREE ASSERTIONS ON THE MLLP BUILDER, the module the item names as stopping at
`ctx.verify_mode == ssl.CERT_REQUIRED`:
- configured -> cert_store_stats()["crl"] >= 1 AND the flag set. The count is what
  separates "loaded" from "silently ignored".
- NEGATIVE CONTROL, pinning the shipped gap: mTLS on, no CRL -> no revocation flag.
  If this ever fails, revocation arrived by another route and the premise needs
  re-deriving. Do not relax it.
- PLACEMENT GUARD: a CRL with no mTLS is not loaded -- nothing to revoke when no
  client certificate is requested.

VERIFIED: mutation test, disarmed the wiring to `if False and ...` -> exactly the
configured-case test failed, both controls survived; restore byte-identical by sha256
(not `git checkout --`, which reverts to HEAD and would have deleted the wiring).
296 passed, 3 skipped across the six affected modules; ruff format, ruff check and
mypy strict clean.

STILL OPEN on the item: scope 3 (posture-keyed fail-closed refusal) and scope 4
(pre-expiry alarm on CertExpiryRunner). And `capath=` remains UNMEASURED, not ruled
out -- no rehash tool on this box.
…use it (BACKLOG #1005)

I recorded capath= as "cannot be tested here" because my probe reported no rehash
tool. THAT WAS FALSE and a peer caught it. Git for Windows ships both openssl and
c_rehash. Measured now, with controls:

  arm                              crls  good        revoked
  capath= hashed dir                  0  ACCEPTED    REFUSED: certificate revoked
  cafile= bundle [POSITIVE CONTROL]   1  ACCEPTED    REFUSED: certificate revoked
  cafile= CA only, no flag [BASELINE] 0  ACCEPTED    ACCEPTED

capath= ENFORCES REVOCATION IDENTICALLY. But cert_store_stats()["crl"] reports ZERO
for it, because a hashed directory is read LAZILY at verification rather than at load
time. So the >= 1 assertion in harden_crl_check -- exactly right for cafile=, and the
whole defence against trap 1 -- IS NOT A VALID LIVENESS CHECK FOR capath= and would
REJECT A WORKING CONFIGURATION. Recorded in the docstring where the next person will
hit it, rather than left as an open question they would re-derive.

WHY MY FIRST ANSWER WAS WRONG, three faults in one probe, each of which alone
produces a confident false negative:
1. subprocess could not CreateProcess c_rehash -- it is a SHELL SCRIPT, not an exe --
   and I swallowed the FileNotFoundError with a bare except and no diagnostic.
2. `openssl rehash` on this build PRINTS "Not available; use c_rehash script" AND
   EXITS 0. Had my subprocess resolved it, my returncode==0 check would have recorded
   a hashed directory that was never hashed. An exit code reading success from a tool
   that just said it cannot do the job.
3. Passing a Windows path into `bash -lc` hashed nothing and still returned 0.

The guard that saved it was asserting the .r0 file EXISTS before reporting any arm,
rather than trusting the rehash step. Without it, three separate mechanisms were each
ready to hand me "capath does not work" from a directory OpenSSL was never given.

94 passed; ruff format, ruff check, mypy strict clean.
… instance (BACKLOG #1005 scope 3)

Band B1 item 3. The revocation sibling of the cleartext exposed-gate, and it fires
on the OPPOSITE condition: those refuse a listener with NO TLS, this refuses one
whose TLS is correct but whose client certificates are never checked for revocation.
Measured on this tree, a revoked-but-chain-valid client is ACCEPTED.

THE ITEM DID NOT NAME THE ACTUAL BLOCKER, which was a missing model field. Source
carried tls_hop_attested and tls_hop_attested_reason; Destination carried those PLUS
tls_revocation_attested. So there was nothing on the inbound side to key an
attestation on. Adding it first was the real first step -- every sibling refusal here
pairs with a per-connection attestation, and a revocation refusal with no escape
would be the only control without one, refusing a site whose PKI covers revocation
outside the engine.

THE GATE keeps the sibling's three rungs in the sibling's order: attested permits;
an UNSTAMPED posture permits, because that call ran outside the enforced gate and
must never acquire a new refusal; otherwise refuse only when the instance is BOTH
enforcing AND PHI.

NO BLUNT PROCESS-WIDE ESCAPE, deliberately. MEFOR_ALLOW_INSECURE_TLS governs weakened
TLS, and a listener that verifies its peers correctly but skips revocation is not a
weakened-TLS hop. Reusing that env var would let one variable silence a control it
was never scoped to.

SCOPED to the three connectors that can hold a client certificate -- MLLP (which also
serves the inbound HTTP listener), HTTP and DIMSE. NOT raw TCP/X12: they have no TLS
option, so there is no certificate to revoke. Note the name mismatch that would bite
a copy-paste: the FACTORY is DICOM() and the CONNECTOR TYPE is DIMSE.

NINE TESTS, one control and seven ladder rungs around it:
- the refusal itself, and the same refusal on DIMSE and HTTP
- POSITIVE CONTROL: a configured tls_crl_file passes, so the refusal can be
  SATISFIED and not merely avoided -- without it the refusal test would pass equally
  against a gate that refuses every mTLS listener
- the per-connection attestation passes
- non-PHI warns, non-enforcing warns, unstamped posture never refuses
- PLACEMENT GUARDS: TLS without mTLS is not a revocation gap, and a connector with no
  mTLS surface is ignored

VERIFIED by mutation: disarming the refusal to `if True or ...` failed EXACTLY the two
refusal tests and left all seven ladder and guard tests passing. Restore byte-identical
by sha256, from a saved copy rather than `git checkout --`, which reverts to HEAD.
257 passed, 3 skipped across seven modules; ruff format, ruff check, mypy strict clean.

REMAINING on the item: scope 4, the CRL pre-expiry alarm on CertExpiryRunner.
…wn (BACKLOG #1005 scope 4)

Band B1 item 4, and the last of the four. The freshness PREFLIGHT already landed
with harden_crl_check (a7afcff) -- it refuses an already-expired CRL at
construction. This is the other half: warn BEFORE that happens.

WHY A CRL BELONGS IN THE CERT MONITOR AT ALL: the operator question is identical --
"is a file I depend on about to expire". CertExpiryRunner already reads notAfter off
every served cert and alerts inside a warn window. A CRL's nextUpdate is the same
shape, so MonitoredCert gains a `kind` discriminator (defaulted, so every existing
construction is unchanged) and _inspect dispatches to read_crl_facts.

BUT IT ALERTS DOWN A SEPARATE SINK METHOD, AND THAT SEPARATION IS THE POINT. An
expiring CERTIFICATE degrades one identity and is fixed by reissuing it. An EXPIRED
CRL makes OpenSSL refuse EVERY client presenting a certificate under that issuer --
not merely revoked ones -- so it is a total interface outage fixed by a PKI refresh.
Emitting both down cert_expiry would hand an operator ONE STRING FOR TWO CAUSES WITH
OPPOSITE REMEDIES, which is the defect this fleet spent tonight finding in an
unrelated banner. The logging sink escalates an expired CRL to ERROR rather than
WARNING for the same reason: the listener is already down, not approaching a
deadline.

INBOUND ONLY, deliberately: a CRL verifies peers we REQUIRE certificates from, and
only an inbound listener does that.

NOT DONE, AND NAMED RATHER THAN OMITTED: the API listener's own
ApiSettings.tls_client_crl_file (added in 5790321) is NOT yet watched. Threading it
needs a new Engine constructor parameter from its caller, which is a wider change
than this item's core. The three connection listeners are covered; the API one is not.

FIVE TESTS:
- a CRL inside the window alerts crl_expiry and NOT cert_expiry -- asserting
  crl_calls alone would pass if the runner fired BOTH, so both are asserted
- an expired CRL reports negative days
- POSITIVE CONTROL: a fresh CRL alerts NOTHING, so the two above are not green
  against a monitor that alerts on every CRL handed to it
- a cert and a CRL on one connection route to their own methods, which is the
  realistic configuration and where the separation earns its keep
- an inbound tls_crl_file is collected from a real Registry, built with the real
  InboundConnection and ConnectionSpec types rather than a fake shape

VERIFIED by mutation: disarming the crl dispatch failed EXACTLY the three routing
tests and left the fresh-CRL control and the collection test passing. Restore
byte-identical by sha256 from a saved copy. 297 passed across seven modules; ruff
format, ruff check clean; mypy strict clean on all 267 source files.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 22, 2026 22:05
…assified (BACKLOG #1005)

CI caught this on four legs and the lander correctly refused to treat it as a flake:
one deterministic assertion naming the remedy. The connection-scoped completeness
floor censuses all 15 connection factories, and #1005's scope 1 added tls_crl_file to
DICOM, Http and MLLP -- a new TLS-shaped parameter that was neither reported nor
exempt. The guard did exactly what it exists for, on my change.

EXEMPT, AND THE REASON HAS TWO HALVES BECAUSE STATING ONLY ONE WOULD BE THE WEAKER:
- it is a material PATH, like the tls_ca_file entry it sits beside, not a posture
  switch; and
- its ABSENCE is GATED rather than reported. check_inbound_revocation (#1005 scope 3)
  refuses an mTLS listener with no CRL on an enforcing PHI instance, the same shape as
  the ADR 0092 hop cell gating tls/tls_verify further down the list.

A connection-scoped reader that merely reported "no CRL configured" would be strictly
WEAKER than the refusal that already exists, which is why reporting is the wrong side
of this fork rather than the more thorough one.

VERIFIED THE EXEMPTION IS LOAD-BEARING: removing it fails exactly that assertion and
leaves the other 30 passing, so the pass is the classification and not a coincidence.
Restore byte-identical by sha256.

31 passed; ruff format and check clean.

WORTH RECORDING: this is option B earning itself. Had 458 merged at its older armed
head, the six older claims would have released against a main that did not carry
#1005 -- and these eight commits would have hit this guard in a follow-up PR instead.
The bigger diff surfaced the gap BEFORE the merge.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 22, 2026 22:38
@wshallwshall
wshallwshall merged commit 3c5cb98 into main Aug 22, 2026
39 of 40 checks passed
@wshallwshall
wshallwshall deleted the claude/builder-1-160cec branch August 22, 2026 22:57
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