# Probe B precondition check — #300

> JIT plan (AGENTS.md §Plans). **Not commissioned. Nothing here has been run.** No execution and no
> config mutation is authorized; neither lane holds a window. doyle sequences admission.
>
> Mirror-excluded by the root JIT-plan predicate (`:(glob)*-PLAN.md`) on its name alone.

This is the **complete bounded precondition check** that must be admitted, run and answered before
`PROVENANCE-PROBE-PLAN.md`'s ten-case battery is runnable at all. It is its own unit with its own
commands, its own budget and its own cleanup, because clearing these unknowns is itself execution on
a shared host and cannot ride inside a plan that is granted for something else.

## What it answers, and what it must NOT do

| # | Question | What a NO changes |
|---|---|---|
| **D1** | Does `UserPromptSubmit` fire at all in `-p` / print mode? | The battery's whole command list rests on it. |
| **D2** | Does a session terminated mid-turn leave a resumable session id on disk? | The battery's resume step resolves `$SID` from disk; without one there is nothing to resume. |

**No automatic fallback.** If D1 is NO, this rig does **not** become an interactive rig, and the
battery is not rewritten on my own authority. A negative answer is a **result** that returns to doyle
with a recommendation, and the decision to re-shape the approach is his. The same for D2. This is
stated as a prohibition rather than a preference because the fall-through I originally wrote is
exactly the thing that would have made the battery reshape itself without a ruling.

**No trust writes.** Nothing in this check writes `.claude.json` or any shared account config. See
Isolation.

## Budget

| Parameter | Value |
|---|---|
| **Process launches** | **1** (one `claude`; no resume launch — D2 is read off disk, not by resuming) |
| Timeout | **180 s** wall, externally enforced, terminated under the identity guard on breach |
| Mid-turn kill delay | **2 s** after submission |
| Wall estimate | **≤ 5 minutes** including setup, harvest and cleanup |
| Retries | **none.** A failed or void check returns as void; it does not re-run on my authority. |

This launch is **not** part of the battery's 15-launch inventory. The two are separate submissions
and separate budgets.

## Isolation — and the limitation, recorded rather than dissolved

**Shared credentials do not imply isolated configuration.** The check authenticates with this
account's credentials from the shared config root; a clean `CLAUDE_CONFIG_DIR` would strand the
Windows credentials and the spawn could not authenticate (`[[v0257-internal-session-isolation]]`). So
this runs *inside* a configuration shared with live sessions. The user-level layer of that config —
user settings and the user-level `CLAUDE.md` — **applies to this spawn**, and is in fact what supplies
the hooks the check observes. "Import-free" below means free of *project-scoped* imports; it does not
mean the session loads nothing.

**Zero writes to that shared config.** `WORK` is an already-trusted key, not a new one:

```
C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\61a5cd70-a460-42b8-9332-60f6bfe05a40\scratchpad\trust-probe31
```

Verified **read-only**, today, and to be re-verified immediately before the run:
- It already carries `hasTrustDialogAccepted` **and** both external-include approvals in the shared
  `.claude.json`, so neither startup gate arises and neither flag needs writing
  (`[[headless-spawn-external-imports-gate]]`, `[[f027-trust-seam-groundtruth]]`).
- **The directory itself is absent** — a previous session removed it and the key outlived it. The run
  recreates that exact path as an empty directory. Nothing existing is modified: no file in it is
  edited, moved or deleted, because there is no directory there to alter.
- **Its parent chain carries no `CLAUDE.md`, no `AGENTS.md`, and no project-scoped `.claude/`**,
  walked to the drive root.

**The directory is never altered to manufacture isolation.** If the pre-run re-verification finds an
import or project config on that chain, the check **stops and reports it** as a finding. I do not
delete, move or rewrite anything to make the directory clean — that would be the write doyle ruled
out, arriving through a different door. If no suitable trusted directory exists at that point, that
is a finding for doyle, not a problem for me to fix by editing one.

Everything else matches the battery: no endpoint created, no `state` written, no live session touched,
PID owned by construction via a direct spawn.

## Commands, as they will be run

