Self-review of the command executor refactor caught a real bug in the
cancel() path: `settle()` called `clearTimers()` which cleared BOTH the
deadline timer (`killTimer`) AND the SIGKILL escalation timer
(`forceTimer`) that `cancel()` had just scheduled. A child that traps
SIGTERM and keeps running would survive as an orphan because no SIGKILL
ever followed.
Fix:
- Split `clearTimers` into `clearKillTimer` + `clearForceTimer`.
- `settle()` only clears `killTimer` — the deadline timer must not fire
after the outcome is settled, but the escalation must keep running
until either the child actually exits OR the escalation itself fires.
- `close` handler clears `forceTimer` (child is dead — no need to send
SIGKILL to a potentially-recycled PID).
- Both `killTimer` and `forceTimer` self-clear (`= null`) when they
fire, so the close-handler clear is a no-op if escalation already
ran.
Regression test: `cancelStage SIGKILL escalation actually fires when
the child ignores SIGTERM` — spawns `/bin/sh -c "trap '' TERM; while :;
do sleep 1; done"`, cancels, then waits past `COMMAND_KILL_GRACE_MS`
and asserts the test completes cleanly (under the bug, the child loop
outlives the test process and leaks into CI workers).
Also (NIT): updated the stale engine.ts file-level JSDoc — it still
listed "Command + builtin executors (v1.2)" as out of scope and "v0.2"
in the headline. Now reflects v1.2 reality (agent + command + builtin
all wired) and documents the non-blocking startStage/pollStage handoff.
All 1373 core tests pass.