Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
},
"metadata": {
"description": "CBEPX fork of the OpenAI Codex plugin for Claude Code: max/ultra effort, per-thread config overrides, gpt-5.6 aliases, rescue agent fixes.",
"version": "1.4.2"
"version": "1.4.3"
},
"plugins": [
{
"name": "codex",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
"version": "1.4.2",
"version": "1.4.3",
"author": {
"name": "OpenAI"
},
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.4.3 — 2026-09-30

### Internal
- No behaviour change. Duplicate helpers in `plugins/codex/scripts` consolidated (`nowIso`, `readStoredJob`, `readJsonOrNull`, `shorten`, `looksLikeVerificationCommand`, the pending-cancel text, the env-based session filter, `buildJsonRpcError`); the queued/running status comparisons go through `lib/job-status.mjs`; dead `fs.mjs` exports removed.
- Tests: shared harness helpers in `tests/helpers.mjs`; `tests/runtime.test.mjs` split by command family (`runtime-setup`, `-review`, `-task`, `-status`, `-cancel`, `-transfer`, `-hooks`), which roughly halves the local suite's wall time; new `module-boundaries` and `job-status` unit tests; eslint `no-unused-vars` enabled (unused imports and locals removed, no logic changes).

## 1.4.2 — 2026-09-30

### Fixed
Expand Down
367 changes: 367 additions & 0 deletions docs/superpowers/plans/2026-09-30-codex-plugin-cc-v1.4.3.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import globals from "globals";
export default [
{
ignores: [
".claude/**",
".githooks/**",
".stryker-tmp/**",
".worktrees/**",
Expand All @@ -27,7 +26,7 @@ export default [
},
rules: {
"no-empty": "off",
"no-unused-vars": "off",
"no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrors: "none" }],
"no-useless-assignment": "off",
"no-useless-escape": "off",
"preserve-caught-error": "off",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cbepx/codex-plugin-cc",
"version": "1.4.2",
"version": "1.4.3",
"private": true,
"type": "module",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex",
"version": "1.4.2",
"version": "1.4.3",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
"author": {
"name": "OpenAI"
Expand Down
6 changes: 6 additions & 0 deletions plugins/codex/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.4.3 — 2026-09-30

### Internal
- No behaviour change. Duplicate helpers in `plugins/codex/scripts` consolidated (`nowIso`, `readStoredJob`, `readJsonOrNull`, `shorten`, `looksLikeVerificationCommand`, the pending-cancel text, the env-based session filter, `buildJsonRpcError`); the queued/running status comparisons go through `lib/job-status.mjs`; dead `fs.mjs` exports removed.
- Tests: shared harness helpers in `tests/helpers.mjs`; `tests/runtime.test.mjs` split by command family (`runtime-setup`, `-review`, `-task`, `-status`, `-cancel`, `-transfer`, `-hooks`), which roughly halves the local suite's wall time; new `module-boundaries` and `job-status` unit tests; eslint `no-unused-vars` enabled (unused imports and locals removed, no logic changes).

## 1.4.2 — 2026-09-30

### Fixed
Expand Down
6 changes: 1 addition & 5 deletions plugins/codex/scripts/app-server-broker.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 { parseArgs } from "./lib/args.mjs";
import { BROKER_BUSY_RPC_CODE, CodexAppServerClient } from "./lib/app-server.mjs";
import { BROKER_BUSY_RPC_CODE, buildJsonRpcError, CodexAppServerClient } from "./lib/app-server.mjs";
import { parseBrokerEndpoint } from "./lib/broker-endpoint.mjs";
import { clearBrokerSessionIfEndpoint, registerBrokerProcess } from "./lib/broker-lifecycle.mjs";

Expand Down Expand Up @@ -61,10 +61,6 @@ function buildStreamThreadIds(method, params, result) {
return threadIds;
}

function buildJsonRpcError(code, message, data) {
return data === undefined ? { code, message } : { code, message, data };
}

function send(socket, message) {
if (socket.destroyed) {
return;
Expand Down
79 changes: 22 additions & 57 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { resolveClaudeSessionPath } from "./lib/claude-session-transfer.mjs";
import { readStdinIfPiped } from "./lib/fs.mjs";
import { collectReviewContext, ensureGitRepository, resolveReviewTarget } from "./lib/git.mjs";
import { isActiveJobStatus, isTerminalRecord } from "./lib/job-status.mjs";
import { loadModelCatalog, resolveModelAlias, supportedEfforts } from "./lib/model-catalog.mjs";
import { binaryAvailable, isPidAlive, terminateRecordedProcess, workerCommandLine } from "./lib/process.mjs";
import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs";
Expand All @@ -33,8 +34,9 @@ import {
generateJobId,
getConfig,
listJobs,
nowIso,
readStoredJob,
recordWorkerPid,
removeJobPidFile,
redactConfigValues,
removeJobRequestFile,
resolveJobPid,
Expand All @@ -51,10 +53,8 @@ import {
buildStatusSnapshot,
cancelDecision,
commitCancel,
emitCancelPending,
isWorkerProvedRecord,
isWorkerTerminalRecord,
readStoredJob,
resolveCancelableJob,
resolveResultJob,
sortJobsNewestFirst
Expand All @@ -65,22 +65,24 @@ import {
createJobProgressUpdater,
createJobRecord,
createProgressReporter,
nowIso,
filterJobsForSession,
getCurrentSessionId,
reapDeadJobs,
registerWorkerCrashGuard,
runTrackedJob,
SESSION_ID_ENV
runTrackedJob
} from "./lib/tracked-jobs.mjs";
import { resolveWorkspaceRoot } from "./lib/workspace.mjs";
import {
emitCancelPending,
renderNativeReviewResult,
renderReviewResult,
renderStoredJobResult,
renderCancelReport,
renderJobStatusReport,
renderSetupReport,
renderStatusReport,
renderTaskResult
renderTaskResult,
shorten
} from "./lib/render.mjs";

const ROOT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
Expand Down Expand Up @@ -266,17 +268,6 @@ function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function shorten(text, limit = 96) {
const normalized = String(text ?? "").trim().replace(/\s+/g, " ");
if (!normalized) {
return "";
}
if (normalized.length <= limit) {
return normalized;
}
return `${normalized.slice(0, limit - 3)}...`;
}

function firstMeaningfulLine(text, fallback) {
const line = String(text ?? "")
.split(/\r?\n/)
Expand Down Expand Up @@ -426,30 +417,13 @@ function renderStatusPayload(report, asJson) {
return asJson ? report : renderStatusReport(report);
}

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"
!isActiveJobStatus(job.status)
) ?? null
);
}
Expand Down Expand Up @@ -528,10 +502,10 @@ function outputJobResult(cwd, reference, asJson) {

async function resolveLatestTrackedTaskThread(cwd, options = {}) {
const workspaceRoot = resolveWorkspaceRoot(cwd);
const sessionId = getCurrentClaudeSessionId();
const sessionId = getCurrentSessionId();
const jobs = sortJobsNewestFirst(reapDeadJobs(workspaceRoot, 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 visibleJobs = filterJobsForSession(jobs);
const activeTask = visibleJobs.find((job) => job.jobClass === "task" && isActiveJobStatus(job.status));
if (activeTask) {
throw new Error(`Task ${activeTask.id} is still running. Use /codex:status before continuing it.`);
}
Expand Down Expand Up @@ -709,18 +683,11 @@ async function executeTaskRun(request) {
const failureMessage =
result.error?.message ??
(result.status !== 0 ? (result.stderr || `Codex turn ended with status "${turnStatus ?? "failed"}"`) : "");
const rendered = renderTaskResult(
{
rawOutput,
failureMessage,
reasoningSummary: result.reasoningSummary
},
{
title: taskMetadata.title,
jobId: request.jobId ?? null,
write: Boolean(request.write)
}
);
const rendered = renderTaskResult({
rawOutput,
failureMessage,
reasoningSummary: result.reasoningSummary
});
const payload = {
status: result.status,
threadId: result.threadId,
Expand Down Expand Up @@ -768,7 +735,7 @@ function buildTaskRunMetadata({ prompt, resumeLast = false }) {
const fallbackSummary = resumeLast ? DEFAULT_CONTINUE_PROMPT : "Task";
return {
title,
summary: shorten(prompt || fallbackSummary)
summary: shorten(prompt || fallbackSummary, 96)
};
}

Expand Down Expand Up @@ -1175,7 +1142,6 @@ async function handleTaskWorker(argv) {
throw new Error("Missing required --job-id for task-worker.");
}

const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
const storedJob = readStoredJob(workspaceRoot, options["job-id"]);
if (!storedJob) {
Expand Down Expand Up @@ -1294,10 +1260,9 @@ function handleTaskResumeCandidate(argv) {
return;
}

const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
const sessionId = getCurrentClaudeSessionId();
const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(reapDeadJobs(workspaceRoot, listJobs(workspaceRoot))));
const sessionId = getCurrentSessionId();
const jobs = filterJobsForSession(sortJobsNewestFirst(reapDeadJobs(workspaceRoot, listJobs(workspaceRoot))));
const candidate = findLatestResumableTaskJob(jobs);

const payload = {
Expand Down Expand Up @@ -1399,7 +1364,7 @@ async function waitForTerminalRecord(workspaceRoot, jobId, timeoutMs) {
const deadline = Date.now() + timeoutMs;
for (;;) {
const stored = readStoredJob(workspaceRoot, jobId);
if (stored && stored.status !== "queued" && stored.status !== "running") {
if (isTerminalRecord(stored)) {
return stored;
}
if (Date.now() >= deadline) {
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/scripts/lib/app-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const DEFAULT_CAPABILITIES = {
]
};

function buildJsonRpcError(code, message, data) {
export function buildJsonRpcError(code, message, data) {
return data === undefined ? { code, message } : { code, message, data };
}

Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/scripts/lib/broker-lifecycle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ async function ensureBrokerSessionLocked(cwd, options, attempt) {
}
identity = post;
}
const { replacer, replacingAt, ...rest } = current;
const { replacer: _replacer, replacingAt: _replacingAt, ...rest } = current;
const promoted = { ...rest, state: "ready", pidIdentity: identity };
return writeBrokerSessionIf(cwd, (latest) => sameClaim(latest, current), promoted) ? promoted : await retryClaim("the broker record kept changing");
};
Expand Down
21 changes: 3 additions & 18 deletions plugins/codex/scripts/lib/codex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import { readJsonFile } from "./fs.mjs";
import { BROKER_BUSY_RPC_CODE, BROKER_ENDPOINT_ENV, CodexAppServerClient } from "./app-server.mjs";
import { loadBrokerSession } from "./broker-lifecycle.mjs";
import { binaryAvailable } from "./process.mjs";
import { isActiveJobStatus } from "./job-status.mjs";
import { looksLikeVerificationCommand, shorten } from "./render.mjs";
import { listJobs } from "./state.mjs";
import { reapDeadJobs } from "./tracked-jobs.mjs";

Expand Down Expand Up @@ -143,23 +145,6 @@ function buildTurnInput(prompt) {
return [{ type: "text", text: prompt, text_elements: [] }];
}

function shorten(text, limit = 72) {
const normalized = String(text ?? "").trim().replace(/\s+/g, " ");
if (!normalized) {
return "";
}
if (normalized.length <= limit) {
return normalized;
}
return `${normalized.slice(0, limit - 3)}...`;
}

function looksLikeVerificationCommand(command) {
return /\b(test|tests|lint|build|typecheck|type-check|check|verify|validate|pytest|jest|vitest|cargo test|npm test|pnpm test|yarn test|go test|mvn test|gradle test|tsc|eslint|ruff)\b/i.test(
command
);
}

function buildTaskThreadName(prompt) {
const excerpt = shorten(prompt, 56);
return excerpt ? `${TASK_THREAD_PREFIX}: ${excerpt}` : TASK_THREAD_PREFIX;
Expand Down Expand Up @@ -1352,7 +1337,7 @@ function assertThreadIsFree(cwd, threadId, excludeJobId = null) {
(job) =>
job.id !== excludeJobId &&
job.threadId === threadId &&
(job.status === "queued" || job.status === "running")
isActiveJobStatus(job.status)
);
if (busy) {
throw new Error(`Thread ${threadId} is busy in job ${busy.id}; wait for it or run cancel ${busy.id} first.`);
Expand Down
18 changes: 7 additions & 11 deletions plugins/codex/scripts/lib/fs.mjs
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

export function ensureAbsolutePath(cwd, maybePath) {
return path.isAbsolute(maybePath) ? maybePath : path.resolve(cwd, maybePath);
}

export function createTempDir(prefix = "codex-plugin-") {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}

export function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}

export function writeJsonFile(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

export function safeReadFile(filePath) {
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
// Missing, unreadable or malformed file → null (state index, job files, model catalogue).
export function readJsonOrNull(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch {
return null;
}
}

export function isProbablyText(buffer) {
Expand Down
Loading
Loading