diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe index 772d2487e..dcc9e3bc2 100755 --- a/bin/gstack-codex-probe +++ b/bin/gstack-codex-probe @@ -418,6 +418,9 @@ _gstack_codex_timeout_wrapper() { sleep 1 _GSTACK_CODEX_WAITED=$((_GSTACK_CODEX_WAITED + 1)) done + # Freeze the command first: killing its children while it runs lets it + # continue past the killed child and print output after its deadline. + kill -STOP "$_cmd_pid" 2>/dev/null pkill -KILL -P "$_cmd_pid" 2>/dev/null kill -KILL "$_cmd_pid" 2>/dev/null exit 124 diff --git a/test/codex-hardening.test.ts b/test/codex-hardening.test.ts index bb345a4a5..c0e8efdc4 100644 --- a/test/codex-hardening.test.ts +++ b/test/codex-hardening.test.ts @@ -357,7 +357,10 @@ describe('gstack-codex-probe: timeout wrapper + namespace hygiene', () => { // tool gate killed the whole call and the partial output was lost. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-watchdog-stubborn-')); try { - for (const tool of native ? ['bash', 'sleep', 'cat'] : []) { + // pkill is part of the watchdog's contract (it reaps the provider's + // children; stock macOS ships /usr/bin/pkill). Without it the orphaned + // `sleep 30` keeps stdout open and the capture waits out the sleep. + for (const tool of native ? ['bash', 'sleep', 'cat', 'pkill'] : []) { const resolved = spawnSync('bash', ['-c', `command -v ${tool}`], { timeout: 5000 }); fs.symlinkSync(resolved.stdout.toString().trim(), path.join(dir, tool)); }