diff --git a/plugins/codex/scripts/stop-review-gate-hook.mjs b/plugins/codex/scripts/stop-review-gate-hook.mjs index 2346bdcf4..bab91a59a 100644 --- a/plugins/codex/scripts/stop-review-gate-hook.mjs +++ b/plugins/codex/scripts/stop-review-gate-hook.mjs @@ -13,7 +13,7 @@ import { sortJobsNewestFirst } from "./lib/job-control.mjs"; import { SESSION_ID_ENV } from "./lib/tracked-jobs.mjs"; import { resolveWorkspaceRoot } from "./lib/workspace.mjs"; -const STOP_REVIEW_TIMEOUT_MS = 15 * 60 * 1000; +const STOP_REVIEW_TIMEOUT_MS = 14 * 60 * 1000; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.resolve(SCRIPT_DIR, ".."); const STOP_REVIEW_TASK_MARKER = "Run a stop-gate review of the previous Claude turn."; @@ -113,7 +113,7 @@ function runStopReview(cwd, input = {}) { return { ok: false, reason: - "The stop-time Codex review task timed out after 15 minutes. Run /codex:review --wait manually or bypass the gate." + "The stop-time Codex review task timed out after 14 minutes. Run /codex:review --wait manually or bypass the gate." }; } diff --git a/tests/commands.test.mjs b/tests/commands.test.mjs index c34b06059..11797760d 100644 --- a/tests/commands.test.mjs +++ b/tests/commands.test.mjs @@ -210,6 +210,27 @@ test("hooks keep session-end cleanup and stop gating enabled", () => { assert.match(source, /session-lifecycle-hook\.mjs/); }); +test("stop-review task timeout leaves headroom under the Stop hook timeout", () => { + const hookSource = read("scripts/stop-review-gate-hook.mjs"); + const hooks = JSON.parse(read("hooks/hooks.json")); + const stopHook = hooks.hooks.Stop.flatMap((entry) => entry.hooks).find((hook) => + String(hook.command ?? "").includes("stop-review-gate-hook.mjs") + ); + const hookTimeoutMs = Number(stopHook?.timeout) * 1000; + const timeoutMatch = hookSource.match(/const STOP_REVIEW_TIMEOUT_MS = ([^;]+);/); + assert.ok(timeoutMatch, "STOP_REVIEW_TIMEOUT_MS should be declared"); + assert.match(timeoutMatch[1], /^[\d\s.*+\-/()]+$/); + const innerTimeoutMs = new Function(`return (${timeoutMatch[1]});`)(); + const timeoutMessageMatch = hookSource.match(/timed out after ([0-9]+) minutes/); + + assert.equal(Number.isFinite(hookTimeoutMs), true); + assert.equal(hookTimeoutMs, 900000); + assert.equal(Number.isFinite(innerTimeoutMs), true); + assert.equal(innerTimeoutMs < hookTimeoutMs, true); + assert.equal(hookTimeoutMs - innerTimeoutMs >= 30000, true); + assert.equal(Number(timeoutMessageMatch?.[1]) * 60 * 1000, innerTimeoutMs); +}); + test("setup command can offer Codex install and still points users to codex login", () => { const setup = read("commands/setup.md"); const readme = fs.readFileSync(path.join(ROOT, "README.md"), "utf8");