Skip to content

fix: two PowerShell unroll defects -- install-gate -Status crash, and a phantom overlap occupant - #447

Merged
wshallwshall merged 2 commits into
mainfrom
claude/cool-tharp-e843f4
Aug 19, 2026
Merged

fix: two PowerShell unroll defects -- install-gate -Status crash, and a phantom overlap occupant#447
wshallwshall merged 2 commits into
mainfrom
claude/cool-tharp-e843f4

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Two unrelated instances of one PowerShell defect, in separate commits so either can be dropped alone.

1. install-gate.ps1 -Status crashed (the brief)

Get-WiredMatchers built a HashSet and ended with a bare return. PowerShell unrolls an enumerable return value into the pipeline, so no caller ever received a set:

matchers actually returned consequence
0 $null .Contains() at the wire-set scan throws; -Status exits 1
1 [String] .Contains() becomes a SUBSTRING test, silently
2 or more [Object[]] membership, which is why this went unnoticed

The one-element arm is why this is a fix and not a crash report. Measured against the shipped rule set, with a config dir wiring exactly one matcher NotebookEdit:

OLD  exit 0   UNWIRED: Agent, Bash, MultiEdit, PowerShell, Task, Workflow, Write
NEW  exit 0   UNWIRED: Agent, Bash, Edit, MultiEdit, PowerShell, Task, Workflow, Write

Edit is a strict substring of NotebookEdit, so it was swallowed. Exit code 0 both times: a clean, confident wrong answer from the audit whose only job is to report which rules never fire.

The empty arm also broke the independent audit below it, which tests $null to mean "Read-Settings threw" and prints settings.json is not valid JSON. Zero matchers returned $null too, so a readable, simply-unwired directory was reported as unparseable:

OLD  UNREADABLE: .claude-backup  <- settings.json is not valid JSON
NEW  not judged: .claude-backup (not a launcher name) -- carries no gate wiring

That branch is correct again as a consequence of the fix, and now says so in a comment.

Not a CI regression. A hosted runner has no ~/.claude, so $ConfigDir resolves empty and the loop never runs. It fires on developer machines, hardest on ones carrying a second .claude-account-* directory.

2. overlap.ps1 printed a phantom occupant (found while sweeping)

Same class, but it survives to disk. Build-Map returns an empty array, PowerShell unrolls it to AutomationNull, and in process that is harmless. It does not survive serialisation: ConvertTo-Json writes "rows": null, the next run reads it back with @($c.rows), and that is a one-element array holding $null. Count is 1, the zero-rows guard does not fire, and the report prints a blank worktree on a blank branch with 0 changed files.

collision_gate.ps1 runs overlap.ps1 -Json on every gated edit and the cache is written before the -File early exit, so the hot path arms it constantly and any bare overlap.ps1 inside the cache window inherits it. An invented collision is not a safe direction to fail in: it is the answer people work around, and a report that cries wolf on a quiet repo is one they stop reading.

Tests

Four new tests, three for the install-gate arms and one for the cache round trip. Each was verified RED on the unfixed script for its own reason rather than incidentally: the empty arm by the null-method crash, the substring arm by content on an exit-0 run, the audit arm by the false UNREADABLE line.

The install-gate tests plant a copy of worktree_gate.ps1 in the synthetic home. Without it $GateDst does not resolve, the implemented-rule set is empty, and every assertion about UNWIRED passes vacuously. Both runs report against 10 implemented rule(s), which is what makes the fixture non-vacuous.

Red/green was proved against a scratch copy of the script rather than by stashing the live file, so no concurrent reader could observe a tree with the fix missing.

63 tests pass across the gate, worktree and coord files. ruff format, ruff check and mypy clean.

Generated with Claude Code

…tops crashing

Get-WiredMatchers built a HashSet and ended with a bare return. PowerShell unrolls an
enumerable return value into the pipeline, so no caller ever received a set:

  0 matchers -> $null       line 302 calls .Contains() on it and the run dies
  1 matcher  -> [String]    .Contains() becomes a SUBSTRING test, silently
  2 or more  -> [Object[]]  membership, which is why nobody noticed for so long

The one-element arm is the dangerous one and it is the reason this is a fix rather than
a crash report. Measured against the shipped rule set: a config dir wiring exactly one
matcher NotebookEdit made String.Contains("Edit") return true, so Edit was treated as
wired and omitted from the UNWIRED line. Exit code 0, a clean and confident wrong answer,
from the audit whose entire job is to say which rules never fire.

The empty arm also broke the independent audit below, which tests $null to mean
"Read-Settings threw" and prints "settings.json is not valid JSON". Zero matchers
returned $null too, so a readable, simply-unwired directory was reported as unparseable.
That branch is correct again as a consequence of this fix, and now says so.

Not a CI regression: a hosted runner has no ~/.claude, so $ConfigDir resolves empty and
the loop never runs. It fires on developer machines, and hardest on ones carrying a
second .claude-account-* directory.

