---
phase: 65-overridecontext-connection-teardown
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - test/integration/test_readonly_load.py
  - src/conn_guard.rs
  - src/lib.rs
autonomous: true
requirements:
  - LIFE-01
  - LIFE-03
tags:
  - duckdb
  - rust
  - ffi
  - lifecycle
  - testing

must_haves:
  truths:
    - "A `_connect_with_watchdog` helper exists in test/integration/test_readonly_load.py and is integrated with the existing run_test shim"
    - "Test `test_in_process_bootstrap_then_readonly_fresh` (B1) exists and FAILS on the v0.9.0 baseline of milestone/v0.9.1 (no fix yet)"
    - "Test `test_in_process_bootstrap_then_readonly_existing` (B2) exists and FAILS on baseline"
    - "Test `test_in_process_load_only_then_readonly` (B3) exists, isolates the leak to `LOAD semantic_views` (no CREATE), and FAILS on baseline"
    - "Test `test_in_process_readonly_then_readwrite` (B4 / D-09) exists and FAILS on baseline"
    - "Test `test_repeated_load_close_no_busy_spin` (B11) exists, opens 50 file-backed DBs sequentially, FAILS on baseline due to first iteration busy-spin"
    - "src/conn_guard.rs exists with a `ConnGuard` RAII type whose Drop calls duckdb_disconnect exactly once and is null-safe"
    - "Spike A4 (lldb backtrace into DBInstanceCache::GetInstanceInternal busy-spin loop) recorded in 65-01-SUMMARY.md"
    - "Spike A6 (BindInfo db_handle exposure check on duckdb=1.10502.0) recorded with conclusion in 65-01-SUMMARY.md"
    - "Spike A7 (nested duckdb_connect/duckdb_disconnect from inside parser_override) recorded with conclusion in 65-01-SUMMARY.md"
  artifacts:
    - path: "src/conn_guard.rs"
      provides: "ConnGuard RAII wrapper around duckdb_connect/duckdb_disconnect"
      contains: "struct ConnGuard"
    - path: "test/integration/test_readonly_load.py"
      provides: "Watchdog helper + B1..B4 + B11 in-process tests"
      contains: "_connect_with_watchdog"
  key_links:
    - from: "src/lib.rs"
      to: "src/conn_guard.rs"
      via: "mod conn_guard declaration"
      pattern: "mod conn_guard"
    - from: "test/integration/test_readonly_load.py"
      to: "duckdb.connect(path, read_only=True)"
      via: "_connect_with_watchdog daemon thread"
      pattern: "_connect_with_watchdog"
---

<objective>
Land the test scaffolding (watchdog + B1..B4 + B11) and the `ConnGuard` RAII type that subsequent plans will plumb in.
Run the three Wave-0 spikes (A4 lldb confirmation, A6 BindInfo probe, A7 parser_override re-entrancy) to lock in the
assumption set before invasive refactors.

Purpose: The phase ships its fix only if (a) we can prove on the baseline that the tests we are about to make pass actually fail today
(satisfies LIFE-03 success-criterion-3: "fails on v0.9.0 baseline and passes on v0.9.1"), and (b) the RAII guard pattern that
Plans 02/03 depend on exists and is unit-tested.

Output: New `src/conn_guard.rs`, five new tests in `test/integration/test_readonly_load.py`, a `_connect_with_watchdog` helper,
and a SUMMARY documenting the spike results (A4/A6/A7) for downstream plans.
</objective>

<execution_context>
@/Users/paul/.claude/get-shit-done/workflows/execute-plan.md
@/Users/paul/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/65-overridecontext-connection-teardown/65-CONTEXT.md
@.planning/phases/65-overridecontext-connection-teardown/65-RESEARCH.md
@.planning/phases/65-overridecontext-connection-teardown/65-VALIDATION.md
@.planning/phases/65-overridecontext-connection-teardown/65-PATTERNS.md
@CLAUDE.md

<interfaces>
<!-- Existing RAII analog the new ConnGuard mirrors -->

