From 930b4fddcb9c27bc74fa30c8d6a26434c1b9381d Mon Sep 17 00:00:00 2001 From: Jianke LIN Date: Sat, 25 Jul 2026 22:52:23 +0200 Subject: [PATCH] fix task prompt argument preservation --- plugins/codex/agents/codex-rescue.md | 3 +- plugins/codex/scripts/codex-companion.mjs | 17 ++++--- .../codex/skills/codex-cli-runtime/SKILL.md | 3 +- tests/commands.test.mjs | 4 +- tests/runtime.test.mjs | 50 +++++++++++++++++++ 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/plugins/codex/agents/codex-rescue.md b/plugins/codex/agents/codex-rescue.md index 7009ec86a..083644358 100644 --- a/plugins/codex/agents/codex-rescue.md +++ b/plugins/codex/agents/codex-rescue.md @@ -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] -- ""`. +- 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. diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 83df468ad..c92dd6d40 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -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 ] [--scope ]", " node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base ] [--scope ] [focus text]", - " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [prompt]", + " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [--prompt-file |-- ]", " node scripts/codex-companion.mjs transfer [--source ] [--json]", " node scripts/codex-companion.mjs status [job-id] [--all] [--json]", " node scripts/codex-companion.mjs result [job-id] [--json]", @@ -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 []; @@ -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 ?? {}) } }); } @@ -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: { diff --git a/plugins/codex/skills/codex-cli-runtime/SKILL.md b/plugins/codex/skills/codex-cli-runtime/SKILL.md index 0e91bfb50..77818732a 100644 --- a/plugins/codex/skills/codex-cli-runtime/SKILL.md +++ b/plugins/codex/skills/codex-cli-runtime/SKILL.md @@ -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 ""` +- `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task [runtime flags] -- ""` Execution rules: - The rescue subagent is a forwarder, not an orchestrator. Its only job is to invoke `task` once and return that stdout unchanged. @@ -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`. diff --git a/tests/commands.test.mjs b/tests/commands.test.mjs index c34b06059..950f1e041 100644 --- a/tests/commands.test.mjs +++ b/tests/commands.test.mjs @@ -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); @@ -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); @@ -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 ""/); + assert.match(runtimeSkill, /codex-companion\.mjs" task \[runtime flags\] -- ""/); 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); diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 8f276835b..96c8f4115 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -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");