# Probe Discipline

Cross-cutting design surface for runtime-spawn-site safety. Closes the
2026-04-29 incident class (Obsidian host vs asar override version drift —
APPCRASH popup spam at probe-time launch).

Authoritative implementation: `bin/lib/probe-cli.js` (3 helpers).

## The Contract: existsOnDisk vs launchProbe

Two probe primitives. Each carries a distinct failure-mode envelope.

### existsOnDisk(cmd)

Pure metadata check. NO process is spawned. Walks `process.env.PATH` (or
accepts an absolute path); returns true iff the binary file is reachable
on disk. On Windows, also iterates `PATHEXT`.

```javascript
const { existsOnDisk } = require('./bin/lib/probe-cli');
if (!existsOnDisk('obsidian')) return; // Skip — binary not on PATH.
```

**Use for:** binaries that may CRASH on launch (Electron apps like
`Obsidian.exe`; Squirrel-installed apps with asar override drift). Zero
APPCRASH risk because no spawn is attempted.

### launchProbe(cmd, args, timeoutMs)

Impure spawn-with-timeout. Spawns via `child_process.spawn` (`shell: false`),
races a timeout against the child's exit. On timeout, sends SIGTERM. Drains
stdout AND stderr to prevent backpressure-induced hangs on chatty CLIs.
Resolves bare names to absolute paths so Windows `.cmd` / `.bat` shims are
routed through `cmd.exe /d /s /c` automatically.

```javascript
const { launchProbe } = require('./bin/lib/probe-cli');
const ok = await launchProbe('codex', ['--version'], 5000);
if (!ok) return; // Skip — binary failed to launch within 5s.
```

**Use for:** non-Electron CLIs where launchability IS the actual signal
(`gemini`, `codex`, `claude`). Bounded blast radius via timeout + drain.

## Runtime-Spawn Site Catalog

Two known runtime-spawn sites are in scope for the probe-discipline audit:

| File:Line                            | External CLI | Posture                                                                                                                                                                                |
| ------------------------------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bin/lib/vault-query.js:218`         | `obsidian`   | Gated at function entry by `isEnabled('obsidian')`; only fires when the user opts in. Allowlisted with the gate as the cited justification.                                           |
| `hooks/gmd-codex-review-hook.js:243` | `codex`      | Fail-soft — the surrounding `try` / `catch` (lines 261-263) silently swallows ENOENT and any other spawn error. Allowlisted with the fail-soft posture as the cited justification.    |

One additional spawn lives inside the implementation module itself:
`bin/lib/probe-cli.js::checkObsidianVersionCoherence` performs
`spawn('cmd.exe', ['/d', '/s', '/c', 'powershell', ...])` to read the
host `ProductVersion`. Sandboxed inside `probe-cli.js`. The audit treats
`probe-cli.js` callsites as exempt-by-location.

## Allowlist Convention

When a callsite legitimately bypasses the `existsOnDisk` / `launchProbe`
precheck pattern, it MUST appear on `tests/audit-probe-discipline.sh`'s
allowlist with documented justification. The allowlist is **data, not
code logic** — additions require explicit human edit, not silent drift.

Each entry: `{ file, line_anchor, justification }`. Justifications must
cite the structural mechanism that makes the precheck unnecessary
(e.g., upstream `isEnabled` gate, fail-soft `try` / `catch` posture,
env-var-controlled test mode). "We'll fix it later" is not a justification.

## Failure Modes Guarded Against

- **APPCRASH-on-launch** — Squirrel-Electron asar-mismatch class. The
  2026-04-29 incident: Obsidian host v1.12.4 + asar override v1.12.7 →
  unhandled COMException at probe-time spawn. `existsOnDisk` avoids the
  spawn entirely; `launchProbe` times out after `timeoutMs`.
- **Popup-spam class** — repeated APPCRASH dialogs flooding the desktop
  UI on hot-loop probe sites. Both primitives bound the spawn count to one.
- **Silent hangs** — chatty stderr backpressure-fills the pipe; the close
  event never fires. `launchProbe` drains stdout AND stderr to prevent this.
- **ENOENT crashes in unhandled spawn paths** — bare `spawn(...)` without
  an `'error'` handler throws on missing binary. `existsOnDisk` returns
  false; `launchProbe` resolves false on the `'error'` event.

---

*Audit: `tests/audit-probe-discipline.sh` (Phase 44 PROBE-07).*
*Implementation: `bin/lib/probe-cli.js` (Phase 36.1 PROBE-01..03 + Phase 43 PROBE-04).*
