Skip to content

Commit c6159de

Browse files
committed
Honor --cwd in resume preflight and close quoted paths with trailing backslash
- task-resume-candidate now accepts --cwd/-C with the same validated resolution as task, and the rescue preflight guidance forwards the requested workspace so the continue/new-thread question is scoped to the right repo - In the raw-argument splitter, a backslash before a closing quote is treated as a literal path separator when the quote is followed by whitespace or end-of-input, so quoted Windows paths like "C:\Program Files\Repo\" tokenize correctly while embedded escaped quotes elsewhere keep their existing behavior
1 parent 7fc8eff commit c6159de

7 files changed

Lines changed: 57 additions & 7 deletions

File tree

plugins/codex/agents/codex-rescue.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Forwarding rules:
2323
- If the user did not explicitly choose `--background` or `--wait`, prefer foreground for a small, clearly bounded rescue request.
2424
- 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.
2525
- For any task expected to exceed a few minutes, add `--background` to the `task` invocation by default so a caller timeout cannot terminate it.
26-
- Pass `--cwd <dir>` explicitly on every `task` invocation, using the intended workspace root forwarded by the caller.
26+
- Treat `--cwd <dir>` and `-C <dir>` as workspace routing controls. Pass `--cwd <dir>` explicitly on every `task` invocation, using the intended workspace root forwarded by the caller.
2727
- You may use the `gpt-5-4-prompting` skill only to tighten the user's request into a better Codex prompt before forwarding it.
2828
- 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.
2929
- 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.

plugins/codex/commands/rescue.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Delegate investigation, an explicit fix request, or follow-up rescue work to the Codex rescue subagent
3-
argument-hint: "[--background|--wait] [--cwd <dir>] [--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [what Codex should investigate, solve, or continue]"
3+
argument-hint: "[--background|--wait] [--cwd <dir>|-C <dir>] [--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [what Codex should investigate, solve, or continue]"
44
allowed-tools: Bash(node:*), AskUserQuestion, Agent
55
---
66

@@ -18,6 +18,7 @@ Execution mode:
1818
- If neither flag is present, default to foreground.
1919
- `--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.
2020
- `--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.
21+
- `--cwd <dir>` and `-C <dir>` are workspace-routing flags. Preserve the directory for the resume preflight and the forwarded `task` call, but do not treat either form as part of the natural-language task text.
2122
- If the request includes `--resume`, do not ask whether to continue. The user already chose.
2223
- If the request includes `--fresh`, do not ask whether to continue. The user already chose.
2324
- Otherwise, before starting Codex, check for a resumable rescue thread from this Claude session by running:
@@ -26,6 +27,12 @@ Execution mode:
2627
node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task-resume-candidate --json
2728
```
2829

30+
- If the request includes `--cwd <dir>` or `-C <dir>`, pass the same directory to that helper as `--cwd <dir>`:
31+
32+
```bash
33+
node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task-resume-candidate --json --cwd "<dir>"
34+
```
35+
2936
- If that helper reports `available: true`, use `AskUserQuestion` exactly once to ask whether to continue the current Codex thread or start a new one.
3037
- The two choices must be:
3138
- `Continue current Codex thread`

plugins/codex/scripts/codex-companion.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -968,8 +968,8 @@ function handleTaskResumeCandidate(argv) {
968968
booleanOptions: ["json"]
969969
});
970970

971-
const cwd = resolveCommandCwd(options);
972-
const workspaceRoot = resolveCommandWorkspace(options);
971+
const cwd = resolveTaskCwd(options);
972+
const workspaceRoot = resolveWorkspaceRoot(cwd);
973973
const sessionId = getCurrentClaudeSessionId();
974974
const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(listJobs(workspaceRoot)));
975975
const candidate = findLatestResumableTaskJob(jobs);

plugins/codex/scripts/lib/args.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ export function splitRawArgumentString(raw) {
8383

8484
if (character === "\\") {
8585
const nextCharacter = raw[index + 1];
86+
const characterAfterQuote = raw[index + 2];
87+
const closesQuotedRegion =
88+
quote !== null &&
89+
nextCharacter === quote &&
90+
(characterAfterQuote === undefined || /\s/.test(characterAfterQuote));
91+
if (closesQuotedRegion) {
92+
current += character;
93+
continue;
94+
}
8695
if (
8796
nextCharacter !== undefined &&
8897
(nextCharacter === "'" || nextCharacter === "\"" || nextCharacter === "\\" || /\s/.test(nextCharacter))

tests/args.test.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,29 @@ test("splitRawArgumentString preserves a quoted Windows path with spaces", () =>
1717
]);
1818
});
1919

20+
test("splitRawArgumentString closes a quoted Windows path after a trailing backslash", () => {
21+
assert.deepEqual(splitRawArgumentString(String.raw`--cwd "C:\Program Files\Repo\" --write`), [
22+
"--cwd",
23+
"C:\\Program Files\\Repo\\",
24+
"--write"
25+
]);
26+
});
27+
28+
test("splitRawArgumentString closes a final quoted Windows path after a trailing backslash", () => {
29+
assert.deepEqual(splitRawArgumentString(String.raw`--cwd "C:\Program Files\Repo\"`), [
30+
"--cwd",
31+
"C:\\Program Files\\Repo\\"
32+
]);
33+
});
34+
2035
test("splitRawArgumentString accepts an escaped quote inside double quotes", () => {
2136
assert.deepEqual(splitRawArgumentString(String.raw`"say \"hello\""`), ['say "hello"']);
2237
});
2338