```powershell
# --- setup -------------------------------------------------------------------
$RIG     = 'C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\19d48c98-d787-4436-90c2-da3f52339ae1\scratchpad\precheck'
$OUT     = "$RIG\out"
$WORK    = 'C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\61a5cd70-a460-42b8-9332-60f6bfe05a40\scratchpad\trust-probe31'
$TRACE   = 'C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-claude-spt\hook-trace.log'
$CONFIG  = $env:CLAUDE_CONFIG_DIR
if (-not $CONFIG) { throw 'CLAUDE_CONFIG_DIR unset - resolve the config root explicitly before running' }
$SLUG    = ($WORK -replace '[:\\]', '-')
$TIMEOUT = 180
$NONCE   = "PRECHECK-" + (Get-Date -Format 'yyyyMMdd-HHmmss')
New-Item -ItemType Directory -Force -Path $OUT | Out-Null

# RE-VERIFY the trusted dir read-only. Report and STOP on any finding; never clean it up.
$bad = @()
$probe = $WORK
while ($probe -and (Split-Path $probe -Parent)) {
    foreach ($f in 'CLAUDE.md','AGENTS.md','.claude') {
        if (Test-Path (Join-Path $probe $f)) { $bad += (Join-Path $probe $f) }
    }
    $probe = Split-Path $probe -Parent
}
$bad = $bad | Where-Object { $_ -notlike "$env:USERPROFILE\.claude*" }   # user-level layer is expected
if ($bad) { $bad | Out-File "$OUT\precheck.ABORT.txt"; throw "project-scoped config on the chain - reporting, not cleaning" }

New-Item -ItemType Directory -Force -Path $WORK | Out-Null    # recreates the already-trusted path
Copy-Item $TRACE "$OUT\hook-trace.pre.log"; Copy-Item "$TRACE.1" "$OUT\hook-trace.1.pre.log"
$tf = Get-Item $TRACE; $MARK = $tf.Length; $MARKID = $tf.CreationTimeUtc

# --- the one launch ----------------------------------------------------------
function Stop-Guarded($id, $start, $label) {
    $live = Get-Process -Id $id -ErrorAction SilentlyContinue
    if ($live -and $live.StartTime -eq $start) { Stop-Process -Id $id -Force; "KILLED $label pid=$id start=$($start.ToString('o'))" }
    else { "NO-KILL $label pid=$id expected_start=$($start.ToString('o')) actual=$($live.StartTime)" }
}

$p    = Start-Process -FilePath 'claude' `
          -ArgumentList '-p', "Reply with exactly: $NONCE", '--output-format', 'json' `
          -WorkingDirectory $WORK -PassThru `
          -RedirectStandardOutput "$OUT\precheck.spawn.json" -RedirectStandardError "$OUT\precheck.spawn.err"
$pid0 = $p.Id; $st0 = $p.StartTime
"LAUNCH pid=$pid0 start=$($st0.ToString('o'))" | Out-File "$OUT\precheck.launches.txt"

Start-Sleep -Seconds 2
Stop-Guarded $pid0 $st0 'spawn' | Out-File "$OUT\precheck.kill.txt"
Wait-Process -Id $pid0 -Timeout $TIMEOUT -ErrorAction SilentlyContinue
if (-not $p.HasExited) {
    "TIMEOUT pid=$pid0 bound=${TIMEOUT}s" | Out-File "$OUT\precheck.timeout.txt"
    Stop-Guarded $pid0 $st0 'timeout' | Out-File "$OUT\precheck.kill.txt" -Append
}

# --- harvest, rotation-aware -------------------------------------------------
$tf2 = Get-Item $TRACE
if ($tf2.Length -lt $MARK -or $tf2.CreationTimeUtc -ne $MARKID) {
    "ROLLED mark_len=$MARK now_len=$($tf2.Length) - EVIDENCE UNAVAILABLE (not a silent-hook finding)" |
        Out-File "$OUT\precheck.trace.txt"
} else {
    $fs = [IO.File]::Open($TRACE,'Open','Read','ReadWrite'); $fs.Seek($MARK,'Begin') | Out-Null
    (New-Object IO.StreamReader($fs)).ReadToEnd() | Out-File "$OUT\precheck.trace.txt"; $fs.Close()
}
Get-ChildItem "$CONFIG\projects\$SLUG\*.jsonl" -ErrorAction SilentlyContinue |
    Select-Object Name, Length, LastWriteTime | Out-File "$OUT\precheck.sessions.txt"
Get-ChildItem "$CONFIG\projects\$SLUG\*.jsonl" -ErrorAction SilentlyContinue |
    ForEach-Object { Copy-Item $_.FullName "$OUT\precheck.$($_.Name)" }
```

## Reading

| Observation | D1 | D2 |
|---|---|---|
| `BEGIN UserPromptSubmit` present in the slice | **YES** — print mode reaches the surface | — |
| slice present, no `BEGIN UserPromptSubmit`, **and** the nonce is in a transcript | **NO** — returns to doyle as a result | — |
| slice says `ROLLED` | **evidence unavailable** — NOT a "no", and must never be written up as one | same |
| nonce absent from every transcript | **instrument failure, check void** — the spawn never got as far as a submission | same |
| a `.jsonl` exists for the slug with the nonce in it | — | **YES** — a resumable id survives the kill |
| no `.jsonl`, or none containing the nonce | — | **NO** — returns to doyle as a result |

D1 and D2 are read from **different** artifacts (trace slice vs transcript directory), so one can be
answered while the other is void. Report them separately; never let a D1 answer imply a D2 one.

**Counts, reported separately even for a single case:** scheduled 1 / attempted / evaluable /
unavailable. A void check is reported as void, not as a negative.

## Cleanup — perri owns it, reported per item

1. Terminate any surviving PID from `precheck.launches.txt` under the identity guard; never by image name.
2. Remove the recreated `WORK` directory (it was created empty by this check and is left empty).
3. Remove the disposable session transcripts under `$CONFIG\projects\$SLUG\` **after** copying them into `$OUT`.
4. Leave `$OUT` in place as the evidence record.
5. **No trust key to remove — none was written.**

Report each item as performed rather than asserting cleanup as a whole.

## Gate

`sh ci/run-gates.sh` PASS and `traceable-reqs check` exit 0 for any code this produces. The rig stays
in the scratchpad and is not committed unless it is kept, in which case it lives under `ci/measure/`.
