Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins/codex/scripts/stop-review-gate-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down Expand Up @@ -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."
};
}

Expand Down
21 changes: 21 additions & 0 deletions tests/commands.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down