Three tests, one per arm, each verified to fail on the unfixed script for its own reason
rather than incidentally. They plant a copy of worktree_gate.ps1 in the synthetic home:
without it $GateDst does not resolve, the implemented-rule set is empty, and every
assertion about UNWIRED passes vacuously.
@wshallwshall
wshallwshall force-pushed the claude/cool-tharp-e843f4 branch from 81de669 to 4ccbf81 Compare August 19, 2026 12:58
wshallwshall added a commit that referenced this pull request Aug 19, 2026
…nd a latent NULL unroll (#449)

* backlog: file #1290 -- the connscale CPU-probe walk times out on hosted Windows runners

A required status check is intermittently red on main. CI run 32255231838 at
de896e0 failed `test (windows-2022, py3.14)` on
test_subtree_re_resolution_picks_up_a_late_spawned_child: the process-table
walk returned None on all 6 attempts in 30s.

The identical leg passed on PR #448 in the same hour on the same runner image,
and neither branch touches the probe, so the nondeterminism is measured rather
than assumed.

The assertion that fires is the one the test's author separated out as an
environment problem, so the re-resolution assertion below it never runs -- the
test is silent on the defect it exists to catch exactly when this fires.

Filed distinct from #1014 (a fixed port-block collision in the smoke test) and
#1210 arm 2 (FD/RSS provenance in the same probe). parse_items before/after:
306/226 open -> 307/227 open, the expected +1/+1/0 for filing one item.

* backlog: file #1291 -- Get-HandledTools unrolls to NULL on two paths

Measured in pwsh 7 via AST extraction: missing-file and zero-tool both return
NULL; one tool returns a bare String, so .Contains() answers True on a
substring of it.

Filed as a LATENT trap, not a live defect. The single caller wraps in @(...),
so shipped behaviour is correct today and a present-tense bug claim would be
false. Recorded that way deliberately.

Two details that decide how it gets fixed and tested: the missing-file early
return unrolls the same as the tail, and that path is the normal one on a box
where the gate was never installed; and a test through -Status cannot
discriminate fixed from unfixed because the caller re-wraps, so the test must
reach the function by AST extraction -- install-gate.ps1 cannot be dot-sourced
without performing a machine-global install.

parse_items 307/227 open -> 308/228 open, the expected +1/+1/0.

* backlog: record #1290's same-head rerun confirmation

Re-running the failed job alone on PR #447 head 4ccbf81, with no code change,
returned completed/success. Identical commit, identical leg, opposite outcome,
which settles nondeterminism more directly than the cross-branch pair the item
was filed on.

Bounded deliberately: it establishes that the failure is not a property of the
tree under test, and establishes nothing about frequency or the runner load
that triggers it, which is what sizing a fix actually needs.

parse_items unchanged at 308/228 open, the expected 0/0/0 for amending a body.

* backlog: file #1292 -- connscale smoke reports an acked message unobserved at intake

The assertion names a CONFIRMED send that engine_read did not see. The harness
already excludes the benign case in the same message (a send with no ACK before
close is explicitly not counted as loss), so the remaining branch is either a
harness reconciliation race or a count-and-log invariant failure.

Filed because nobody has separated those two, and they have very different
severities. Calling it a flake picks the comfortable branch without evidence.

Red on main (run 32255231838, windows-2022) and on PR 448's rebased head (run
32259244062, ubuntu-latest), with different record shapes -- two platforms, two
branches, which is evidence against a single fixed off-by-one. It already
carries flaky(reruns=2) and failed anyway, so it exhausted its retries.

Kept distinct from #1014 (port-block collision, same file) and #1290 (CPU-probe
walk timeout, same suite). Two of the three sit inside required contexts, which
is why the queue keeps stalling.

Severity written in the conditional per CLAUDE.md section 0: zero instances, so
nothing is affected today. P1 rests on the branch not yet ruled out.

parse_items 308/228 -> 309/229, the expected +1/+1/0.

* backlog: file #1293 -- worktree removal orphans unlanded ledger numbers

owns() (ledger_check.py:219-231) compares the current worktree path to the one
recorded at allocation, with no fallback when that directory is gone. Ownership
is non-transferable, so an orphaned number can never be satisfied again.

PR #397 is stranded by this now: #1264 was allocated by a worktree that no
longer exists, is not on main, and #397's only conflict is docs/BACKLOG.md.
Resolving re-introduces a heading absent from main, which counts as an addition,
so ownership is consulted and refuses -- for everyone, permanently.

The gate is failing closed and is correct. The defect is that there is no route
back, and that the refusal only arrives after the resolution work is done.

Records three candidate routes without picking one, and notes the fix must not
be "widen the gate" -- the --ci leg already skips ownership by design.

Also records the operational sweep that falls out: before removing a worktree,
check whether it owns an allocation not yet on main. Run against the 16
worktrees proposed for removal today; returned zero, with #1264 as the positive
control proving the sweep can detect the class.

The first draft named a worktree slug and the forbidden-content gate refused it.
Rewritten generically rather than allowlisted, per the rule that a refused slug
is the gate working.
@wshallwshall
wshallwshall merged commit 4cac401 into main Aug 19, 2026
38 checks passed
@wshallwshall
wshallwshall deleted the claude/cool-tharp-e843f4 branch August 19, 2026 14:35
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