39+
test("splitRawArgumentString preserves an escaped quote before a non-boundary character", () => {
40+
assert.deepEqual(splitRawArgumentString(String.raw`"say \"hi\"..."`), ['say "hi"...']);
41+
});
42+
2443
test("splitRawArgumentString collapses an escaped backslash", () => {
2544
assert.deepEqual(splitRawArgumentString(String.raw`C:\\repo`), [String.raw`C:\repo`]);
2645
});

tests/commands.test.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ test("rescue command absorbs continue semantics", () => {
107107
assert.match(rescue, /--model <model\|spark>/);
108108
assert.match(rescue, /--effort <none\|minimal\|low\|medium\|high\|xhigh>/);
109109
assert.match(rescue, /task-resume-candidate --json/);
110+
assert.match(rescue, /task-resume-candidate --json --cwd "<dir>"/);
111+
assert.match(rescue, /request includes `--cwd <dir>` or `-C <dir>`.*same directory.*helper/i);
110112
assert.match(rescue, /AskUserQuestion/);
111113
assert.match(rescue, /Continue current Codex thread/);
112114
assert.match(rescue, /Start a new Codex thread/);
@@ -132,6 +134,7 @@ test("rescue command absorbs continue semantics", () => {
132134
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);
133135
assert.match(agent, /expected to exceed a few minutes.*`--background`/i);
134136
assert.match(agent, /pass `--cwd <dir>` explicitly/i);
137+
assert.match(agent, /`--cwd <dir>` and `-C <dir>`.*workspace routing controls/i);
135138
assert.match(agent, /Use exactly one `Bash` call/i);
136139
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);
137140
assert.match(agent, /Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`/i);

tests/runtime.test.mjs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -547,8 +547,9 @@ test("task --resume-last resumes the latest persisted task thread", () => {
547547
assert.equal(result.stdout, "Resumed the prior run.\nFollow-up prompt accepted.\n");
548548
});
549549

550-
test("task-resume-candidate returns the latest rescue thread from the current session", () => {
550+
test("task-resume-candidate uses an explicit workspace cwd from an unrelated invocation directory", () => {
551551
const workspace = makeTempDir();
552+
const invocationDir = makeTempDir();
552553
const stateDir = resolveStateDir(workspace);
553554
const jobsDir = path.join(stateDir, "jobs");
554555
fs.mkdirSync(jobsDir, { recursive: true });
@@ -598,8 +599,8 @@ test("task-resume-candidate returns the latest rescue thread from the current se
598599
"utf8"
599600
);
600601

601-
const result = run("node", [SCRIPT, "task-resume-candidate", "--json"], {
602-
cwd: workspace,
602+
const result = run("node", [SCRIPT, "task-resume-candidate", "-C", workspace, "--json"], {
603+
cwd: invocationDir,
603604
env: {
604605
...process.env,
605606
CODEX_COMPANION_SESSION_ID: "sess-current"
@@ -614,6 +615,17 @@ test("task-resume-candidate returns the latest rescue thread from the current se
614615
assert.equal(payload.candidate.threadId, "thr_current");
615616
});
616617

618+
test("task-resume-candidate rejects a nonexistent explicit workspace cwd", () => {
619+
const invocationDir = makeTempDir();
620+
const missingDir = path.join(invocationDir, "missing-workspace");
621+
const result = run("node", [SCRIPT, "task-resume-candidate", "--cwd", missingDir, "--json"], {
622+
cwd: invocationDir
623+
});
624+
625+
assert.notEqual(result.status, 0);
626+
assert.match(result.stderr, /Task workspace directory does not exist/);
627+
});
628+
617629
test("task --resume-last does not resume a task from another Claude session", () => {
618630
const repo = makeTempDir();
619631
const binDir = makeTempDir();

0 commit comments

Comments
 (0)