3b9d1b3cde
Port from openai/codex#37527: Terminate timed-out hook process trees. A shell hook that forked helpers (scanners, watchers, "cmd &") and then hit its timeout left those descendants running forever — subprocess.run() only kills the direct child. Worse, descendants holding the inherited pipe write ends could stall run()'s post-kill communicate() drain. - agent/shell_hooks.py _spawn(): spawn hooks in their own process group on POSIX (process_group=0, Python >=3.11); on timeout/error, reap the whole tree via the shared kill_process_tree() helper, then drain bounded (1s). Hooks that complete in time keep their descendants, so intentionally detached helpers survive successful runs (mirrors codex semantics). - hermes_cli/_subprocess_compat.py: rename _kill_git_process_tree -> kill_process_tree (it was never git-specific; taskkill /T /F on Windows, ownership-gated os.killpg on POSIX). Backward-compat alias retained. - tests/agent/test_shell_hooks_tree_kill.py: real-subprocess regression tests (descendant killed on timeout, preserved on success, own-group spawn, fast-path contract, fail-open). Sabotage-verified: reverting the process_group spawn fails exactly the two new behavior tests. Gap proven live on main first: a forking hook timed out at 2s and its descendant survived; same probe against this branch shows it reaped.