diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a50001ee..17e56d88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,7 +194,12 @@ jobs: # not start until 397 s (6 m 37 s) into the step, leaving 83 s # of the 480 s for the whole suite; the kill landed 96 s after # the first test started. How many tests the current tree has is - # not measured -- the run never reached a total. + # not measured -- the run never reached a total. That head is + # not on develop: the PR was rebased on merge, and the commit + # carrying this same change there is `a774ac4d`. Both are named + # because the run really did run on `9c8d3673`; + # `git show | git patch-id --stable` prints + # ad968515ab03ec2249ff050a284e1b90f3fe5ba0 for either one. # # The two compile figures are NOT a toolchain comparison and must # not be read as one. 28,721 lines of test code and 17,144 lines of @@ -206,13 +211,70 @@ jobs: # Note which way that cuts on the build half: Sources grew by those # 17,144 lines and the Build step still came back 240.40s -> 230.46s. # - # 16 minutes (960 s) is 2.04x of 392 s + 78 s = 470 s -- this - # image's measured compile plus the OTHER image's measured run, - # because this image's run figure does not exist yet: the step was - # killed before the suite finished. The first green run on this - # image replaces that addend and this number with its own - # measurement. - timeout-minutes: 16 + # The promise above -- that the first green run on this image + # replaces that addend and this number with its own measurement -- + # is kept here, and the replacement does not keep the number. + # Withdrawn, quoted verbatim: "16 minutes (960 s) is 2.04x of + # 392 s + 78 s = 470 s -- this image's measured compile plus the + # OTHER image's measured run, because this image's run figure does + # not exist yet: the step was killed before the suite finished." + # What was wrong with it is not the borrowed addend but the + # stitching: it adds two figures from two different runs and bounds + # a step that neither of them is. A step budget bounds a step's + # wall clock, and that is a field pair of the Actions API, present + # on every run that reached this step: + # + # gh api repos/NoiXdev/macSCP/actions/runs//jobs --jq '.jobs[].steps[] | select(.name=="Unit-Tests") | "\(.conclusion) \((.completed_at|fromdateiso8601) - (.started_at|fromdateiso8601))s"' + # + # 38225383495 macos-15 329 s success + # 40933631584 macos-latest 493 s failure -- the 480 s budget's + # own kill plus the overhead of + # reporting it, not a suite duration + # 41045404989 macos-latest 581 s success (that RUN went red + # later, on the warning gate below; + # this step did not) + # 41047833837 macos-latest 517 s success + # 41048445836 macos-latest 640 s success + # + # 640 s is the measured maximum of a step that finished. The spread + # it sits in matters more than the maximum: 517 -> 640 s is a + # factor of 1.24 across two runs ONE commit apart on the same + # image, and the compile half alone moved 345.71s -> 492.86s, a + # factor of 1.43. That pair isolates the variance about as well as + # anything here can: the only difference between the two trees is + # 23 added lines of CLAUDE.md, which no target compiles. The runs + # had heads `b9ba9d27` and `6f1fe42a`, and the rebase on merge left + # both unreachable, so the command below names the develop commits + # carrying those same two changes instead -- measured 2026-10-31, + # it prints one file and 23 insertions for either pair: + # + # git diff --stat 6583396e 1ce3e9ff + # + # A budget has to clear that variance, not the mean. + # + # 20 minutes (1200 s) is 1.88x of 640 s. The two derivations this + # comment has carried landed at 2.09x and 2.04x, so this is the + # first one under 2 -- but neither of them states 2x as a + # requirement, so "the 2x floor" is a reading of them rather than a + # quote, and it is written here as a reading. 2x of 640 s is + # 1280 s, which makes 22 minutes (1320 s, 2.06x) the smallest + # whole-minute budget holding that reading. 20 is the maintainer's + # decision of 2026-10-31, taken while the best figure on offer was + # the 558.79 s the withdrawn stitching produced -- against which + # 1200 s read as 2.15x. The series above was measured after that + # decision, not before it, and 1.88x is what it leaves standing. + # + # Raising this does not crowd the JOB budget above it. Same API, + # one level up -- measured 2026-10-31: + # + # gh api repos/NoiXdev/macSCP/actions/runs//jobs --jq '.jobs[] | "\(.conclusion) \((.completed_at|fromdateiso8601) - (.started_at|fromdateiso8601))s"' + # + # gives 753 s and 937 s for the two green runs above, against the + # job's 35 minutes (2100 s). A step that ran the full 1200 s would + # put the worst of those at 937 - 640 + 1200 = 1497 s, 0.71x of the + # job budget, so the job still kills a hung step later than this + # step does -- which is the ordering the step budget exists for. + timeout-minutes: 20 run: | set -o pipefail swift test --event-stream-output-path "$RUNNER_TEMP/test-events.jsonl" \ diff --git a/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift b/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift index 196e55f5..a05f27b7 100644 --- a/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift +++ b/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift @@ -257,7 +257,10 @@ struct ConnectMainActorLivenessTests { /// wording and the withdrawn condition. `bar` is derived FROM `ambient`, /// so that test reduced to `ambient > 4 s`, and it let a control of one /// second stand as adequate resolution for a three-second ceiling. - /// Run 41040277938 (2026-10-30, `c8041d19`) is where that broke: test 2 + /// Run 41040277938 (2026-10-30, `c8041d19` -- the head that run + /// had; the PR was rebased on merge, so the same change sits on + /// develop as `bcd0bc61`, which `git patch-id --stable` gives the + /// same id for) is where that broke: test 2 /// printed `ambient=1.004113042 seconds gap=21.006908125 seconds /// ceiling=3.012339126 seconds` and FAILED on the gap. Nothing was /// blocking — the run's other 7974 tests passed. One sample of 1 s does diff --git a/Tests/macSCPCoreTests/ConnectionDiagnosticsJumpTests.swift b/Tests/macSCPCoreTests/ConnectionDiagnosticsJumpTests.swift index 9cb2593a..ee3027c6 100644 --- a/Tests/macSCPCoreTests/ConnectionDiagnosticsJumpTests.swift +++ b/Tests/macSCPCoreTests/ConnectionDiagnosticsJumpTests.swift @@ -22,9 +22,23 @@ import Testing /// **Hang bound, not a ceiling** (2026-09-28). Every case here races steps /// against a budget through `DetachedProbe`, whose deadline is armed at /// CREATION and so bounds how long a body waited for a thread as well as how -/// long it ran. `.timeLimit` is the net for a case that truly hangs, and it -/// is deliberately far larger than any step budget below, so no assertion in -/// this suite depends on it. +/// long it ran. `.timeLimit` is the net for a case that truly hangs, and no +/// assertion in this suite depends on it. +/// +/// Until 2026-11-01 that sentence read "`.timeLimit` is the net for a case +/// that truly hangs, and it is deliberately far larger than any step budget +/// below, so no assertion in this suite depends on it". The withdrawn clause +/// was false of this file on the day it was written: the two roomy cases +/// below were already at `.seconds(600)` against this suite's 300 s bound, +/// `61850a9c` having set BOTH nine days earlier: `git blame` dates the +/// `stepTimeout:` line inside `theJumpsResolveNamesTheJumpsAddresses` and +/// the one inside `theJumpsSecretNeverReachesTheReport` to that commit, and +/// the "Hang bound, not a ceiling" heading above to `ae19a53a`. Named rather +/// than numbered +/// because the first draft of this sentence cited two line numbers and the +/// four lines it added moved both of them (CLAUDE.md, "prefer the symbol to +/// the line number"). What the sentence was about survives without the +/// withdrawn clause. /// /// **Every case here takes that bound from the suite** (2026-10-05). Four /// cases used to carry `@Test(.timeLimit(.minutes(1)))` of their own, diff --git a/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift b/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift index 46d36ea8..5545d0ef 100644 --- a/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift +++ b/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift @@ -26,8 +26,18 @@ import Testing /// how long the body waited for a thread as well as how long it ran. On a /// three-core CI runner a body that computes nothing can miss a small budget /// entirely, and the step then settles as `notStarted`. `.timeLimit` is the -/// net for a case that truly hangs; it is deliberately far larger than any -/// step budget below, so no assertion in this suite depends on it. +/// net for a case that truly hangs, and no assertion in this suite depends +/// on it. +/// +/// Until 2026-11-01 that sentence continued "it is deliberately far larger +/// than any step budget below, so no assertion in this suite depends on it", +/// and the clause quoted here is withdrawn: `roomy` is 600 s against this +/// suite's 300 s bound, so the budget is the larger of the two. The ordering +/// was never what made the claim true. A case asserting a dial's outcome +/// wants a budget its own deadline cannot win — see `roomy` — and a case +/// asserting the deadline sets a tight budget of its own and says so. Either +/// way the assertion is on the outcome, not on a clock, which is the +/// property the sentence was about. @Suite("ConnectionDiagnostics", .timeLimit(.minutes(5))) struct ConnectionDiagnosticsTests { /// The secret the SSH dial cases hand the runner. Named rather than @@ -70,6 +80,43 @@ struct ConnectionDiagnosticsTests { /// the source text of what it checks. private static let slashBearingSecret = "wJalrDiagnostics/TestSecret/Value" + /// The step budget for a case whose claim is the OUTCOME of a dial, not + /// how long the dial took to report it. Roomy for the reason the suite + /// note above gives: `DetachedProbe` arms its deadline at CREATION, so a + /// small budget bounds how long a body waited for a thread as much as how + /// long it ran, and on a starved runner the deadline settles the step + /// before the body says what it met. + /// + /// 600 s, the figure `ResolveNamesTests.roomy` and the two roomy cases in + /// `ConnectionDiagnosticsJumpTests` already carry. One of those two, + /// `theJumpsSecretNeverReachesTheReport`, was moved off `.seconds(10)` + /// for exactly this failure on 2026-09-19 by `61850a9c`, whose diff is + /// one file, 10 insertions and 2 deletions — the other roomy case came + /// off `.seconds(30)` in the same commit — and which left the five + /// literals here standing. They fell 42 days later: CI run 41244948849 + /// (2026-10-31, `e4e5d054`) read `.timedOut` in + /// `theSSHDialNeverPutsTheSecretInTheReport`, which fails that case's + /// anchor for the wrong reason. That run's own figures say why 10 s could + /// not hold — this command prints `8184 8307`: + /// + /// gh run view 41244948849 --log-failed | grep -o 'ed after [0-9]*\.[0-9]* seconds' | sed -E 's/ed after ([0-9]+)\..*/\1/' | awk '{n++; if ($1>=10) k++} END {print k, n}' + /// + /// 8184 of the 8307 cases carrying a duration line reported a wall clock + /// AT OR ABOVE the budget those five handed the runner — the pipeline's + /// comparison is `>= 10` on the integer seconds — against a run total of + /// 116.701 s; the case that went red reported 64.975 s itself. A budget + /// below almost every case's wall clock does not bound the step's work, + /// only whether its body was given a thread in time. + /// + /// A roomy budget costs these five nothing, which is what makes it safe + /// here and unmeasured elsewhere: every target they use either answers + /// at once (the rig) or refuses at once (a closed loopback port, and the + /// skipped case never dials). None of them can park on this budget, so + /// raising it cannot lengthen a run. The same is NOT established for the + /// echo and trace cases, which send real probes — `docs/BACKLOG.md`, + /// "Wall-clock ceilings still in the tree", eighth shape. + private static let roomy: Duration = .seconds(600) + // MARK: - Resolve @Test func resolveFindsALoopbackAddressForLocalhost() async { @@ -1074,7 +1121,7 @@ struct ConnectionDiagnosticsTests { descriptor: .descriptor(for: .ssh), values: values, secrets: FixedSecretSource(value: secret), sessionID: UUID(), jump: nil, throughput: DiagnosticThroughputSettings(), - internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: .seconds(10) + internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: Self.roomy ).run() let dial = try #require(report.steps.first { $0.id == DiagnosticStepID.dial }) @@ -1101,7 +1148,7 @@ struct ConnectionDiagnosticsTests { let report = await ConnectionDiagnostics( descriptor: .descriptor(for: .ssh), values: values, secrets: nil, jump: nil, throughput: DiagnosticThroughputSettings(), - internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: .seconds(10) + internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: Self.roomy ).run() let dial = try #require(report.steps.first { $0.id == DiagnosticStepID.dial }) @@ -1341,7 +1388,7 @@ struct ConnectionDiagnosticsTests { let report = await ConnectionDiagnostics( descriptor: .descriptor(for: .s3), values: values, secrets: nil, jump: nil, throughput: DiagnosticThroughputSettings(), - internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: .seconds(10) + internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: Self.roomy ).run() let dial = try #require(report.steps.first { $0.id == DiagnosticStepID.dial }) @@ -1565,7 +1612,7 @@ struct ConnectionDiagnosticsTests { let report = await ConnectionDiagnostics( descriptor: .descriptor(for: .s3), values: values, secrets: nil, jump: nil, throughput: DiagnosticThroughputSettings(), - internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: .seconds(10) + internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: Self.roomy ).run() let dial = try #require(report.steps.first { $0.id == DiagnosticStepID.dial }) @@ -1584,7 +1631,7 @@ struct ConnectionDiagnosticsTests { let report = await ConnectionDiagnostics( descriptor: .descriptor(for: .webdav), values: values, secrets: nil, jump: nil, throughput: DiagnosticThroughputSettings(), - internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: .seconds(10) + internetSpeed: DiagnosticInternetSpeedSettings(service: .off), stepTimeout: Self.roomy ).run() let dial = try #require(report.steps.first { $0.id == DiagnosticStepID.dial }) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 61eb749b..26c27385 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -17,7 +17,7 @@ changes it in the entry and at most updates the line here to match. | BUG: a second session through the same jump does not open, the UI returns to the session info, and the first connection drops | **Open, recorded 2026-09-18** from the maintainer's report: after connecting through a jump, opening a second session over the same jump does not open it — the UI jumps back to the session info, and the first connection drops. Two halves, kept separate because only one was measured. **Read, with a scratch test confirming the predicate:** the "back to the session info" half has a cause. Every sidebar connect selects the row first, so the unconnected-tab branch in `ContentView+Detail.swift` runs; the session-info branch (`SessionOverviewView`, `:847`, `overviewSession(for: tab)` with `mode == .new`) is chosen before `ConnectionFormView` (`:876`), and the host-key card is rendered only inside the form (`ConnectionFormView.swift:383-384`). A jump session hits this first because the second session's target behind the jump is new, so its host key is unknown and a prompt is raised — which the overview then covers, along with any `.needsPerson` failure or pre-dial refusal text. A scratch test (SCRATCH-4) confirmed the predicate: `overviewSession(for:) != nil` while the host-key prompt is pending, so the overview branch wins regardless. **A 1.5.0 user's report, 2026-10-01, widens this and closes half of it.** The maintainer relayed a report from a 1.5.0 user: a session COPIED from another and edited, so a new host; the diagnostic log ends `connect start … kind=ssh` / `connect auth method=agent` / **`connect hostkey unknown`**; the GUI then jumps to the host overview and freezes hard enough to need the task manager. **No jump host is involved.** That matters for this row's own reasoning, which said a jump session "hits this first" because the target behind it is new — the jump was never necessary. An unknown host key on any session the overview is willing to show is enough, and copying an entry is the ordinary way a user gets one. The freeze is the same cause seen from the user's side: the dial waits for a TOFU decision whose card is on a surface the overview covered, so nothing on screen can answer it and nothing times out. **Fixed on `develop`, and the line is nameable**: `Sources/MacSCPAppKit/DetailSurfacePlan.swift:93-95` now reads `guard let overviewSession, formMode == .new, !hostKeyPromptPending else { return .form }` — the overview is chosen only when no prompt is pending. `ConnectionSurfacePlan` carries the same first guard (`guard !hostKeyPromptPending else { return .form }`), and its doc comment states the reason: TOFU's trust card lives inside `ConnectionFormView`, so a surface that hid the form would hide the one thing a first connection to an unknown host needs answered. **Pinned**, over all five liveness states, by `Tests/macSCPAppKitTests/DetailSurfacePlanTests.swift:82`, `aPendingHostKeyPromptIsNeverCoveredByTheOverview(liveness:)`. The maintainer independently observed it fixed in the 2026-09-28 dev build, which is the same code. **So the “back to the session info” half is closed — but not shipped**: v1.5.0 is the latest release and `develop` is several hundred commits ahead of `main`, so every user on 1.5.0 still meets it. That is a release question, not a code one. The “first connection drops” half below is untouched by this. **Measured, not reproduced:** the "first connection drops" half. 6 of 6 live-rig scenarios (Docker, password auth on both hops — the same session twice through one jump, two different sessions through one jump, the jump host directly then a session through it and the reverse, the same session twice with concurrent dials, two different jumps at once) left the first connection intact, and 4 of 4 App tests with fakes (including the real `connectFromSidebar` path against a jump that refuses at once) left tab 1 connected with 0 disconnects. No shared per-jump state was found by reading: each dial builds its own jump `SSHClient` (`CitadelFileSystem.swift:459-513`), `disconnect()` closes only its own client and jump client (`:1387-1391`), and nothing in Core or the App is keyed by the jump host or session. **Two open questions for the maintainer:** after the event, is the first tab still in the tab strip, and what does it show (the browser, the lost surface, or the overview)? And which login does the jump use — password, private key, or agent? **The "back to the session info" half: done 2026-09-18** (`docs/superpowers/plans/2026-09-18-jump-and-groups.md`, Task 1, `11ab0923`, `64f66a8e`). The unconnected-tab branch was extracted into one pure `DetailSurfacePlan.surface(…)`, which the view now switches on at one decision point; the overview is held back while a host-key prompt is pending, while a `.needsPerson` failure (or a pre-dial refusal) is unread, and while `formMode != .new`. A `ConnectionViewModel.unacknowledgedFailure` marker was added so the form also raises that failure's text on mount, not only on a later transition — round 0 had left the reject's text reaching the form only sometimes. **"The first connection drops": measured, not reproduced** (Task 2, `0aa7c977`, `96ebc52d`). A gated matrix, `CitadelFileSystemIntegrationTests.twoLiveConnectionsThroughOneJumpStayIndependent`, ran all 6 pairings (the same session twice; two sessions through one jump; the jump host directly then through it, and the reverse; the same session twice dialled concurrently; two different jumps at once) × 3 jump-hop logins (password, a runtime-generated private key, and agent) = 18 cases, each proving connection 1, opening and proving connection 2, re-proving connection 1 beside and after connection 2 closes. 18 of 18 green across 6 full runs; two planted defects (a mid-run disconnect of connection 1; a process-wide registry that closes a shared jump's earlier client) were each caught as a red run before being reverted, so the matrix is sensitive, not merely quiet. This row stays **open** for "the first connection drops"; the two questions above are still unanswered. **Maintainer answers 2026-09-19 (in chat):** after the event the first tab was still in the strip and showed the lost-connection surface ("Verbindung verloren"), and the jump logs in through a **login set**. So the first connection really dropped — the half recorded above as not reproduced is real, and the one variable none of the 18 gated cases (password, key, agent on the jump; `0aa7c977`, `96ebc52d`) covered is a login-set jump. Next: reproduce with a login-set jump on the rig, then fix. | | [Liveness assertion rebuild](superpowers/specs/2026-08-08-testsuite-hang-investigation.md) | **Done 2026-09-03** (`9d00aa90`, `0f9ebd2d`, `0b43601d`). The tally closed at 17 of 20 full gated runs red under the old fixed ceiling; the rebuilt ceiling is `min(max(ambient * 3, .seconds(2)), .seconds(4))` against dials of at least 5 s, planted probe 10/10 red both alone and under a loaded full gated run, unplanted 10/10 green alone and 10/10 green (liveness suite) in `MACSCP_ITEST=1 swift test`, and a green run now prints its ambient/gap/ceiling. Open: the frozen-peer hang reproduced during the measurement, cause still unmeasured; the false-red regime above the 4 s cap is reachable (ambient 5.726 s observed under a planted run) though no unplanted gap above 4 s has been seen yet. | | Diagnostics: error text as a leak route | **Measured 2026-09-03** (Task 5 re-review of the connection-tools plan): `DialSupport.reason(for:)` renders `RemoteFSError` through `String(describing:)`, so any backend error that carries configuration text (an endpoint string with userinfo, a path) can reach a diagnosis row; `URLText.withoutUserinfo` is a backstop that a `/` in the password defeats. The plan pinned the two paths its own rows added. **Done 2026-09-04** (the diagnostics-leak-route plan, `bd6ec81f` + this row's own commit): `DialSupport.reason(for:)`'s `RemoteFSError` arm is now an exhaustive `switch`, no `default`, one fixed English sentence per case — 10 cases (`connectionFailed`, `authenticationFailed`, `jumpAuthenticationFailed`, `notFound`, `permissionDenied`, `protocolError`, `bucketListForbidden`, `bucketListEmpty`, `bucketLevelRefused`, `crossBucketRenameRefused`), plus the 7 `BucketLevelOperation` cases (`write`, `delete`, `createDirectory`, `deleteTree`, `rename`, `presignedURL`, `readStream`) covered through a parameterised iteration over `allCases`. Paths are kept (a path is this project's own string, the finding itself); the two free-text payloads (`connectionFailed(reason:)`, `protocolError(reason:)`) and the cross-bucket rename's two paths are dropped. Red observed before the fix, on a planted `RemoteFSError.connectionFailed(reason:)` carrying `s3://KEY:SECRET@host` through the dial-contribution seam: the plain shape (no `/` in the secret) was already stripped by `DiagnosticStep.init`'s `URLText.withoutUserinfo` backstop — only "one fixed sentence per case" was red on that shape; the slash-bearing shape (a secret with two `/` characters, the shape of a real S3 secret key) was red on all four expectations, the credential reaching the step's outcome, the plain-text report and the Markdown report, because the backstop's authority scan ends at the `/` before it finds an `@` to cut at. **The count of endpoint-bearing S3 sites was wrong twice, in two pushed commit bodies, and this row is the record.** `bd6ec81f`'s body said "four" `S3FileSystem` sites throwing `"Invalid S3 endpoint: "`; `cb90f9b1`'s body corrected that to three and then miscounted in its own way, presenting `:1062` as "a fourth site" carrying the endpoint under a different message — as if it were the only other one. Neither commit can be amended (both are pushed), so the measurement below is the one to read. Re-derived 2026-10-05 at `3c565be9`: no site interpolates the endpoint any more — each throws one of the four `S3EndpointReason` constants (`Sources/macSCPCore/S3/S3RequestSigning.swift`), `unparseable`, `noHostForBucket`, `requestURLUnbuildable` and `noHost` — so the hand-written enumeration is replaced by the commands that find the sites. `grep -n 'S3EndpointReason\.' Sources/macSCPCore/S3/S3FileSystem.swift` printed **9** lines on 2026-10-05. Per constant, same file, same date: `grep -n 'S3EndpointReason\.unparseable' Sources/macSCPCore/S3/S3FileSystem.swift` 3 lines, `grep -n 'S3EndpointReason\.noHostForBucket' Sources/macSCPCore/S3/S3FileSystem.swift` 2, `grep -n 'S3EndpointReason\.requestURLUnbuildable' Sources/macSCPCore/S3/S3FileSystem.swift` 3, and `grep -n 'S3EndpointReason\.noHost)' Sources/macSCPCore/S3/S3FileSystem.swift` 1 — the closing paren is load-bearing, because without it (`grep -n 'S3EndpointReason\.noHost' Sources/macSCPCore/S3/S3FileSystem.swift`) the pattern also matches `noHostForBucket` and comes back 3. One site sits outside that file: `grep -n 'S3EndpointReason\.' Sources/macSCPCore/S3/S3RequestSigning.swift` prints 1 line, the signer's own `noHost` that `bd6ec81f` named as the signer's — for **10 in the S3 backend**. The eleventh match of that pattern is a doc comment, not a throw site, and is named rather than filtered away: `grep -n 'S3EndpointReason\.' Sources/macSCPCore/S3/S3FileSystem.swift Sources/macSCPCore/S3/S3RequestSigning.swift Sources/macSCPCore/S3/S3FieldSchema.swift` prints 11 lines on 2026-10-05 — the ten sites plus one mention in `S3FieldSchema.swift`'s comments; the enum's own `static let` lines match none of these patterns, having no `.` after the type name — `grep -n 'static let' Sources/macSCPCore/S3/S3RequestSigning.swift` prints 4 lines on 2026-10-05, file-wide, all four of them the enum's. **Corrected 2026-10-05**, re-derived at `3c565be9`: the enumeration this replaces read “Recounted by hand at HEAD `6e16b677`, every line verified, `\(config.endpoint)` interpolated into a `RemoteFSError.connectionFailed(reason:)`: `"Failed to build S3 request URL for endpoint"` at `S3FileSystem.swift:736`, `:1062`, `:1171` (3); `"Invalid S3 endpoint host"` at `S3FileSystem.swift:1052`, `:1162` (2); `"S3 endpoint has no host"` at `S3FileSystem.swift:1215` (1); `"Invalid S3 endpoint"` at `S3FileSystem.swift:730`, `:1045`, `:1156` (3). Total **9 sites in `S3FileSystem.swift`**, and one more outside it — `S3RequestSigning.swift:32`, the second `"S3 endpoint has no host"`, which `bd6ec81f` named as the signer's — for **10 in the S3 backend**.” Both of its halves were superseded. The line numbers were derived at a head this row no longer describes: they had already been re-measured once, on 2026-09-28, by the row below that records this staleness, and seven of those nine values moved again by exactly four lines before 2026-10-05 — which is why a command replaces them and not a fresh list. And the four English sentences, quoted by hand, are now the constants above: three of the four values gained a clause the quote does not carry (`unparseable`, `noHostForBucket`, `requestURLUnbuildable`), while `noHost`'s value is still the quoted sentence exactly. Nothing the old enumeration asserted is dropped — its per-message counts (3, 2, 1, 3) and its totals, 9 and 10, all reproduce above; it is named here so the earlier measurement stays findable. The sentence just before the measurement was corrected with it: it pointed at “the enumeration below” and now points at “the measurement below”, what follows no longer being an enumeration. This marker sits mid-cell because the measurement it replaces sat here and a correction belongs beside what it corrects — and so does one other, which makes this row one of a pair rather than an exception. Counted 2026-10-05 by reading all six corrected rows instead of assuming: the note closes the cell on four of the other five — "The S3 delete lookup pages the whole parent listing", "A key that is both an object and a prefix", "Raw error text can still reach the CLI and the browser banner" and "S3: the tools of s3Manager (maintainer wishlist)" — while the fifth, "S3 `delete(path:)` on a directory is a silent no-op", carries its note mid-cell for exactly this reason, the sentence it corrects being that cell's SECOND sentence with the rest of the row after it. **Corrected 2026-10-05**, by the final fix wave of the same plan: that last clause read “the citation it corrects sitting in that cell's opening sentence with the rest of the row after it.”, and “opening sentence” is withdrawn. It was a locational claim written without checking, inside the very paragraph that exists because the previous version of this same sentence was an enumeration written without counting — the same failure one layer down, which is why it is corrected here rather than quietly. Measured with a command that runs from inside a table cell as committed (no pipe; `LC_ALL=C` so `length` and `index` count bytes; the row matched by a `^`-anchored regex, so the cell you are reading cannot be in its own answer): `LC_ALL=C awk '/^. S3 .delete\(path:\). on a directory is a silent no-op/ { s=index($0,"**Measured 2026-09-04**"); c=substr($0,s,length($0)-s-1); printf "cell-bytes=%d opening-sentence-ends=%d corrected-sentence-starts=%d\n", length(c), index(c,"other operation.")+15, index(c,"`S3FileSystem.delete(path:)`") }' docs/BACKLOG.md` printed `cell-bytes=3494 opening-sentence-ends=154 corrected-sentence-starts=156` on 2026-10-05, re-run after this clause was written and unchanged by it — the corrected sentence begins two bytes past the end of the opening one, so it is the second and not the first. The opening sentence is that row's dated lead, “**Measured 2026-09-04** (Task 2 of the CLI test matrix, probe R5) — sibling of the `deleteTree`-on-a-file row above: same contract, the other operation.”, and the citation the note withdraws stood in the sentence after it. The five are named rather than counted so the next reader lands on the same set and can check each in one look. (The "Invalid S3 endpoint host" lines also match a bare `grep` for "Invalid S3 endpoint", which is how a count of that message drifts; they are counted once, under their own message.) `reason(for:)` has 8 callers at HEAD, recounted in the same pass (the plan's own spec said 5; the extra 3 are the S3/WebDAV probes, which the fix reaches too): `Sources/macSCPCore/Diagnostics/ContributionProbes.swift:51, :97`, `Sources/macSCPCore/Diagnostics/DialProbes.swift:71, :80, :300` (the last unqualified, called from inside `DialSupport` itself), `Sources/macSCPCore/S3/S3AccessProbe.swift:149`, `Sources/macSCPCore/WebDAV/WebDAVClaimsProbe.swift:63, :83`. The guard (`Tests/macSCPCoreTests/DiagnosticsNoDescribingGuardTests.swift`, 8 `@Test`s after the final fix round, 5 before it) scans every `.swift` file under `Sources/macSCPCore/Diagnostics/` (11 at HEAD, all directly in it, none carrying a raw-string delimiter; the enumeration is recursive since the final fix round, so a file filed into a future subdirectory cannot escape the scan by being filed) for `String(describing:` — a negative check (`noDiagnosticsFileDescribesAnErrorValue`) over text blanked of comments AND strings, paired with two positives per CLAUDE.md "Guards that name what they watch" (the scan reaches at least 5 files including `DialProbes.swift`; the unblanked `DialProbes.swift` still states the rule in prose) and a self-test that plants the call and drives it through the guard's own scanning function. **The final fix round added a SECOND negative check** (`noDiagnosticsFileBuildsASentenceOutOfAnErrorValue`), because the first one could not see the shape that actually reaches a row: `SwiftSource.stripCommentsAndStrings` consumes a string literal to its closing quote as one span, so blanking a literal blanks what it interpolates, and `return "…\(String(describing: error))"` or `"\(error)"` — the spelling `DialProbes.swift`'s `protocolError` comment names as `SSHAgentClient`'s — were invisible to it by construction, not by accident. The second check reads the same files with only the COMMENTS blanked (a new `SwiftSource.stripComments`, same fail-closed behaviour, self-tested beside its sibling) and rejects `\(String(describing:` plus any interpolation of a single bare identifier that is one of two exact names (`error`, `err`) or ends in `Error`; a member access such as `\(operation.rawValue)` does not match, so the rows this module builds out of hosts, paths and durations stay legal. Its positive is `Tests/MacSCPTestSupport/ErrorInterpolationFixture.swift` — real compiling code in both shapes, under a directory this guard never enumerates — matched through the guard's own scanning function. Mutation-tested, both shapes, each caught by the new check alone: `RESULT: RED — 8 test(s) ran, and the plant was caught by: macSCPCoreTests.DiagnosticsNoDescribingGuardTests/noDiagnosticsFileBuildsASentenceOutOfAnErrorValue()/DiagnosticsNoDescribingGuardTests.swift:134:6` for a planted `return "\(error)"` and again for a planted `return "the connection failed: \(String(describing: error))"` in the `RemoteFSError` arm. The recursion was measured by hand rather than through `scripts/mutation-probe`: a plant that only ADDS an untracked file changes nothing `git diff` can see, and the script reported `PLANT CHANGED NOTHING`; planting `Sources/macSCPCore/Diagnostics/Nested/NestedProbe.swift` by hand turned `noDiagnosticsFileDescribesAnErrorValue` red naming that file, and removing it turned all 8 green again. Mutation-tested: `RESULT: RED — 5 test(s) ran, and the plant was caught by: macSCPCoreTests.DiagnosticsNoDescribingGuardTests/noDiagnosticsFileDescribesAnErrorValue()/DiagnosticsNoDescribingGuardTests.swift:89:6` (`scripts/mutation-probe --filter DiagnosticsNoDescribingGuardTests --apply "perl -0pi -e 's/return \"the connection failed\"/return String(describing: error)/' Sources/macSCPCore/Diagnostics/DialProbes.swift"`). **The `default` arm was measured, not assumed, and came back GREEN** (final fix round, 2026-09-04): `DialSupport.reason(for:)`'s last arm renders a foreign error's `localizedDescription`, behind `DiagnosticStep.init`'s `URLText.withoutUserinfo` backstop only — the backstop the slash-bearing shape defeats. `aForeignErrorsLocalizedSentenceNeverReachesTheRow` plants a `URLError(.unsupportedURL)` carrying a credential-bearing failing URL in its `userInfo` through the dial seam and reads back the step's outcome, its detail, the plain-text report and the Markdown report: none of them carries either half of the credential. `NSURLErrorDomain` has no per-code localized sentence that quotes the URL (a manually built one describes the domain and the numeric code and nothing else), and `localizedDescription` does not consult the failing-URL keys — the value is reachable only through `description`, which prints the whole `userInfo`, and through `String(describing:)`, and this module reaches for neither. So no `URLError`-to-fixed-sentence mapping was written; the test stays as the regression that would catch one being added. Sensitivity measured, not assumed: `RESULT: RED — 1 test(s) ran, and the plant was caught by: macSCPCoreTests.ConnectionDiagnosticsTests/aForeignErrorsLocalizedSentenceNeverReachesTheRow()/ConnectionDiagnosticsTests.swift:931:6` with the arm planted as `(error as NSError).description`. Two measurements worth keeping beside it: `URL(string:)` returns nil for a userinfo password containing `/`, so the URL-valued failing-URL key cannot hold this shape at all and the string-valued key is the only route (spelled as its own literal — the `Foundation` constant is deprecated as of macOS 15.4 and would be a build warning); and the first draft of that test leaked the credential through a `#require` on the URL-building expression, which printed the whole interpolated string in the failure message — CLAUDE.md's "two exits" caught in the act, fixed by naming the value and asserting only Bools. The information trade: `protocolError`'s server-composed text no longer reaches a row at all — some of those messages were genuinely useful (a server can quote the request line it refused), and dropping them was taken because the backends compose that text out of the same user-controlled endpoint field. The way back, if that detail is wanted again, is a separate structured field carried alongside the fixed sentence, not a wider `URLText.withoutUserinfo` — the backstop is structurally unable to close this hole, only not composing the description can (per `bd6ec81f`'s own commit body). | -| Wall-clock ceilings still in the tree | **Measured 2026-09-03** (final review of the connection-tools plan): three tests keep an upper bound on elapsed time that a starved three-core runner can overrun — `TerminalPanelViewModelTests.neverFiresWhenTheOpenFails` polls on the main actor under a 5 s ceiling (red at 10.149 s in one of ten local full runs, against an ambient control of 10.1995 s in the same run), plus the two ceilings the connection-tools final review parked and the NETSPIKE-gated one at `ConnectionDiagnosticsTests.swift:164` (re-find them with `grep -rn "elapsed <" Tests/`). **Update 2026-09-04** (`c93c79bb`): the four `elapsed <` ceilings are gone — `BoundedCloseTests` (red on CI run 33814867360 at 15.343890416999999 s against 15 s), the NETSPIKE-gated TCP ping, the subprocess cancellation, the liveness resolution race — each replaced by the ordering it was standing in for (a harness `.timeLimit` against a longer bound, or the outcome check already beside it). Still open: the polling helpers. Counted 2026-09-04 with `grep -rln "let deadline = ContinuousClock.now + " Tests`: 8 files carry a `waitUntil`-style helper with a wall-clock deadline (macSCPCoreTests/TerminalPanelViewModelTests.swift macSCPCoreTests/ConnectionViewModelTests.swift macSCPCoreTests/SubprocessRunnerTests.swift macSCPCoreTests/WebDAVFileSystemIntegrationTests.swift macSCPCoreTests/LoopbackHTTPStub.swift macSCPAppKitTests/AlreadyOpenSessionTests.swift macSCPAppKitTests/ReconnectPathTests.swift macSCPAppKitTests/ConnectAttemptHandoffTests.swift), plus the 200 × 10 ms loop at `TerminalPanelViewModelTests.swift:32`; the terminal test named above uses the 5 s one at `:1010`. Each is a ceiling the runner can overrun; the iteration-bounded `Task.yield` helpers (`TransferQueueViewModelTests`) are not, and are the model. A plan of its own: one shared helper, iteration- or notification-bounded under a harness limit. **A fifth shape, 2026-09-04** (`6832d5a1`): a duration on the PROBE's side is a ceiling on the runner's side — `ConnectionDiagnosticsTests.aProbeThatIgnoresCancellationDoesNotHoldTheStepPastItsDeadline` held its probe on a 12 s Dispatch timer and asserted the step returned first; CI run 33819886384 took 32.987 s to resume the test and the timer won (`stillRunning` false). The probe now waits on a `Gate` the test opens only after the step has returned, so the ordering is the test's own sequence; the harness `.timeLimit` is the only clock. Re-find such shapes with `grep -rn "asyncAfter\\|Task.sleep(for: .seconds" Tests/` — a sleep whose expiry is compared against something the runner must schedule. **Done 2026-09-04** (`96e1b28e`, `da4ee4ba`, `d31637e2`, `d4423be8`, plus this row's own commit): the polling-without-a-clock plan closes it. One shared `pollUntil(_:every:_:)` (`Tests/MacSCPTestSupport/PollUntil.swift`) carries no deadline of its own — it ends only on cancellation, which every calling suite's `.timeLimit` performs — and both test targets now poll through it. **Removed**, counted by diffing `func`-name occurrences per file between `96e1b28e` and `d4423be8` (`git diff 96e1b28e..d4423be8 --stat`: 17 files changed): 10 helper function definitions — 7× `func waitUntil` (`AlreadyOpenSessionTests`, `ConnectAttemptHandoffTests`, `ReconnectPathTests`, `SSHTerminalViewSizingTests`, `ConnectionViewModelTests`, `TerminalPanelViewModelTests` ×2), 1× `func becomesTrue` (`SSHTerminalViewSizingTests`), 1× `func completesWithin` (`TransferQueueViewModelTests`), 1× `func tryClaim` plus the `RaceClaim` actor it existed for (`ConnectionViewModelTests`). **Callers**: `grep -rc "pollUntil(" Tests \| awk -F: '{s+=$2} END {print s}'` = 117 (includes `PollUntil.swift`'s own definition line, `PollUntilTests`' 3 tests of the helper itself, and the guard's own 9 textual mentions of the string in doc comments and `.contains(...)` calls — the guard excludes its own file from what it scans). 16 suite files reach `pollUntil`, all carrying `.timeLimit(`: 12 direct callers (the text `pollUntil(` appears in the file) and 4 indirect callers reaching it only through `LoopbackHTTPStub.waitForRequests` (`S3SessionIsolationTests`, `S3RedirectAuthorizationMeasurementTests`, `S3RedirectControlTests`, `ConnectFailureSecrecyTests`); 23 files in `Tests/` carry `.timeLimit(` at least once (31 occurrences total, most predating this plan). **The guard** (`Tests/macSCPCoreTests/PollingGuardTests.swift`, 5 `@Test`s, each a negative paired with a positive per CLAUDE.md "Guards that name what they watch"): `theTreePollsThroughTheSharedHelper` (≥ 10 files call `pollUntil`), `noTestCarriesItsOwnDeadline` (no `let deadline = ContinuousClock.now` / `ContinuousClock.now.advanced(by:`), `noTestAssertsAnElapsedCeiling` (no `elapsed\s*<=?\s*\.`), `aFloorExistsSoTheCeilingCheckHasSomethingToRead` (a real `elapsed >=`/`elapsed >` exists, so the ceiling regex has something to read), `everyCallerOfPollUntilDeclaresATimeLimit` — structural rather than a flat per-file scan, because that version was red on `LoopbackHTTPStub.swift` (a helper file with no suite of its own) and missed the four indirect S3/secrecy callers: a file declaring neither `@Suite` nor `@Test` is a helper, every `func` it declares whose brace-balanced body contains `pollUntil(` is a helper name (found: `waitForRequests`), and every suite file that calls `pollUntil(` directly or calls a helper name must carry `.timeLimit(`, paired with two positive checks (the helper set is non-empty and contains `waitForRequests`; the direct-caller set is non-empty). Mutation-tested with `scripts/mutation-probe`, both RED: `RESULT: RED — 5 test(s) ran, and the plant was caught by: ✘ Test noTestCarriesItsOwnDeadline()` (planted a `let deadline = ContinuousClock.now + .seconds(5)` before one `pollUntil` call in `TerminalPanelViewModelTests`) and `RESULT: RED — 5 test(s) ran, and the plant was caught by: ✘ Test everyCallerOfPollUntilDeclaresATimeLimit()` (stripped `.timeLimit(.minutes(1))` from `ConnectFailureSecrecyTests`, an indirect-only caller). **Fixed along the way**: `LivenessProbeRaceTests.swift`'s comment quoting `elapsed < .seconds(20)` verbatim matched the guard's own ceiling regex (CLAUDE.md, "Source-scanning guards read comments too") — reworded to prose, no behavioural change. `SSHTerminalViewSizingTests`'s 15 `pollUntil` calls and `LivenessProbeDropIntegrationTests.waitForIdleQueue` had silently dropped their pre-migration poll interval (10 ms and 50 ms respectively, both deliberate choices) to the 5 ms default when Task 3 migrated them; both now pass `every:` with the original interval. **Done 2026-09-04** (`2ddc59f4` Task 1, `79f6e693` Task 2, plus this row's own commit — the "ceilings under other spellings" plan): every site the paragraph above named is closed. `Box.waitForFirstDelivery(timeout:)` lost its parameter and its ceiling task, replaced by `withTaskCancellationHandler` around `parkUntilDelivered()` whose `onCancel` resumes the waiter. `LoopbackTLSStub.swift:110`'s `ready.wait(timeout: .seconds(60))` and `EmbeddedKeyPorterTests.swift:159`/`:171`'s two `finished.wait(timeout: .seconds(10))` calls are now `wait()`, ending only through the calling suite's `.timeLimit`. `ConnectMainActorLivenessTests.swift`'s five-second `DeadlineNotEnforced` sibling is gone; the bootstrap is awaited directly (`try await bootstrap.connect(...).get()`) under the suite's own `.timeLimit`, with the floor (`elapsed > .milliseconds(300)`) and the query-started check both kept. `CitadelShellIntegrationTests.swift`'s `collectUntil(timeout:)` lost its timeout parameter and its ten-second sibling sleeper — it reads `shell.output` until the marker appears or the task is cancelled — and `shellExitEndsOutputStream` reads `Task.isCancelled` instead of racing a second sleeping child. Mutation-tested, four RESULT lines across the two tasks: `RESULT: RED — 26 test(s) ran, and the plant was caught by: ✘ Test embedNeverOpensAnExternalKeyPath()` (the latch never raised, Task 1); `RESULT: RED — 5 test(s) ran, and the plant was caught by: ✘ Test firesOnceWhenTheShellIsRunning() ✘ Test waitsForTheFlushWhenTheShellIsStillOpening()` (the delivery callback never fires, Task 1); `RESULT: RED — 4 test(s) ran, and the plant was caught by: ✘ Test theConnectDeadlineAlsoCoversNameResolution()` (the connect deadline moved past the harness limit, Task 2); `RESULT: RED — 6 test(s) ran, and the plant was caught by: ✘ Test echoRoundtrip()` (a marker that never appears, gated `MACSCP_ITEST=1`, Task 2). `PollingGuardTests` gained the two remaining spellings under this row's own commit: `noLatchIsWaitedOnWithATimeout` (no `wait(timeout:` anywhere in `Tests/` outside `AsyncSignalTests.swift`, `Support/AsyncSignal.swift`, and the one exemption below, matched by the sentence beside it rather than by file name) and `noSleepingChildRacesWorkInAGroup` (no `addTask` whose body's first statement is `Task.sleep(for:`, scanned over comment-and-string-blanked source — a private stripper copied from `TabContextMenuWiringGuardTests`' own, this project's stated preference over a shared one that has already drifted once). Both mutation-tested RED: `RESULT: RED — 7 test(s) ran, and the plant was caught by: macSCPCoreTests.PollingGuardTests/noLatchIsWaitedOnWithATimeout()/PollingGuardTests.swift:100:6` (a `wait(timeout:)` call planted in `PollUntilTests.swift`, outside every exemption) and `RESULT: RED — 7 test(s) ran, and the plant was caught by: macSCPCoreTests.PollingGuardTests/noSleepingChildRacesWorkInAGroup()/PollingGuardTests.swift:145:6` (an `addTask` planted with `Task.sleep(for: .seconds(3))` as its first statement, in `CitadelFileSystemIntegrationTests.swift`). **Still open, and staying, per the plan's own Global Constraints**: `SubprocessRunnerTests.swift:422`'s `started.wait(timeout: startBound)` — the bound IS the saturation this test measures, gated `MACSCP_SATURATION`, and exempted by that same sentence rather than by its file name; `AsyncSignalTests.swift:39`, `:64`, `:83` — the tests OF the bounded API `wait(timeout:)` itself, which the site above still calls. A fourth open item, from Task 2's own fix round 1 (`ac91cb9c`, this row's sibling task): `ConnectMainActorLivenessTests.theConnectDeadlineAlsoCoversNameResolution` used to await a NIO `EventLoopFuture` with `.get()`, which the framework documents as not respecting cancellation — so a `.timeLimit` could record the red and then the run would park on an await nothing would release, the same 0% CPU shape `docs/superpowers/specs/2026-08-08-testsuite-hang-investigation.md` describes, reached from the other side. `awaitCancellably(_:)` replaced it there. Two more `.get()` calls on a NIO future carried the identical hazard and no suite `.timeLimit` at all, so a hang on either would have had no ceiling to stop it: `AgentAuthTests.swift:88` (`promise.futureResult.get()` inside `nextOffer`) and `SSHPrivateKeyLoaderTests.swift:399` (`keyPromise.futureResult.get()`) — verified at HEAD 2026-09-04, the natural second and third callers of `awaitCancellably`. **Done, this commit**: `awaitCancellably` moved out of `ConnectMainActorLivenessTests.swift`, its original and only caller, into a public helper at `Tests/MacSCPTestSupport/AwaitCancellably.swift` (`MacSCPTestSupport`'s target dependencies gained `NIOCore` and `NIOConcurrencyHelpers`, which both test targets already carried transitively through `macSCPCore`); both sites now call it and carry their own `.timeLimit(.minutes(1))` (neither suite had one before). `PollingGuardTests.noEventLoopFutureIsAwaitedWithGet` closes the gap structurally: no `.get()` in `Tests/` may follow an identifier ending `future`/`Future`/`futureResult`, scanned over comment-and-string-blanked source per CLAUDE.md "Source-scanning guards read comments too" (`EventLoopFuture` itself ends in `Future`, and several doc comments, including this helper's own, spell `` `EventLoopFuture.get()` `` verbatim to explain the hazard). Counted at the parent (`a61df6bd`, before conversion) with `git grep -n "\.get()" a61df6bd -- Tests/`: **12** `.get()` calls under `Tests/`, not the 9 this row and `6f79b7bd`'s own commit body first said — that undercount silently dropped 3 of the 12 lines, all three from `Tests/macSCPAppKitTests/`, when the final enumeration was written from memory mid-session instead of re-read from the full `Tests/`-wide grep the sentence itself cites; a fix round (reviewer-caught, `2026-09-04`) recounted and corrected both this row and `PollingGuardTests`' own doc comment. The 12, in full: 2 were the `EventLoopFuture.get()` sites above (converted), 6 were `Result.get()` (`BandwidthBucketTests.swift:97`, `HostKeyValidationTests.swift:76`, `EmbeddedKeyPorterTests.swift:171`, `UpdateCheckerTests.swift:18`, `SSHAgentClientTests.swift:32`, and `SSHTerminalViewSizingTests.swift:327`'s `try outcome.get()` — an unrelated stdlib API the new guard's regex does not match, since none of their receiver identifiers end `future`/`Future`/`futureResult`), and 4 were prose mentions of `` `EventLoopFuture.get()` `` (not calls): `ConnectMainActorLivenessTests.swift:428` and `:503`, plus `LivenessProbeRaceTests.swift:13` and `LivenessProbeDropIntegrationTests.swift:1077`, both in `Tests/macSCPAppKitTests/`. The guard itself has three known limits, all structural rather than oversights: its helper derivation stops at one hop — a helper file's function counts as a polling helper only when its own body names `pollUntil` directly, so a helper that instead called another helper function would go unseen, though no file in the tree does that today, counted by hand — and its `.timeLimit(` check reads a whole file as one span rather than one suite at a time, so `LivenessProbeDropIntegrationTests.swift`, which holds an unlimited suite (`FileCloseBoundRelationTests`, declared around `:115`) beside the gated, ten-minute-limited suite (declared around `:907`), passes today only because both of the file's `pollUntil` calls happen to sit in the limited suite — a `pollUntil` added to the unlimited one would pass just the same, and even a comment mentioning the trait anywhere in the file would satisfy the check as written. A third, found in fix round 1 of the ceilings-under-other-spellings plan's Task 3: `noSleepingChildRacesWorkInAGroup` required the sleep to be the literal first statement inside the added child's braces, and `AsyncSignal.race(timeout:_:)` (`Tests/macSCPCoreTests/Support/AsyncSignal.swift:119`-`143`) was exactly the shape that check exists to catch — a sleeping child beside the real work inside a task group — but wrapped the sleep in an error-handling block one level in, so the token right after the brace was that wrapper rather than the sleep call, and the scan passed over it without matching. Nothing enforced that shape until then but the site's own tests (`AsyncSignalTests.swift`, which exercise `wait(timeout:)` directly) and the saturation site's comment (`SubprocessRunnerTests.swift:422`, "the bound IS the measurement" — the one caller of `wait(timeout:)` left in the tree, so a behavioural change here would show up there first). **Turned into a named exemption, this commit** (guard-followups task): the regex now also matches a sleep wrapped one level inside a `do {}` (`addTask(?:\([^)]*\))?\s*\{\s*(?:do\s*\{\s*)?(?:try\??\s+)?(?:await\s+)?Task\.sleep\(for:`), and `AsyncSignal.race`'s `addTask` (`:124`-`138`) is excluded only while its comment beside the sleep (`:126`-`131`) carries the sentence "the timeout IS the API under test here" — matched by that sentence rather than by file name, the same shape `noLatchIsWaitedOnWithATimeout`'s saturation-site exemption already used, so a rewording without also removing the shape turns the check red instead of quietly staying green (verified by deleting the sentence locally: 2 issues, both from `noSleepingChildRacesWorkInAGroup`, then restored). `SleepingChildRegexFixture.swift` gained a second demonstration, `demonstratesTheDoWrappedBannedShape`, and the guard's own positive check against it moved from "matches something" to a count of exactly 2. Mutation-tested RED: `RESULT: RED — 10 test(s) ran, and the plant was caught by: macSCPCoreTests.PollingGuardTests/noSleepingChildRacesWorkInAGroup()/PollingGuardTests.swift:174:6` (a `do {}`-wrapped sleeping child planted in `PollUntilTests.swift`, outside every exemption; reverted after measuring). `PollingGuardTests` carries 10 `@Test`s at HEAD, unchanged in count by this fix — one existing test widened, none added. Three production-bound `deadline: now.advanced(by:)` arguments to `ICMPEcho.waitForReply`/`NetworkTrace.waitForAnswer` (`ICMPEchoTests.swift:411`, `NetworkTraceTests.swift:790`, `:895`) are a parameter of the code under test, not a test's own ceiling, and read a local `now` rather than `ContinuousClock.now`, so neither guard pattern reaches them. Two iteration-bounded `waitUntil` helpers read no clock and stay, per the plan's architecture note: `EditSessionManagerTests.swift:147` and `TransferQueueViewModelTests.swift:308` (both `Task.yield`-style, bounded by an iteration count, not a deadline). Two normal-path joins are Never-failing and each preceded by `continuation.finish()`, so they end today and would be unkillable only if the task they join ever stopped ending: `_ = await adHoc.value` at `ReconnectPathTests.swift:604`, `_ = await task.value` at `ConnectAttemptHandoffTests.swift:397`. `LivenessProbeDropIntegrationTests`'s printed "was still in flight 60s later" (`:1240`, `:1387`; a 300 × 200 ms loop each) is a floor, not a measurement — the line only prints once the test has already failed. **A footer deviation, 2026-09-04**: three commits on `develop` carry the footer `Co-Authored-By: Claude Fable 5.1 ` instead of this project's `Claude Fable 5` — `8f66d41c`, `88c079a8`, `42bc9460` — left unrewritten: `git log --format=%H 8f66d41c..develop` names 329 commits after the first of the three, and their 8-character hashes appear 206 times as substrings across `docs/**/*.md` (counted 2026-09-04) — rewriting `8f66d41c` would move every commit after it, breaking that citation trail for a footer typo. The deviation is recorded here instead of corrected. **Recount 2026-09-04** (at `bd6ec81f`): the row's own method left "their" ambiguous between the three footer commits and the full range, so here is the exact command for each number. Commit count — `git rev-list --count 8f66d41c..bd6ec81f` — is 331. Hash-reference count — summing, over every one of those 331 short hashes, its occurrences as a substring across the `*.md` files `docs/` held at that revision (`git log --format=%h 8f66d41c..bd6ec81f > h-8f66d41c.txt && git grep -o -F -f h-8f66d41c.txt bd6ec81f -- 'docs/*.md' > n-8f66d41c.txt && wc -l < n-8f66d41c.txt; rm -f h-8f66d41c.txt n-8f66d41c.txt`) — is 210. **Both commands were repaired on 2026-10-06, both figures re-derived by running the repaired forms, and this is what they withdraw.** Neither of the two could be run in the form it was committed, because each carried a table cell's escaped pipe; sliced out of the committed bytes at `47be4172` and handed to `bash` on 2026-10-06, the first exits **128** with `fatal: ambiguous argument`, naming as an unknown revision the character this passage writes as `..`, and the second exits **2** with `syntax error near unexpected token` naming `do`, which reproduces what the row "The wall-clock-ceilings row's two commit-census commands cannot be run as committed" measured — that row is the finding, and this pass closes it. The withdrawn wording is reproduced here with the two characters that should be one pipe written as `..`, which is that row's own device for naming an escape without spelling it, and it is the only departure from verbatim, because spelling the escape inside a cell reproduces the defect. The first read “Commit count — `git log --format=%h 8f66d41c..HEAD .. wc -l` — is 331.”, the second “Hash-reference count — summing, over every one of those 331 short hashes, its occurrences as a substring across `docs/**/*.md` (`git log --format=%h 8f66d41c..HEAD .. while read -r h; do grep -roE "$h" --include='*.md' -r docs; done .. wc -l`) — is 210.” **Three things changed besides the pipe, and each is a property the old form did not have.** Both ranges now end at `bd6ec81f`, the revision this passage already pinned both figures to in prose, rather than at `..HEAD`, which answered a different question at every head. The second command reads that revision through `git grep ` rather than the working tree, so a later commit touching `docs/` cannot move the figure out from under the sentence that states it. And the loop is gone, which is the half the finding doubted could be done in a cell at all: `git grep` takes all 331 patterns from a file with `-f`, which removes the loop and the pipe together, leaving two redirects, one `<` and no pipeline anywhere. The two scratch files are written into the repository root and the command removes them itself on the same line, after a `;` rather than an `&&` so that the removal runs on a failing path too — verified 2026-10-06 under `bash` and under `zsh` by substituting a revision that does not exist for `bd6ec81f`: both scratch files are removed and `git status --porcelain` is empty. **The committed line's own exit status is 0**, because `rm -f` is last and succeeds — the 128 belongs to the `git grep` segment before the `;` — and the same line with the trailing `; rm -f …` taken off exits **128** and leaves both files behind, which is the comparison that shows the `;` doing the work rather than the status doing it. **Corrected 2026-10-06 in a fix round**, having read “verified 2026-10-06 by substituting a revision that does not exist for `bd6ec81f`, which exits 128 and leaves `git status --porcelain` empty.”: the property was true and the number was not, and it was wrong because of the very `;` the sentence exists to justify. That hazard is the one the row "Probes and a shared checkout do not mix" records, and the shape is the precedent the closing section of this file sets with its own two-copy command. **The one pathspec reaches below `docs/`, and this is the positive check that it does** rather than quietly matching only the top level: git's pathspec wildcard crosses `/`, and the same command narrowed to `'docs/superpowers/*.md'` printed **140** of the 210 on 2026-10-06, leaving 70 in `docs/` itself. **Both figures re-run from these committed bytes after this passage was written**, extracted from the text above rather than retyped: 331 and 210, with `git status --porcelain` empty before and after each. Both numbers grew from the row's own (329 / 206) because two more commits landed on `develop` between that measurement and this one; the three footer hashes themselves still account for only 5 of the 210 references. **A fourth footer deviation, 2026-09-05**: `c42ffd5a` (the detachable-tabs plan's Task 5 commit) also carries `Claude Fable 5.1 ` — pushed together with `3fd16e67` on 2026-09-05, the trailer check printed the mismatch but did not gate the push, and it is left unrewritten for the same reason the three above are: it is pushed history, and the project's precedent is to record such a commit here rather than rewrite it. **A sixth spelling, 2026-09-04** (`ad133cc4`): a `Task.detached` sleeping timer racing detached work through a shared continuation — `WebDAVFileSystemWriteTests.withDeadline(seconds:)`, red on CI run 33856445475 at 23.984 s against 5 s; now `abandonable`, resumed with `nil` only by the cancellation the suite's `.timeLimit` performs. `PollingGuardTests` does not scan this spelling either (its sleeping-child check looks inside `addTask`); re-find with `grep -rn "Task.detached" Tests/` and read each for a sleep beside work. **A seventh shape, 2026-09-18** (final review of the jump-and-groups plan, T6-2): an outcome asserted `.ok` for a REAL probe raced against its budget is a ceiling too. `ConnectionDiagnosticsJumpTests.aJumpSessionIsWalkedJumpFirstThenTheTargetThroughIt` asserted ten `.ok` rows, four of them real loopback probes under the 5 s step and 20 s trace budgets; since `ac894cb9` it asserts the order and that no row is `.failed`. Its pre-existing twin, `ConnectionDiagnosticsTests.theRunnerWalksTheUniversalStepsInTheOrderTheReportPrints`, had the same treatment and is CLOSED — **re-read 2026-10-01**, which is how this sentence was found to be stale rather than by searching for it. `grep -rn "\[\.ok" Tests/macSCPCoreTests/ConnectionDiagnostics*.swift` returns nothing, and the case asserts the five step IDs in order exactly and then that no row is `.failed`, with a comment beside it naming this rule, the 14.67 s ambient stall, and `ac894cb9` as the treatment its jump twin got. No assertion of an `.ok` chain over real probes is left in the tree. | +| Wall-clock ceilings still in the tree | **Measured 2026-09-03** (final review of the connection-tools plan): three tests keep an upper bound on elapsed time that a starved three-core runner can overrun — `TerminalPanelViewModelTests.neverFiresWhenTheOpenFails` polls on the main actor under a 5 s ceiling (red at 10.149 s in one of ten local full runs, against an ambient control of 10.1995 s in the same run), plus the two ceilings the connection-tools final review parked and the NETSPIKE-gated one at `ConnectionDiagnosticsTests.swift:164` (re-find them with `grep -rn "elapsed <" Tests/`). **Update 2026-09-04** (`c93c79bb`): the four `elapsed <` ceilings are gone — `BoundedCloseTests` (red on CI run 33814867360 at 15.343890416999999 s against 15 s), the NETSPIKE-gated TCP ping, the subprocess cancellation, the liveness resolution race — each replaced by the ordering it was standing in for (a harness `.timeLimit` against a longer bound, or the outcome check already beside it). Still open: the polling helpers. Counted 2026-09-04 with `grep -rln "let deadline = ContinuousClock.now + " Tests`: 8 files carry a `waitUntil`-style helper with a wall-clock deadline (macSCPCoreTests/TerminalPanelViewModelTests.swift macSCPCoreTests/ConnectionViewModelTests.swift macSCPCoreTests/SubprocessRunnerTests.swift macSCPCoreTests/WebDAVFileSystemIntegrationTests.swift macSCPCoreTests/LoopbackHTTPStub.swift macSCPAppKitTests/AlreadyOpenSessionTests.swift macSCPAppKitTests/ReconnectPathTests.swift macSCPAppKitTests/ConnectAttemptHandoffTests.swift), plus the 200 × 10 ms loop at `TerminalPanelViewModelTests.swift:32`; the terminal test named above uses the 5 s one at `:1010`. Each is a ceiling the runner can overrun; the iteration-bounded `Task.yield` helpers (`TransferQueueViewModelTests`) are not, and are the model. A plan of its own: one shared helper, iteration- or notification-bounded under a harness limit. **A fifth shape, 2026-09-04** (`6832d5a1`): a duration on the PROBE's side is a ceiling on the runner's side — `ConnectionDiagnosticsTests.aProbeThatIgnoresCancellationDoesNotHoldTheStepPastItsDeadline` held its probe on a 12 s Dispatch timer and asserted the step returned first; CI run 33819886384 took 32.987 s to resume the test and the timer won (`stillRunning` false). The probe now waits on a `Gate` the test opens only after the step has returned, so the ordering is the test's own sequence; the harness `.timeLimit` is the only clock. Re-find such shapes with `grep -rn "asyncAfter\\|Task.sleep(for: .seconds" Tests/` — a sleep whose expiry is compared against something the runner must schedule. **Done 2026-09-04** (`96e1b28e`, `da4ee4ba`, `d31637e2`, `d4423be8`, plus this row's own commit): the polling-without-a-clock plan closes it. One shared `pollUntil(_:every:_:)` (`Tests/MacSCPTestSupport/PollUntil.swift`) carries no deadline of its own — it ends only on cancellation, which every calling suite's `.timeLimit` performs — and both test targets now poll through it. **Removed**, counted by diffing `func`-name occurrences per file between `96e1b28e` and `d4423be8` (`git diff 96e1b28e..d4423be8 --stat`: 17 files changed): 10 helper function definitions — 7× `func waitUntil` (`AlreadyOpenSessionTests`, `ConnectAttemptHandoffTests`, `ReconnectPathTests`, `SSHTerminalViewSizingTests`, `ConnectionViewModelTests`, `TerminalPanelViewModelTests` ×2), 1× `func becomesTrue` (`SSHTerminalViewSizingTests`), 1× `func completesWithin` (`TransferQueueViewModelTests`), 1× `func tryClaim` plus the `RaceClaim` actor it existed for (`ConnectionViewModelTests`). **Callers**: `grep -rc "pollUntil(" Tests \| awk -F: '{s+=$2} END {print s}'` = 117 (includes `PollUntil.swift`'s own definition line, `PollUntilTests`' 3 tests of the helper itself, and the guard's own 9 textual mentions of the string in doc comments and `.contains(...)` calls — the guard excludes its own file from what it scans). 16 suite files reach `pollUntil`, all carrying `.timeLimit(`: 12 direct callers (the text `pollUntil(` appears in the file) and 4 indirect callers reaching it only through `LoopbackHTTPStub.waitForRequests` (`S3SessionIsolationTests`, `S3RedirectAuthorizationMeasurementTests`, `S3RedirectControlTests`, `ConnectFailureSecrecyTests`); 23 files in `Tests/` carry `.timeLimit(` at least once (31 occurrences total, most predating this plan). **The guard** (`Tests/macSCPCoreTests/PollingGuardTests.swift`, 5 `@Test`s, each a negative paired with a positive per CLAUDE.md "Guards that name what they watch"): `theTreePollsThroughTheSharedHelper` (≥ 10 files call `pollUntil`), `noTestCarriesItsOwnDeadline` (no `let deadline = ContinuousClock.now` / `ContinuousClock.now.advanced(by:`), `noTestAssertsAnElapsedCeiling` (no `elapsed\s*<=?\s*\.`), `aFloorExistsSoTheCeilingCheckHasSomethingToRead` (a real `elapsed >=`/`elapsed >` exists, so the ceiling regex has something to read), `everyCallerOfPollUntilDeclaresATimeLimit` — structural rather than a flat per-file scan, because that version was red on `LoopbackHTTPStub.swift` (a helper file with no suite of its own) and missed the four indirect S3/secrecy callers: a file declaring neither `@Suite` nor `@Test` is a helper, every `func` it declares whose brace-balanced body contains `pollUntil(` is a helper name (found: `waitForRequests`), and every suite file that calls `pollUntil(` directly or calls a helper name must carry `.timeLimit(`, paired with two positive checks (the helper set is non-empty and contains `waitForRequests`; the direct-caller set is non-empty). Mutation-tested with `scripts/mutation-probe`, both RED: `RESULT: RED — 5 test(s) ran, and the plant was caught by: ✘ Test noTestCarriesItsOwnDeadline()` (planted a `let deadline = ContinuousClock.now + .seconds(5)` before one `pollUntil` call in `TerminalPanelViewModelTests`) and `RESULT: RED — 5 test(s) ran, and the plant was caught by: ✘ Test everyCallerOfPollUntilDeclaresATimeLimit()` (stripped `.timeLimit(.minutes(1))` from `ConnectFailureSecrecyTests`, an indirect-only caller). **Fixed along the way**: `LivenessProbeRaceTests.swift`'s comment quoting `elapsed < .seconds(20)` verbatim matched the guard's own ceiling regex (CLAUDE.md, "Source-scanning guards read comments too") — reworded to prose, no behavioural change. `SSHTerminalViewSizingTests`'s 15 `pollUntil` calls and `LivenessProbeDropIntegrationTests.waitForIdleQueue` had silently dropped their pre-migration poll interval (10 ms and 50 ms respectively, both deliberate choices) to the 5 ms default when Task 3 migrated them; both now pass `every:` with the original interval. **Done 2026-09-04** (`2ddc59f4` Task 1, `79f6e693` Task 2, plus this row's own commit — the "ceilings under other spellings" plan): every site the paragraph above named is closed. `Box.waitForFirstDelivery(timeout:)` lost its parameter and its ceiling task, replaced by `withTaskCancellationHandler` around `parkUntilDelivered()` whose `onCancel` resumes the waiter. `LoopbackTLSStub.swift:110`'s `ready.wait(timeout: .seconds(60))` and `EmbeddedKeyPorterTests.swift:159`/`:171`'s two `finished.wait(timeout: .seconds(10))` calls are now `wait()`, ending only through the calling suite's `.timeLimit`. `ConnectMainActorLivenessTests.swift`'s five-second `DeadlineNotEnforced` sibling is gone; the bootstrap is awaited directly (`try await bootstrap.connect(...).get()`) under the suite's own `.timeLimit`, with the floor (`elapsed > .milliseconds(300)`) and the query-started check both kept. `CitadelShellIntegrationTests.swift`'s `collectUntil(timeout:)` lost its timeout parameter and its ten-second sibling sleeper — it reads `shell.output` until the marker appears or the task is cancelled — and `shellExitEndsOutputStream` reads `Task.isCancelled` instead of racing a second sleeping child. Mutation-tested, four RESULT lines across the two tasks: `RESULT: RED — 26 test(s) ran, and the plant was caught by: ✘ Test embedNeverOpensAnExternalKeyPath()` (the latch never raised, Task 1); `RESULT: RED — 5 test(s) ran, and the plant was caught by: ✘ Test firesOnceWhenTheShellIsRunning() ✘ Test waitsForTheFlushWhenTheShellIsStillOpening()` (the delivery callback never fires, Task 1); `RESULT: RED — 4 test(s) ran, and the plant was caught by: ✘ Test theConnectDeadlineAlsoCoversNameResolution()` (the connect deadline moved past the harness limit, Task 2); `RESULT: RED — 6 test(s) ran, and the plant was caught by: ✘ Test echoRoundtrip()` (a marker that never appears, gated `MACSCP_ITEST=1`, Task 2). `PollingGuardTests` gained the two remaining spellings under this row's own commit: `noLatchIsWaitedOnWithATimeout` (no `wait(timeout:` anywhere in `Tests/` outside `AsyncSignalTests.swift`, `Support/AsyncSignal.swift`, and the one exemption below, matched by the sentence beside it rather than by file name) and `noSleepingChildRacesWorkInAGroup` (no `addTask` whose body's first statement is `Task.sleep(for:`, scanned over comment-and-string-blanked source — a private stripper copied from `TabContextMenuWiringGuardTests`' own, this project's stated preference over a shared one that has already drifted once). Both mutation-tested RED: `RESULT: RED — 7 test(s) ran, and the plant was caught by: macSCPCoreTests.PollingGuardTests/noLatchIsWaitedOnWithATimeout()/PollingGuardTests.swift:100:6` (a `wait(timeout:)` call planted in `PollUntilTests.swift`, outside every exemption) and `RESULT: RED — 7 test(s) ran, and the plant was caught by: macSCPCoreTests.PollingGuardTests/noSleepingChildRacesWorkInAGroup()/PollingGuardTests.swift:145:6` (an `addTask` planted with `Task.sleep(for: .seconds(3))` as its first statement, in `CitadelFileSystemIntegrationTests.swift`). **Still open, and staying, per the plan's own Global Constraints**: `SubprocessRunnerTests.swift:422`'s `started.wait(timeout: startBound)` — the bound IS the saturation this test measures, gated `MACSCP_SATURATION`, and exempted by that same sentence rather than by its file name; `AsyncSignalTests.swift:39`, `:64`, `:83` — the tests OF the bounded API `wait(timeout:)` itself, which the site above still calls. A fourth open item, from Task 2's own fix round 1 (`ac91cb9c`, this row's sibling task): `ConnectMainActorLivenessTests.theConnectDeadlineAlsoCoversNameResolution` used to await a NIO `EventLoopFuture` with `.get()`, which the framework documents as not respecting cancellation — so a `.timeLimit` could record the red and then the run would park on an await nothing would release, the same 0% CPU shape `docs/superpowers/specs/2026-08-08-testsuite-hang-investigation.md` describes, reached from the other side. `awaitCancellably(_:)` replaced it there. Two more `.get()` calls on a NIO future carried the identical hazard and no suite `.timeLimit` at all, so a hang on either would have had no ceiling to stop it: `AgentAuthTests.swift:88` (`promise.futureResult.get()` inside `nextOffer`) and `SSHPrivateKeyLoaderTests.swift:399` (`keyPromise.futureResult.get()`) — verified at HEAD 2026-09-04, the natural second and third callers of `awaitCancellably`. **Done, this commit**: `awaitCancellably` moved out of `ConnectMainActorLivenessTests.swift`, its original and only caller, into a public helper at `Tests/MacSCPTestSupport/AwaitCancellably.swift` (`MacSCPTestSupport`'s target dependencies gained `NIOCore` and `NIOConcurrencyHelpers`, which both test targets already carried transitively through `macSCPCore`); both sites now call it and carry their own `.timeLimit(.minutes(1))` (neither suite had one before). `PollingGuardTests.noEventLoopFutureIsAwaitedWithGet` closes the gap structurally: no `.get()` in `Tests/` may follow an identifier ending `future`/`Future`/`futureResult`, scanned over comment-and-string-blanked source per CLAUDE.md "Source-scanning guards read comments too" (`EventLoopFuture` itself ends in `Future`, and several doc comments, including this helper's own, spell `` `EventLoopFuture.get()` `` verbatim to explain the hazard). Counted at the parent (`a61df6bd`, before conversion) with `git grep -n "\.get()" a61df6bd -- Tests/`: **12** `.get()` calls under `Tests/`, not the 9 this row and `6f79b7bd`'s own commit body first said — that undercount silently dropped 3 of the 12 lines, all three from `Tests/macSCPAppKitTests/`, when the final enumeration was written from memory mid-session instead of re-read from the full `Tests/`-wide grep the sentence itself cites; a fix round (reviewer-caught, `2026-09-04`) recounted and corrected both this row and `PollingGuardTests`' own doc comment. The 12, in full: 2 were the `EventLoopFuture.get()` sites above (converted), 6 were `Result.get()` (`BandwidthBucketTests.swift:97`, `HostKeyValidationTests.swift:76`, `EmbeddedKeyPorterTests.swift:171`, `UpdateCheckerTests.swift:18`, `SSHAgentClientTests.swift:32`, and `SSHTerminalViewSizingTests.swift:327`'s `try outcome.get()` — an unrelated stdlib API the new guard's regex does not match, since none of their receiver identifiers end `future`/`Future`/`futureResult`), and 4 were prose mentions of `` `EventLoopFuture.get()` `` (not calls): `ConnectMainActorLivenessTests.swift:428` and `:503`, plus `LivenessProbeRaceTests.swift:13` and `LivenessProbeDropIntegrationTests.swift:1077`, both in `Tests/macSCPAppKitTests/`. The guard itself has three known limits, all structural rather than oversights: its helper derivation stops at one hop — a helper file's function counts as a polling helper only when its own body names `pollUntil` directly, so a helper that instead called another helper function would go unseen, though no file in the tree does that today, counted by hand — and its `.timeLimit(` check reads a whole file as one span rather than one suite at a time, so `LivenessProbeDropIntegrationTests.swift`, which holds an unlimited suite (`FileCloseBoundRelationTests`, declared around `:115`) beside the gated, ten-minute-limited suite (declared around `:907`), passes today only because both of the file's `pollUntil` calls happen to sit in the limited suite — a `pollUntil` added to the unlimited one would pass just the same, and even a comment mentioning the trait anywhere in the file would satisfy the check as written. A third, found in fix round 1 of the ceilings-under-other-spellings plan's Task 3: `noSleepingChildRacesWorkInAGroup` required the sleep to be the literal first statement inside the added child's braces, and `AsyncSignal.race(timeout:_:)` (`Tests/macSCPCoreTests/Support/AsyncSignal.swift:119`-`143`) was exactly the shape that check exists to catch — a sleeping child beside the real work inside a task group — but wrapped the sleep in an error-handling block one level in, so the token right after the brace was that wrapper rather than the sleep call, and the scan passed over it without matching. Nothing enforced that shape until then but the site's own tests (`AsyncSignalTests.swift`, which exercise `wait(timeout:)` directly) and the saturation site's comment (`SubprocessRunnerTests.swift:422`, "the bound IS the measurement" — the one caller of `wait(timeout:)` left in the tree, so a behavioural change here would show up there first). **Turned into a named exemption, this commit** (guard-followups task): the regex now also matches a sleep wrapped one level inside a `do {}` (`addTask(?:\([^)]*\))?\s*\{\s*(?:do\s*\{\s*)?(?:try\??\s+)?(?:await\s+)?Task\.sleep\(for:`), and `AsyncSignal.race`'s `addTask` (`:124`-`138`) is excluded only while its comment beside the sleep (`:126`-`131`) carries the sentence "the timeout IS the API under test here" — matched by that sentence rather than by file name, the same shape `noLatchIsWaitedOnWithATimeout`'s saturation-site exemption already used, so a rewording without also removing the shape turns the check red instead of quietly staying green (verified by deleting the sentence locally: 2 issues, both from `noSleepingChildRacesWorkInAGroup`, then restored). `SleepingChildRegexFixture.swift` gained a second demonstration, `demonstratesTheDoWrappedBannedShape`, and the guard's own positive check against it moved from "matches something" to a count of exactly 2. Mutation-tested RED: `RESULT: RED — 10 test(s) ran, and the plant was caught by: macSCPCoreTests.PollingGuardTests/noSleepingChildRacesWorkInAGroup()/PollingGuardTests.swift:174:6` (a `do {}`-wrapped sleeping child planted in `PollUntilTests.swift`, outside every exemption; reverted after measuring). `PollingGuardTests` carries 10 `@Test`s at HEAD, unchanged in count by this fix — one existing test widened, none added. Three production-bound `deadline: now.advanced(by:)` arguments to `ICMPEcho.waitForReply`/`NetworkTrace.waitForAnswer` (`ICMPEchoTests.swift:411`, `NetworkTraceTests.swift:790`, `:895`) are a parameter of the code under test, not a test's own ceiling, and read a local `now` rather than `ContinuousClock.now`, so neither guard pattern reaches them. Two iteration-bounded `waitUntil` helpers read no clock and stay, per the plan's architecture note: `EditSessionManagerTests.swift:147` and `TransferQueueViewModelTests.swift:308` (both `Task.yield`-style, bounded by an iteration count, not a deadline). Two normal-path joins are Never-failing and each preceded by `continuation.finish()`, so they end today and would be unkillable only if the task they join ever stopped ending: `_ = await adHoc.value` at `ReconnectPathTests.swift:604`, `_ = await task.value` at `ConnectAttemptHandoffTests.swift:397`. `LivenessProbeDropIntegrationTests`'s printed "was still in flight 60s later" (`:1240`, `:1387`; a 300 × 200 ms loop each) is a floor, not a measurement — the line only prints once the test has already failed. **A footer deviation, 2026-09-04**: three commits on `develop` carry the footer `Co-Authored-By: Claude Fable 5.1 ` instead of this project's `Claude Fable 5` — `8f66d41c`, `88c079a8`, `42bc9460` — left unrewritten: `git log --format=%H 8f66d41c..develop` names 329 commits after the first of the three, and their 8-character hashes appear 206 times as substrings across `docs/**/*.md` (counted 2026-09-04) — rewriting `8f66d41c` would move every commit after it, breaking that citation trail for a footer typo. The deviation is recorded here instead of corrected. **Recount 2026-09-04** (at `bd6ec81f`): the row's own method left "their" ambiguous between the three footer commits and the full range, so here is the exact command for each number. Commit count — `git rev-list --count 8f66d41c..bd6ec81f` — is 331. Hash-reference count — summing, over every one of those 331 short hashes, its occurrences as a substring across the `*.md` files `docs/` held at that revision (`git log --format=%h 8f66d41c..bd6ec81f > h-8f66d41c.txt && git grep -o -F -f h-8f66d41c.txt bd6ec81f -- 'docs/*.md' > n-8f66d41c.txt && wc -l < n-8f66d41c.txt; rm -f h-8f66d41c.txt n-8f66d41c.txt`) — is 210. **Both commands were repaired on 2026-10-06, both figures re-derived by running the repaired forms, and this is what they withdraw.** Neither of the two could be run in the form it was committed, because each carried a table cell's escaped pipe; sliced out of the committed bytes at `47be4172` and handed to `bash` on 2026-10-06, the first exits **128** with `fatal: ambiguous argument`, naming as an unknown revision the character this passage writes as `..`, and the second exits **2** with `syntax error near unexpected token` naming `do`, which reproduces what the row "The wall-clock-ceilings row's two commit-census commands cannot be run as committed" measured — that row is the finding, and this pass closes it. The withdrawn wording is reproduced here with the two characters that should be one pipe written as `..`, which is that row's own device for naming an escape without spelling it, and it is the only departure from verbatim, because spelling the escape inside a cell reproduces the defect. The first read “Commit count — `git log --format=%h 8f66d41c..HEAD .. wc -l` — is 331.”, the second “Hash-reference count — summing, over every one of those 331 short hashes, its occurrences as a substring across `docs/**/*.md` (`git log --format=%h 8f66d41c..HEAD .. while read -r h; do grep -roE "$h" --include='*.md' -r docs; done .. wc -l`) — is 210.” **Three things changed besides the pipe, and each is a property the old form did not have.** Both ranges now end at `bd6ec81f`, the revision this passage already pinned both figures to in prose, rather than at `..HEAD`, which answered a different question at every head. The second command reads that revision through `git grep ` rather than the working tree, so a later commit touching `docs/` cannot move the figure out from under the sentence that states it. And the loop is gone, which is the half the finding doubted could be done in a cell at all: `git grep` takes all 331 patterns from a file with `-f`, which removes the loop and the pipe together, leaving two redirects, one `<` and no pipeline anywhere. The two scratch files are written into the repository root and the command removes them itself on the same line, after a `;` rather than an `&&` so that the removal runs on a failing path too — verified 2026-10-06 under `bash` and under `zsh` by substituting a revision that does not exist for `bd6ec81f`: both scratch files are removed and `git status --porcelain` is empty. **The committed line's own exit status is 0**, because `rm -f` is last and succeeds — the 128 belongs to the `git grep` segment before the `;` — and the same line with the trailing `; rm -f …` taken off exits **128** and leaves both files behind, which is the comparison that shows the `;` doing the work rather than the status doing it. **Corrected 2026-10-06 in a fix round**, having read “verified 2026-10-06 by substituting a revision that does not exist for `bd6ec81f`, which exits 128 and leaves `git status --porcelain` empty.”: the property was true and the number was not, and it was wrong because of the very `;` the sentence exists to justify. That hazard is the one the row "Probes and a shared checkout do not mix" records, and the shape is the precedent the closing section of this file sets with its own two-copy command. **The one pathspec reaches below `docs/`, and this is the positive check that it does** rather than quietly matching only the top level: git's pathspec wildcard crosses `/`, and the same command narrowed to `'docs/superpowers/*.md'` printed **140** of the 210 on 2026-10-06, leaving 70 in `docs/` itself. **Both figures re-run from these committed bytes after this passage was written**, extracted from the text above rather than retyped: 331 and 210, with `git status --porcelain` empty before and after each. Both numbers grew from the row's own (329 / 206) because two more commits landed on `develop` between that measurement and this one; the three footer hashes themselves still account for only 5 of the 210 references. **A fourth footer deviation, 2026-09-05**: `c42ffd5a` (the detachable-tabs plan's Task 5 commit) also carries `Claude Fable 5.1 ` — pushed together with `3fd16e67` on 2026-09-05, the trailer check printed the mismatch but did not gate the push, and it is left unrewritten for the same reason the three above are: it is pushed history, and the project's precedent is to record such a commit here rather than rewrite it. **A sixth spelling, 2026-09-04** (`ad133cc4`): a `Task.detached` sleeping timer racing detached work through a shared continuation — `WebDAVFileSystemWriteTests.withDeadline(seconds:)`, red on CI run 33856445475 at 23.984 s against 5 s; now `abandonable`, resumed with `nil` only by the cancellation the suite's `.timeLimit` performs. `PollingGuardTests` does not scan this spelling either (its sleeping-child check looks inside `addTask`); re-find with `grep -rn "Task.detached" Tests/` and read each for a sleep beside work. **A seventh shape, 2026-09-18** (final review of the jump-and-groups plan, T6-2): an outcome asserted `.ok` for a REAL probe raced against its budget is a ceiling too. `ConnectionDiagnosticsJumpTests.aJumpSessionIsWalkedJumpFirstThenTheTargetThroughIt` asserted ten `.ok` rows, four of them real loopback probes under the 5 s step and 20 s trace budgets; since `ac894cb9` it asserts the order and that no row is `.failed`. Its pre-existing twin, `ConnectionDiagnosticsTests.theRunnerWalksTheUniversalStepsInTheOrderTheReportPrints`, had the same treatment and is CLOSED — **re-read 2026-10-01**, which is how this sentence was found to be stale rather than by searching for it. `grep -rn "\[\.ok" Tests/macSCPCoreTests/ConnectionDiagnostics*.swift` returns nothing, and the case asserts the five step IDs in order exactly and then that no row is `.failed`, with a comment beside it naming this rule, the 14.67 s ambient stall, and `ac894cb9` as the treatment its jump twin got. No assertion of an `.ok` chain over real probes is left in the tree. **An eighth shape, 2026-10-31** (CI run 41244948849, `e4e5d054`): a step budget below the suite's own wall clock settles a row by its DEADLINE, and a case asserting that row's outcome then fails for the runner's reason rather than its own. `ConnectionDiagnosticsTests.theSSHDialNeverPutsTheSecretInTheReport` handed the runner `stepTimeout: .seconds(10)` against a closed loopback port and read `.timedOut` where its anchor requires `.failed` — the guard recorded "a closed port did not fail the SSH dial: .timedOut" and the case failed after 64.975 s, in a run whose total was 116.701 s. 8184 of the 8307 cases carrying a duration line in that run reported a wall clock AT OR ABOVE 10 s (the pipeline compares `>= 10` on the integer seconds), so the budget bounded only whether `DetachedProbe` was given a thread in time, which is this row's fifth shape arriving through a production parameter the test chooses. The pipeline printing `8184 8307` is written beside `ConnectionDiagnosticsTests.roomy`, where a pipe is allowed and a cell's is not. **The same shape had already been bought one file at a time, twice**: `61850a9c` (2026-09-19) moved `ConnectionDiagnosticsJumpTests.theJumpsSecretNeverReachesTheReport` off `.seconds(10)` for exactly this reason, in a commit touching one file, and `ResolveNamesTests` had already named its own 600 s `roomy`. The five literals in `ConnectionDiagnosticsTests` were never swept and fell 42 days later. **Fixed in this row's own commit**: one `private static let roomy: Duration = .seconds(600)` carrying the measurement, and all five call sites through it — `grep -rn 'stepTimeout: Self.roomy' Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift` prints 5. Raising it costs those five nothing, which is the argument that makes 600 s safe there: every target either answers at once (the rig) or refuses at once (a closed loopback port), and the skipped case never dials, so none can park on the larger budget. **Open, and deliberately NOT swept**: `grep -rnF -e 'stepTimeout: .seconds(2)' -e 'stepTimeout: .seconds(3)' -e 'stepTimeout: .seconds(5)' Tests/macSCPCoreTests/ICMPEchoTests.swift Tests/macSCPCoreTests/NetworkTraceTests.swift` prints 6 lines, one occurrence each (counted with `-o` the same day, also 6). Five sit in cases that assert a finished outcome while sending REAL echo or trace probes, where the cost of a roomy budget is unmeasured; the sixth is `NetworkTraceTests.traceStep`, whose own comment calls 5 s generous and records the 5.004 s that justified it, so that one is a measured choice and not an oversight. Two further sites route through a per-file `diagnostics(...)` factory shared with cases where a tight budget may be load-bearing (`InternetSpeedProbeTests` at 2 s, `ThroughputRigTests` at 20 s). Each needs classifying before any value changes, which is why this pass fixed the file that fell and recorded the rest rather than sweeping all four. **The seventh shape's closing claim stays true as worded** — it is about an `.ok` CHAIN, and `grep -rn '\[\.ok' Tests/macSCPCoreTests/ConnectionDiagnostics*.swift` still returns nothing — but a SINGLE `.ok` asserted over a real probe is this eighth shape and remains in the tree, e.g. `ICMPEchoTests.theRunnerPutsTheEchoBetweenTheTCPPingAndTheDial`. | | Starved CI runners settle diagnostics probes as `notStarted`, and the positive companions catch it — two reds in five test-step runs on 2026-09-28 | **Measured 2026-09-28**, at HEAD `9d58cbfb` (the typed-findings branch). One of the seven parameterised arguments (`target.invalid;id`, the first) failed in `Tests/macSCPCoreTests/ConnectionDiagnosticsJumpTests.swift:589`: `target.resolveOnJump came back skipped("the jump host was not reached")` where the case expects `.unavailable(DiagnosticReason.jumpProbeHostRefused)`. The other six passed, which is the shape of a race rather than a wrong answer — a logic error would fail all seven identically. **The mechanism, and why this case is sensitive:** the test's factory (`:1694`, `:1704`) passes `stepTimeout: .seconds(5)`, which is a probe budget an assertion depends on, i.e. a wall-clock ceiling in effect (CLAUDE.md). `DetachedProbe` arms its deadline at CREATION, so that budget bounds cooperative-pool queueing latency as well as the work — the same mechanism root-caused on 2026-09-19 for run 35405472152. A jump dial that waits for a thread longer than five seconds settles by its deadline, and the step is skipped before it can reach the refusal the case exists to check. **What this is NOT:** the branch that surfaced it does not touch this test file (unchanged since `df018e89`, which shipped green), and its only code change in `Sources/macSCPCore/Diagnostics/DialProbes.swift` is one new `case .finding` arm, which cannot alter a path whose error is not a finding. **The numbers, counted in this pass, and they correct a claim I made first.** Same code, four attempts: Build 20 min+ (cancelled), 3:29, 4:10, 3:58; Unit-Tests never reached, 4:55 (red), 6:26 (green), 5:57 (green). The base `43330df0` the day before: Build 2:55, Unit-Tests 4:18. So on IDENTICAL code the build varied by more than sixfold (3:29 to over 20 minutes) and the test step by 30% (4:55 to 6:26). The branch added 25 tests to 6598, **0.38%**, which cannot drive a swing of that size — the first explanation offered ("our added load pushed a marginal test over") is not supported by these numbers and is withdrawn. The runner's own variance is the driver; the five-second budget is what makes this case notice it. **Rate so far: 1 red in 3 test-step runs of this code** — two at `9d58cbfb` (4:55 red, 6:26 green) and one at `08c728be` (5:57 green), which adds only this row and so compiles the same sources — and green in every local full run (ten cores, so the queueing this depends on does not arise here). One red run is not a rate; this row exists so the next sighting adds to a count rather than starting one. **Sibling of** "Wall-clock ceilings still in the tree" above, which measured 8 files, 12 `waitUntil` definitions and 79 callers on 2026-09-04 — this is a thirteenth shape of the same defect, and the fix belongs with that row rather than raising this one budget. Not started. **Second red, 2026-09-28, run 36443098104 at `029661bd`, and it names the mechanism outright.** Three cases, all in `Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift`: `aBackendErrorsOwnDescriptionNeverReachesTheRow` (`:1138`), `aForeignErrorsLocalizedSentenceNeverReachesTheRow` (`:1209`) and `aURLWithUserinfoNeverReachesTheReport` (`:1291`). **Each failed after 49.6 s** in a run whose whole suite took 67.175 s — they were waiting for a thread, not computing. The outcome read `.notStarted("this Mac was too busy to start the measurement")` where the cases expect `.failed(…)`. That outcome is the third one added on 2026-09-25 by the answered-decisions plan for exactly this state, and these three cases predate it. **What went red is the POSITIVE COMPANION, not the property.** All three are secrecy cases, and every negative check in them — the planted credential absent from the outcome, the detail, the plain text and the Markdown — passed. Red were `dial.outcome == .failed(neutral)` and `== .failed(rendered)`, the assertions that exist so the negatives cannot pass by reading nothing. This is CLAUDE.md's "a negative check needs a positive check beside it" doing its job: without those two lines the three cases would have gone GREEN while checking nothing at all. **The fix is not to relax them.** Either the cases drive the dial through a launch seam instead of a real `DetachedProbe` (`ConnectionDiagnosticsJumpTests` already injects `jumpDialLaunch:`), or `notStarted` becomes a state they assert about deliberately. Raising a budget would only move the threshold. **Counted across today: seven test-step runs of this code, two red** — run 36435034775 attempt 2 (the jump case) and run 36443098104 (these three) — with no test failing twice, which is what a starvation class looks like rather than one broken case. **Partly addressed 2026-09-28** (`70624fb4`): `ConnectionDiagnosticsTests`'s own `run` helper defaulted to `stepTimeout: .seconds(5)` over bodies built from `constantContribution`, which compute nothing, so the budget measured only whether the runner would hand out a thread. Counted before changing it — 12 call sites, the 2 that pass their own budget are exactly the two cases that WANT the deadline to fire — so the new 60-second default reaches ten cases and cannot reach those two; the suite gained `.timeLimit(.minutes(5))` as the hang bound, the shape `aStepBeyondItsTimeoutIsSettledByItsDeadline` already uses. Proved rather than assumed: planting `case .answered: return timer.finish(.ok, "")` in `ConnectionDiagnostics.bounded` turns four of the five red at exactly the assertions CI reported, so they are still sharp; `theS3DialWithACredentialInItsEndpointLeaksNeither` stayed green under that plant and is pinned by neither. **Run 36447816570 went green after the fix, and that proves nothing** — the same code went green twice between the two reds. What would be evidence is a comparable count of runs, which this row is here to accumulate. **Closed 2026-09-28** (`ae19a53a`) for both remaining shapes. The jump half: `ConnectionDiagnosticsJumpTests` carried the same five-second default — 24 call sites of `Self.diagnostics(`, the 2 with their own budget being `.milliseconds(200)` (a case that wants the deadline) and `.seconds(60)` (the value already chosen here once) — so both factory defaults went to sixty seconds and the suite gained `.timeLimit(.minutes(5))`; planting `jumpProbeHostRefused → jumpExecRefused` turns the case red. And the last `.ok`-chain over real probes, `theRunnerWalksTheUniversalStepsInTheOrderTheReportPrints`, now asserts the order exactly with “no row failed” beneath it — the treatment its jump twin got in `ac894cb9`; planting a `.failed` return in `bounded` turns it red at `failed.isEmpty`. **A correction to this row's own first version, which said “the other 8 files the parent row measures”.** That number was read from the MIDDLE of the parent row and is superseded by its own “Done 2026-09-04” section. Measured at HEAD on 2026-09-28: `grep -rln "let deadline = ContinuousClock.now + " Tests/` → **0**; all **4** `elapsed <` hits are prose in doc comments; the **5** surviving `waitUntil`-shaped helpers carry no clock at all (no `ContinuousClock`, no `Date()`, no `sleep`, no `deadline`); `pollUntil` is reached from **43** files. The polling half was genuinely finished three weeks ago, and this row propagated a stale figure for half a day. | @@ -348,7 +348,7 @@ changes it in the entry and at most updates the line here to match. | `scripts/mutation-probe` has no tests at all, and every probe verdict in the diff-view plan's record came from it | **Open, recorded 2026-10-16** by the final fix wave of the diff-view plan. **What exists.** `scripts/mutation-probe` is the harness this project's mutation testing runs through, and `CLAUDE.md` requires it for any plant that edits a tracked file. Its verdict logic is the whole of its value: nine `RESULT:` endings that must not be conflated, a control run whose failures are subtracted from the planted run's, a known-issue and warning filter, a `RAN < CONTROL_RAN` ending, and a declaration-location strip so a plant that moves a test's line does not make the control's own failure look like a catch. **Nothing tests any of it.** Every `mutation-probe` hit under `Tests/` is a doc comment recording a past run: `/usr/bin/grep -rln mutation-probe Tests` prints five file paths, one per line (`CLIMatrixITests.swift`, `ArchivePreparationTests.swift`, `CitadelFileSystemIntegrationTests.swift`, `Support/CLIMatrix.swift`, `ArchiveCommandChannelTests.swift`), and reading each shows no invocation — and `/usr/bin/grep -rn mutation .github/workflows/` returns nothing, exit 1 (both run 2026-10-16). **Corrected 2026-10-16 in the record round; two faults in one sentence.** It read “`/usr/bin/grep -rlno mutation-probe Tests` ⟨SEP⟩ `wc -l` prints the files”, where ⟨SEP⟩ stands for a shell pipe written with a literal cell separator. **This correction spells that character nowhere**, for the reason the row “A stray cell separator truncates the body the census commands read” gives of itself: a literal one here would be the defect rather than a description of it, and eliding that one character is what lets the rest of the withdrawn wording stand verbatim. The sentence as committed is recoverable from the commit that introduced it, `ea330ada`. Two faults, then. The command field-split this row where every census command in this file's closing section splits a table row, so the first of them read a body that stopped mid-command. And it does not print the files: `-l` makes `-n` and `-o` inert, so run as committed it printed `5`. The claim it was making is true and is now made by a command that prints it and needs no pipe at all. **Why it matters now.** Three defects in that verdict logic were found during this plan and two classes were fixed (the script's own header records all three, measured against `f0269692`); the measurement was by hand and the record is prose, so a loosening of the logic would be caught by nobody — and a loosening reads as an all-clear, which is the exact failure the script was written to refuse. **What a fix looks like.** A shell test that drives the script against a throwaway git repository with a one-test package, one case per ending: a plant that does not compile (`BUILD FAILED`), a pattern that matches nothing (`PLANT CHANGED NOTHING`), a filter that matches no test (`NO TESTS RAN`), a pre-existing failure (named apart, never as a catcher), a `withKnownIssue` test (not a catcher), a warning-only `Issue.record` (not a failure), a plant that ends the runner early (`RUN INCOMPLETE`), a real catch (`RED`), and a plant nothing notices (`GREEN`). The dirty-tree gate and the index-safe revert are two more. The cost is a fixture package and a slow test, which is why this is a row and not a task. Not started. | | Which of two selected files is the LEFT side of a diff is decided by the sort, not by the click order | **Open, recorded 2026-10-16** by the final fix wave of the diff-view plan. **What exists.** `ComparisonRequest.forTwoSelected(_:on:)` makes the first of the two selected rows the left side and the second the right. The selection reaches it from `RemoteFileTableView`'s `currentSelection()` and `menuNeedsUpdate`, both of which read `table.selectedRowIndexes` — an `IndexSet`, which iterates in ascending row index — and map it through `items`. So the order is the table's, which is the SORT order: clicking a column header swaps which file is the diff's left-hand side, and the order the user clicked the two rows in is recorded nowhere. The doc comment said “in selection order” until 2026-10-16 and now says what it is. **Why it may be fine.** The table order is the one order both the user and the window can see, and a comparison is symmetric apart from which column a line lands in. **Why it may not.** Re-sorting the pane silently reverses every `-` and `+` in a diff that is already open, and a user who clicked the newer file first has no way to say so. **What a fix would be.** Record the click order in the table coordinator (an ordered list the selection callback appends to and prunes) and hand THAT to the factory; the window would also want to say which side is which, which it does today only by the two root paths in its header. The behaviour was deliberately not changed by the fix wave that recorded this. Not started. | -| The only layout guard in the tree asserts point figures measured on Darwin 27, and CI runs macos-15 | **Open, recorded 2026-10-17** by the adjudication of `5855b592`, flagged by that fix's own implementer rather than found later. `FileActionEditorLayoutGuardTests` compares a measured width against `FileActionEditor.sheetWidth`, and both sides of that comparison are font metrics. Every figure in its record was taken locally on Darwin 27; the CI runner is macos-15 and has never run the suite, so whether the 109 points between German's 411 and the granted 520 absorb a text-engine difference is untested rather than argued. Those 109 points were chosen to absorb exactly that, with no measurement behind the choice. **Next step** is to read the four figures off a CI run of this suite and write them beside the local ones, before the guard is extended to the other thirteen fixed-width views and the same unverified assumption is made fourteen times. **Done 2026-10-18 for its first claim only; the second claim is NOT answered and moved to a row of its own.** The title's first half, that the guard is the only one of its kind, stopped being true when `BoundedViewLayoutGuardTests` extended the measurement to all 33 bounded views (`4199f784` through `2eb05835`): `FileActionEditorLayoutGuardTests` no longer exists and `FileActionEditor.sheetWidth` is now `FileActionEditor.bound`. Withdrawn, quoted verbatim: the body began “`FileActionEditorLayoutGuardTests` compares a measured width against `FileActionEditor.sheetWidth`”, which names a suite and a constant the tree no longer has. The second half, that the figures were taken on Darwin 27 and the CI runner is macos-15, was NOT answered: the plan's Task 1, which was to read the four figures off a CI run, was deferred and never run, because `.github/workflows/ci.yml` starts a run only on a push to `main` or `develop` and on `pull_request`, and the plan was not to open a pull request without the maintainer. The sentence “the CI runner is macos-15 and has never run the suite” is therefore still true of this branch, and the sentence “before the guard is extended to the other thirteen fixed-width views and the same unverified assumption is made fourteen times.” is the one the plan went on to do: the assumption is now made 33 times, not fourteen. Closing this row without that reading would have been closing it by association, so the open question is carried by the row opening “Every layout figure the guards hold was measured on Darwin 27”. | +| The only layout guard in the tree asserts point figures measured on Darwin 27, and CI runs macos-15 | **Open, recorded 2026-10-17** by the adjudication of `5855b592`, flagged by that fix's own implementer rather than found later. `FileActionEditorLayoutGuardTests` compares a measured width against `FileActionEditor.sheetWidth`, and both sides of that comparison are font metrics. Every figure in its record was taken locally on Darwin 27; the CI runner is macos-15 and has never run the suite, so whether the 109 points between German's 411 and the granted 520 absorb a text-engine difference is untested rather than argued. Those 109 points were chosen to absorb exactly that, with no measurement behind the choice. **Next step** is to read the four figures off a CI run of this suite and write them beside the local ones, before the guard is extended to the other thirteen fixed-width views and the same unverified assumption is made fourteen times. **Done 2026-10-18 for its first claim only; the second claim is NOT answered and moved to a row of its own.** The title's first half, that the guard is the only one of its kind, stopped being true when `BoundedViewLayoutGuardTests` extended the measurement to all 33 bounded views (`4199f784` through `2eb05835`): `FileActionEditorLayoutGuardTests` no longer exists and `FileActionEditor.sheetWidth` is now `FileActionEditor.bound`. Withdrawn, quoted verbatim: the body began “`FileActionEditorLayoutGuardTests` compares a measured width against `FileActionEditor.sheetWidth`”, which names a suite and a constant the tree no longer has. The second half, that the figures were taken on Darwin 27 and the CI runner is macos-15, was NOT answered: the plan's Task 1, which was to read the four figures off a CI run, was deferred and never run, because `.github/workflows/ci.yml` starts a run only on a push to `main` or `develop` and on `pull_request`, and the plan was not to open a pull request without the maintainer. The sentence “the CI runner is macos-15 and has never run the suite” is therefore still true of this branch, and the sentence “before the guard is extended to the other thirteen fixed-width views and the same unverified assumption is made fourteen times.” is the one the plan went on to do: the assumption is now made 33 times, not fourteen. Closing this row without that reading would have been closing it by association, so the open question is carried by the row opening “Every layout figure the guards hold was measured on Darwin 27”. **The title's second half stopped being true on 2026-10-30**, and it is left standing above rather than edited, so this note can quote it: “and CI runs macos-15”. `a774ac4d` moved the gate to `macos-latest`. What that changed about the open question is read in the row this one handed it to, not here. | | `TabItemView` caps a tab's width at its root, and no `SheetBound` can express it | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan; found by the review of that plan's Task 5 and carried here instead of fixed. **What the plan promised.** Its goal sentence reads “Every view in `MacSCPAppKit` that bounds its own size declares that bound through one marker, is measured against it in all four catalogues, and contains no localized control that can be silently squeezed.” That sentence is not literally true. **The exception.** `grep -rn -F -e '.frame(minWidth: 120, maxWidth: 200, maxHeight: .infinity)' Sources/MacSCPAppKit/` returns one line, `TabStripView.swift:540`, in the private `TabItemView`, after its `.padding(.horizontal, 12)`. It bounds a tab's width between 120 and 200 points, so it can clip a title; it is not among the 33 conformers (`grep -c 'BoundedView' Sources/MacSCPAppKit/TabStripView.swift` prints 0); and none of the three commands the 2026-10-17 rows counted with could find it, because the call carries a `maxHeight` those patterns do not allow. **Why it is not a `SheetBound`.** `SheetBound` has three cases, `width`, `size(width:height:)` and `capped(minWidth:maxWidth:)`, and the last takes no height, so the frame has no spelling in it. A root-level `minWidth`-only floor cannot clip and is a different category; a cap that also carries a `maxHeight` is neither declared nor measured. **Not measured.** This row does not say that a tab title clips in any catalogue: the plan never hosted the view. **Next step** is the maintainer's choice between widening `SheetBound` (a fourth case, then a registry entry whose builder is a tab over a long title) and writing down that a tab is out of scope, which a scan could then hold by name. | | Nine registry entries are measured empty because `sizeThatFits` never runs `.onAppear`, and seven of them cover no row | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan. **What was measured.** `NSHostingController.sizeThatFits` lays a view out without appearing it, so a sheet that loads its rows in `.onAppear` reports the same figure with rows and without. `swift test --filter aSheetThatLoadsItsRowsOnAppearIsMeasuredEmpty` seeds a snippet store with two snippets, measures it and an empty one, and gets the same width and height from both. Eight listing sheets read their rows that way; `ImportFromSourceSheet` is the ninth, for a different reason: its rows arrive through `await model.load(...)` and a registry builder is synchronous. **The nine:** `AuditLogSheet`, `FileActionsSheet`, `HiddenImportsSheet`, `KnownHostsSheet`, `LoginSetsSheet`, `ServerCertificatesSheet`, `SnippetsSheet`, `SSHKeysSheet` and `ImportFromSourceSheet`. **Two are genuinely covered**, because the empty-state text they measure moves with the catalogue: `AuditLogSheet` (minimum width 157.5, 157.5, 202.5 and 207.5 of 640 for de, en, fr, pl, loose since its filter bar became a menu in `40f05b10`) and `HiddenImportsSheet`, the only entry on either axis whose HEIGHT moves (207, 191, 207 and 207 of 460). **Seven are not.** Their minimum is identical in all four catalogues on both axes, with 2.4 to 8.3 times slack (the earlier wording, in `b175deec`, was “with 3 to 8 times slack”; on width, with 640 over 267 for `FileActionsSheet` up to 780 over 94 for `ImportFromSourceSheet`): `FileActionsSheet` 267 of 640, `KnownHostsSheet` 148 of 720, `LoginSetsSheet` 231 of 720, `ServerCertificatesSheet` 130 of 900, `SnippetsSheet` 231 of 720, `SSHKeysSheet` 159 of 720 and `ImportFromSourceSheet` 94 of 780. What they still buy is a row in the set-equality scan and a red if a later change narrows a bound below the sheet's chrome; they do not measure a row, which is where a long localized or user-supplied string would sit. Figures from `MACSCP_BOUND_FIGURES=1 swift test --filter BoundedViewFigureReportTests` on 2026-10-18, which prints 40 lines that start with `boundfigures:`, one per registry entry. **Next step** is a way to put rows on screen that does not need `.onAppear`: take the loaded rows as plain inputs, as `TagFilterSheet` and `WhatsNewSheet` do, which is a source change to seven sheets; or host the view in a window so that it appears, which changes the instrument every other figure here was taken with. | | `GenerateKeySheet`'s RSA key-size picker is migrated to a rigid component and measured by nothing | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan. **What exists.** The `keys.generate.bits` row of `GenerateKeySheet` became a `SegmentedChoice` in `795cb7c5`. The sheet keeps its form in `@State private var form = GenerateKeyForm()` (`grep -n -F -e '@State private var form = GenerateKeyForm()' Sources/MacSCPAppKit/SSHKeysSheet.swift` prints one line), so a registry builder cannot select RSA, and the registry's one entry for the sheet, “a fresh form, with the type choice on screen”, measures the type picker alone. **What is known.** That entry measures a minimum width of 377 points in every catalogue against the 420 the sheet grants since `beff2664`, which widened it from 380 when the type picker left 3 points of slack (377 of 380); `MACSCP_BOUND_FIGURES=1 swift test --filter BoundedViewFigureReportTests` prints the line. The size picker's own width in any catalogue was not measured. **Next step** is to let a builder pass the form in (an initializer parameter with the current value as its default) and add a second entry with RSA selected. | @@ -359,7 +359,7 @@ changes it in the entry and at most updates the line here to match. | `scripts/mutation-probe` prints the names of the tests that caught a plant, deletes its log, and so never shows the issue message | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan. **What was measured.** The script reports `RESULT: RED` with the catcher test NAMES read from the event stream, and removes the suite log: `grep -n -F -e 'rm -f "$SUITE_LOG"' scripts/mutation-probe` prints three lines (under the session's ugrep and `/usr/bin/grep`). A guard's diagnostic content is its issue message, so a probe run through the harness shows that a test went red and not what it said. **Who hit it.** Task 7 of the sheet-bounds-and-control-rigidity plan, which worked around it by having the `--apply` command also run the filtered test into a scratch log, and which its ledger records as the second task of that plan to meet the limit. The workaround is a hand-rolled addition to a harness whose whole value is that nothing is hand-rolled. **Next step** is to keep the log when asked (`--keep-log `, say) or print the first issue line of each catcher. | | The rigid-control scan's allow-list is a debt register: five raw segmented sites survive outside any bounded view, and 37 `Toggle(` lines, one of which is now a measured compressible localized control inside a bounded view, are read by nothing | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan. `RigidControlScanTests` holds the compressible control styles to an exact allow-list. **Five raw segmented sites.** `grep -rnF '.pickerStyle(.segmented)' Sources/MacSCPAppKit/` returns 6 lines, under both greps: one is the component in `RigidControls.swift` and five are the survivors the allow-list names, two in `SessionExportImportSheets.swift`, one in `BrowserSheets.swift` and two in `TunnelProfilesSheet.swift`, none inside a `BoundedView`. They are listed because the plan migrated the controls inside bounded views and in the resizable containers and did not measure these; nothing says they are safe, and the list is a debt register that a migration will force an edit to. **The `Toggle(` lines.** The earlier wording of this row, in `b175deec`, was “37 styleless `Toggle(` sites are compressible checkboxes nothing scans”, and its command does not establish either property. `grep -rn -e '[^a-zA-Z]Toggle(' Sources/MacSCPAppKit` prints 37 lines under the session's ugrep and `/usr/bin/grep`: every `Toggle(` line, styled or not. `grep -rn -F -e '.toggleStyle(' Sources/MacSCPAppKit` prints 2 lines, `.checkbox` in `RigidControls.swift` (the component's own) and `.button` in `FileSearchBar.swift`, so two of the 37 are styled, and one more is a menu item in `MacSCPCommands.swift` (`window.keepOnTop`). **Measured 2026-10-18 by the branch's final fix wave, which upgrades this row from “style unclassified” to at least one confirmed compressible localized control inside a bounded view.** The sentences this replaces read “The other 34 are unclassified: whether each draws as a plain checkbox, sits in a grouped `Form`, or carries an empty label was not looked at. The plan's Task 11 review reported that on macOS a bare `Toggle("label", isOn:)` is a compressible checkbox by default; that was not measured here, and this row asserts it no further than that.” Thirty-three of the 34 are still unclassified in exactly that sense. The thirty-fourth is not: `RigidControlTests.theJumpToggleIsCompressibleWhereTheComponentIsNot` hosts a styleless `Toggle` carrying the real `de` catalogue value of `form.jump.toggle` (“Über einen Zwischenhost verbinden (ProxyJump)”) and reports minimum 21.0 against ideal 319.0 — a 298-point span the control absorbs with nothing overflowing — where the same label through `CheckboxRow` reports 319.0 for both. Per catalogue, bare and then through the component: `de` 21.0/319.0 and 319.0/319.0, `en` 21.0/332.0 and 332.0/332.0, `fr` 21.0/335.0 and 335.0/335.0, `pl` 21.0/302.0 and 302.0/302.0, printed by `MACSCP_BOUND_FIGURES=1 swift test --filter theJumpToggleIsCompressibleWhereTheComponentIsNot` on Darwin 27.0.0 (macOS 27.0.1, arm64); the test itself asserts the relationship rather than the figures. **And three of the 37 sit inside a bounded view.** `grep -n -e '[^a-zA-Z]Toggle(' Sources/MacSCPAppKit/ConnectionFormView.swift` prints `469`, `953` and `1253` (verified 2026-10-18 under both greps); each carries a localized label, `:953` is the jump sentence measured above, and `ConnectionFormView.bound` is `.capped(minWidth: 420, maxWidth: 460)`. `MACSCP_BOUND_FIGURES=1 swift test --filter BoundedViewFigureReportTests` prints `boundfigures: ConnectionFormView [a new SSH connection, with the schema's own fields] wide de 399.0/487.0 en 399.0/500.0 fr 438.0/503.0 pl 399.0/470.0 of 460.0`: the guard is green on the minimum in every catalogue while the ideal stands above what the bound grants, by 27 points in `de`, 40 in `en`, 43 in `fr` and 10 in `pl`. So something in that form is being squeezed today. WHICH control, and by how much, is NOT measured here: the entry's figure is the whole form's, and the three toggles are not measured apart from it. What is established is only that the scan reads none of the 37, so “the checkbox style appears only in the component” does not mean no compressible checkbox exists elsewhere. No allow-list was built over them, because it would be a register larger than the thing it guards. The same blind spot exists for a styleless `Picker`. **Next step** is to measure rather than list: put a representative of each site class in the registry, or decide which of the 37 sit in a container narrow enough to matter. The first such measurement is the one above; what is left is the 33 still-unclassified sites, the per-control share of `ConnectionFormView`'s squeeze, and the same blind spot for a styleless `Picker`. | | `sectionBodies()` in `CLISettingsCompletionGuardTests` reads the section and its extracted row together, which weakens strings that only the section should satisfy | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan; found by the re-review of its Task 10. `aece2e2b` extracted `CompletionShellRow` out of the CLI settings section, and `sectionBodies()` now returns the section's body joined with the row's, so a string the guard wants in the section can be satisfied by the row. Two checks were narrowed to the section's span alone (`CompletionShellRow(` in the section, the `allCases` iteration in the row); the re-reviewer's estimate is that about seven strings still read the union, among them `ShellCompletionRecipe.line(`, `Text(completionLine)`, `setString(completionLine,` and `CLICompletionPresentation.forState(`. That estimate was not recounted here; what was counted is that `grep -n -F -e 'sectionBodies()' Tests/macSCPAppKitTests/CLISettingsCompletionGuardTests.swift` prints 6 lines, the definition and five call sites. The row carries only a `shell` binding and cannot plausibly satisfy any of them today, so this is a latent weakening and not a current hole. Its `rowDeclaration` comment is also stale for one string. **Next step** is to give each string its own span, as the two narrowed ones have. | -| Every layout figure the guards hold was measured on Darwin 27, and no CI run has read them on macos-15 | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan, split out of the row “The only layout guard in the tree asserts point figures measured on Darwin 27, and CI runs macos-15”, which that closeout closed for its first claim only. **What is known.** The plan's Task 1, to read the four figures off a CI run, was deferred and never run: `.github/workflows/ci.yml` triggers on a push to `main` or `develop` and on `pull_request`, `grep -c -F -e 'workflow_dispatch' .github/workflows/ci.yml` prints 0, and `git branch -r --list '*sheet*'` prints nothing, so the branch has no remote. The plan went on with local figures, on the ruling that if macos-15 disagrees the remedy is to widen each bound to the maximum measured across catalogues and platforms, one pass over the registry. **The exposure is wider than when this was first written.** The guard measures 33 views instead of one. The thinnest margins on 2026-10-18, from `MACSCP_BOUND_FIGURES=1 swift test --filter BoundedViewFigureReportTests`: `TagFilterSheet` 306 of 320 (14 points), `ConnectionFormView` 438 of 460 (22 points, two entries), `GenerateKeySheet` 377 of 420 (43) and `PresignedURLSheet` 361 of 420 (59). The resizable-container floors, from `MACSCP_FLOOR_FIGURES=1 swift test --filter ResizableContainerFloorReportTests`, are thinner: the search bar needs 269.5 of 280 in French (10.5) and the sidebar's tags pop-up 137.5 of 150 (12.5). A text-engine difference between Darwin 27 and macos-15 of that size would turn a check red for a reason that is no defect. **Next step** is the first push or pull request that triggers a run, and then reading the checks' verdict; the two reporters are gated off, so the figures themselves need a run with the gates set. | +| Every layout figure the guards hold was measured on Darwin 27, and no CI run has read them on macos-15 | **Open, recorded 2026-10-18** by the closeout of the sheet-bounds-and-control-rigidity plan, split out of the row “The only layout guard in the tree asserts point figures measured on Darwin 27, and CI runs macos-15”, which that closeout closed for its first claim only. **What is known.** The plan's Task 1, to read the four figures off a CI run, was deferred and never run: `.github/workflows/ci.yml` triggers on a push to `main` or `develop` and on `pull_request`, `grep -c -F -e 'workflow_dispatch' .github/workflows/ci.yml` prints 0, and `git branch -r --list '*sheet*'` prints nothing, so the branch has no remote. The plan went on with local figures, on the ruling that if macos-15 disagrees the remedy is to widen each bound to the maximum measured across catalogues and platforms, one pass over the registry. **The exposure is wider than when this was first written.** The guard measures 33 views instead of one. The thinnest margins on 2026-10-18, from `MACSCP_BOUND_FIGURES=1 swift test --filter BoundedViewFigureReportTests`: `TagFilterSheet` 306 of 320 (14 points), `ConnectionFormView` 438 of 460 (22 points, two entries), `GenerateKeySheet` 377 of 420 (43) and `PresignedURLSheet` 361 of 420 (59). The resizable-container floors, from `MACSCP_FLOOR_FIGURES=1 swift test --filter ResizableContainerFloorReportTests`, are thinner: the search bar needs 269.5 of 280 in French (10.5) and the sidebar's tags pop-up 137.5 of 150 (12.5). A text-engine difference between Darwin 27 and macos-15 of that size would turn a check red for a reason that is no defect. **Answered in part 2026-10-31, and the premise it rests on withdrawn.** The next step named here happened. Withdrawn, quoted verbatim: “**Next step** is the first push or pull request that triggers a run, and then reading the checks' verdict” — PR 8 (created 2026-10-29T18:44:18Z, merged 2026-10-30T12:18:46Z) ran the suite seven times on the branch `ci/runner-image-with-a-current-xcode`, two of them green. Withdrawn with it, quoted verbatim: the title's “and no CI run has read them on macos-15” and the body's “A text-engine difference between Darwin 27 and macos-15 of that size would turn a check red for a reason that is no defect.” Neither can be answered as written any more, because the gate no longer runs macos-15: `a774ac4d` moved it to `macos-latest`, which reports itself as macOS 26.6.2, build 25G83, Swift 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101), target arm64-apple-macosx26.0. **What the two green runs show.** Both ran the guards and both passed them. Run 41048445836 printed `Suite "Bounded view layout guard" passed after 133.328 seconds` and `Suite "Resizable container floors" passed after 110.880 seconds`; run 41047833837 printed the same two suites at 164.481 s and 139.807 s. Read either with `gh run view 41048445836 --repo NoiXdev/macSCP --log` and search the output for `Suite "Bounded view layout guard"`. So the local Darwin 27 figures survive a Darwin 26 runner, for all 33 bounded views and for the resizable floors. **What is still open is the part this row was actually about.** By how much they survive is unmeasured: both reporters are gated behind `MACSCP_BOUND_FIGURES=1` and `MACSCP_FLOOR_FIGURES=1`, so the runs produced a verdict and no figures, and the thinnest margin on record — the search bar's 10.5 points in French — is still a local number. A pass is not a margin, and a margin is what a later catalogue change will spend. **Next step** is one gated run on the CI image, reading the 33 bound figures and the floors off it and writing them beside the local ones. | | The rigidity work's coverage debt, in one row: the newest control kind has no component, three checks cannot fail, one seam is unfenced, and two labels collide | **Open, recorded 2026-10-18** by the final fix wave of the sheet-bounds-and-control-rigidity plan, which folded NINE findings into one row rather than nine, because each is a hole in the same net and none of them is worth a page. **Corrected 2026-10-18, same day:** this read “which folded seven findings into one row rather than seven”; items (8) and (9) were added when the branch's last fix round deferred them here. Every figure below was re-run on 2026-10-18 from the repository root, under the session's ugrep and `/usr/bin/grep` alike. **(1) The menu is the kind the branch reached for, and it is the one kind without a component.** `SegmentedChoice`, `RadioChoice` and `CheckboxRow` exist; there is no `MenuChoice`. `grep -rn 'pickerStyle[(][.]menu[)]' Sources/MacSCPAppKit` prints 7 lines (parentheses bracketed so this row does not match its own pattern): `ConnectionFormView` `:986` and `:1093`, `AuditLogSheet:120`, `DiagnosticsPanel:529`, `FileSearchBar:120`, `SheetFacetPicker:53`, `SidebarTagFilterBar:218`. Each is a `Picker`, the style and a hand-written `.fixedSize()`, and the only thing holding that `.fixedSize()` in place is `RigidControlScanTests.everyHandWrittenMenuCarriesFixedSize` plus the rule in `RigidControls.swift`'s header — a rule and a scan where the other three kinds have a type that will not compile the violation. Two of the seven were created by this branch as the remedy for a real defect, which is what makes the asymmetry worth writing down. **(2) Nothing fences the one production seam that can blank 33 sheets' frames.** `SheetBound.isSuspendedForMeasurement` is a `@TaskLocal` declared in `Sources/MacSCPAppKit/SheetBound.swift:77` and read at `:85`; bound to `true` it makes `sheetBound(_:)` apply no frame at all. `grep -rn -F -e '$isSuspendedForMeasurement' Sources` prints 0 lines and the same command over `Tests` prints 11, so today only tests bind it — and no check says so. A single `withValue(true)` in a view's body would leave every width and height guard green while the app drew unbounded sheets. The branch built scans against a typealias and a refining protocol of `BoundedView`, routes nobody has taken; this is the route that exists. **(3) `BoundedViewVisibilityTests.everyBoundedViewIsReachableFromTests` is a tautology, and its doc comment is false.** The test builds a 14-element array of metatypes and asserts `types.count == 14`; the compile is the only check, which the file says outright. Of the fourteen, eleven are constructed by `BoundedViewRegistry` anyway (counted 2026-10-18: each of the eleven names appears in `Tests/macSCPAppKitTests/BoundedViewRegistry.swift`), `TagFilterJoinPicker` by `ResizableContainerFloorTests:248`, and `TerminalSettingsTab` and `CLISettingsSection` by nothing — `grep -rn 'TerminalSettingsTab(' Tests` and `grep -rn 'CLISettingsSection(' Tests` both print nothing. So this list is the only reason Task 3 widened those two production types from `private` to internal. The doc comment says “The views that bound their own size” and “every one of the fourteen”, and three of the fourteen are not `BoundedView`s at all: `grep -rn 'struct TerminalSettingsTab' Sources/MacSCPAppKit`, the same for `CLISettingsSection` and `TagFilterJoinPicker`, print `: View {` in each case. **(4) A seventh resizable container lands with nothing red.** `ResizableContainerFloorTests.containers` is a hand-written list of six, and the completeness check beside it, `allSixContainersAreMeasuredAndNoneMeasuresAsNothing`, compares that list against a hardcoded set of the same six names — two literals edited together, which is not an independent reading. The contrast is in the same tree: a `BoundedView` is found by a protocol marker, `grep -rn -e ': BoundedView {' Sources/MacSCPAppKit/` prints 33, and set equality against the registry is what proves completeness there. **(5) `LayoutMeasureTests.buildingOutsideTheOverrideUnderMeasures` goes red on a German-preferred machine.** It asserts that a view built OUTSIDE the locale override measures narrower than one built inside it, which holds only while the process language differs from the locale measured (`de`). The plan ledger records the outside figure for `de` as 373 against 411 inside, measured on an English-preferred machine. On a machine whose `AppleLanguages` begins with `de` the two are the same view and the expectation is false. **Predicted, not measured**: no such machine was available here, and no CI runner has one. The fix shape is to pin the SwiftUI behaviour against a locale the process cannot be in, or to skip when the process language matches. **(6) `.labelsHidden()` and the components' `accessibilityLabel` argument are unfenced.** `grep -rn -F -e 'accessibilityLabel' Tests/macSCPAppKitTests/RigidControlTests.swift Tests/macSCPAppKitTests/RigidControlScanTests.swift` prints nothing, and every component sample in `RigidControlTests` passes the empty string, so neither the argument reaching the control nor the picker's own label staying hidden is held by anything. Deleting `.labelsHidden()` from `RigidControls.swift` was already listed as unexercised by the Task 8 review. **(7) Two accessibility collisions, both created by the migration.** `grep -n -F -e 'LabeledContent(label)' Sources/MacSCPAppKit/SettingsView.swift` prints `:1024` and `:1928`, and in both the same `label` is passed again to the `SegmentedChoice` inside it, so assistive technology may announce it twice. And `grep -n -F -e 'sidebar.filter.button' Sources/MacSCPAppKit/SidebarTagFilterBar.swift` prints four lines: `:98`, the filter button's own title; `:101`, a different key (`sidebar.filter.button.selected %lld`); `:128`, the dialog's headline; and `:200`, where this branch gave the join picker that same key as its accessibility label — one string now naming three different things. **(8) Two array indices stand in for names.** `RigidControlScanTests` reaches its component list as `Self.components[0]` (the segmented kind) and `Self.components[2]` (the checkbox), in `theCallChainReaderSeesWhatItIsWrittenToSee` and `theCallSiteScanReadsCodeAndNotProse`. Reordering the literal repoints both silently, and this project's rule is that a guard spelling something it could read is waiting for a rename — an index is the same bet with no name at all. The fix is a lookup by `name`. Deferred here by the coordinator on 2026-10-18 rather than taken in the final fix wave. **(9) A second `NSWindow` nobody closes.** `RigidControlTests.renderedSegmentedWidth` builds one per call and never closes it: three shapes times four catalogues is twelve a run, in a suite that runs on every CI build. It is the same defect the fix wave removed from `BoundedViewStateOnScreenTests`, and the fix is the three lines that one now carries — `window.isReleasedWhenClosed = false` and `defer { window.close() }` beside the window's construction, plus the comment saying why the first line comes before the second (ARC owns the window, so a close that also released it would be a second release). Deferred here on 2026-10-18 for the same reason as (8). **Not in this row**, because the fix wave took it instead: `BoundedViewStateOnScreenTests` opened sixteen `NSWindow`s a run and closed none; it now closes each. **Next step** is the maintainer's to scope. The cheapest two are (7), which is two edits and a catalogue key, and (3), which is a deletion plus narrowing two types back to `private` if nothing else needs them. (1) is the one with design in it. | | `RemoteFSError.bucketListEmpty` has no producer since `612f1ece`, and six comments still describe it or its sibling as they were | **Open, recorded 2026-10-19** by the fix round of Task 6 of the s3-bucket-settings plan, from that task's review. **What changed.** `612f1ece` removed the only `throw` of `RemoteFSError.bucketListEmpty` from `S3FileSystem.connect`, so an account with no buckets connects. **What was measured.** `grep -rn -e 'throw RemoteFSError.bucketListEmpty' -e 'throw .bucketListEmpty' Sources --include='*.swift'` prints nothing and exits 1; the one hit before the change was `S3FileSystem.swift` line 177 of `b4620035`. The case still has consumers: `grep -rn -e 'bucketListEmpty' Sources --include='*.swift'` prints 19 lines, which are 20 occurrences by `grep -rno -e 'bucketListEmpty' Sources --include='*.swift'` (that command prints 20 lines, one per occurrence, because `TransferFailureKind.swift` line 101 holds two). One of the 20 is a comment in `S3FileSystem.swift` that names the case on purpose. `grep -rn 'core.connect.s3BucketListEmpty' Sources --include='*.strings'` prints four lines, one per catalogue. **The five comment sites that are now false or stale, each opening with the words quoted here.** (1) `ConnectionViewModel.swift` near line 2306, “The two S3 bucket-list outcomes (2026-09-02), both reachable ONLY” — one of the two is reachable from nothing. (2) `ConnectionViewModel.swift` near line 2310, “`.bucketListEmpty` is connect-time only — `connect` is its single” thrower — there is no thrower. (3) `ConnectionViewModel.swift` near line 2319, “`.bucketListEmpty` blames no row, because nothing in the form is wrong” — still a true description of the arm, which is now dead; listed because it goes with the arm. (4) `RemoteBrowserViewModel.swift` near line 1278, “`.bucketListEmpty` needs no arm — `connect` is its only thrower” — the first half is false, and its second half, “a later empty account is an empty browser, not an error”, is now true of every empty account. (5) `RemoteFSError.swift` near line 24, the doc comment on the case, “because a connect that opens on an empty browser explains nothing at the form” — the reason `612f1ece` withdrew in `S3FileSystem.connect`, still standing where a contributor reads what the case means. **A sixth, older and not caused by that commit:** `RemoteFSError.swift` near line 16 says `bucketListForbidden` is “only at connect time”, which `RemoteBrowserViewModel.swift` near line 1274 contradicts with a measured note (`listBuckets` runs again on every listing of `/` and on a `stat` of a bucket). Task 6's own report first cleared that line as “still true”; its review found it stale. Line numbers are approximate on purpose: they moved before and will again, so find each by its quoted words. **Also unreachable, outside Core:** row 2 of the §2 table in `docs/superpowers/specs/2026-09-02-s3-bucket-browser-design.md` (“200 with zero buckets”) describes an outcome that no longer occurs; Task 13 of the same plan carries that and the four catalogue strings. **The decision this row asks for.** Keep the case, as a reserved outcome, or delete it with its consumers and its four catalogue strings in one pass, with its own review: the localization parity checks read every key a case declares, so the strings cannot go while the case stands. Either way the five comments change in that same pass. **Recount before acting:** run the four commands above again, since every figure here is a count of a tree that moves. **Re-checked 2026-10-22, by Task 13 of the same plan, at `f3277b1c`** (which is `b1537a05` plus four commits: the ledger, two backlog rows and one comment-only edit in `S3FileSystem.swift`). **The four commands above reproduce.** Run unedited, once with `/usr/bin/grep` and once with the session's `grep` (which is `ugrep -G`; none of the four carries a `$`, so the two agree): the first prints nothing and exits 1; the second prints 19 lines; the third prints 20 lines, one per occurrence; the fourth prints four lines, one per catalogue. The figures are the ones this row gave, and the S3FileSystem comment edited in the same pass (`c6dda748`) did not change them, since the recount ran after it. **The six comment sites are all still in the tree, each once, and this row names them correctly:** five, and the older sixth. One command finds the line holding a fragment of each, `/usr/bin/grep -n -F -e 'The two S3 bucket-list outcomes' -e 'is connect-time only' -e 'blames no row, because' -e 'needs no arm' -e 'explains nothing at the form' -e 'only at connect time' Sources/macSCPCore/Presentation/ConnectionViewModel.swift Sources/macSCPCore/Presentation/RemoteBrowserViewModel.swift Sources/macSCPCore/RemoteFS/RemoteFSError.swift`, which prints six lines, one per site, in `ConnectionViewModel.swift` at 2307, 2310 and 2320, `RemoteBrowserViewModel.swift` at 1293 and `RemoteFSError.swift` at 16 and 26. Two of those lines are the second line of a wrapped sentence (the site that opens “`.bucketListEmpty` blames no row” puts its first words at the end of the line before, and the site on the case's doc comment begins one line above 26). **The numbers “near line 1278” and “near line 24” above have drifted** (`RemoteBrowserViewModel.swift` is now 15 lines further down for that site, `RemoteFSError.swift` one); the row said they were approximate on purpose and they are, so find each by its words. **One more occurrence was looked at and not added:** `RemoteBrowserViewModel.swift` near 1330 names `bucketListEmpty` in a comment as an example of what “used to print as a bare case name”. That is a description of a past rendering, which `612f1ece` does not make false; this is Task 13's judgement, not the reviews'. **The artifacts outside Core, as this row said Task 13 carries them.** Row 2 of the §2 table of `docs/superpowers/specs/2026-09-02-s3-bucket-browser-design.md` is corrected in place by `280eff4b`, with the row's cells kept and a correction paragraph below the table. The plan's Task 13 cited that row as line 44. It is line 45: `git grep -n -F -e '200 with zero buckets' b1537a05 -- docs/superpowers/specs/2026-09-02-s3-bucket-browser-design.md` prints line 45, as does the same command at `612f1ece`, and the file has not changed since 2026-09-02, so the citation was one off when it was written. The four `core.connect.s3BucketListEmpty` catalogue strings are not touched, as this row said they must not be while the case stands. **The decision this row asks for is still open** and is the maintainer's: keep the case, or delete it with its consumers and strings in one pass with its own review. | | `macscp-probe-lock` is still on the Docker rig, under a COMPLIANCE retention until 2026-12-31, and a gated test that asserts on the bucket list will see it | **Open, recorded 2026-10-22** by Task 13 of the s3-bucket-settings plan, from the design's section 4 (`docs/superpowers/specs/2026-10-19-s3-bucket-settings-design.md`, the paragraph “Left behind by the probe”), which was written on 2026-10-19. **What is there.** A bucket named `macscp-probe-lock` on the S3 half of the Docker rig. The design says it “holds one object under a COMPLIANCE retention until 2026-12-31, which by design nobody can delete”. The same design measured a version under COMPLIANCE retention answering 403 on delete even with the governance bypass header (its section 4 table). The ledger's account of how it got there is “undeletable until 2026-12-31 because of a careless COMPLIANCE write already made on this branch” (`docs/superpowers/ledgers/2026-10-19-s3-bucket-settings.md`, the dispatch of Task 10). **When it was last seen.** Task 4's report records, after its last gated run, that “`GET /` on the rig lists `macscp-probe-lock`, `macscp-second`, `macscp-seed` only (checked)” (`docs/superpowers/ledgers/2026-10-19-s3-bucket-settings/task-4-report.md`). It was NOT looked at on 2026-10-22: the rig is not running that day (`docker ps` showed no SSH on port 2222 and no store on port 19000, per the ledger's Task 13 dispatch entry). So “still there” rests on Task 4's observation and on the retention date, not on a check made when this row was written. **What it touches.** (1) Any gated test that asserts on the root key's bucket LIST sees a third bucket. Read from source on 2026-10-22, not run: the two gated tests in `Tests/macSCPCoreTests/S3FileSystemIntegrationTests.swift` that list `/` in bucket-list mode are `rootKeyWithTheToggleListsBothBuckets`, which checks membership with `names.contains(...)` and so tolerates the extra bucket, and `scopedKeyWithTheToggleSeesOnlyItsOwnBucket`, whose key's policy names `macscp-seed` only. Both lines can be found with `/usr/bin/grep -n -F -e 'names.contains("macscp-second")' -e 'items.count == 1' Tests/macSCPCoreTests/S3FileSystemIntegrationTests.swift`. (2) `docker/test-server/README.md`, section “Proof, measured 2026-09-25 (RustFS)”, prints “status=200 names=['macscp-second', 'macscp-seed']” for the root key's ListBuckets. That is a dated record, true on its date, and it is not edited here; it is no longer what the rig answers while the bucket stands. (3) The four gated suites this branch added and their shared fake carry a header comment saying that the rig's seeded buckets and `macscp-probe-lock` are never named by them (the comments' own claim, not re-checked here); the comments are found with `/usr/bin/grep -rl -F -e 'macscp-probe-lock' Tests`, which lists `S3BucketSettingsGatedTests.swift`, `S3CORSGatedTests.swift`, `S3CreateBucketGatedTests.swift`, `S3ObjectLockGatedTests.swift` and `Support/S3SettingsFakeTransport.swift`. **The decision outstanding, which is the maintainer's.** The design states it in these words: “Either it stands until then, or the rig's volume is reset and reseeded (`up -d` reseeds idempotently).” This row records the leftover and does not resolve it. A reset has to be done from the main checkout, never from a worktree, and nothing here puts a COMPLIANCE retention on a bucket that is meant to be deleted. **After 2026-12-31** the retention lapses by its own date; the bucket and its object are then ordinary and can be emptied and removed by hand, which this row has not tried. | @@ -380,6 +380,8 @@ changes it in the entry and at most updates the line here to match. | `RemoteFSFindingTests.everySample`'s doc comment says “five of twenty-three”, and the switch it counts has 26 | **Open, recorded 2026-10-22** by Task 13 of the s3-bucket-settings plan, from the ledger's Task 5 re-review entry (“Deferred minor, and it is an instance of the very rule it was found under”), which flagged it for the final review. **What is there.** The doc comment of `everySample` in `Tests/macSCPCoreTests/RemoteFSFindingTests.swift` reads “counted in the switch below, 2026-10-19: five of twenty-three”. `awk '/static var everySample/,/static var everyCase/ { if ($0 ~ /return \(/) n++ } END { print n }' Tests/macSCPCoreTests/RemoteFSFindingTests.swift` prints 26 on 2026-10-22 (a count of lines in the range that open a `return (`, which is the ledger's own command with its pipe folded into the `awk`; the ledger's `awk` and `grep -c` form printed 26 as well when run today). The ledger: it “was already 24 at `1ba02d92`, so the number was off by one before this round and this round added two cases without touching it”. The “five” half, the findings that carry a payload, is correct. **Why it is a row and not an edit:** a Minor does not enter the fix loop, and a controller fix would skip review. This is the project's rule on cardinality in comments, in a test file, with the counting command beside the corrected figure. | | A store that pretty-prints inside a CORS leaf element would hand the sheet a rule that macSCP's own validation refuses | **Open, recorded 2026-10-22** by Task 13 of the s3-bucket-settings plan, from the ledger's Task 3 re-review entry (“Deferred minor: because M-1 dropped the trim on text values ... worth a backlog line at closeout”), re-measured here. **The cause.** `S3SettingsXML.parseCORS` keeps text exactly as written, spaces included (`S3CORSTests.spacesAroundAValueSurviveTheRoundTrip` holds that, deliberately). **The measurement.** A document with each of an `AllowedOrigin` and an `AllowedMethod` whose text is a line feed, two spaces, the value, and a line feed parses to origin scalars beginning 10, 32, 32 and ending 10, and a method whose scalars are 10, 32, 32, 71, 69, 84, 10; that rule's `validate()` answers `methodNotAllowed` with the whole string. Measured 2026-10-22 by a throwaway Core test (deleted, not in the tree), so the figures are not reproducible from a committed command. **Why it is not a defect today.** The ledger records that AWS and the rig emit compact leaves; neither was re-checked here (the rig is not running). **What it would be:** a rule the store itself returned shown as invalid. Fixing it means deciding whether the parser trims, which the Task 3 fix round decided against for a reason that is in the ledger; this row does not reopen that. | | Sight checks for the S3 bucket settings (2026-10-22 build) | **Open, recorded 2026-10-22**, grouped from the s3-bucket-settings plan's design (`docs/superpowers/specs/2026-10-19-s3-bucket-settings-design.md`) and ledger (`docs/superpowers/ledgers/2026-10-19-s3-bucket-settings.md`) rather than one row per check, by Task 13, which was told not to launch the app. Nothing on this branch was seen on screen: the ledger's Task 10 closing entry says “nothing visual was ever verified”, and no later task or review changed that. Every item below is something the tests pin by reading source or by a measurement of bounds, and no scan can answer by looking. They are derived from the design's own sentences about what must appear, not from observation, so a check that finds a different thing on screen has found a gap in this list as likely as a defect. **SC-1 — the menu and where it sits.** *Do:* connect an S3 session, then an SFTP one and a WebDAV one. *Look for:* a menu titled “Connection Actions” in the toolbar's free area, which the design says is “its own capsule in the toolbar's free area, left of the existing icon group, styled like that group”; absent for SFTP and WebDAV (“At SFTP the capsule is absent”), with nothing drawn in its place (the design: “an empty list renders **nothing at all** — not an empty menu”); four rows, “Create Bucket…”, “Versioning…”, “CORS Rules…” and “Object Lock…”; at the bucket list only Create Bucket is live, and inside a bucket the other three are live and Create Bucket is not. **SC-2 — an account with no buckets.** *Do:* connect, in bucket-list mode, with a key whose account holds no bucket. *Look for:* the connect succeeds and does not stop at the form; the empty browser is not blank in the sense the design forbids (“the empty state it creates must offer the one action that resolves it rather than being blank”): Create Bucket… is live in the menu. **SC-3 — the create sheet.** *Look for:* the two switches, Versioning and Object Lock, each with its sentence about what cannot be undone once chosen (“Object Lock can never be switched off for this bucket, and it keeps versioning on for good. If you switch it on here, you cannot undo it.”); a name already in the list answering “Your list already has a bucket with this name. Choose a different name.”; the name hint under the field. **SC-4 — versioning says what suspension is.** *Look for:* three states and not two, with the design's rule that the control “must not present suspension as 'off'”; “Suspending stops new versions from being kept.” followed by the sentence that the stored versions stay; and on a bucket that uses Object Lock, no suspension offered, with “This bucket uses Object Lock, so its versioning cannot be switched off.” **SC-5 — the CORS form and text.** *Look for:* the fields by default and a switch to the rules as text; a configuration the form cannot represent switching the form off and naming what it cannot show (“the form disables itself and names what it cannot show, rather than round-tripping and dropping it”); Save refused for a document that would lose something. **SC-6 — the COMPLIANCE ceremony, on both sheets.** *Look for:* a tick the design says “names the exact date beyond which nobody can undo this” (for a file: “I understand that nobody, including me, can erase this file's stored data or shorten this retention before %@, and that this cannot be undone.”, where the placeholder is the date); Apply unavailable until it is ticked; the tick refused as stale when the day changes under it (“The day has changed since you ticked the box. Untick it and tick it again to confirm the new day.”); and no such ceremony for GOVERNANCE or a legal hold (“GOVERNANCE and legal hold are both reversible and get no equivalent ceremony”). **SC-7 — four languages.** *Look for:* the long sentences of SC-3, SC-4 and SC-6 in German, French and Polish, none clipped and none pushing a button out of the sheet. The layout guards measure the bounds with a model of the layout, and this file already has a row saying their figures were measured on Darwin 27 and not read on the macos-15 runner (“Every layout figure the guards hold was measured on Darwin 27, and no CI run has read them on macos-15”). **SC-8 — the audit line for each write.** *Do:* make each of the five writes, then open the audit log. *Look for:* one line per write, under File Ops, in the order made; a refused write shown as refused with the app's own sentence and not the store's words. Not started. | +| The gate's toolchain and the maintainer's are two versions apart, and the newer one diagnoses what the older let through | **Open, recorded 2026-10-31**, measured across the seven runs of PR 8. `a774ac4d` moved the gate off `macos-15` (Xcode 16.4, Swift 6.1.2) to `macos-latest`, which reports macOS 26.6.2 build 25G83, Swift 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) and target arm64-apple-macosx26.0; this Mac runs Xcode 27.1 and Swift 6.4. So three toolchains are now in play and no two of them are the same. **The divergence is not theoretical.** Run 41045404989 went red on the warning gate alone, on a tree every earlier `macos-15` run had built clean: `Tests/macSCPAppKitTests/EmptyContainerListTests.swift:131:50: warning: main actor-isolated static property 'allLines' can not be referenced from a nonisolated context`, 26 occurrences at that one location, which is why the gate counts unique locations and reported 1. Fixed in `6583396e` by annotating the test `@MainActor`. Read it back with `gh run view 41045404989 --repo NoiXdev/macSCP --log` and search for `allLines`. **What is measured besides it.** The image's own timing variance, which the `Unit-Tests` step comment in `.github/workflows/ci.yml` carries with its command and its series: 517 s against 640 s for the same step one commit apart, where that commit changes 23 lines of CLAUDE.md and nothing a target compiles. And `.timeLimit` firing at random on a saturated runner, which CLAUDE.md's 2026-10-30 entry measures and which cost 110 bound raises in `bcd0bc61`. **What is NOT measured, and is the reason this row is open rather than closed:** nothing has been run to find what Swift 6.4 diagnoses that 6.3.3 does not. The gap runs the other way now — a local build is the newer compiler — so the next instance of this failure is a warning the maintainer sees and the gate does not, which is the direction that cannot be caught by a gate at all. **Next step** is to decide whether the gate pins a toolchain (`maxim-lobanov/setup-xcode` with a version, against the image's default) so that local and gate agree by construction, or whether the divergence is accepted and the warning gate is trusted to catch its own side of it. **Answered in part 2026-11-01, and not by a diagnostic.** The clause above, "nothing has been run to find what Swift 6.4 diagnoses that 6.3.3 does not", is withdrawn as far as "nothing has been run" goes: a full local `swift test` was run on this Mac while fixing run 41244948849, and it found a divergence — but a RENDERING one, not a warning. `ViewTestabilitySpike`'s case "Varying only isRegex leaves the pixels unchanged" fails here and passes on the gate. Locally: `Expectation failed: off == on`, two 840x80 bitmaps whose fingerprints are `6d8a304f01dbd6e1` and `5a7939e70c422d9d`, the whole run being 5091 tests in 408 suites passed plus 2884 tests in 242 suites failed with that one issue. On the gate, in the very run this row's fix was chasing, the same case passed after 1.265 s and its suite after 114.785 s — read it back with `gh run view 41244948849 --repo NoiXdev/macSCP --log` and search for `Varying only isRegex`. The case's own doc comment states the property as measured — that a `Toggle` is AppKit-backed and "its checked state therefore stays out of the bitmap" — so what is false on macOS 27 is the measurement the case was written to record, not merely the assertion on top of it. **The precise three versions, measured the same day**, since this row previously described the local one only as "Xcode 27.1 and Swift 6.4": `sw_vers` prints macOS 27.0.1 build 26A434 and `swift --version` prints Apple Swift version 6.4.2 (swiftlang-6.4.2.1.9 clang-2100.4.1.1), target arm64-apple-macosx27.0.0. Both commands are pipe-free and run from this cell as committed. **What this does NOT answer**, and why the row stays open: a rendering difference is not a diagnostic difference, so the sentence's actual subject — a warning the newer compiler emits and the gate never sees — is still unmeasured, and the local red above now sits between the maintainer and any future local full run, which makes it the more urgent half. Nothing here was changed to chase it: the spike is in `macSCPAppKitTests` and the commit that found it touches only `macSCPCoreTests` and this file, so the red is pre-existing and environmental by construction as well as by the two verdicts above. | +| A finished relaunch fix has sat unlanded on a local branch since 2026-09-05 | **Open, recorded 2026-10-31.** Found while sweeping local branches: of 32, 26 were content-identical to develop or main and were deleted, and `claude/nice-leakey-5da0b9` was the one with a commit of its own. `git cherry origin/develop claude/nice-leakey-5da0b9` prints one `+` line, for `ed0b70af` — `fix(app): the relaunch helper waits for this process, not for a second`, dated 2026-09-05. It is on neither develop nor main; the same command against `origin/main` prints the same one line. **What it carries.** `AppRelauncher.relaunch()` spawned `sh -c "sleep 1; open "` and then called `NSApp.terminate`; since the deferred `applicationShouldTerminate(_:)` tears every live tab down first, a teardown longer than one second made the relaunch a plain quit, because `open` against a bundle whose instance is still running only activates it. The fix hands the helper this process's pid, polls `kill -0`, and caps the wait at `QuitWatchdog.bound` plus `TabTeardown.boundedStagesCeilingSeconds`. Its commit message records 239 lines of tests and mutation probes at 5 of 5 against the old `sleep 1; open` and at 5 of 5 against an unconditional `open` at the cap. **Why it is a row and not just a rebase.** Eight weeks unlanded means it has never been reviewed by anyone but its author, it predates the `.timeLimit` raises and the `macos-latest` move, and `BoundedSFTPSession` was made public by it — an API change that no landed code asked for. None of that is an argument against it; all of it is work the landing needs. **Next step** is to rebase it onto develop, run the suite, and review it as a fresh branch, or to close it with the reason written down. Until one of those happens the branch is the only copy, and a branch sweep that trusts `git cherry` less carefully than this one did would take it. | ## Done (pointers stay so the justifications stay findable)