diff --git a/gitnexus-cursor-integration/hooks/gitnexus-hook.cjs b/gitnexus-cursor-integration/hooks/gitnexus-hook.cjs index ffddd163e..f97cd2d56 100644 --- a/gitnexus-cursor-integration/hooks/gitnexus-hook.cjs +++ b/gitnexus-cursor-integration/hooks/gitnexus-hook.cjs @@ -339,6 +339,16 @@ function resolveCliPath() { } } +// The Cursor host enforces hooks.json's postToolUse `timeout` (10s) against +// the whole hook process. The npx fallback used to add a flat +5s on top of +// the inner budget (7000 → 12000ms), past the host deadline, so Cursor killed +// the hook before the child finished and the augmentation was always lost on +// cold-start machines — the exact scenario the fallback exists for. Cap the +// fallback under the host budget, leaving headroom for node startup and the +// final stdout write. +const CURSOR_HOST_BUDGET_MS = 10000; +const CURSOR_NPX_HEADROOM_MS = 2000; + function runGitNexusCli(cliPath, args, cwd, timeout) { const isWin = process.platform === 'win32'; if (cliPath) { @@ -350,9 +360,13 @@ function runGitNexusCli(cliPath, args, cwd, timeout) { windowsHide: true, }); } + const npxTimeout = Math.min( + timeout + 5000, + CURSOR_HOST_BUDGET_MS - CURSOR_NPX_HEADROOM_MS, + ); return spawnSync(isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args], { encoding: 'utf-8', - timeout: timeout + 5000, + timeout: npxTimeout, cwd, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, diff --git a/gitnexus/test/unit/cursor-hook.test.ts b/gitnexus/test/unit/cursor-hook.test.ts index 4fe58eafa..2bac8a643 100644 --- a/gitnexus/test/unit/cursor-hook.test.ts +++ b/gitnexus/test/unit/cursor-hook.test.ts @@ -441,6 +441,25 @@ describe('Cursor hook debug logging', () => { }); }); +// ─── Source code regression: npx fallback stays under the host budget ───── + +describe('Cursor hook npx fallback host budget', () => { + const source = fs.readFileSync(CURSOR_HOOK, 'utf-8'); + + it('caps the npx fallback timeout below the hooks.json postToolUse budget', () => { + // hooks.json grants postToolUse 10s. The fallback branch adds +5s on top + // of the inner 7s budget, which used to reach 12s — past the host + // deadline, so Cursor killed the hook and cold-start users never saw + // augmentation. Assert the cap wiring exists and the effective fallback + // budget (7000 + 5000, clamped by 10000 - 2000) can never exceed the + // host budget again. + expect(source).toContain('CURSOR_HOST_BUDGET_MS = 10000'); + expect(source).toContain('CURSOR_NPX_HEADROOM_MS = 2000'); + expect(source).toContain('Math.min('); + expect(source).not.toMatch(/timeout:\s*timeout \+ 5000/); + }); +}); + // ─── Source code regression: concurrency guard (#1486) ───────────── describe('Cursor hook concurrency guard', () => {