# F-019 — idle-delivery translation binary not resolved against install_dir (v0.14.2)

**doyle design → todlando build → doyle gate.** Root CONFIRMED on real claude-spt v0.6.2 (perri + doyle).

## The bug (perri, real claude-spt, Windows — the #1 CC-on-Windows blocker)

Idle message delivery typed the envelope into Claude Code's input box but **never submitted** (deliveries "stacked" unsent). After an exhaustive black-box dig (byte encoding, win32-input-mode ?9001h, bracketed-paste, focus ?1004h, pacing/coalescing — all moot), the root is:

**The `[message-idle-translation-binary]` never ran.** The adapter ships a **bare relative** path `path = "cc-spt-idle-translate"` (no `.exe`). spt-core passes it **verbatim** to the broker, which spawns it as `Command::new(path)` (broker.rs:1677) — resolved against the **daemon's cwd/PATH**, NOT the adapter install dir. It isn't on PATH → spawn fails → `build_translation` returns `None` → the session **FAILS CLOSED to raw inject** (broker.rs:1671-1677), which types the message + Enter raw (the same no-submit symptom). So the adapter's `{text}{delay}{key:enter}{commit}` choreography — which is CORRECT — never executed.

**Proof:** (1) ground-truth instrumentation showed the inject worker DOES enqueue `{key:enter}`→`0x0d` etc. when a binary runs; (2) 0 resident translation processes for live endpoints; (3) perri set the manifest path to the ABSOLUTE `_github` exe → fresh endpoint → the binary ran and the message **submitted**.

## Root cause (one missed surface)

REQ-INSTALL-11 already says *"adapter command templates resolve their program against the adapter's install dir before PATH"* and ships `resolve_program_in_dir(program, install_dir)` (runtime.rs:527 — joins install_dir, adds `.exe` on Windows when present, else bare PATH fallback). Session/digest/psyche programs already route through it (`ResolvedManifest::with_install_dir` → runtime.rs:338). **The translation binary path was MISSED** — `prepare_harness_spawn` (harnesshost.rs:142-145) clones `manifest.message_idle_translation_binary.path` verbatim, never resolving it.

## Fix — route the translation binary through the existing resolver

Resolve `translation_binary` against `install_dir` via the EXISTING `resolve_program_in_dir`, exactly like the session/digest/psyche programs:

- `install_dir` is available at `launch_harness_brokered_in` (harnesshost.rs:185-196, the `install_dir` arg) — it's already recorded on the broker session (W3a/ADR-0025).
- Cleanest seam: in `launch_harness_brokered_in`, after `prepare_harness_spawn` returns `PreparedSpawn`, map `translation_binary` through `spt_runtime::runtime::resolve_program_in_dir(&path, install_dir)` when `install_dir` is `Some` (mirror the bare-fallback for `None`). Then the SpawnReq carries the resolved absolute path → `Command::new` succeeds → the real binary runs.
  - (Alternative: pass `install_dir` into `prepare_harness_spawn` and resolve there, co-located with the other program resolution. Builder's choice — keep it co-located with how the session program is resolved if cleaner.)

No adapter change needed: the shipped `path = "cc-spt-idle-translate"` then resolves to `<install_dir>/cc-spt-idle-translate.exe`.

## Sibling-surface audit (do NOT patch only the translation binary)

REQ-INSTALL-11 missed the translation binary — check for OTHER missed adapter-program surfaces and route them through `resolve_program_in_dir` too, or confirm they already resolve:
- `[session.self]` / `[session.resume]` program — confirm (likely via ResolvedManifest).
- `[session.psyche_init]` / `psyche_resume` / `echo_commune` / `signoff` / `notif` runner programs (livehost/psyche).
- `[digest]` extractor — confirm (digest.rs uses install_dir).
- any `[session.*]` runners / hooks that spawn an adapter-shipped program.
Grep every spawn of an adapter-manifest program; each must resolve against install_dir. List what you found + what you fixed.

## Evidence to land (ONE commit)

- `// [impl->REQ-INSTALL-11]` on the translation-binary resolution (+ any sibling surfaces fixed).
- **unit** (`// [unit->REQ-INSTALL-11]`): `translation_binary_resolves_against_install_dir` — a PreparedSpawn/launch path with a bare translation path + a temp install_dir containing the (stub) binary → the resolved path is the absolute `<install_dir>/<name>(.exe)`; bare-PATH fallback when install_dir is None / file absent (mirror `resolve_program_prefers_install_dir` / `_falls_back_to_path`, runtime.rs:805).
- No traceable stage change (REQ-INSTALL-11 already [doc,impl,unit]); this extends its impl/unit coverage to the missed surface. Note "F-019" in the commit.

## Gates (todlando; doyle re-gates)

1. `cargo clippy --workspace --all-targets`.
2. `cargo test -p spt-daemon --lib` + `cargo test -p spt-runtime --lib` (resolve_program_in_dir + the new translation unit) — `spt-daemon` HAS a lib target (use `--lib`); `spt` does NOT (`--bin spt`).
3. `traceable-reqs check` exit 0.
4. **HITL (perri/operator):** real claude-spt, shipped manifest (relative path), fresh endpoint, `spt send` → CC submits the envelope. (perri's rig is the only real CC-on-Windows.)

## Coordination

perri reverts her absolute-path manifest edit (interim workaround; not portable). On this fix shipping in v0.14.2, the shipped relative path works as authored — perri verifies on real claude-spt. This is the fix that makes spt-hosted idle delivery actually work for CC on Windows.

## Lesson (KNOWN-HAZARDS candidate)

A bare/relative adapter-shipped program path that fails `Command::new` FAILS CLOSED to raw inject **silently** — masquerading as "the binary ran but mis-behaved." Before deep behavioral diagnosis of a translation/adapter binary, confirm it actually SPAWNED (resident process; a deployed edit changes behavior). Consider a louder signal than silent raw-inject fallback when a declared translation binary fails to spawn (e.g., a one-time `TRANSLATION_SPAWN_FAILED:<path>` log) — would have surfaced this in seconds.
