---
name: ccs-win-pty-program-resolve
description: "Windows PTY-spawn bug: bare program (node CLI `ccs`) resolved to extensionless shim → CreateProcessW os error 193. Fixed: spt_term::winprog PATHEXT resolver + cmd/powershell wrap at spawn_program_in. REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE, PR #20 off main (v0.8.3). doyle solo in worktree."
metadata: 
  node_type: memory
  type: project
  originSessionId: 9d0930b3-34c1-4dce-943f-72ccdc8fb726
---

**ccs Windows PTY-spawn fix** — operator dogfood 2026-06-16: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\nvm4w\nodejs\ccs → os error 193 (%1 not a valid Win32 application)`. doyle diagnosed+fixed SOLO in an isolated worktree (operator-assigned), separate from todlando's v0.9.0.

**Root cause:** `harnesshost::prepare_harness_spawn` passes the bare `[session.self]` program token (`ccs`) raw → broker → portable-pty `CommandBuilder` whose Windows `which` takes the FIRST PATH match = the extensionless node/Git-Bash shim (shipped beside `ccs.cmd`); CreateProcessW can't exec a non-PE file → 193. The `\0` in the error = portable-pty's NUL-terminated-buffer display, benign. std::process::Command path is fine (std handles PATHEXT/.cmd); only the portable-pty PTY path was broken.

**Fix:** new `spt_term::winprog::resolve_for_pty` (+ pure `resolve_in` kernel) wired into `PtySession::spawn_program_in` — the SINGLE CommandBuilder chokepoint (broker.rs:840), so all broker harness + shell spawns covered. PATHEXT precedence (real .exe/.com before .bat/.cmd), wrap .cmd/.bat→`cmd.exe /d /c`, .ps1→`powershell -NoProfile -File`; Unix passthrough. Bypasses portable-pty's shim-first which. CAVEAT: cmd /c inherits cmd quoting for spaces/metachars in paths — adequate for common install paths, robust-quoting is a follow-on if it bites.

**Evidence:** REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE (doc+impl+unit+int) · KNOWN-HAZARDS 5.12 + conformance row · 5 unit (resolve_in precedence) · 1 windows-gated int (tests/winspawn.rs: a .cmd spawns via the wrap, the 193 regression). Gate green local: clippy --workspace --all-targets -D warnings, traceable EXIT0, xtask check OK, spt-term suite + broker seam test.

**LESSON:** test harness modeled case-SENSITIVE file existence but PATHEXT exts are uppercase (.CMD) vs on-disk lowercase (ccs.cmd) — real Windows FS is case-INSENSITIVE; the impl was right, the test exists_set+assertions needed case/sep normalization (NTFS-mirroring). Also: `&&`-chaining a gate on `cmd | grep` gates on grep's exit, not cmd's — masked a compile error; use PIPESTATUS for real exit codes.

**STATUS: SHIPPED v0.8.4 (PUBLISHED 2026-06-17, counter 20, PATCH, publisher leg 14× clean). Hashes: linux 3ba0e390… win 183df369…; tag 29a7170, bump c9ff19f, signed rel-primary-2026, update-set v20. doyle hash-check ✓. On fleet update path.** (Merged main @6cbb11a → deployah self-drive.)** CI was green both runners pre-merge. FIRST CI RED was a real catch (not flake): Linux clippy -D warnings flagged the resolution kernel (Launch/resolve_in/helpers) as dead-code — on non-windows non-test, resolve_for_pty is a passthrough so they're unused; my local clippy was windows-only. FIX: `#[cfg(any(windows, test))]` on the kernel (present on windows=used + test=any-runner, absent on linux lib). LESSON: a windows-only impl behind cfg → its cross-platform-looking helpers are dead on the OTHER platform's non-test build; gate to windows+test, and a windows-local clippy WON'T catch it — only the Linux CI runner does. ccsfix worktree removed post-merge. doyle merged + handed deployah w/ vetted [0.8.4] CHANGELOG. RELATED: picker STATUS work (4-state amber harness-only + project_history-empty bug) → todlando stacked slice (REQ-PICKER-1) [[harness-adapter-agnostic-resolution]].