From src/catalog.rs (PreparedStmt RAII pattern — copy shape):
```rust
struct PreparedStmt { ptr: ffi::duckdb_prepared_statement }
impl PreparedStmt {
    unsafe fn prepare(conn: ffi::duckdb_connection, sql: &CStr) -> Result<Self, String> { ... }
    fn raw(&self) -> ffi::duckdb_prepared_statement { self.ptr }
}
impl Drop for PreparedStmt {
    fn drop(&mut self) { unsafe { ffi::duckdb_destroy_prepare(&mut self.ptr) }; }
}
```

Target shape for ConnGuard (planner-prescribed):
- pub(crate) struct ConnGuard { conn: ffi::duckdb_connection }
- pub(crate) unsafe fn open(db: ffi::duckdb_database) -> Result<Self, String>
- pub(crate) fn raw(&self) -> ffi::duckdb_connection
- impl Drop calling ffi::duckdb_disconnect(&mut self.conn) when !self.conn.is_null()
- Module gated `#[cfg(feature = "extension")]`; re-export at crate root behind same gate

From test/integration/test_readonly_load.py:
- Existing helpers: `open_writable(path)`, `open_readonly(path)`, `bootstrap_in_subprocess(...)`
- Existing test-runner shim: `run_test(name, fn) -> bool` aggregated by `main()` returning 0/1
- PEP 723 script header at lines 2-5; file runs under `uv run`

