Compare commits

..

2 Commits

Author SHA1 Message Date
Dominik Kundel be94a4b1d2 fix: reuse existing app server for auth checks 2026-04-07 20:00:14 -07:00
Dominik Kundel 8936a95c3e Use app-server auth status for Codex readiness 2026-04-07 19:59:00 -07:00
15 changed files with 56 additions and 846 deletions
+2 -2
View File
@@ -5,13 +5,13 @@
},
"metadata": {
"description": "Codex plugins to use in Claude Code for delegation and code review.",
"version": "1.0.3"
"version": "1.0.2"
},
"plugins": [
{
"name": "codex",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
"version": "1.0.3",
"version": "1.0.2",
"author": {
"name": "OpenAI"
},
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@openai/codex-plugin-cc",
"version": "1.0.3",
"version": "1.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openai/codex-plugin-cc",
"version": "1.0.3",
"version": "1.0.2",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^25.5.0",
+1 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@openai/codex-plugin-cc",
"version": "1.0.3",
"version": "1.0.2",
"private": true,
"type": "module",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
@@ -9,8 +9,6 @@
"node": ">=18.18.0"
},
"scripts": {
"bump-version": "node scripts/bump-version.mjs",
"check-version": "node scripts/bump-version.mjs --check",
"prebuild": "mkdir -p plugins/codex/.generated/app-server-types && codex app-server generate-ts --out plugins/codex/.generated/app-server-types",
"build": "tsc -p tsconfig.app-server.json",
"test": "node --test tests/*.test.mjs"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codex",
"version": "1.0.3",
"version": "1.0.2",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
"author": {
"name": "OpenAI"
@@ -32,7 +32,6 @@ Actively try to disprove the change.
Look for violated invariants, missing guards, unhandled failure paths, and assumptions that stop being true under stress.
Trace how bad inputs, retries, concurrent actions, or partially completed operations move through the code.
If the user supplied a focus area, weight it heavily, but still report any other material issue you can defend.
{{REVIEW_COLLECTION_GUIDANCE}}
</review_method>
<finding_bar>
+14 -37
View File
@@ -241,7 +241,6 @@ function buildAdversarialReviewPrompt(context, focusText) {
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
});
}
@@ -288,30 +287,6 @@ function isActiveJobStatus(status) {
return status === "queued" || status === "running";
}
function getCurrentClaudeSessionId() {
return process.env[SESSION_ID_ENV] ?? null;
}
function filterJobsForCurrentClaudeSession(jobs) {
const sessionId = getCurrentClaudeSessionId();
if (!sessionId) {
return jobs;
}
return jobs.filter((job) => job.sessionId === sessionId);
}
function findLatestResumableTaskJob(jobs) {
return (
jobs.find(
(job) =>
job.jobClass === "task" &&
job.threadId &&
job.status !== "queued" &&
job.status !== "running"
) ?? null
);
}
async function waitForSingleJobSnapshot(cwd, reference, options = {}) {
const timeoutMs = Math.max(0, Number(options.timeoutMs) || DEFAULT_STATUS_WAIT_TIMEOUT_MS);
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || DEFAULT_STATUS_POLL_INTERVAL_MS);
@@ -332,23 +307,17 @@ async function waitForSingleJobSnapshot(cwd, reference, options = {}) {
async function resolveLatestTrackedTaskThread(cwd, options = {}) {
const workspaceRoot = resolveWorkspaceRoot(cwd);
const sessionId = getCurrentClaudeSessionId();
const jobs = sortJobsNewestFirst(listJobs(workspaceRoot)).filter((job) => job.id !== options.excludeJobId);
const visibleJobs = filterJobsForCurrentClaudeSession(jobs);
const activeTask = visibleJobs.find((job) => job.jobClass === "task" && (job.status === "queued" || job.status === "running"));
const activeTask = jobs.find((job) => job.jobClass === "task" && (job.status === "queued" || job.status === "running"));
if (activeTask) {
throw new Error(`Task ${activeTask.id} is still running. Use /codex:status before continuing it.`);
}
const trackedTask = findLatestResumableTaskJob(visibleJobs);
const trackedTask = jobs.find((job) => job.jobClass === "task" && job.status === "completed" && job.threadId);
if (trackedTask) {
return { id: trackedTask.threadId };
}
if (sessionId) {
return null;
}
return findLatestTaskThread(workspaceRoot);
}
@@ -890,9 +859,17 @@ function handleTaskResumeCandidate(argv) {
const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
const sessionId = getCurrentClaudeSessionId();
const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(listJobs(workspaceRoot)));
const candidate = findLatestResumableTaskJob(jobs);
const sessionId = process.env[SESSION_ID_ENV] ?? null;
const jobs = sortJobsNewestFirst(listJobs(workspaceRoot));
const candidate =
jobs.find(
(job) =>
job.jobClass === "task" &&
job.threadId &&
job.status !== "queued" &&
job.status !== "running" &&
(!sessionId || job.sessionId === sessionId)
) ?? null;
const payload = {
available: Boolean(candidate),
@@ -925,7 +902,7 @@ async function handleCancel(argv) {
const cwd = resolveCommandCwd(options);
const reference = positionals[0] ?? "";
const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env });
const { workspaceRoot, job } = resolveCancelableJob(cwd, reference);
const existing = readStoredJob(workspaceRoot, job.id) ?? {};
const threadId = existing.threadId ?? job.threadId ?? null;
const turnId = existing.turnId ?? job.turnId ?? null;
+1 -1
View File
@@ -188,7 +188,7 @@ class SpawnedCodexAppServerClient extends AppServerClientBase {
async initialize() {
this.proc = spawn("codex", ["app-server"], {
cwd: this.cwd,
env: this.options.env ?? process.env,
env: this.options.env,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32" ? (process.env.SHELL || true) : false,
windowsHide: true
+1 -10
View File
@@ -639,16 +639,7 @@ async function startThread(client, cwd, options = {}) {
const response = await client.request("thread/start", buildThreadParams(cwd, options));
const threadId = response.thread.id;
if (options.threadName) {
try {
await client.request("thread/name/set", { threadId, name: options.threadName });
} catch (err) {
// Only suppress "unknown variant/method" errors from older CLI versions
// that don't support thread/name/set. Rethrow auth, network, or server errors.
const msg = String(err?.message ?? err ?? "");
if (!msg.includes("unknown variant") && !msg.includes("unknown method")) {
throw err;
}
}
await client.request("thread/name/set", { threadId, name: options.threadName });
}
return response;
}
+30 -154
View File
@@ -2,11 +2,9 @@ import fs from "node:fs";
import path from "node:path";
import { isProbablyText } from "./fs.mjs";
import { formatCommandFailure, runCommand, runCommandChecked } from "./process.mjs";
import { runCommand, runCommandChecked } from "./process.mjs";
const MAX_UNTRACKED_BYTES = 24 * 1024;
const DEFAULT_INLINE_DIFF_MAX_FILES = 2;
const DEFAULT_INLINE_DIFF_MAX_BYTES = 256 * 1024;
function git(cwd, args, options = {}) {
return runCommand("git", args, { cwd, ...options });
@@ -16,64 +14,6 @@ function gitChecked(cwd, args, options = {}) {
return runCommandChecked("git", args, { cwd, ...options });
}
function listUniqueFiles(...groups) {
return [...new Set(groups.flat().filter(Boolean))].sort();
}
function normalizeMaxInlineFiles(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) {
return DEFAULT_INLINE_DIFF_MAX_FILES;
}
return Math.floor(parsed);
}
function normalizeMaxInlineDiffBytes(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) {
return DEFAULT_INLINE_DIFF_MAX_BYTES;
}
return Math.floor(parsed);
}
function measureGitOutputBytes(cwd, args, maxBytes) {
const result = git(cwd, args, { maxBuffer: maxBytes + 1 });
if (result.error && /** @type {NodeJS.ErrnoException} */ (result.error).code === "ENOBUFS") {
return maxBytes + 1;
}
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(formatCommandFailure(result));
}
return Buffer.byteLength(result.stdout, "utf8");
}
function measureCombinedGitOutputBytes(cwd, argSets, maxBytes) {
let totalBytes = 0;
for (const args of argSets) {
const remainingBytes = maxBytes - totalBytes;
if (remainingBytes < 0) {
return maxBytes + 1;
}
totalBytes += measureGitOutputBytes(cwd, args, remainingBytes);
if (totalBytes > maxBytes) {
return totalBytes;
}
}
return totalBytes;
}
function buildBranchComparison(cwd, baseRef) {
const mergeBase = gitChecked(cwd, ["merge-base", "HEAD", baseRef]).stdout.trim();
return {
mergeBase,
commitRange: `${mergeBase}..HEAD`,
reviewRange: `${baseRef}...HEAD`
};
}
export function ensureGitRepository(cwd) {
const result = git(cwd, ["rev-parse", "--show-toplevel"]);
const errorCode = result.error && "code" in result.error ? result.error.code : null;
@@ -221,115 +161,55 @@ function formatUntrackedFile(cwd, relativePath) {
return [`### ${relativePath}`, "```", buffer.toString("utf8").trimEnd(), "```"].join("\n");
}
function collectWorkingTreeContext(cwd, state, options = {}) {
const includeDiff = options.includeDiff !== false;
const status = gitChecked(cwd, ["status", "--short", "--untracked-files=all"]).stdout.trim();
const changedFiles = listUniqueFiles(state.staged, state.unstaged, state.untracked);
function collectWorkingTreeContext(cwd, state) {
const status = gitChecked(cwd, ["status", "--short"]).stdout.trim();
const stagedDiff = gitChecked(cwd, ["diff", "--cached", "--binary", "--no-ext-diff", "--submodule=diff"]).stdout;
const unstagedDiff = gitChecked(cwd, ["diff", "--binary", "--no-ext-diff", "--submodule=diff"]).stdout;
const untrackedBody = state.untracked.map((file) => formatUntrackedFile(cwd, file)).join("\n\n");
let parts;
if (includeDiff) {
const stagedDiff = gitChecked(cwd, ["diff", "--cached", "--binary", "--no-ext-diff", "--submodule=diff"]).stdout;
const unstagedDiff = gitChecked(cwd, ["diff", "--binary", "--no-ext-diff", "--submodule=diff"]).stdout;
const untrackedBody = state.untracked.map((file) => formatUntrackedFile(cwd, file)).join("\n\n");
parts = [
formatSection("Git Status", status),
formatSection("Staged Diff", stagedDiff),
formatSection("Unstaged Diff", unstagedDiff),
formatSection("Untracked Files", untrackedBody)
];
} else {
const stagedStat = gitChecked(cwd, ["diff", "--shortstat", "--cached"]).stdout.trim();
const unstagedStat = gitChecked(cwd, ["diff", "--shortstat"]).stdout.trim();
const untrackedBody = state.untracked.map((file) => formatUntrackedFile(cwd, file)).join("\n\n");
parts = [
formatSection("Git Status", status),
formatSection("Staged Diff Stat", stagedStat),
formatSection("Unstaged Diff Stat", unstagedStat),
formatSection("Changed Files", changedFiles.join("\n")),
formatSection("Untracked Files", untrackedBody)
];
}
const parts = [
formatSection("Git Status", status),
formatSection("Staged Diff", stagedDiff),
formatSection("Unstaged Diff", unstagedDiff),
formatSection("Untracked Files", untrackedBody)
];
return {
mode: "working-tree",
summary: `Reviewing ${state.staged.length} staged, ${state.unstaged.length} unstaged, and ${state.untracked.length} untracked file(s).`,
content: parts.join("\n"),
changedFiles
content: parts.join("\n")
};
}
function collectBranchContext(cwd, baseRef, options = {}) {
const includeDiff = options.includeDiff !== false;
const comparison = options.comparison ?? buildBranchComparison(cwd, baseRef);
function collectBranchContext(cwd, baseRef) {
const mergeBase = gitChecked(cwd, ["merge-base", "HEAD", baseRef]).stdout.trim();
const commitRange = `${mergeBase}..HEAD`;
const currentBranch = getCurrentBranch(cwd);
const changedFiles = gitChecked(cwd, ["diff", "--name-only", comparison.commitRange]).stdout.trim().split("\n").filter(Boolean);
const logOutput = gitChecked(cwd, ["log", "--oneline", "--decorate", comparison.commitRange]).stdout.trim();
const diffStat = gitChecked(cwd, ["diff", "--stat", comparison.commitRange]).stdout.trim();
const logOutput = gitChecked(cwd, ["log", "--oneline", "--decorate", commitRange]).stdout.trim();
const diffStat = gitChecked(cwd, ["diff", "--stat", commitRange]).stdout.trim();
const diff = gitChecked(cwd, ["diff", "--binary", "--no-ext-diff", "--submodule=diff", commitRange]).stdout;
return {
mode: "branch",
summary: `Reviewing branch ${currentBranch} against ${baseRef} from merge-base ${comparison.mergeBase}.`,
content: includeDiff
? [
formatSection("Commit Log", logOutput),
formatSection("Diff Stat", diffStat),
formatSection(
"Branch Diff",
gitChecked(cwd, ["diff", "--binary", "--no-ext-diff", "--submodule=diff", comparison.commitRange]).stdout
)
].join("\n")
: [
formatSection("Commit Log", logOutput),
formatSection("Diff Stat", diffStat),
formatSection("Changed Files", changedFiles.join("\n"))
].join("\n"),
changedFiles,
comparison
summary: `Reviewing branch ${currentBranch} against ${baseRef} from merge-base ${mergeBase}.`,
content: [
formatSection("Commit Log", logOutput),
formatSection("Diff Stat", diffStat),
formatSection("Branch Diff", diff)
].join("\n")
};
}
function buildAdversarialCollectionGuidance(options = {}) {
if (options.includeDiff !== false) {
return "Use the repository context below as primary evidence.";
}
return "The repository context below is a lightweight summary. Inspect the target diff yourself with read-only git commands before finalizing findings.";
}
export function collectReviewContext(cwd, target, options = {}) {
export function collectReviewContext(cwd, target) {
const repoRoot = getRepoRoot(cwd);
const currentBranch = getCurrentBranch(repoRoot);
const maxInlineFiles = normalizeMaxInlineFiles(options.maxInlineFiles);
const maxInlineDiffBytes = normalizeMaxInlineDiffBytes(options.maxInlineDiffBytes);
const state = getWorkingTreeState(cwd);
const currentBranch = getCurrentBranch(cwd);
let details;
let includeDiff;
let diffBytes;
if (target.mode === "working-tree") {
const state = getWorkingTreeState(repoRoot);
diffBytes = measureCombinedGitOutputBytes(
repoRoot,
[
["diff", "--cached", "--binary", "--no-ext-diff", "--submodule=diff"],
["diff", "--binary", "--no-ext-diff", "--submodule=diff"]
],
maxInlineDiffBytes
);
includeDiff =
options.includeDiff ??
(listUniqueFiles(state.staged, state.unstaged, state.untracked).length <= maxInlineFiles &&
diffBytes <= maxInlineDiffBytes);
details = collectWorkingTreeContext(repoRoot, state, { includeDiff });
details = collectWorkingTreeContext(repoRoot, state);
} else {
const comparison = buildBranchComparison(repoRoot, target.baseRef);
const fileCount = gitChecked(repoRoot, ["diff", "--name-only", comparison.commitRange]).stdout.trim().split("\n").filter(Boolean).length;
diffBytes = measureGitOutputBytes(
repoRoot,
["diff", "--binary", "--no-ext-diff", "--submodule=diff", comparison.commitRange],
maxInlineDiffBytes
);
includeDiff = options.includeDiff ?? (fileCount <= maxInlineFiles && diffBytes <= maxInlineDiffBytes);
details = collectBranchContext(repoRoot, target.baseRef, { includeDiff, comparison });
details = collectBranchContext(repoRoot, target.baseRef);
}
return {
@@ -337,10 +217,6 @@ export function collectReviewContext(cwd, target, options = {}) {
repoRoot,
branch: currentBranch,
target,
fileCount: details.changedFiles.length,
diffBytes,
inputMode: includeDiff ? "inline-diff" : "self-collect",
collectionGuidance: buildAdversarialCollectionGuidance({ includeDiff }),
...details
};
}
+4 -10
View File
@@ -278,7 +278,7 @@ export function resolveResultJob(cwd, reference) {
throw new Error("No finished Codex jobs found for this repository yet.");
}
export function resolveCancelableJob(cwd, reference, options = {}) {
export function resolveCancelableJob(cwd, reference) {
const workspaceRoot = resolveWorkspaceRoot(cwd);
const jobs = sortJobsNewestFirst(listJobs(workspaceRoot));
const activeJobs = jobs.filter((job) => job.status === "queued" || job.status === "running");
@@ -291,18 +291,12 @@ export function resolveCancelableJob(cwd, reference, options = {}) {
return { workspaceRoot, job: selected };
}
const sessionScopedActiveJobs = filterJobsForCurrentSession(activeJobs, options);
if (sessionScopedActiveJobs.length === 1) {
return { workspaceRoot, job: sessionScopedActiveJobs[0] };
if (activeJobs.length === 1) {
return { workspaceRoot, job: activeJobs[0] };
}
if (sessionScopedActiveJobs.length > 1) {
if (activeJobs.length > 1) {
throw new Error("Multiple Codex jobs are active. Pass a job id to /codex:cancel.");
}
if (getCurrentSessionId(options)) {
throw new Error("No active Codex jobs to cancel for this session.");
}
throw new Error("No active Codex jobs to cancel.");
}
-1
View File
@@ -7,7 +7,6 @@ export function runCommand(command, args = [], options = {}) {
env: options.env,
encoding: "utf8",
input: options.input,
maxBuffer: options.maxBuffer,
stdio: options.stdio ?? "pipe",
shell: process.platform === "win32" ? (process.env.SHELL || true) : false,
windowsHide: true
-227
View File
@@ -1,227 +0,0 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const TARGETS = [
{
file: "package.json",
values: [
{
label: "version",
get: (json) => json.version,
set: (json, version) => {
json.version = version;
}
}
]
},
{
file: "package-lock.json",
values: [
{
label: "version",
get: (json) => json.version,
set: (json, version) => {
json.version = version;
}
},
{
label: "packages[\"\"].version",
get: (json) => json.packages?.[""]?.version,
set: (json, version) => {
requireObject(json.packages?.[""], "package-lock.json packages[\"\"]");
json.packages[""].version = version;
}
}
]
},
{
file: "plugins/codex/.claude-plugin/plugin.json",
values: [
{
label: "version",
get: (json) => json.version,
set: (json, version) => {
json.version = version;
}
}
]
},
{
file: ".claude-plugin/marketplace.json",
values: [
{
label: "metadata.version",
get: (json) => json.metadata?.version,
set: (json, version) => {
requireObject(json.metadata, ".claude-plugin/marketplace.json metadata");
json.metadata.version = version;
}
},
{
label: "plugins[codex].version",
get: (json) => findMarketplacePlugin(json).version,
set: (json, version) => {
findMarketplacePlugin(json).version = version;
}
}
]
}
];
function usage() {
return [
"Usage:",
" node scripts/bump-version.mjs <version>",
" node scripts/bump-version.mjs --check [version]",
"",
"Options:",
" --check Verify manifest versions. Uses package.json when version is omitted.",
" --root <dir> Run against a different repository root.",
" --help Print this help."
].join("\n");
}
function parseArgs(argv) {
const options = {
check: false,
root: process.cwd(),
version: null
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--check") {
options.check = true;
} else if (arg === "--root") {
const root = argv[i + 1];
if (!root) {
throw new Error("--root requires a directory.");
}
options.root = root;
i += 1;
} else if (arg === "--help" || arg === "-h") {
options.help = true;
} else if (arg.startsWith("-")) {
throw new Error(`Unknown option: ${arg}`);
} else if (options.version) {
throw new Error(`Unexpected extra argument: ${arg}`);
} else {
options.version = arg;
}
}
options.root = path.resolve(options.root);
return options;
}
function validateVersion(version) {
if (!VERSION_PATTERN.test(version)) {
throw new Error(`Expected a semver-like version such as 1.0.3, got: ${version}`);
}
}
function requireObject(value, label) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`Expected ${label} to be an object.`);
}
}
function findMarketplacePlugin(json) {
const plugin = json.plugins?.find((entry) => entry?.name === "codex");
requireObject(plugin, ".claude-plugin/marketplace.json plugins[codex]");
return plugin;
}
function readJson(root, file) {
const filePath = path.join(root, file);
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function writeJson(root, file, json) {
const filePath = path.join(root, file);
fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
}
function readPackageVersion(root) {
const packageJson = readJson(root, "package.json");
if (typeof packageJson.version !== "string") {
throw new Error("package.json version must be a string.");
}
validateVersion(packageJson.version);
return packageJson.version;
}
function checkVersions(root, expectedVersion) {
const mismatches = [];
for (const target of TARGETS) {
const json = readJson(root, target.file);
for (const value of target.values) {
const actual = value.get(json);
if (actual !== expectedVersion) {
mismatches.push(`${target.file} ${value.label}: expected ${expectedVersion}, found ${actual ?? "<missing>"}`);
}
}
}
return mismatches;
}
function bumpVersion(root, version) {
const changedFiles = [];
for (const target of TARGETS) {
const json = readJson(root, target.file);
const before = JSON.stringify(json);
for (const value of target.values) {
value.set(json, version);
}
if (JSON.stringify(json) !== before) {
writeJson(root, target.file, json);
changedFiles.push(target.file);
}
}
return changedFiles;
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
console.log(usage());
return;
}
const version = options.version ?? (options.check ? readPackageVersion(options.root) : null);
if (!version) {
throw new Error(`Missing version.\n\n${usage()}`);
}
validateVersion(version);
if (options.check) {
const mismatches = checkVersions(options.root, version);
if (mismatches.length > 0) {
throw new Error(`Version metadata is out of sync:\n${mismatches.join("\n")}`);
}
console.log(`All version metadata matches ${version}.`);
return;
}
const changedFiles = bumpVersion(options.root, version);
const touched = changedFiles.length > 0 ? changedFiles.join(", ") : "no files changed";
console.log(`Set version metadata to ${version}: ${touched}.`);
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
-88
View File
@@ -1,88 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { makeTempDir, run } from "./helpers.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SCRIPT = path.join(ROOT, "scripts", "bump-version.mjs");
function writeJson(filePath, json) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function makeVersionFixture() {
const root = makeTempDir();
writeJson(path.join(root, "package.json"), {
name: "@openai/codex-plugin-cc",
version: "1.0.2"
});
writeJson(path.join(root, "package-lock.json"), {
name: "@openai/codex-plugin-cc",
version: "1.0.2",
lockfileVersion: 3,
packages: {
"": {
name: "@openai/codex-plugin-cc",
version: "1.0.2"
}
}
});
writeJson(path.join(root, "plugins", "codex", ".claude-plugin", "plugin.json"), {
name: "codex",
version: "1.0.2"
});
writeJson(path.join(root, ".claude-plugin", "marketplace.json"), {
metadata: {
version: "1.0.2"
},
plugins: [
{
name: "codex",
version: "1.0.2"
}
]
});
return root;
}
test("bump-version updates every release manifest", () => {
const root = makeVersionFixture();
const result = run("node", [SCRIPT, "--root", root, "1.2.3"], {
cwd: ROOT
});
assert.equal(result.status, 0, result.stderr);
assert.equal(readJson(path.join(root, "package.json")).version, "1.2.3");
assert.equal(readJson(path.join(root, "package-lock.json")).version, "1.2.3");
assert.equal(readJson(path.join(root, "package-lock.json")).packages[""].version, "1.2.3");
assert.equal(readJson(path.join(root, "plugins", "codex", ".claude-plugin", "plugin.json")).version, "1.2.3");
assert.equal(readJson(path.join(root, ".claude-plugin", "marketplace.json")).metadata.version, "1.2.3");
assert.equal(readJson(path.join(root, ".claude-plugin", "marketplace.json")).plugins[0].version, "1.2.3");
});
test("bump-version check mode reports stale metadata", () => {
const root = makeVersionFixture();
writeJson(path.join(root, "package.json"), {
name: "@openai/codex-plugin-cc",
version: "1.0.3"
});
const result = run("node", [SCRIPT, "--root", root, "--check"], {
cwd: ROOT
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /plugins\/codex\/\.claude-plugin\/plugin\.json version/);
assert.match(result.stderr, /\.claude-plugin\/marketplace\.json metadata\.version/);
});
-80
View File
@@ -69,23 +69,6 @@ test("resolveReviewTarget requires an explicit base when no default branch can b
);
});
test("collectReviewContext keeps inline diffs for tiny adversarial reviews", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('INLINE_MARKER');\n");
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(context.inputMode, "inline-diff");
assert.equal(context.fileCount, 1);
assert.match(context.collectionGuidance, /primary evidence/i);
assert.match(context.content, /INLINE_MARKER/);
});
test("collectReviewContext skips untracked directories in working tree review", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
@@ -118,66 +101,3 @@ test("collectReviewContext skips broken untracked symlinks instead of crashing",
assert.match(context.content, /### broken-link/);
assert.match(context.content, /skipped: broken symlink or unreadable file/i);
});
test("collectReviewContext falls back to lightweight context for larger adversarial reviews", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
for (const name of ["a.js", "b.js", "c.js"]) {
fs.writeFileSync(path.join(cwd, name), `export const value = "${name}-v1";\n`);
}
run("git", ["add", "a.js", "b.js", "c.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "a.js"), 'export const value = "SELF_COLLECT_MARKER_A";\n');
fs.writeFileSync(path.join(cwd, "b.js"), 'export const value = "SELF_COLLECT_MARKER_B";\n');
fs.writeFileSync(path.join(cwd, "c.js"), 'export const value = "SELF_COLLECT_MARKER_C";\n');
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(context.inputMode, "self-collect");
assert.equal(context.fileCount, 3);
assert.match(context.collectionGuidance, /lightweight summary/i);
assert.match(context.collectionGuidance, /read-only git commands/i);
assert.doesNotMatch(context.content, /SELF_COLLECT_MARKER_[ABC]/);
assert.match(context.content, /## Changed Files/);
});
test("collectReviewContext falls back to lightweight context for oversized single-file diffs", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "export const value = 'v1';\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), `export const value = '${"x".repeat(512)}';\n`);
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target, { maxInlineDiffBytes: 128 });
assert.equal(context.fileCount, 1);
assert.equal(context.inputMode, "self-collect");
assert.ok(context.diffBytes > 128);
assert.doesNotMatch(context.content, /xxx/);
assert.match(context.content, /## Changed Files/);
});
test("collectReviewContext keeps untracked file content in lightweight working tree context", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
for (const name of ["a.js", "b.js"]) {
fs.writeFileSync(path.join(cwd, name), `export const value = "${name}-v1";\n`);
}
run("git", ["add", "a.js", "b.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "a.js"), 'export const value = "TRACKED_MARKER_A";\n');
fs.writeFileSync(path.join(cwd, "b.js"), 'export const value = "TRACKED_MARKER_B";\n');
fs.writeFileSync(path.join(cwd, "new-risk.js"), 'export const value = "UNTRACKED_RISK_MARKER";\n');
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(context.inputMode, "self-collect");
assert.equal(context.fileCount, 3);
assert.doesNotMatch(context.content, /TRACKED_MARKER_[AB]/);
assert.match(context.content, /## Untracked Files/);
assert.match(context.content, /UNTRACKED_RISK_MARKER/);
});
-229
View File
@@ -273,33 +273,6 @@ test("adversarial review accepts the same base-branch targeting as review", () =
assert.match(result.stdout, /Missing empty-state guard/);
});
test("adversarial review asks Codex to inspect larger diffs itself", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
installFakeCodex(binDir);
initGitRepo(repo);
fs.mkdirSync(path.join(repo, "src"));
for (const name of ["a.js", "b.js", "c.js"]) {
fs.writeFileSync(path.join(repo, "src", name), `export const value = "${name}-v1";\n`);
}
run("git", ["add", "src/a.js", "src/b.js", "src/c.js"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });
fs.writeFileSync(path.join(repo, "src", "a.js"), 'export const value = "PROMPT_SELF_COLLECT_A";\n');
fs.writeFileSync(path.join(repo, "src", "b.js"), 'export const value = "PROMPT_SELF_COLLECT_B";\n');
fs.writeFileSync(path.join(repo, "src", "c.js"), 'export const value = "PROMPT_SELF_COLLECT_C";\n');
const result = run("node", [SCRIPT, "adversarial-review"], {
cwd: repo,
env: buildEnv(binDir)
});
assert.equal(result.status, 0, result.stderr);
const state = JSON.parse(fs.readFileSync(path.join(binDir, "fake-codex-state.json"), "utf8"));
assert.match(state.lastTurnStart.prompt, /lightweight summary/i);
assert.match(state.lastTurnStart.prompt, /read-only git commands/i);
assert.doesNotMatch(state.lastTurnStart.prompt, /PROMPT_SELF_COLLECT_[ABC]/);
});
test("review includes reasoning output when the app server returns it", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
@@ -436,105 +409,6 @@ test("task-resume-candidate returns the latest rescue thread from the current se
assert.equal(payload.candidate.threadId, "thr_current");
});
test("task --resume-last does not resume a task from another Claude session", () => {
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 otherEnv = {
...buildEnv(binDir),
CODEX_COMPANION_SESSION_ID: "sess-other"
};
const currentEnv = {
...buildEnv(binDir),
CODEX_COMPANION_SESSION_ID: "sess-current"
};
const firstRun = run("node", [SCRIPT, "task", "initial task"], {
cwd: repo,
env: otherEnv
});
assert.equal(firstRun.status, 0, firstRun.stderr);
const candidate = run("node", [SCRIPT, "task-resume-candidate", "--json"], {
cwd: repo,
env: currentEnv
});
assert.equal(candidate.status, 0, candidate.stderr);
assert.equal(JSON.parse(candidate.stdout).available, false);
const resume = run("node", [SCRIPT, "task", "--resume-last", "follow up"], {
cwd: repo,
env: currentEnv
});
assert.equal(resume.status, 1);
assert.match(resume.stderr, /No previous Codex task thread was found for this repository\./);
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
assert.equal(fakeState.lastTurnStart.threadId, "thr_1");
assert.equal(fakeState.lastTurnStart.prompt, "initial task");
});
test("task --resume-last ignores running tasks from other Claude sessions", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
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 stateDir = resolveStateDir(repo);
fs.mkdirSync(path.join(stateDir, "jobs"), { recursive: true });
fs.writeFileSync(
path.join(stateDir, "state.json"),
`${JSON.stringify(
{
version: 1,
config: { stopReviewGate: false },
jobs: [
{
id: "task-other-running",
status: "running",
title: "Codex Task",
jobClass: "task",
sessionId: "sess-other",
threadId: "thr_other",
summary: "Other session active task",
updatedAt: "2026-03-24T20:05:00.000Z"
}
]
},
null,
2
)}\n`,
"utf8"
);
const env = {
...buildEnv(binDir),
CODEX_COMPANION_SESSION_ID: "sess-current"
};
const status = run("node", [SCRIPT, "status", "--json"], {
cwd: repo,
env
});
assert.equal(status.status, 0, status.stderr);
assert.deepEqual(JSON.parse(status.stdout).running, []);
const resume = run("node", [SCRIPT, "task", "--resume-last", "follow up"], {
cwd: repo,
env
});
assert.equal(resume.status, 1);
assert.match(resume.stderr, /No previous Codex task thread was found for this repository\./);
});
test("session start hook exports the Claude session id and plugin data dir for later commands", () => {
const repo = makeTempDir();
const envFile = path.join(makeTempDir(), "claude-env.sh");
@@ -1498,109 +1372,6 @@ test("cancel stops an active background job and marks it cancelled", async (t) =
assert.match(fs.readFileSync(logFile, "utf8"), /Cancelled by user/);
});
test("cancel without a job id ignores active jobs from other Claude sessions", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
const jobsDir = path.join(stateDir, "jobs");
fs.mkdirSync(jobsDir, { recursive: true });
const logFile = path.join(jobsDir, "task-other.log");
fs.writeFileSync(logFile, "", "utf8");
fs.writeFileSync(
path.join(stateDir, "state.json"),
`${JSON.stringify(
{
version: 1,
config: { stopReviewGate: false },
jobs: [
{
id: "task-other",
status: "running",
title: "Codex Task",
jobClass: "task",
sessionId: "sess-other",
summary: "Other session run",
updatedAt: "2026-03-24T20:05:00.000Z",
logFile
}
]
},
null,
2
)}\n`,
"utf8"
);
const env = {
...process.env,
CODEX_COMPANION_SESSION_ID: "sess-current"
};
const status = run("node", [SCRIPT, "status", "--json"], {
cwd: workspace,
env
});
assert.equal(status.status, 0, status.stderr);
assert.deepEqual(JSON.parse(status.stdout).running, []);
const cancel = run("node", [SCRIPT, "cancel", "--json"], {
cwd: workspace,
env
});
assert.equal(cancel.status, 1);
assert.match(cancel.stderr, /No active Codex jobs to cancel for this session\./);
const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8"));
assert.equal(state.jobs[0].status, "running");
});
test("cancel with a job id can still target an active job from another Claude session", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
const jobsDir = path.join(stateDir, "jobs");
fs.mkdirSync(jobsDir, { recursive: true });
const logFile = path.join(jobsDir, "task-other.log");
fs.writeFileSync(logFile, "", "utf8");
fs.writeFileSync(
path.join(stateDir, "state.json"),
`${JSON.stringify(
{
version: 1,
config: { stopReviewGate: false },
jobs: [
{
id: "task-other",
status: "running",
title: "Codex Task",
jobClass: "task",
sessionId: "sess-other",
summary: "Other session run",
updatedAt: "2026-03-24T20:05:00.000Z",
logFile
}
]
},
null,
2
)}\n`,
"utf8"
);
const env = {
...process.env,
CODEX_COMPANION_SESSION_ID: "sess-current"
};
const cancel = run("node", [SCRIPT, "cancel", "task-other", "--json"], {
cwd: workspace,
env
});
assert.equal(cancel.status, 0, cancel.stderr);
assert.equal(JSON.parse(cancel.stdout).jobId, "task-other");
const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8"));
assert.equal(state.jobs[0].status, "cancelled");
});
test("cancel sends turn interrupt to the shared app-server before killing a brokered task", async () => {
const repo = makeTempDir();
const binDir = makeTempDir();