Skip to content
Draft
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
3 changes: 2 additions & 1 deletion plugins/codex/agents/codex-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Selection guidance:

Forwarding rules:

- Use exactly one `Bash` call to invoke `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task ...`.
- Use exactly one `Bash` call to invoke `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task [runtime flags] -- "<raw prompt>"`.
- Put every runtime flag before `--` and the complete user request after it as one argument. Never re-tokenize prompt text or interpret flag-looking words inside it as runtime controls.
- 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.
- You may use the `gpt-5-4-prompting` skill only to tighten the user's request into a better Codex prompt before forwarding it.
Expand Down
17 changes: 11 additions & 6 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ 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] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [--prompt-file <path>|-- <prompt>]",
" node scripts/codex-companion.mjs transfer [--source <claude-jsonl>] [--json]",
" node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
" node scripts/codex-companion.mjs result [job-id] [--json]",
Expand Down Expand Up @@ -127,8 +127,8 @@ function normalizeReasoningEffort(effort) {
return normalized;
}

function normalizeArgv(argv) {
if (argv.length === 1) {
function normalizeArgv(argv, options = {}) {
if (options.splitSingleArgument !== false && argv.length === 1) {
const [raw] = argv;
if (!raw || !raw.trim()) {
return [];
Expand All @@ -139,11 +139,12 @@ function normalizeArgv(argv) {
}

function parseCommandInput(argv, config = {}) {
return parseArgs(normalizeArgv(argv), {
...config,
const { splitSingleArgument, ...parseConfig } = config;
return parseArgs(normalizeArgv(argv, { splitSingleArgument }), {
...parseConfig,
aliasMap: {
C: "cwd",
...(config.aliasMap ?? {})
...(parseConfig.aliasMap ?? {})
}
});
}
Expand Down Expand Up @@ -761,6 +762,10 @@ async function handleReview(argv) {

async function handleTask(argv) {
const { options, positionals } = parseCommandInput(argv, {
// A task prompt is free text, not a shell command line. Re-tokenizing a
// single prompt argument strips quotes/backslashes and lets prose such as
// "--model" or "--write" mutate runtime options.
splitSingleArgument: false,
valueOptions: ["model", "effort", "cwd", "prompt-file"],
booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"],
aliasMap: {
Expand Down
3 changes: 2 additions & 1 deletion plugins/codex/skills/codex-cli-runtime/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ user-invocable: false
Use this skill only inside the `codex:codex-rescue` subagent.

Primary helper:
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task "<raw arguments>"`
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task [runtime flags] -- "<raw prompt>"`

Execution rules:
- The rescue subagent is a forwarder, not an orchestrator. Its only job is to invoke `task` once and return that stdout unchanged.
Expand All @@ -25,6 +25,7 @@ Execution rules:

Command selection:
- Use exactly one `task` invocation per rescue handoff.
- Pass runtime flags as separate arguments before `--`, then pass the complete user request as one prompt argument after `--`. This boundary keeps flag-like prose, quotes, backslashes, and newlines inside the prompt.
- 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 `--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`.
Expand Down
4 changes: 3 additions & 1 deletion tests/commands.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ test("rescue command absorbs continue semantics", () => {
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, /Use exactly one `Bash` call/i);
assert.match(agent, /every runtime flag before `--` and the complete user request after it as one argument/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 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);
Expand All @@ -142,6 +143,7 @@ test("rescue command absorbs continue semantics", () => {
assert.match(agent, /only to tighten the user's request into a better Codex prompt/i);
assert.match(agent, /Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work/i);
assert.match(runtimeSkill, /only job is to invoke `task` once and return that stdout unchanged/i);
assert.match(runtimeSkill, /Pass runtime flags as separate arguments before `--`/i);
assert.match(runtimeSkill, /Do not call `setup`, `review`, `adversarial-review`, `status`, `result`, or `cancel`/i);
assert.match(runtimeSkill, /use the `gpt-5-4-prompting` skill to rewrite the user's request into a tighter Codex prompt/i);
assert.match(runtimeSkill, /That prompt drafting is the only Claude-side work allowed/i);
Expand Down Expand Up @@ -192,7 +194,7 @@ test("internal docs use task terminology for rescue runs", () => {
const promptingSkill = read("skills/gpt-5-4-prompting/SKILL.md");
const promptRecipes = read("skills/gpt-5-4-prompting/references/codex-prompt-recipes.md");

assert.match(runtimeSkill, /codex-companion\.mjs" task "<raw arguments>"/);
assert.match(runtimeSkill, /codex-companion\.mjs" task \[runtime flags\] -- "<raw prompt>"/);
assert.match(runtimeSkill, /Use `task` for every rescue request/i);
assert.match(runtimeSkill, /task --resume-last/i);
assert.match(promptingSkill, /Use `task` when the task is diagnosis/i);
Expand Down
50 changes: 50 additions & 0 deletions tests/runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,56 @@ test("task runs without auth preflight so Codex can refresh an expired session",
assert.match(result.stdout, /Handled the requested task/);
});

test("task preserves a single free-text prompt without parsing its contents as options", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
const statePath = path.join(binDir, "fake-codex-state.json");
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
run("git", ["add", "README.md"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });

const prompt = [
"Review the plan: swap to claude -p --model claude-haiku --output-format json.",
String.raw`Config lives at C:\Users\me\Projects\my-app\config\app-config.json.`,
'The log line was "RUN ERROR: exit 1: Not logged in" and then --write the summary.'
].join("\n");
const result = run(process.execPath, [SCRIPT, "task", prompt], {
cwd: repo,
env: buildEnv(binDir),
shell: false
});

assert.equal(result.status, 0, result.stderr);
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
assert.equal(fakeState.lastTurnStart.prompt, prompt);
assert.equal(fakeState.lastTurnStart.model, null);
});

test("task keeps runtime options before the explicit prompt boundary", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
const statePath = path.join(binDir, "fake-codex-state.json");
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
run("git", ["add", "README.md"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });

const prompt = "Explain why --model claude-haiku must stay in the prompt.";
const result = run(process.execPath, [SCRIPT, "task", "--model", "spark", "--", prompt], {
cwd: repo,
env: buildEnv(binDir),
shell: false
});

assert.equal(result.status, 0, result.stderr);
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
assert.equal(fakeState.lastTurnStart.prompt, prompt);
assert.equal(fakeState.lastTurnStart.model, "gpt-5.3-codex-spark");
});

test("transfer delegates the current Claude session directly to native import", () => {
const home = makeTempDir();
const repo = path.join(home, "repo");
Expand Down