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
5 changes: 3 additions & 2 deletions plugins/codex/agents/codex-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ Forwarding rules:

- Use exactly one `Bash` call to invoke `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task ...`.
- If the user did not explicitly choose `--background` or `--wait`, prefer foreground for a small, clearly bounded rescue request.
- If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer background execution.
- If the user explicitly chose `--background`, call `task --background --await` so the Bash command remains alive until the Codex daemon job reaches a terminal state.
- If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer `task --background --await`.
- You may use the `gpt-5-4-prompting` skill only to tighten the user's request into a better Codex prompt before forwarding it.
- Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work beyond shaping the forwarded prompt text.
- Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own.
- Do not inspect the repository, read files, grep, cancel jobs, summarize output, or do any follow-up work of your own.
- Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`. This subagent only forwards to `task`.
- Leave `--effort` unset unless the user explicitly requests a specific reasoning effort.
- Leave model unset by default. Only add `--model` when the user explicitly asks for a specific model.
Expand Down
7 changes: 4 additions & 3 deletions plugins/codex/commands/rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ $ARGUMENTS

Execution mode:

- If the request includes `--background`, run the `codex:codex-rescue` subagent in the background.
- If the request includes `--background`, run the `codex:codex-rescue` subagent in the background and make the forwarded task command use `task --background --await`.
- If the request includes `--wait`, run the `codex:codex-rescue` subagent in the foreground.
- If neither flag is present, default to foreground.
- `--background` and `--wait` are execution flags for Claude Code. Do not forward them to `task`, and do not treat them as part of the natural-language task text.
- `--background` and `--wait` are execution flags for Claude Code. Do not treat them as part of the natural-language task text.
- `--model` and `--effort` are runtime-selection flags. Preserve them for the forwarded `task` call, but do not treat them as part of the natural-language task text.
- If the request includes `--resume`, do not ask whether to continue. The user already chose.
- If the request includes `--fresh`, do not ask whether to continue. The user already chose.
Expand All @@ -39,9 +39,10 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task-resume-candidate -
Operating rules:

- The subagent is a thin forwarder only. It should use one `Bash` call to invoke `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task ...` and return that command's stdout as-is.
- For background requests, the single `Bash` call must invoke `task --background --await ...` so the command completes only when the Codex daemon job reaches a terminal state.
- Return the Codex companion stdout verbatim to the user.
- Do not paraphrase, summarize, rewrite, or add commentary before or after it.
- Do not ask the subagent to inspect files, monitor progress, poll `/codex:status`, fetch `/codex:result`, call `/codex:cancel`, summarize output, or do follow-up work of its own.
- Do not ask the subagent to inspect files, call `/codex:cancel`, summarize output, or do follow-up work of its own.
- Leave `--effort` unset unless the user explicitly asks for a specific reasoning effort.
- Leave the model unset unless the user explicitly asks for one. If they ask for `spark`, map it to `gpt-5.3-codex-spark`.
- Leave `--resume` and `--fresh` in the forwarded request. The subagent handles that routing when it builds the `task` command.
Expand Down
111 changes: 105 additions & 6 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
const ROOT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
const REVIEW_SCHEMA = path.join(ROOT_DIR, "schemas", "review-output.schema.json");
const DEFAULT_STATUS_WAIT_TIMEOUT_MS = 240000;
const DEFAULT_AWAIT_TIMEOUT_MS = 6 * 60 * 60 * 1000;
const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
const VALID_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
const MODEL_ALIASES = new Map([["spark", "gpt-5.3-codex-spark"]]);
Expand All @@ -77,8 +78,9 @@ function printUsage() {
" node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]",
" node scripts/codex-companion.mjs review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>]",
" node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [focus text]",
" node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
" node scripts/codex-companion.mjs task [--background [--await]] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
" node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
" node scripts/codex-companion.mjs await <job-id> [--timeout-ms <ms>] [--poll-interval-ms <ms>] [--json]",
" node scripts/codex-companion.mjs result [job-id] [--json]",
" node scripts/codex-companion.mjs cancel [job-id] [--json]"
].join("\n")
Expand Down Expand Up @@ -288,6 +290,26 @@ function isActiveJobStatus(status) {
return status === "queued" || status === "running";
}

function isTerminalJobStatus(status) {
return status === "completed" || status === "failed" || status === "cancelled";
}

function normalizeTimeoutMs(value, fallback) {
if (value == null) {
return fallback;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.max(0, parsed) : fallback;
}

function normalizePollIntervalMs(value) {
if (value == null) {
return DEFAULT_STATUS_POLL_INTERVAL_MS;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.max(100, parsed) : DEFAULT_STATUS_POLL_INTERVAL_MS;
}

function getCurrentClaudeSessionId() {
return process.env[SESSION_ID_ENV] ?? null;
}
Expand All @@ -313,8 +335,8 @@ function findLatestResumableTaskJob(jobs) {
}

async function waitForSingleJobSnapshot(cwd, reference, options = {}) {
const timeoutMs = Math.max(0, Number(options.timeoutMs) || DEFAULT_STATUS_WAIT_TIMEOUT_MS);
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || DEFAULT_STATUS_POLL_INTERVAL_MS);
const timeoutMs = normalizeTimeoutMs(options.timeoutMs, DEFAULT_STATUS_WAIT_TIMEOUT_MS);
const pollIntervalMs = normalizePollIntervalMs(options.pollIntervalMs);
const deadline = Date.now() + timeoutMs;
let snapshot = buildSingleJobSnapshot(cwd, reference);

Expand All @@ -330,6 +352,53 @@ async function waitForSingleJobSnapshot(cwd, reference, options = {}) {
};
}

async function waitForTerminalJobSnapshot(cwd, reference, options = {}) {
const timeoutMs = normalizeTimeoutMs(options.timeoutMs, DEFAULT_AWAIT_TIMEOUT_MS);
const pollIntervalMs = normalizePollIntervalMs(options.pollIntervalMs);
const deadline = Date.now() + timeoutMs;
let snapshot = buildSingleJobSnapshot(cwd, reference);

while (!isTerminalJobStatus(snapshot.job.status) && Date.now() < deadline) {
await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
snapshot = buildSingleJobSnapshot(cwd, reference);
}

return {
...snapshot,
waitTimedOut: !isTerminalJobStatus(snapshot.job.status),
timeoutMs
};
}

async function awaitJobResult(cwd, reference, options = {}) {
const snapshot = await waitForTerminalJobSnapshot(cwd, reference, {
timeoutMs: options.timeoutMs,
pollIntervalMs: options.pollIntervalMs
});

if (snapshot.waitTimedOut) {
outputCommandResult(snapshot, renderJobStatusReport(snapshot.job), options.json);
process.exitCode = 124;
return snapshot;
}

const storedJob = readStoredJob(snapshot.workspaceRoot, snapshot.job.id);
const payload = {
job: snapshot.job,
storedJob
};
outputCommandResult(payload, renderStoredJobResult(snapshot.job, storedJob), options.json);

if (snapshot.job.status !== "completed") {
process.exitCode = 1;
}

return {
...snapshot,
storedJob
};
}

async function resolveLatestTrackedTaskThread(cwd, options = {}) {
const workspaceRoot = resolveWorkspaceRoot(cwd);
const sessionId = getCurrentClaudeSessionId();
Expand Down Expand Up @@ -551,7 +620,7 @@ function buildTaskRunMetadata({ prompt, resumeLast = false }) {
}

function renderQueuedTaskLaunch(payload) {
return `${payload.title} started in the background as ${payload.jobId}. Check /codex:status ${payload.jobId} for progress.\n`;
return `${payload.title} queued as ${payload.jobId}.\nThis command is only launch-complete; daemon completion is not signaled here.\nCheck /codex:status ${payload.jobId} for progress.\nFor completion notification, use: task --background --await ...\n`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use await <job-id> in background launch hint

After task --background finishes, the launch message tells users to run task --background --await ... for completion notification, but that command starts a new task instead of waiting on the queued job that was just created. In real usage this can duplicate expensive work or apply changes twice when users follow the suggested command literally. The hint here should point to await <job-id> (using the emitted job id) for observing completion of the existing job.

Useful? React with 👍 / 👎.

}

function getJobKindLabel(kind, jobClass) {
Expand Down Expand Up @@ -731,8 +800,8 @@ async function handleReview(argv) {

async function handleTask(argv) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["model", "effort", "cwd", "prompt-file"],
booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"],
valueOptions: ["model", "effort", "cwd", "prompt-file", "timeout-ms", "poll-interval-ms"],
booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background", "await"],
aliasMap: {
m: "model"
}
Expand Down Expand Up @@ -770,6 +839,14 @@ async function handleTask(argv) {
jobId: job.id
});
const { payload } = enqueueBackgroundTask(cwd, job, request);
if (options.await) {
await awaitJobResult(cwd, job.id, {
json: options.json,
timeoutMs: options["timeout-ms"],
pollIntervalMs: options["poll-interval-ms"]
});
return;
}
outputCommandResult(payload, renderQueuedTaskLaunch(payload), options.json);
return;
}
Expand All @@ -792,6 +869,25 @@ async function handleTask(argv) {
);
}

async function handleAwait(argv) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["cwd", "timeout-ms", "poll-interval-ms"],
booleanOptions: ["json"]
});

const reference = positionals[0] ?? "";
if (!reference) {
throw new Error("`await` requires a job id.");
}

const cwd = resolveCommandCwd(options);
await awaitJobResult(cwd, reference, {
json: options.json,
timeoutMs: options["timeout-ms"],
pollIntervalMs: options["poll-interval-ms"]
});
}

async function handleTaskWorker(argv) {
const { options } = parseCommandInput(argv, {
valueOptions: ["cwd", "job-id"]
Expand Down Expand Up @@ -1006,6 +1102,9 @@ async function main() {
case "status":
await handleStatus(argv);
break;
case "await":
await handleAwait(argv);
break;
case "result":
handleResult(argv);
break;
Expand Down
5 changes: 3 additions & 2 deletions plugins/codex/skills/codex-cli-runtime/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ Execution rules:

Command selection:
- Use exactly one `task` invocation per rescue handoff.
- If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only. Strip it before calling `task`, and do not treat it as part of the natural-language task text.
- If the forwarded request includes `--background`, strip it from the natural-language task text and call `task --background --await`.
- If the forwarded request includes `--wait`, treat that as Claude-side execution control only. Strip it before calling `task`, and do not treat it as part of the natural-language task text.
- If the forwarded request includes `--model`, normalize `spark` to `gpt-5.3-codex-spark` and pass it through to `task`.
- If the forwarded request includes `--effort`, pass it through to `task`.
- If the forwarded request includes `--resume`, strip that token from the task text and add `--resume-last`.
Expand All @@ -38,6 +39,6 @@ Command selection:
Safety rules:
- Default to write-capable Codex work in `codex:codex-rescue` unless the user explicitly asks for read-only behavior.
- Preserve the user's task text as-is apart from stripping routing flags.
- Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own.
- Do not inspect the repository, read files, grep, cancel jobs, summarize output, or do any follow-up work of your own.
- Return the stdout of the `task` command exactly as-is.
- If the Bash call fails or Codex cannot be invoked, return nothing.
14 changes: 8 additions & 6 deletions tests/commands.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test("rescue command absorbs continue semantics", () => {
assert.match(rescue, /Start a new Codex thread/);
assert.match(rescue, /run the `codex:codex-rescue` subagent in the background/i);
assert.match(rescue, /default to foreground/i);
assert.match(rescue, /Do not forward them to `task`/i);
assert.match(rescue, /Do not treat them as part of the natural-language task text/i);
assert.match(rescue, /`--model` and `--effort` are runtime-selection flags/i);
assert.match(rescue, /Leave `--effort` unset unless the user explicitly asks for a specific reasoning effort/i);
assert.match(rescue, /If they ask for `spark`, map it to `gpt-5\.3-codex-spark`/i);
Expand All @@ -119,6 +119,7 @@ test("rescue command absorbs continue semantics", () => {
assert.match(rescue, /If the user chooses continue, add `--resume`/i);
assert.match(rescue, /If the user chooses a new thread, add `--fresh`/i);
assert.match(rescue, /thin forwarder only/i);
assert.match(rescue, /task --background --await/i);
assert.match(rescue, /Return the Codex companion stdout verbatim to the user/i);
assert.match(rescue, /Do not paraphrase, summarize, rewrite, or add commentary before or after it/i);
assert.match(rescue, /return that command's stdout as-is/i);
Expand All @@ -127,9 +128,10 @@ test("rescue command absorbs continue semantics", () => {
assert.match(agent, /--fresh/);
assert.match(agent, /thin forwarding wrapper/i);
assert.match(agent, /prefer foreground for a small, clearly bounded rescue request/i);
assert.match(agent, /If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer background execution/i);
assert.match(agent, /If the user explicitly chose `--background`, call `task --background --await`/i);
assert.match(agent, /If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer `task --background --await`/i);
assert.match(agent, /Use exactly one `Bash` call/i);
assert.match(agent, /Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own/i);
assert.match(agent, /Do not inspect the repository, read files, grep, cancel jobs, summarize output, or do any follow-up work of your own/i);
assert.match(agent, /Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`/i);
assert.match(agent, /Leave `--effort` unset unless the user explicitly requests a specific reasoning effort/i);
assert.match(agent, /Leave model unset by default/i);
Expand All @@ -147,10 +149,10 @@ test("rescue command absorbs continue semantics", () => {
assert.match(runtimeSkill, /Leave `--effort` unset unless the user explicitly requests a specific effort/i);
assert.match(runtimeSkill, /Leave model unset by default/i);
assert.match(runtimeSkill, /Map `spark` to `--model gpt-5\.3-codex-spark`/i);
assert.match(runtimeSkill, /If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only/i);
assert.match(runtimeSkill, /Strip it before calling `task`/i);
assert.match(runtimeSkill, /If the forwarded request includes `--background`, strip it from the natural-language task text and call `task --background --await`/i);
assert.match(runtimeSkill, /If the forwarded request includes `--wait`, treat that as Claude-side execution control only/i);
assert.match(runtimeSkill, /`--effort`: accepted values are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`/i);
assert.match(runtimeSkill, /Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own/i);
assert.match(runtimeSkill, /Do not inspect the repository, read files, grep, cancel jobs, summarize output, or do any follow-up work of your own/i);
assert.match(runtimeSkill, /If the Bash call fails or Codex cannot be invoked, return nothing/i);
assert.match(readme, /`codex:codex-rescue` subagent/i);
assert.match(readme, /if you do not pass `--model` or `--effort`, Codex chooses its own defaults/i);
Expand Down
Loading