diff --git a/packages/core/src/__tests__/platform.mock.test.ts b/packages/core/src/__tests__/platform.mock.test.ts index a5435b19c..68f32ef29 100644 --- a/packages/core/src/__tests__/platform.mock.test.ts +++ b/packages/core/src/__tests__/platform.mock.test.ts @@ -129,16 +129,18 @@ describe("resolveWindowsShell", () => { // --------------------------------------------------------------------------- describe("killProcessTree", () => { - it("calls taskkill with /T /PID (no /F) for SIGTERM on Windows", async () => { + it("calls taskkill with /T /F /PID for SIGTERM on Windows (always force-kill)", async () => { setPlatform("win32"); resolveExecFile(""); // taskkill succeeds const mod = await import("../platform.js"); await mod.killProcessTree(1234, "SIGTERM"); + // On Windows we always pass /F regardless of signal — taskkill without /F sends + // WM_CLOSE which is unreliable for headless Node.js console processes. expect(mockExecFile).toHaveBeenCalledWith( "taskkill", - ["/T", "/PID", "1234"], + ["/T", "/F", "/PID", "1234"], expect.any(Function), ); }); diff --git a/packages/core/src/agent-workspace-hooks.ts b/packages/core/src/agent-workspace-hooks.ts index 2a87e369f..e9fa1f293 100644 --- a/packages/core/src/agent-workspace-hooks.ts +++ b/packages/core/src/agent-workspace-hooks.ts @@ -271,8 +271,8 @@ exit \$exit_code * Build a Node.js wrapper script for a given binary (gh or git). * * On Windows, bash scripts cannot be executed directly, so we generate: - * - .js — the actual interception logic (Node.js) - * - .cmd — a tiny CMD shim: @node "%~dp0.js" %* + * - .cjs — the actual interception logic (Node.js, forced CJS mode) + * - .cmd — a tiny CMD shim: @node "%~dp0.cjs" %* * * The .js script replicates what the bash wrapper does: * - gh: intercepts `gh pr create` and `gh pr merge` diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index b5b599b56..4b4275b78 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -82,10 +82,11 @@ export async function killProcessTree( // kill AO itself. pid<0 is never valid. Guard both. if (pid <= 0) return; if (isWindows()) { - // taskkill /T kills the process tree; /F forces termination (SIGKILL equivalent). - // Without /F, taskkill sends WM_CLOSE allowing the process to shut down gracefully - // (SIGTERM equivalent). This preserves the SIGTERM→wait→SIGKILL escalation pattern. - const args = signal === "SIGKILL" ? ["/T", "/F", "/PID", String(pid)] : ["/T", "/PID", String(pid)]; + // Always use /F (force) on Windows. taskkill without /F sends WM_CLOSE, which + // only works for GUI windows; headless Node.js console processes may ignore it, + // leaving orphaned processes. Callers that do SIGTERM→wait→SIGKILL escalation + // are unaffected: the SIGKILL step simply finds the process already dead. + const args = ["/T", "/F", "/PID", String(pid)]; try { await execFileAsync("taskkill", args); } catch {