From test/integration/test_concurrent_ddl.py (daemon-thread analog):
- `threading.Thread(target=..., daemon=True)`, `t.start()`, `t.join(timeout=N)`, post-join `t.is_alive()` failure path
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Wave-0 spikes — confirm A4 busy-spin, A6 BindInfo surface, A7 parser_override re-entrancy</name>
  <files>
    .planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md
  </files>
  <read_first>
    .planning/phases/65-overridecontext-connection-teardown/65-RESEARCH.md
    .planning/phases/65-overridecontext-connection-teardown/65-CONTEXT.md
    src/lib.rs
    src/parse.rs
    src/catalog.rs
    test/integration/test_readonly_load.py
  </read_first>
  <action>
    Run three non-blocking spike investigations and record findings in `65-01-SPIKES.md`. Each spike is bounded (~5 min)
    and the outputs feed Plan 02/03 decisions. Do NOT modify production code in this task.

    Spike A4 (CONFIRM busy-spin diagnosis per RESEARCH §2.4):
    - Build the extension (`just build`).
    - Reproduce the hang per RESEARCH §2.1 minimal repro inside a Python script saved to `$TMPDIR/65_repro.py`
      (LOAD semantic_views, CREATE TABLE, CREATE SEMANTIC VIEW, w.close(), gc.collect(), then `duckdb.connect(path, read_only=True)`).
    - With the script paused mid-hang, run `top -pid <pid>` (or `ps -M -p <pid>`) for ~3 s and confirm one thread at ~100% CPU.
    - Run `lldb -p <pid>` then `bt all` and capture the backtrace into `65-01-SPIKES.md`.
    - Expected frame: `duckdb::DBInstanceCache::GetInstanceInternal` (the `while (!weak_cache_entry.expired())` loop near
      `cpp/include/duckdb.cpp:278022-278024`). If the backtrace shows a futex wait or different frame, STOP — diagnosis
      is wrong and the planner must restart. Record either result verbatim.

    Spike A6 (BindInfo db_handle exposure per RESEARCH §13.1):
    - Search the `duckdb` Rust binding crate version pinned in `Cargo.toml` (=1.10502.0): `cargo doc -p duckdb --no-deps`
      OR `rg "db_handle|get_database|duckdb_database" target/doc/duckdb/vtab/` to find any `BindInfo`/`FunctionInfo`
      method that returns a `duckdb_database`. Also `rg "duckdb_database" $(rustc --print sysroot)` is not useful — stay
      within the `duckdb` crate.
    - Conclusion options to record:
      (i) `BindInfo` exposes `duckdb_database` directly → Plan 03 can have bind callbacks call `ConnGuard::open(bind_info.db_handle())`.
      (ii) Not exposed → Plan 03 must store `db_handle` inside the `extra_info` struct (`CatalogReader` carrying `db` per shape (b),
      or new `CatalogHandle` per shape (a)).
    - Record the conclusion and which Plan-03 shape it implies.

    Spike A7 (parser_override re-entrancy per RESEARCH §13.2):
    - In a scratch branch (do not commit), add inside `sv_parser_override_rust` at top, before any real work, a temporary:
      ```
      let mut probe: ffi::duckdb_connection = std::ptr::null_mut();
      let rc = unsafe { ffi::duckdb_connect(/*db_handle from OverrideContext*/, &mut probe) };
      assert_eq!(rc, ffi::DuckDBSuccess);
      unsafe { ffi::duckdb_disconnect(&mut probe) };
      ```
      (Use the existing `ctx.catalog.raw()`-derived db handle for now via `duckdb_get_database(ctx.catalog.raw(), ...)` if exposed,
      or skip this spike line in favour of the empirical evidence from Plan 02 once `db_handle` is plumbed.)
    - Run one `CREATE SEMANTIC VIEW` via `just test-sql` for any existing test that touches parser_override.
    - Confirm no deadlock / no segfault. Discard the spike code (do not commit). Record the outcome.
    - If A7 evidence cannot be obtained without plumbing `db_handle` (which is Plan 02's job), mark A7 as DEFERRED-to-Plan-02
      and document the rationale; this is acceptable since the production work in Plan 02 will surface a deadlock immediately if
      A7 is wrong.

    Write outcomes into `.planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md` with sections
    `## A4`, `## A6`, `## A7` containing verbatim evidence (lldb backtrace, cargo doc grep output, spike test result).
  </action>
  <verify>
    <automated>test -f .planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md && grep -q "## A4" .planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md && grep -q "## A6" .planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md && grep -q "## A7" .planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md</automated>
  </verify>
  <acceptance_criteria>
    - File `.planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md` exists with `## A4`, `## A6`, `## A7` sections.
    - A4 section contains a captured lldb `bt` (or equivalent) backtrace OR a documented STOP if diagnosis was wrong.
    - A4 section explicitly states whether `DBInstanceCache::GetInstanceInternal` busy-spin frame appears (`CONFIRMED` / `REFUTED`).
    - A6 section states one of `BindInfo-EXPOSES-db_handle` or `BindInfo-DOES-NOT-EXPOSE-db_handle` and names the Plan-03 shape implied (a or b).
    - A7 section states `RE-ENTRANCY-SAFE`, `RE-ENTRANCY-UNSAFE` (with deadlock evidence), or `DEFERRED-TO-PLAN-02` (with rationale).
    - No production source files modified by this task (`git diff --stat src/ cpp/` returns empty).
  </acceptance_criteria>
  <done>
    Three spike outcomes recorded; planner's assumption set is either confirmed or explicitly revisited before invasive work begins.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Introduce `src/conn_guard.rs` (ConnGuard RAII) + crate wiring</name>
  <files>
    src/conn_guard.rs
    src/lib.rs
  </files>
  <read_first>
    src/catalog.rs
    src/lib.rs
    .planning/phases/65-overridecontext-connection-teardown/65-PATTERNS.md
  </read_first>
  <behavior>
    - On successful `unsafe ConnGuard::open(db)` where `db` is a valid `duckdb_database`, the returned guard wraps a
      non-null `duckdb_connection`, and `raw()` returns it.
    - When the guard is dropped, `duckdb_disconnect` is called exactly once on the held pointer; the pointer field is
      set to null inside `drop()` so a no-op-on-double-drop invariant holds even if the type is later wrapped/moved.
    - On `duckdb_connect` failure (rc != DuckDBSuccess), `ConnGuard::open` returns `Err("duckdb_connect failed: …")`
      without leaking a partially-initialised handle.
    - The type is `Send` (DuckDB connections are usable from any single thread at a time) but NOT `Sync` (per RESEARCH
      §PATTERNS — a guard belongs to one scope).
    - Module is feature-gated `#[cfg(feature = "extension")]`; `cargo test` under default features compiles without
      pulling in any FFI symbols.
  </behavior>
  <action>
    Create `src/conn_guard.rs` mirroring the `PreparedStmt` RAII pattern in `src/catalog.rs:176-206`:

    Module contents (gated by `#[cfg(feature = "extension")]`):
    - `use libduckdb_sys as ffi;`
    - `pub(crate) struct ConnGuard { conn: ffi::duckdb_connection }`
    - `pub(crate) unsafe fn open(db: ffi::duckdb_database) -> Result<Self, String>` — calls `ffi::duckdb_connect(db, &mut conn)`,
      checks `rc == ffi::DuckDBSuccess`, returns `Err("duckdb_connect failed (rc={rc})".to_string())` otherwise.
    - `pub(crate) fn raw(&self) -> ffi::duckdb_connection` — returns `self.conn` by value (pointer-sized copy).
    - `impl Drop for ConnGuard` — inside drop: if `!self.conn.is_null()`, call `unsafe { ffi::duckdb_disconnect(&mut self.conn) }`.
      After disconnect, the FFI call zeroes the pointer through `&mut`. Be defensive: set `self.conn = ptr::null_mut()` if
      `duckdb_disconnect` does not zero (verify by reading the libduckdb-sys docs comment for `duckdb_disconnect`).
    - `unsafe impl Send for ConnGuard {}` with a SAFETY comment cribbed from `src/catalog.rs:108-112` adapted to "guard
      transfer between threads is safe because the underlying duckdb_connection is owned exclusively by this guard".
    - DO NOT implement `Sync`. DO NOT derive `Copy` or `Clone`.

    Add `#[cfg(feature = "extension")] mod conn_guard;` near the other `mod` declarations in `src/lib.rs` (currently around
    `src/lib.rs:1-30` where `mod parse;`, `mod catalog;`, etc. live). No public re-export needed beyond the crate root — Plans
    02/03 will reach into it via `crate::conn_guard::ConnGuard`.

    Add Rust unit tests inside `src/conn_guard.rs` under `#[cfg(test)]`:
    1. `drop_is_idempotent_when_null` — manually construct `ConnGuard { conn: std::ptr::null_mut() }`, drop it, assert no
       panic. This test can compile under default features (no FFI call exercised).
    2. `open_and_drop_cycles_disconnect` — gated `#[cfg(all(test, feature = "extension"))]` is NOT runnable under bundled
       feature (FFI stubs panic per `deferred-items.md`). Use the same `#[cfg(not(feature = "extension"))]` pattern as
       `catalog::tests::two_statement_guard_then_dml_smoke` (`src/catalog.rs:361-405`): open a `duckdb::Connection` via the
       bundled crate, get its raw `*const c_void` if exposed, otherwise leave a single signature-only smoke test that
       confirms the type compiles and Drop signature is correct.
    3. Proptest `conn_guard_drop_handles_arbitrary_pointer_state` (satisfies VALIDATION B14 Rust unit + proptest
       requirement) — use the existing `proptest` dev-dependency (declared in `Cargo.toml`; pattern: `src/ident.rs`
       `proptest!` block). Fuzz the ConnGuard's pointer state across construction/drop cycles: generate an arbitrary
       `usize` value `addr`, build `let g = ConnGuard { conn: if addr == 0 { std::ptr::null_mut() } else { addr as ffi::duckdb_connection } };` and then `std::mem::forget(g)` for non-null cases (we must NOT actually call duckdb_disconnect on a fabricated pointer), while exercising `drop(g)` for the null case. Assert no panic across e.g. 256 cases. The proptest's value is verifying the `!self.conn.is_null()` branch in Drop is the only thing gating disconnect — i.e., the type's Drop behaviour is purely a function of the null check. Keep the proptest body ≤5 lines as planned. Place inside the `#[cfg(test)] mod tests { use super::*; use proptest::prelude::*; proptest! { ... } }` block.
  </action>
  <verify>
    <automated>cargo test --lib conn_guard -- --nocapture 2>&1 | tee $TMPDIR/65_conn_guard.log; grep -E "test result: ok|0 failed" $TMPDIR/65_conn_guard.log</automated>
  </verify>
  <acceptance_criteria>
    - File `src/conn_guard.rs` exists with `pub(crate) struct ConnGuard`, `pub(crate) unsafe fn open`, `pub(crate) fn raw`,
      and `impl Drop` blocks present. Verify with `grep -E "struct ConnGuard|fn open|fn raw|impl Drop for ConnGuard" src/conn_guard.rs`
      returning ≥4 matches.
    - `src/lib.rs` contains `#[cfg(feature = "extension")] mod conn_guard;` (or equivalent). Verify with
      `grep -E "mod conn_guard" src/lib.rs` returning ≥1 match.
    - `cargo build` succeeds under default features (`cargo build`).
    - `cargo build --features extension --no-default-features` succeeds (extension build path).
    - `cargo test --lib conn_guard` exits 0 with at least the `drop_is_idempotent_when_null` test passing.
    - `unsafe impl Send for ConnGuard` present; no `impl Sync` line present. Verify with
      `grep -c "Send for ConnGuard" src/conn_guard.rs` ≥ 1 and `grep -c "Sync for ConnGuard" src/conn_guard.rs` == 0.
    - No `impl Clone for ConnGuard` or `#[derive(Clone)]` on the struct. Verify with `grep -E "Clone" src/conn_guard.rs` returning no `ConnGuard`-attached matches.
  </acceptance_criteria>
  <done>
    `ConnGuard` exists, is module-wired, compiles in both bundled and extension feature modes, and the null-drop unit test
    passes. Plans 02 and 03 can now import `crate::conn_guard::ConnGuard`.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: Add `_connect_with_watchdog` + B1..B4 + B11 tests to `test_readonly_load.py` (must FAIL on baseline)</name>
  <files>
    test/integration/test_readonly_load.py
  </files>
  <read_first>
    test/integration/test_readonly_load.py
    test/integration/test_concurrent_ddl.py
    .planning/phases/65-overridecontext-connection-teardown/65-RESEARCH.md
    .planning/phases/65-overridecontext-connection-teardown/65-PATTERNS.md
  </read_first>
  <behavior>
    - `_connect_with_watchdog(path, watchdog_seconds=5, **kwargs)` opens `duckdb.connect(path, **kwargs)` inside a
      daemon thread, joins with timeout, raises `TimeoutError` on join expiry, re-raises any exception captured inside
      the thread, otherwise returns `(connection, elapsed_seconds)`.
    - On the v0.9.0 baseline of `milestone/v0.9.1` (before Plans 02/03 land), the following five tests MUST fail with
      `TimeoutError` after ~5 seconds wall-clock per test:
        - `test_in_process_bootstrap_then_readonly_fresh` (B1)
        - `test_in_process_bootstrap_then_readonly_existing` (B2)
        - `test_in_process_load_only_then_readonly` (B3)
        - `test_in_process_readonly_then_readwrite` (B4)
        - `test_repeated_load_close_no_busy_spin` (B11) — fails on the first iteration's RO reopen
    - All existing tests in the file (subprocess-based; B5 group) MUST continue to pass — do not touch their bodies.
  </behavior>
  <action>
    Edit `test/integration/test_readonly_load.py`. Keep all existing helpers, tests, and the `run_test`/`main` shim
    intact. Append the new content at appropriate spots:

    1) Add `_connect_with_watchdog(path, watchdog_seconds=5, **kwargs)` near the other top-level helpers. Use exactly the
       skeleton from RESEARCH §7.3 (daemon thread, `result` dict, `time.monotonic()` start/elapsed, post-join `t.is_alive()`
       check raising `TimeoutError`, exception re-raise). Include a docstring referencing Phase 65 RESEARCH §2 root cause
       and the documented thread-leak-on-failure caveat (per VALIDATION §Manual-Only Verifications row 2).

    2) Add five test functions (no pytest framework; same `def test_*()` shape as existing tests, registered into the
       `run_test` aggregation in `main()`):

       - `test_in_process_bootstrap_then_readonly_fresh()`:
           - Create `tempfile.TemporaryDirectory()`; `db = str(Path(tmp) / "fresh.duckdb")`.
           - `w = open_writable(db)`; execute `CREATE TABLE t (i INT)` and a minimal `CREATE SEMANTIC VIEW v AS TABLES (t1 AS t PRIMARY KEY (i)) DIMENSIONS (t1.i AS t1.i) METRICS (t1.c AS COUNT(*))`.
           - `w.close(); del w; gc.collect()`.
           - Call `_connect_with_watchdog(db, watchdog_seconds=5, read_only=True)`.
           - Assert elapsed < 5.0; `ro.execute("LOAD semantic_views")`; assert `SELECT name FROM list_semantic_views()` returns `[("v",)]`.
           - `ro.close()` in finally.

       - `test_in_process_bootstrap_then_readonly_existing()`:
           - Use `bootstrap_in_subprocess(db, ...)` (existing helper) to create the DB outside-process.
           - Then in-process: `w = open_writable(db)`; `w.execute("LOAD semantic_views")`; `w.close(); gc.collect()`.
           - Call `_connect_with_watchdog(db, watchdog_seconds=5, read_only=True)`. Same assertions as B1.

       - `test_in_process_load_only_then_readonly()`:
           - Open `w = open_writable(db)`; execute `LOAD semantic_views` ONLY (no `CREATE`).
           - `w.close(); gc.collect()`.
           - Call `_connect_with_watchdog(db, watchdog_seconds=5, read_only=True)`; assert elapsed < 5.0.
           - This isolates the leak to extension LOAD, not CREATE (RESEARCH §2.2 framing).

       - `test_in_process_readonly_then_readwrite()` (D-09 / B4):
           - Use `bootstrap_in_subprocess(db, ...)` to populate.
           - In-process: `ro = open_readonly(db)`; `ro.close(); gc.collect()`.
           - Call `_connect_with_watchdog(db, watchdog_seconds=5)` (read-write, no `read_only=True`); assert elapsed < 5.0.

       - `test_repeated_load_close_no_busy_spin()` (B11):
           - Loop 50 iterations:
               - Create fresh tempfile path `db = tmp_path / f"loop_{i}.duckdb"`.
               - `w = open_writable(db)`; `w.execute("LOAD semantic_views")`; `w.execute("CREATE TABLE t (i INT)")`;
                 `w.execute("CREATE SEMANTIC VIEW v_{i} AS ...")`; `w.close(); gc.collect()`.
               - `_connect_with_watchdog(db, watchdog_seconds=5, read_only=True)`; assert elapsed < 5.0; `ro.close()`.
           - Total runtime budget: 50 * 5 s = 250 s worst case on baseline (each fails fast). After fix: should complete in
             well under 10 s.
           - Document inline that this test is RUN LAST in the file (re-order the `run_test(...)` calls in `main()` so it
             follows the four single-iteration tests) to minimise thread-leak surface on baseline failure.

    3) Wire all five new tests into the existing `main()` aggregator (the `run_test(...)`-summing block). Order them AFTER
       the existing subprocess tests so existing green coverage remains visible if the new tests hang/leak threads on baseline.

    4) Do NOT remove or modify `bootstrap_in_subprocess` or any existing test — the subprocess style remains as
       deployment-style smoke per LIFE-03 / B5.

    Per RESEARCH §7.3: each daemon watchdog thread will leak on baseline (DuckDB busy-spin is uninterruptible from Python).
    Add a one-paragraph file-top docstring update or inline comment near `_connect_with_watchdog` documenting the leak and
    the "run last under pytest-timeout" mitigation. This is intentional and acceptable for a fail-once regression test.

    Critical baseline-failure check (informational, document only — do not gate the task on it because committing failing
    tests is the goal of this task): note in `65-01-SUMMARY.md` that running this file on baseline produces 5 timeouts in
    the new tests while all subprocess tests stay green.
  </action>
  <verify>
    <automated>python -c "import ast,sys; t=ast.parse(open('test/integration/test_readonly_load.py').read()); names={n.name for n in ast.walk(t) if isinstance(n, ast.FunctionDef)}; required={'_connect_with_watchdog','test_in_process_bootstrap_then_readonly_fresh','test_in_process_bootstrap_then_readonly_existing','test_in_process_load_only_then_readonly','test_in_process_readonly_then_readwrite','test_repeated_load_close_no_busy_spin'}; missing=required-names; sys.exit(0 if not missing else (print('MISSING:',missing) or 1))"</automated>
  </verify>
  <acceptance_criteria>
    - Function `_connect_with_watchdog` defined in `test/integration/test_readonly_load.py`. Verify with the AST check above.
    - Five new test functions present: `test_in_process_bootstrap_then_readonly_fresh`, `test_in_process_bootstrap_then_readonly_existing`,
      `test_in_process_load_only_then_readonly`, `test_in_process_readonly_then_readwrite`, `test_repeated_load_close_no_busy_spin`.
    - All five new tests are wired into the `main()` `run_test(...)` aggregator. Verify with `grep -c "run_test(.*test_in_process_\|run_test(.*test_repeated_load_close" test/integration/test_readonly_load.py` ≥ 5.
    - Existing helpers `open_writable`, `open_readonly`, `bootstrap_in_subprocess` still present (no deletions). Verify
      with `grep -E "def open_writable|def open_readonly|def bootstrap_in_subprocess" test/integration/test_readonly_load.py` returning ≥3.
    - `_connect_with_watchdog` uses `threading.Thread(..., daemon=True)`. Verify with `grep -A 5 "def _connect_with_watchdog" test/integration/test_readonly_load.py | grep -E "daemon=True|threading.Thread"` returns ≥1.
    - Watchdog-leak documentation present in or near `_connect_with_watchdog` docstring/comment. Verify with
      `grep -E "leak|leaks|busy-spin|Phase 65" test/integration/test_readonly_load.py` returns ≥1 match within ±20 lines of the helper definition.
    - File still parses as a Python script (`python -m py_compile test/integration/test_readonly_load.py`).
  </acceptance_criteria>
  <done>
    Watchdog scaffolding lands as failing-on-baseline tests. Plan 04 will re-run them after the fix is in to verify they
    flip green; the baseline-fail evidence is what closes LIFE-03 success criterion 3.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Rust ↔ DuckDB C-API | `duckdb_connect`/`duckdb_disconnect` calls cross the unsafe FFI boundary; ConnGuard's Drop is the only place `duckdb_disconnect` is invoked from new code in this phase |
