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
121 changes: 113 additions & 8 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
const VALID_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
const MODEL_ALIASES = new Map([["spark", "gpt-5.3-codex-spark"]]);
const STOP_REVIEW_TASK_MARKER = "Run a stop-gate review of the previous Claude turn.";
const MAX_PROMPT_BYTES = 800 * 1024;
const TRUNCATION_MARKER = "\n\n[content truncated to fit prompt size limit]\n";
const LIGHTWEIGHT_GUIDANCE =
"The repository context below is a lightweight summary. Inspect the target diff yourself with read-only git commands before finalizing findings.";

function printUsage() {
console.log(
Expand Down Expand Up @@ -235,17 +239,116 @@ async function handleSetup(argv) {
outputResult(options.json ? finalReport : renderSetupReport(finalReport), options.json);
}

function buildAdversarialReviewPrompt(context, focusText) {
export function buildAdversarialReviewPrompt(context, focusText) {
const template = loadPromptTemplate(ROOT_DIR, "adversarial-review");
const initial = renderAdversarialReviewPrompt(
template,
context,
focusText,
context.collectionGuidance,
context.content
);
if (Buffer.byteLength(initial, "utf8") <= MAX_PROMPT_BYTES) {
return initial;
}

const lightweightContent = buildLightweightAdversarialReviewContent(context);
const lightweight = renderAdversarialReviewPrompt(
template,
context,
focusText,
LIGHTWEIGHT_GUIDANCE,
lightweightContent
);
if (Buffer.byteLength(lightweight, "utf8") <= MAX_PROMPT_BYTES && lightweightContent !== context.content) {
return lightweight;
}

const overhead = Buffer.byteLength(
renderAdversarialReviewPrompt(template, context, focusText, LIGHTWEIGHT_GUIDANCE, ""),
"utf8"
);
const budget = MAX_PROMPT_BYTES - overhead - Buffer.byteLength(TRUNCATION_MARKER, "utf8");

if (budget < 0) {
return hardTruncateWithMarker(
renderAdversarialReviewPrompt(template, context, "", LIGHTWEIGHT_GUIDANCE, ""),
MAX_PROMPT_BYTES
);
}

return renderAdversarialReviewPrompt(
template,
context,
focusText,
LIGHTWEIGHT_GUIDANCE,
`${truncateToByteBudget(lightweightContent, budget)}${TRUNCATION_MARKER}`
);
}

function renderAdversarialReviewPrompt(template, context, focusText, guidance, content) {
return interpolateTemplate(template, {
REVIEW_KIND: "Adversarial Review",
TARGET_LABEL: context.target.label,
USER_FOCUS: focusText || "No extra focus provided.",
REVIEW_COLLECTION_GUIDANCE: context.collectionGuidance,
REVIEW_INPUT: context.content
REVIEW_COLLECTION_GUIDANCE: guidance,
REVIEW_INPUT: content
});
}

function buildLightweightAdversarialReviewContent(context) {
const parts = [];

if (context.summary) {
parts.push(`Summary: ${context.summary}`);
}
if (Array.isArray(context.changedFiles) && context.changedFiles.length > 0) {
parts.push(`Changed files (${context.changedFiles.length}):\n${context.changedFiles.slice(0, 50).join("\n")}`);
} else if (typeof context.fileCount === "number") {
parts.push(`Changed file count: ${context.fileCount}`);
}

return parts.length > 0 ? parts.join("\n\n") : context.content;
}

function truncateToByteBudget(value, maxBytes) {
if (maxBytes <= 0) {
return "";
}
if (Buffer.byteLength(value, "utf8") <= maxBytes) {
return value;
}

const buffer = Buffer.from(value, "utf8");
let end = Math.min(maxBytes, buffer.length);
// Avoid ending mid-sequence so the truncated prompt stays valid UTF-8.
while (end > 0 && (buffer[end] & 0xc0) === 0x80) {
end -= 1;
}
return buffer.subarray(0, end).toString("utf8");
}

function hardTruncateWithMarker(value, maxBytes) {
const head = truncateToByteBudget(
value,
Math.max(0, maxBytes - Buffer.byteLength(TRUNCATION_MARKER, "utf8"))
);
return `${head}${TRUNCATION_MARKER}`;
}

function isDirectExecution() {
const entry = process.argv[1];
if (!entry) return false;
const moduleFile = fileURLToPath(import.meta.url);
// Compare canonical (realpath) forms so symlinked install paths
// (plugin cache, macOS /var vs /private/var) still match the script.
try {
return fs.realpathSync.native(entry) === fs.realpathSync.native(moduleFile);
} catch {
return path.resolve(entry) === moduleFile;
}
}

function ensureCodexAvailable(cwd) {
const availability = getCodexAvailability(cwd);
if (!availability.available) {
Expand Down Expand Up @@ -1020,8 +1123,10 @@ async function main() {
}
}

main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
});
if (isDirectExecution()) {
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
});
}
126 changes: 126 additions & 0 deletions tests/codex-companion.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { buildAdversarialReviewPrompt } from '../plugins/codex/scripts/codex-companion.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.join(__dirname, '..', 'plugins', 'codex');
const TEMPLATE_PATH = path.join(ROOT_DIR, 'prompts', 'adversarial-review.md');
const MAX_PROMPT_BYTES = 800 * 1024;
const TEMPLATE = fs.readFileSync(TEMPLATE_PATH, 'utf8');

