Address issues found by code review:
1. Bug: timeout/signal-killed child reported as 'completed' with exit
code 0 because close handler ignored the signal parameter. Now
treats any signal termination as timed_out.
2. Bug: cancelled job gets overwritten by watcher on child exit. The
watcher now re-reads the job file before writing and skips if the
status has been changed to 'cancelled'.
3. Inconsistency: watcher path skipped stderr noise filtering. Added
filterStderrNoise to the watcher (duplicated from execute.ts to
keep the watcher self-contained).
4. getJobResult now guards against missing result field instead of
using non-null assertion.
The async startJob previously relied on Node.js event listeners in the
CLI process to capture child output and finalize the job file. But the
CLI process exits immediately after returning the job ID, killing the
event loop before the close handler fires — leaving jobs stuck at
'running' forever.
Fix: startJob now spawns a companion watcher process (job-watcher.ts)
that is itself detached and outlives the CLI. The watcher:
- Spawns the actual client (codex/claude/opencode)
- Captures stdout/stderr
- Writes the final job record to disk on child exit
- Has its own 10-minute timeout safety net
The CLI returns the job ID immediately. The watcher independently
finalizes the job. The CLI no longer needs to stay alive.
Also updates tests to mock the watcher spawn via injectable
spawnWatcher option.
When codex exec receives a prompt as a positional argument, it still
tries to read additional input from stdin (prints 'Reading additional
input from stdin...'). With stdio stdin set to 'ignore' or default,
codex blocks indefinitely waiting for stdin that never comes.
Fix: use stdio ['pipe', 'pipe', 'pipe'] and immediately close stdin
via child.stdin.end() in both execute.ts (sync) and jobs.ts (async).
This signals EOF to codex so it proceeds with the positional prompt.