| Python test thread ↔ DuckDB C++ thread | The watchdog daemon thread crosses a thread boundary into uninterruptible C++ code; on baseline the thread leaks |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-65-01 | Tampering | ConnGuard::Drop double-disconnect | mitigate | `if !self.conn.is_null()` guard inside `Drop`; null-out post-disconnect; null-drop unit test in Task 2 |
| T-65-02 | Denial of Service | Watchdog daemon thread leak on baseline failure | accept | Documented as intentional in helper docstring per VALIDATION §Manual-Only row 2; tests run last in file to minimise blast |
| T-65-03 | Information Disclosure | None — no PII, no network surface, in-process lifecycle bug | accept | Phase 65 is pure internal lifecycle; no information-disclosure surface change |

Security enforcement is enabled but the phase's threat surface is genuinely narrow (no auth/network/user-input changes).
</threat_model>

<verification>
After all tasks in this plan complete:

1. `cargo build` and `cargo build --features extension --no-default-features` both succeed.
2. `cargo test --lib conn_guard` passes (null-drop unit test).
3. `python -m py_compile test/integration/test_readonly_load.py` succeeds.
4. The AST check in Task 3's `<verify>` returns all six required function names.
5. `grep -E "mod conn_guard" src/lib.rs` returns ≥1 match.
6. `git diff --stat` shows changes confined to `src/conn_guard.rs`, `src/lib.rs`, and `test/integration/test_readonly_load.py`,
   plus the new `.planning/phases/65-overridecontext-connection-teardown/65-01-SPIKES.md`.