function buildPromptBaseline(template, label, focusText, guidance) {
return template
.replace('{{REVIEW_KIND}}', 'Adversarial Review')
.replace('{{TARGET_LABEL}}', label)
.replace('{{USER_FOCUS}}', focusText || 'No extra focus provided.')
.replace('{{REVIEW_COLLECTION_GUIDANCE}}', guidance)
.replace('{{REVIEW_INPUT}}', '');
}

function buildContentToHitExactCap(template, label, focusText, guidance, capBytes) {
const basePrompt = buildPromptBaseline(template, label, focusText, guidance);
const overhead = Buffer.byteLength(basePrompt, 'utf8');
return 'x'.repeat(capBytes - overhead);
}

test('buildAdversarialReviewPrompt: small content passes through verbatim', () => {
// Given
const context = {
target: { label: 'branch feature/x vs main' },
collectionGuidance: 'Use the repository context below as primary evidence.',
content: 'hello world'
};
const focusText = 'test focus';

// When
const result = buildAdversarialReviewPrompt(context, focusText);

// Then
assert.ok(Buffer.byteLength(result, 'utf8') < MAX_PROMPT_BYTES);
assert.equal(result.includes('hello world'), true);
assert.equal(result.includes('branch feature/x vs main'), true);
assert.equal(result.includes('test focus'), true);
});

test('buildAdversarialReviewPrompt: exact cap boundary keeps full content', () => {
// Given
const label = 'branch feature/at-limit vs main';
const focusText = 'focus';
const guidance = 'Use the repository context below as primary evidence.';
const content = buildContentToHitExactCap(TEMPLATE, label, focusText, guidance, MAX_PROMPT_BYTES);
const context = {
target: { label },
collectionGuidance: guidance,
content
};

// When
const result = buildAdversarialReviewPrompt(context, focusText);

// Then
assert.equal(Buffer.byteLength(result, 'utf8'), MAX_PROMPT_BYTES);
assert.equal(result.includes(content.slice(0, 100)), true);
assert.equal(result.includes(content.slice(-100)), true);
});

test('buildAdversarialReviewPrompt: 1MB input falls back to lightweight guidance', () => {
// Given
const context = {
target: { label: 'branch big vs main' },
collectionGuidance: 'Use the repository context below as primary evidence.',
content: 'x'.repeat(1024 * 1024)
};
const focusText = '';

// When
const result = buildAdversarialReviewPrompt(context, focusText);

// Then
assert.ok(Buffer.byteLength(result, 'utf8') <= MAX_PROMPT_BYTES);
assert.equal(
result.includes('lightweight summary') || result.includes('Inspect the target diff yourself'),
true
);
assert.equal(result.includes('x'.repeat(1024 * 1024)), false);
});

test('buildAdversarialReviewPrompt: 5MB input uses truncation fallback', () => {
// Given
const context = {
target: { label: 'branch huge vs main' },
collectionGuidance: 'Use the repository context below as primary evidence.',
content: 'x'.repeat(5 * 1024 * 1024)
};
const focusText = '';

// When
const result = buildAdversarialReviewPrompt(context, focusText);

// Then
assert.ok(Buffer.byteLength(result, 'utf8') <= MAX_PROMPT_BYTES);
assert.equal(result.toLowerCase().includes('truncated'), true);
});

test('buildAdversarialReviewPrompt: utf8 byte accounting stays under cap', () => {
// Given
const content = 'あ'.repeat(250 * 1024);
const context = {
target: { label: 'branch jp vs main' },
collectionGuidance: 'Use the repository context below as primary evidence.',
content
};
const focusText = '';

// When
const result = buildAdversarialReviewPrompt(context, focusText);

// Then
assert.equal(Buffer.byteLength(content, 'utf8'), 750 * 1024);
assert.ok(Buffer.byteLength(result, 'utf8') <= MAX_PROMPT_BYTES);
assert.equal(result.includes('あ'), true);
});