A worker subprocess that inherits the worker's fd 2 (the pipe write-end
when cmd.Stderr is a non-*os.File like our MultiWriter for panic
detection) and outlives the worker keeps the pipe open after the worker
exits. cmd.Wait then blocks forever in its internal I/O drain loop, the
fork() select never fires on cmdwait, and the master never spawns the
next worker — a real 10-minute outage observed on rais-main where a
vscode-server grandchild had inherited the pipe.
The new Config.WaitDelay (default = TerminateTimeout) is wired straight
into cmd.WaitDelay. After the worker exits, Go's exec force-closes
parentIOPipes once WaitDelay elapses, the I/O goroutine sees EOF, and
cmd.Wait returns so the master can proceed.
Also harden the exit-code extraction: prefer cmd.ProcessState (always
set after process exit) and use errors.As so a WaitDelay-wrapped exit
error still reports the true code instead of falling back to 1.
Integration test reproduces the bug with a real binary that spawns a
long-running grandchild via cmd.Stderr = os.Stderr and verifies cmd.Wait
hangs without WaitDelay and returns within budget with it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>