* fix(windows): bound generic-hook runner timeout ownership and nested git (#3493)
Native Windows generic-hook run.cjs parents survived 30-66 min despite
3-10s manifest budgets. Two runner-side gaps plus unbounded nested git
calls left the generic hook path without reliable termination.
run.cjs (generic branch):
- Replace the blocking spawnSync generic dispatch with an async spawn
child supervisor (runGenericChild) that owns a setTimeout and reaps the
OS process tree while the child is still alive. spawnSync only returns
after the child fully exits, so post-call cleanup was unreachable.
- POSIX: spawn detached and process.kill(-pid, SIGKILL) the group with a
direct-child fallback. Windows: taskkill /T /F /PID bounded by its own
timeout:2000 so the reap cannot hang the handler. Job Object kill-on-
close remains the documented escalation if the Windows PID-survival
test regresses.
- Always arm a runner-owned timer: resolveGenericTimeoutMs falls back to
DEFAULT_GENERIC_TIMEOUT_MS (59500ms = max manifest budget - cushion)
when manifest resolution is null (closes the unbounded Gap A) and is
bypassed when the manifest resolves.
- Pin the exit-code contract (result.status ?? 0 semantics): numeric exit
propagates; signal-only, spawn error, reap failure, and timeout fail
open with 0; single-resolve guard; timer cleared on every terminal path.
- Add a require.main===module guard + module.exports so internals are
unit-testable without executing dispatch. #3490 worker path, allowlist,
provenance, and fail-open are unchanged.
Nested git bounding:
- Add shared BOUNDED_GIT_TIMEOUT_MS=2000 (scripts/lib + templates/hooks/
lib, byte-identical) and apply it at the 8 owned sites: the 4 unbounded
resolveTranscriptPath git calls and the 4 over-budget 5000ms calls
(resolveOmcRoot x2, code-simplifier x2). Already-bounded compliant sites
(context-guard-stop 1000ms, workflow-drift-guard 2000ms) are untouched.
Tests:
- run-cjs-generic-timeout: exports/selection, POSIX group reap, A2 exit
states, single-terminalization.
- windows-prompt-hook-runner: generic-path Windows PID-survival reap
assertion (runs on the windows-latest CI lane).
- bounded-git-timeout-parity: both constant copies match.
- windows-hide-hooks: two-tier static scan (every generic-hook git call
bounded; owned sites must use the shared constant; compliant sites
preserved) with independent-tier negative seeds.
- installer: require bounded-git-timeout.mjs in templates/hooks/lib.
Fixes#3493
* fix(windows): reap detached generic-hook tree on runner termination (#3493)
Hostile review found that runGenericChild detached the hook into its own
POSIX process group but reaped only from the inner timeout callback. If
run.cjs was terminated or cancelled BEFORE its timer fired (outer
hooks.json timeout, Ctrl-C, parent kill), the detached hook tree was
orphaned - reintroducing the #3493 orphan class for the outer-termination
path and regressing prior POSIX cancellation behavior.
- Register SIGTERM/SIGINT/SIGHUP and process 'exit' handlers while the
generic child is live; on runner termination reap the process tree
(idempotent, single-terminal guard) before exiting fail-open.
- Remove the handlers on every terminal path (timeout/exit/error) so no
listeners leak across in-process supervisor calls.
- reapTree now returns success/failure and inspects the Windows taskkill
status.
- Add a POSIX subprocess test that kills the runner before its timeout and
asserts the detached child and grandchild are reaped.
* fix(windows): guarantee runner exit on failed reap; non-blocking taskkill (#3493)
Second hostile review found the timeout path could still leave run.cjs
alive on Windows: reapTree returned false when taskkill was missing/denied/
timed out, but the live ChildProcess handle kept the event loop alive, so
run.cjs never exited — the exact "run.cjs parent alive" symptom of #3493.
- Add child.unref() on the timeout path so the runner ALWAYS exits fail-open
even when the best-effort tree reap does not complete.
- Make the Windows reap non-blocking: spawn `taskkill /T /F` detached +
unref'd (fire-and-forget) instead of a synchronous spawnSync that could
block up to 2000ms past the outer hooks.json budget. POSIX group-kill is
already instantaneous.
- Drop the now-unused spawnSync import.
- Clarify the POSIX outer-cancellation test comment (Windows programmatic
SIGTERM force-terminates rather than delivering a catchable signal, so that
path is POSIX-specific; killing the grandchild proves the whole group,
including the direct hook child, was reaped).
---------
Co-authored-by: clawdbot <clawdbot@users.noreply.github.com>