From ec1149e6307ea9473b40a96e03df5066fc2b66f8 Mon Sep 17 00:00:00 2001 From: ALV0612 Date: Wed, 2 Sep 2026 17:02:52 +0700 Subject: [PATCH 1/2] fix(review): preserve adversarial focus text --- plugins/codex/scripts/codex-companion.mjs | 70 ++++++++++++++++++++++- plugins/codex/scripts/lib/args.mjs | 4 ++ tests/runtime.test.mjs | 23 ++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 83df468ad..8b379471b 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -138,8 +138,68 @@ function normalizeArgv(argv) { return argv; } +function normalizeAdversarialReviewArgv(argv) { + if (argv.length !== 1) { + return argv; + } + + const [raw] = argv; + if (!raw || !raw.trim()) { + return []; + } + + const tokens = splitRawArgumentString(raw); + const normalized = []; + const valueOptions = new Set(["base", "scope", "model", "cwd"]); + const booleanOptions = new Set(["json", "background", "wait"]); + const aliasMap = { C: "cwd", m: "model" }; + + for (let index = 0; index < tokens.length;) { + const token = tokens[index]; + if (token === "--") { + normalized.push(token); + if (index + 1 < tokens.length) { + normalized.push(tokens.slice(index + 1).join(" ")); + } + return normalized; + } + + let key = null; + let hasInlineValue = false; + if (token.startsWith("--")) { + const [rawKey, inlineValue] = token.slice(2).split("=", 2); + key = aliasMap[rawKey] ?? rawKey; + hasInlineValue = inlineValue !== undefined; + } else if (token.startsWith("-") && token !== "-") { + key = aliasMap[token.slice(1)] ?? token.slice(1); + } + + if (booleanOptions.has(key)) { + normalized.push(token); + index += 1; + continue; + } + + if (valueOptions.has(key)) { + normalized.push(token); + index += 1; + if (!hasInlineValue && index < tokens.length) { + normalized.push(tokens[index]); + index += 1; + } + continue; + } + + normalized.push(tokens.slice(index).join(" ")); + return normalized; + } + + return normalized; +} + function parseCommandInput(argv, config = {}) { - return parseArgs(normalizeArgv(argv), { + const argvNormalizer = config.argvNormalizer ?? normalizeArgv; + return parseArgs(argvNormalizer(argv), { ...config, aliasMap: { C: "cwd", @@ -715,7 +775,10 @@ async function handleReviewCommand(argv, config) { booleanOptions: ["json", "background", "wait"], aliasMap: { m: "model" - } + }, + ...(config.opaqueFocus + ? { argvNormalizer: normalizeAdversarialReviewArgv, stopAtFirstPositional: true } + : {}) }); const cwd = resolveCommandCwd(options); @@ -1037,7 +1100,8 @@ async function main() { break; case "adversarial-review": await handleReviewCommand(argv, { - reviewName: "Adversarial Review" + reviewName: "Adversarial Review", + opaqueFocus: true }); break; case "task": diff --git a/plugins/codex/scripts/lib/args.mjs b/plugins/codex/scripts/lib/args.mjs index 6b1518502..9c4517fe6 100644 --- a/plugins/codex/scripts/lib/args.mjs +++ b/plugins/codex/scripts/lib/args.mjs @@ -5,6 +5,7 @@ export function parseArgs(argv, config = {}) { const options = {}; const positionals = []; let passthrough = false; + const stopAtFirstPositional = Boolean(config.stopAtFirstPositional); for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; @@ -21,6 +22,7 @@ export function parseArgs(argv, config = {}) { if (!token.startsWith("-") || token === "-") { positionals.push(token); + passthrough = stopAtFirstPositional; continue; } @@ -46,6 +48,7 @@ export function parseArgs(argv, config = {}) { } positionals.push(token); + passthrough = stopAtFirstPositional; continue; } @@ -68,6 +71,7 @@ export function parseArgs(argv, config = {}) { } positionals.push(token); + passthrough = stopAtFirstPositional; } return { options, positionals }; diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 8f276835b..a91bbc03a 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -386,6 +386,29 @@ test("adversarial review renders structured findings over app-server turn/start" assert.match(result.stdout, /Missing empty-state guard/); }); +test("adversarial review keeps long option examples in a single focus argument", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + const focus = "Review the build script command build_v6_artifact.py --variant v6.2 --model pm_v6_2"; + 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 }); + fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); + + const result = run("node", [SCRIPT, "adversarial-review", focus], { + cwd: repo, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.equal(fakeState.lastTurnStart.model, null); + assert.match(fakeState.lastTurnStart.prompt, new RegExp(focus.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); +}); + test("adversarial review accepts the same base-branch targeting as review", () => { const repo = makeTempDir(); const binDir = makeTempDir(); From 6efffaa78fd513e4428cd1592c8cbf262d2560f5 Mon Sep 17 00:00:00 2001 From: ALV0612 Date: Wed, 2 Sep 2026 17:38:18 +0700 Subject: [PATCH 2/2] fix(review): preserve raw focus suffix --- plugins/codex/scripts/codex-companion.mjs | 38 +++++++++-------------- plugins/codex/scripts/lib/args.mjs | 28 ++++++++++------- tests/runtime.test.mjs | 18 +++++++++++ 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 8b379471b..edf7b81da 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -6,7 +6,7 @@ import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; -import { parseArgs, splitRawArgumentString } from "./lib/args.mjs"; +import { parseArgs, splitRawArgumentString, splitRawArgumentStringWithSpans } from "./lib/args.mjs"; import { buildPersistentTaskThreadName, DEFAULT_CONTINUE_PROMPT, @@ -139,16 +139,12 @@ function normalizeArgv(argv) { } function normalizeAdversarialReviewArgv(argv) { - if (argv.length !== 1) { - return argv; - } + if (argv.length !== 1) return argv; const [raw] = argv; - if (!raw || !raw.trim()) { - return []; - } + if (!raw || !raw.trim()) return []; - const tokens = splitRawArgumentString(raw); + const tokens = splitRawArgumentStringWithSpans(raw); const normalized = []; const valueOptions = new Set(["base", "scope", "model", "cwd"]); const booleanOptions = new Set(["json", "background", "wait"]); @@ -156,44 +152,40 @@ function normalizeAdversarialReviewArgv(argv) { for (let index = 0; index < tokens.length;) { const token = tokens[index]; - if (token === "--") { - normalized.push(token); - if (index + 1 < tokens.length) { - normalized.push(tokens.slice(index + 1).join(" ")); - } + if (token.value === "--") { + const focusStart = tokens[index + 1]?.start; + if (focusStart !== undefined) normalized.push("--", raw.slice(focusStart)); return normalized; } let key = null; let hasInlineValue = false; - if (token.startsWith("--")) { - const [rawKey, inlineValue] = token.slice(2).split("=", 2); + if (token.value.startsWith("--")) { + const [rawKey, inlineValue] = token.value.slice(2).split("=", 2); key = aliasMap[rawKey] ?? rawKey; hasInlineValue = inlineValue !== undefined; - } else if (token.startsWith("-") && token !== "-") { - key = aliasMap[token.slice(1)] ?? token.slice(1); + } else if (token.value.startsWith("-") && token.value !== "-") { + key = aliasMap[token.value.slice(1)] ?? token.value.slice(1); } if (booleanOptions.has(key)) { - normalized.push(token); + normalized.push(token.value); index += 1; continue; } - if (valueOptions.has(key)) { - normalized.push(token); + normalized.push(token.value); index += 1; if (!hasInlineValue && index < tokens.length) { - normalized.push(tokens[index]); + normalized.push(tokens[index].value); index += 1; } continue; } - normalized.push(tokens.slice(index).join(" ")); + normalized.push(raw.slice(token.start)); return normalized; } - return normalized; } diff --git a/plugins/codex/scripts/lib/args.mjs b/plugins/codex/scripts/lib/args.mjs index 9c4517fe6..d95a1379c 100644 --- a/plugins/codex/scripts/lib/args.mjs +++ b/plugins/codex/scripts/lib/args.mjs @@ -77,13 +77,15 @@ export function parseArgs(argv, config = {}) { return { options, positionals }; } -export function splitRawArgumentString(raw) { +export function splitRawArgumentStringWithSpans(raw) { const tokens = []; let current = ""; let quote = null; let escaping = false; + let tokenStart = null; - for (const character of raw) { + for (let index = 0; index < raw.length; index += 1) { + const character = raw[index]; if (escaping) { current += character; escaping = false; @@ -91,6 +93,7 @@ export function splitRawArgumentString(raw) { } if (character === "\\") { + tokenStart ??= index; escaping = true; continue; } @@ -105,28 +108,29 @@ export function splitRawArgumentString(raw) { } if (character === "'" || character === "\"") { + tokenStart ??= index; quote = character; continue; } if (/\s/.test(character)) { - if (current) { - tokens.push(current); + if (tokenStart !== null) { + tokens.push({ value: current, start: tokenStart, end: index }); current = ""; + tokenStart = null; } continue; } + tokenStart ??= index; current += character; } - if (escaping) { - current += "\\"; - } - - if (current) { - tokens.push(current); - } - + if (escaping) current += "\\"; + if (tokenStart !== null) tokens.push({ value: current, start: tokenStart, end: raw.length }); return tokens; } + +export function splitRawArgumentString(raw) { + return splitRawArgumentStringWithSpans(raw).map((token) => token.value); +} diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index a91bbc03a..224e10df5 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -409,6 +409,24 @@ test("adversarial review keeps long option examples in a single focus argument", assert.match(fakeState.lastTurnStart.prompt, new RegExp(focus.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); }); +test("adversarial review preserves backslashes, quotes, and whitespace in focus text", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + const focus = 'Review C:\\temp\\foo and keep "quoted spacing" exactly'; + 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 }); + fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); + + const result = run("node", [SCRIPT, "adversarial-review", focus], { cwd: repo, env: buildEnv(binDir) }); + assert.equal(result.status, 0, result.stderr); + const prompt = JSON.parse(fs.readFileSync(statePath, "utf8")).lastTurnStart.prompt; + assert.match(prompt, new RegExp(focus.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); +}); + test("adversarial review accepts the same base-branch targeting as review", () => { const repo = makeTempDir(); const binDir = makeTempDir();