{
  "summary": "Root cause is proven, not merely suspected: process-static tempfile ownership. `crates/spt-daemon/tests/sync.rs:50-57` stores `TempDir` in `static HOME: OnceLock<TempDir>`; Rust does not drop statics at process exit, so tempfile never receives `Drop`. Under nextest's process-per-test model, each test that calls `init_home()` leaks one `.tmp*` directory. The Windows reproduction reported by Main is discriminating: the exact sync test exits normally with +1 persistent directory (the static HOME), while force-killing it after one second leaves +2 (static HOME plus the lexical fixture at `sync.rs:229`). This rules out broker file-handle retention as the normal-run root and proves hard termination as a separate amplifier. The repository contains 34 `OnceLock<TempDir>` homes plus one equivalent `static Mutex<Option<TempDir>>`; a source census found 134 direct activation callsites (114 integration/helper callsites plus 20 `spt-msg` unit-test `isolate()` callsites). Sync contributes 3 nextest processes/leaks; `digest_cross_node` contributes 7. No Rust call uses tempfile `keep`, `into_path`, `persist`, `persist_noclobber`, `disable_cleanup`, or `mem::forget`; the four `NamedTempFile` uses are lexical. The minimal normal/panic fix is to replace static owning TempDirs with a per-test scoped home lease. A complete kill-safe fix additionally needs cleanup owned outside the killable test process (runner-owned TEMP sandbox or an out-of-process reaper); no in-process Drop/finalizer can survive `TerminateProcess`/SIGKILL.",
  "files": [
    {
      "path": "crates/spt-daemon/tests/sync.rs",
      "description": "Exact failing lifecycle. `HOME` is process-static at 50; `init_home` allocates at 51-57. `net_broker` at 82-91 starts `Broker::serve` on a detached, unjoined thread. `PullLink::pull` at 166-218 opens requester/responder stores, spawns and joins the requester thread, and calls real `request_sync`/`serve_sync`. The failing test starts at 227, calls `init_home` at 228, creates the second lexical TempDir at 229, creates two broker journal trees at 230-231, tracked roots/scratch at 232-234, and performs three real pulls through 344. Normal process exit leaks HOME only; hard kill leaks HOME plus `dir`."
    },
    {
      "path": "crates/spt-daemon/tests/digest_cross_node.rs",
      "description": "Same root at 50-57. `net_broker` at 81-90 detaches an unjoined broker serve thread; `spawn_dispatcher_for` at 124-139 returns only a stop flag, not a join handle; `ensure_digest_hub` at 145-171 starts a process-lifetime control thread. Seven tests initialize HOME and allocate a second lexical TempDir at 224-228, 289-293, 329-333, 407-410, 498-501, 555-559, and 636-640. Thus nextest leaks seven static homes per run of this integration target; a hard-killed case can also leave its lexical broker directory."
    },
    {
      "path": "crates/spt-daemon/src/broker.rs",
      "description": "`Broker` owns `Arc<EffectJournal>` and a stop latch at 3620-3650. Bind opens the journal at 3772-3812. `serve` is an infinite accept loop at 3858-3878. A correct teardown seam already exists: `Broker::stop` at 3880-3892 wakes accept. Sync/digest helpers discard the `JoinHandle` and never call this. This is a thread/resource hygiene defect, but the Windows normal-run +1 result falsifies it as the source of the lexical TempDir leak."
    },
    {
      "path": "crates/spt-daemon/src/effect.rs",
      "description": "`EffectJournal::Inner` holds a `File` at 317-331; `open` creates and permanently holds the append file at 338-356. Rust's Windows `OpenOptions` defaults include `FILE_SHARE_DELETE`, consistent with the observed lexical directory successfully disappearing on a normal sync run despite detached brokers."
    },
    {
      "path": "crates/spt-daemon/src/sync.rs",
      "description": "Transient bundle cleanup is manual rather than RAII. Server creates `serve-*.bundle` at 237-239 and removes it only at 286; `?` exits during open/metadata/read/send at 259-283 and create-bundle failure at 240-252 can strand a bundle. Requester creates `recv-*.bundle` at 391-400 and explicitly removes only at 453, 458, 465, 493, and 501; timeout/read, decode, seek/write, flush, metadata, fetch, and `BrokerEvent::Error` exits can strand it. These omissions can add bytes inside an already leaked/killed fixture, but cannot create a `.tmp*` directory and are not the normal-run root."
    },
    {
      "path": "crates/spt-daemon/tests/",
      "description": "Exhaustive static-home owner census in this directory (path:static line): `access.rs:34`, `attach.rs:48`, `attach_idempotent_replay.rs:89`, `attach_resize_capture.rs:111`, `attach_resize_repaint.rs:78`, `brain_decouple.rs:75`, `brain_resume_conn_deadlock.rs:52`, `conn_blackhole_lifecycle.rs:105`, `control_stamp_lifetime.rs:61`, `controller_lease.rs:46`, `digest_cross_node.rs:50`, `dispatch.rs:43`, `driven_by_selfheal.rs:90`, `endpoint_lifecycle.rs:49`, `false_promote.rs:80`, `inject_control_wedge.rs:518`, `input_ack_deadlock.rs:165`, `mesh.rs:60`, `mesh_recovery.rs:31`, `notif_quiet_delivery.rs:31` (the one `Mutex<Option<TempDir>>`), `pump.rs:40`, `redispatch.rs:43`, `redispatch_stall.rs:58`, `registry_lifecycle.rs:42`, `render_lifecycle.rs:30`, `resize_geometry_epoch.rs:57`, `resize_presentation_barrier.rs:61`, `restart_replay_lifetime.rs:148`, `resume.rs:31`, `spawn_truth.rs:33`, `sync.rs:50`, `transport_death_eof.rs:186`, `wanmsg.rs:40`. All other entries are `OnceLock<TempDir>`."
    },
    {
      "path": "crates/spt-msg/src/lib.rs",
      "description": "Test helper repeats the defect: `static HOME: OnceLock<TempDir>` at 40, allocated by `test_home::isolate` at 46-51. Twenty unit tests directly call `isolate` (`deliver.rs`, `listener.rs`, `ready.rs`, `ring.rs`); nextest makes each a separate leaking process."
    },
    {
      "path": "crates/spt-msg/tests/killer_quickstart.rs",
      "description": "Another `OnceLock<TempDir>` at 22, allocated by `init_home` at 25-31 and activated by tests at 59 and 117."
    },
    {
      "path": "crates/spt-live/src/digest.rs",
      "description": "Scoped `NamedTempFile::new` calls at 195, 212, and 306. No persistence/keep call and no static owner."
    },
    {
      "path": "crates/spt-live/src/history.rs",
      "description": "Scoped `NamedTempFile::new` at 221. No persistence/keep call."
    },
    {
      "path": "crates/spt-daemon/tests/fixtures/service_fixture.rs",
      "description": "Deliberate child exits at 106, 113, 198, and 204. Other fixture exits are `dispatch_fixture.rs:35`; `spt/tests/fixtures/gh_fixture.rs:63,70`, `git_fixture.rs:20`, and `post_step_fixture.rs:38,42`; mock adapter binaries also exit deliberately. None creates a TempDir itself. These exits bypass child destructors but release cwd/handles when the child dies; they are not on sync/digest's path."
    },
    {
      "path": "crates/spt-daemon/src/daemon.rs",
      "description": "A self-reexecuted test child deliberately calls `process::exit(0)` at 2550. Its unmanaged `%TEMP%` pid/diagnostic files are created around 2555/2581 and removed by the parent at 2655-2658. This is a named-file cleanup risk on parent panic, not the `.tmp*` directory family."
    },
    {
      "path": "traceable-reqs.toml",
      "description": "REQ-TEST-TMPDIR-HYGIENE is at 3003-3005. Its candidate list is now resolved: normal leak is static non-Drop; force termination independently prevents lexical Drop; child cwd/handle retention is not present in the named sync/digest paths."
    }
  ],
  "architecture": "Ranked, falsifiable causes\n\n1. CONFIRMED — static TempDir never reaches Drop. Prediction: a successful exact sync run leaves exactly one new `.tmp*` directory. Observed on Windows. Call chain: nextest launches one test process → `two_tier_sync_lands_and_gate_refuses_server_side` (`sync.rs:227`) → `init_home` (228) → `HOME.get_or_init` (51) → `TempDir::new` (53) → normal process termination with HOME still static. The same shape exists in 35 static owners. The 14,319-dir incident is quantitatively plausible: 134 direct activation sites means roughly 107 full-equivalent nextest passes produce 14,338 directories even before helper fan-out and killed lexical fixtures.\n\n2. CONFIRMED AMPLIFIER — hard termination bypasses every lexical destructor. Prediction: kill sync after both allocations and before function return leaves two new directories. Observed on Windows (+2): static HOME plus `sync.rs:229`. This is distinct from panic-with-unwind. There is no `panic = \"abort\"` test profile in the repository; ordinary assertion panic unwinds lexical TempDirs, while `TerminateProcess`/SIGKILL/test-runner kill cannot.\n\n3. REJECTED AS NORMAL ROOT, SECONDARY HYGIENE BUG — detached broker/dispatcher threads. Prediction if their handles prevented Windows deletion: a successful sync run would also retain `sync.rs:229` (delta +2), and explicit `Broker::stop` would reduce it. Observed normal delta is only +1. Still replace the helper with an RAII `BrokerFixture { broker, serve_join }` whose Drop calls `broker.stop()`, closes the NetHost, and joins; this removes resource leakage and makes panic cleanup deterministic.\n\n4. REJECTED FOR NAMED TESTS — child cwd or inherited handle retention. Sync and digest_cross_node spawn Rust threads, not OS fixture children; no `.current_dir(...)` exists in either integration file. Context-store git operations are synchronous and complete before test return. Deliberate `process::exit` calls are in separate fixture binaries and the daemon self-reexec test, not these call chains.\n\n5. REJECTED — tempfile persistence APIs. Repository-wide alternate-pattern searches found no `keep`, `into_path`, `persist(_noclobber)`, `disable_cleanup`, or `mem::forget` applied to tempfile objects. Four NamedTempFiles are scoped. Cargo.lock uses tempfile 3.27.0; its destructor cleanup is only invoked on Drop and silently ignores deletion errors, exactly why static ownership is fatal.\n\n6. REAL BUT NOT DIRECTORY ROOT — sync scratch cleanup omissions. Faults can leave `serve-*.bundle`/`recv-*.bundle` inside a retained fixture and inflate its size, but these paths do not allocate `.tmp*` directories.\n\nSmallest fix\n\nFor the proven normal/panic leak, centralize a test-home lease and migrate every static owner: the static may retain synchronization/path/refcount state, but MUST NOT own `TempDir`. `init_home()` should return a `#[must_use] TestHomeLease`, callsites hold `let _home = init_home();`, and the final lease removes the shared tree. This cleans on success and unwind and preserves cargo-test parallel serialization. In sync/digest, place every lexical broker/scratch temp beneath that leased root and use an RAII broker guard.\n\nThat source-only RAII change cannot satisfy the demonstrated hard-kill case: code in a terminated process cannot run a finalizer. The minimal complete mechanism is a runner-owned per-test `%TEMP%` sandbox (or a breakaway out-of-process reaper) whose owner survives the test process, waits for it, then retries `remove_dir_all` on Windows. Put both SPT_HOME and lexical `tempdir_in(...)` allocations below that sandbox. A suite-start stale sweep is useful defense-in-depth but is not a substitute for post-child cleanup.\n\nMinimal real-seam regression\n\nUse `crates/spt-daemon/tests/sync.rs` itself, not a tempfile unit mock. A surviving parent test launches the current sync test executable with an exact child arm (the repository already uses this re-exec pattern at `crates/spt-daemon/tests/twohost.rs:1641-1644`). The child executes the real `init_home` → lexical tempdir → two `net_broker` binds and writes a readiness marker outside the sandbox only after both paths exist. Run two deterministic arms: (a) child panics after readiness; (b) parent force-kills and waits after readiness. The parent/reaper then asserts the unique sandbox contains zero `.tmp*` children (and is removable), with a normal-success arm as control. Marker synchronization replaces the nondeterministic one-second sleep and exercises exactly the proven static + scoped + broker-thread shape. The regression must fail pre-fix with one leftover after normal/panic static ownership and two after kill; a shallow `TempDir` Drop test would not cover the real defect."
}