7. `just test-all` is NOT required to be green at the end of Plan 01 — the new in-process tests are expected to fail on
   baseline. Document the failure pattern in `65-01-SUMMARY.md` so reviewers can confirm the "fails on baseline" half of
   LIFE-03 success criterion 3.
</verification>

<success_criteria>
- ConnGuard exists, compiles in both feature modes, has a null-drop unit test, and is module-declared in `src/lib.rs`.
- Watchdog helper and five in-process tests exist and are wired into the file's `run_test` aggregator.
- Three Wave-0 spike outcomes (A4, A6, A7) recorded with verbatim evidence in `65-01-SPIKES.md`.
- No production behaviour changed in this plan — the fix is in Plans 02/03; this plan provides the scaffolding and the
  baseline-failure evidence.
</success_criteria>

<output>
Create `.planning/phases/65-overridecontext-connection-teardown/65-01-SUMMARY.md` when done. Summary MUST include:
- Spike outcomes (A4/A6/A7) summarised with file pointer to `65-01-SPIKES.md`.
- For each new test, a one-line description of expected baseline behaviour (timeout) and post-fix behaviour (pass).
- The `ConnGuard` public API (`unsafe fn open`, `fn raw`, `Drop`) and the Send-not-Sync rationale.
- Any deviation from the planner's prescribed shape and why.
</output>
