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
64 changes: 60 additions & 4 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -138,8 +138,60 @@ function normalizeArgv(argv) {
return argv;
}

function normalizeAdversarialReviewArgv(argv) {
if (argv.length !== 1) return argv;

const [raw] = argv;
if (!raw || !raw.trim()) return [];

const tokens = splitRawArgumentStringWithSpans(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.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.value.startsWith("--")) {
const [rawKey, inlineValue] = token.value.slice(2).split("=", 2);
key = aliasMap[rawKey] ?? rawKey;
hasInlineValue = inlineValue !== undefined;
} else if (token.value.startsWith("-") && token.value !== "-") {
key = aliasMap[token.value.slice(1)] ?? token.value.slice(1);
}

if (booleanOptions.has(key)) {
normalized.push(token.value);
index += 1;
continue;
}
if (valueOptions.has(key)) {
normalized.push(token.value);
index += 1;
if (!hasInlineValue && index < tokens.length) {
normalized.push(tokens[index].value);
index += 1;
}
continue;
}

normalized.push(raw.slice(token.start));
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",
Expand Down Expand Up @@ -715,7 +767,10 @@ async function handleReviewCommand(argv, config) {
booleanOptions: ["json", "background", "wait"],
aliasMap: {
m: "model"
}
},
...(config.opaqueFocus
? { argvNormalizer: normalizeAdversarialReviewArgv, stopAtFirstPositional: true }
: {})
});

const cwd = resolveCommandCwd(options);
Expand Down Expand Up @@ -1037,7 +1092,8 @@ async function main() {
break;
case "adversarial-review":
await handleReviewCommand(argv, {
reviewName: "Adversarial Review"
reviewName: "Adversarial Review",
opaqueFocus: true
});
break;
case "task":
Expand Down
32 changes: 20 additions & 12 deletions plugins/codex/scripts/lib/args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -21,6 +22,7 @@ export function parseArgs(argv, config = {}) {

if (!token.startsWith("-") || token === "-") {
positionals.push(token);
passthrough = stopAtFirstPositional;
continue;
}

Expand All @@ -46,6 +48,7 @@ export function parseArgs(argv, config = {}) {
}

positionals.push(token);
passthrough = stopAtFirstPositional;
continue;
}

Expand All @@ -68,25 +71,29 @@ export function parseArgs(argv, config = {}) {
}

positionals.push(token);
passthrough = stopAtFirstPositional;
}

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;
continue;
}

if (character === "\\") {
tokenStart ??= index;
escaping = true;
continue;
}
Expand All @@ -101,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);
}
41 changes: 41 additions & 0 deletions tests/runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,47 @@ 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 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();
Expand Down