diff --git a/plugin/vendor/codex/.claude-plugin/plugin.json b/plugin/vendor/codex/.claude-plugin/plugin.json index 8d04c2a..da26202 100644 --- a/plugin/vendor/codex/.claude-plugin/plugin.json +++ b/plugin/vendor/codex/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex", - "version": "1.0.2", + "version": "1.0.4", "description": "Use Codex from Claude Code to review code or delegate tasks.", "author": { "name": "OpenAI" diff --git a/plugin/vendor/codex/prompts/adversarial-review.md b/plugin/vendor/codex/prompts/adversarial-review.md index c8f8123..78668af 100644 --- a/plugin/vendor/codex/prompts/adversarial-review.md +++ b/plugin/vendor/codex/prompts/adversarial-review.md @@ -32,6 +32,7 @@ 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}} diff --git a/plugin/vendor/codex/scripts/codex-companion.mjs b/plugin/vendor/codex/scripts/codex-companion.mjs index 201d1c7..35222fd 100644 --- a/plugin/vendor/codex/scripts/codex-companion.mjs +++ b/plugin/vendor/codex/scripts/codex-companion.mjs @@ -11,8 +11,8 @@ import { buildPersistentTaskThreadName, DEFAULT_CONTINUE_PROMPT, findLatestTaskThread, + getCodexAuthStatus, getCodexAvailability, - getCodexLoginStatus, getSessionRuntimeStatus, interruptAppServerTurn, parseStructuredOutput, @@ -176,19 +176,19 @@ function firstMeaningfulLine(text, fallback) { return line ?? fallback; } -function buildSetupReport(cwd, actionsTaken = []) { +async function buildSetupReport(cwd, actionsTaken = []) { const workspaceRoot = resolveWorkspaceRoot(cwd); const nodeStatus = binaryAvailable("node", ["--version"], { cwd }); const npmStatus = binaryAvailable("npm", ["--version"], { cwd }); const codexStatus = getCodexAvailability(cwd); - const authStatus = getCodexLoginStatus(cwd); + const authStatus = await getCodexAuthStatus(cwd); const config = getConfig(workspaceRoot); const nextSteps = []; if (!codexStatus.available) { nextSteps.push("Install Codex with `npm install -g @openai/codex`."); } - if (codexStatus.available && !authStatus.loggedIn) { + if (codexStatus.available && !authStatus.loggedIn && authStatus.requiresOpenaiAuth) { nextSteps.push("Run `!codex login`."); nextSteps.push("If browser login is blocked, retry with `!codex login --device-auth` or `!codex login --with-api-key`."); } @@ -202,14 +202,14 @@ function buildSetupReport(cwd, actionsTaken = []) { npm: npmStatus, codex: codexStatus, auth: authStatus, - sessionRuntime: getSessionRuntimeStatus(), + sessionRuntime: getSessionRuntimeStatus(process.env, workspaceRoot), reviewGateEnabled: Boolean(config.stopReviewGate), actionsTaken, nextSteps }; } -function handleSetup(argv) { +async function handleSetup(argv) { const { options } = parseCommandInput(argv, { valueOptions: ["cwd"], booleanOptions: ["json", "enable-review-gate", "disable-review-gate"] @@ -231,7 +231,7 @@ function handleSetup(argv) { actionsTaken.push(`Disabled the stop-time review gate for ${workspaceRoot}.`); } - const finalReport = buildSetupReport(cwd, actionsTaken); + const finalReport = await buildSetupReport(cwd, actionsTaken); outputResult(options.json ? finalReport : renderSetupReport(finalReport), options.json); } @@ -241,18 +241,16 @@ 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 }); } -function ensureCodexReady(cwd) { - const authStatus = getCodexLoginStatus(cwd); - if (!authStatus.available) { +function ensureCodexAvailable(cwd) { + const availability = getCodexAvailability(cwd); + if (!availability.available) { throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/codex:setup`."); } - if (!authStatus.loggedIn) { - throw new Error("Codex CLI is not authenticated. Run `!codex login` and retry."); - } } function buildNativeReviewTarget(target) { @@ -290,6 +288,30 @@ 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); @@ -310,22 +332,28 @@ 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 activeTask = jobs.find((job) => job.jobClass === "task" && (job.status === "queued" || job.status === "running")); + const visibleJobs = filterJobsForCurrentClaudeSession(jobs); + const activeTask = visibleJobs.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 = jobs.find((job) => job.jobClass === "task" && job.status === "completed" && job.threadId); + const trackedTask = findLatestResumableTaskJob(visibleJobs); if (trackedTask) { return { id: trackedTask.threadId }; } + if (sessionId) { + return null; + } + return findLatestTaskThread(workspaceRoot); } async function executeReviewRun(request) { - ensureCodexReady(request.cwd); + ensureCodexAvailable(request.cwd); ensureGitRepository(request.cwd); const target = resolveReviewTarget(request.cwd, { @@ -429,7 +457,7 @@ async function executeReviewRun(request) { async function executeTaskRun(request) { const workspaceRoot = resolveWorkspaceRoot(request.cwd); - ensureCodexReady(request.cwd); + ensureCodexAvailable(request.cwd); const taskMetadata = buildTaskRunMetadata({ prompt: request.prompt, @@ -728,7 +756,7 @@ async function handleTask(argv) { }); if (options.background) { - ensureCodexReady(cwd); + ensureCodexAvailable(cwd); requireTaskRequest(prompt, resumeLast); const job = buildTaskJob(workspaceRoot, taskMetadata, write); @@ -862,17 +890,9 @@ function handleTaskResumeCandidate(argv) { const cwd = resolveCommandCwd(options); const workspaceRoot = resolveCommandWorkspace(options); - 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 sessionId = getCurrentClaudeSessionId(); + const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(listJobs(workspaceRoot))); + const candidate = findLatestResumableTaskJob(jobs); const payload = { available: Boolean(candidate), @@ -905,7 +925,7 @@ async function handleCancel(argv) { const cwd = resolveCommandCwd(options); const reference = positionals[0] ?? ""; - const { workspaceRoot, job } = resolveCancelableJob(cwd, reference); + const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env }); const existing = readStoredJob(workspaceRoot, job.id) ?? {}; const threadId = existing.threadId ?? job.threadId ?? null; const turnId = existing.turnId ?? job.turnId ?? null; @@ -967,7 +987,7 @@ async function main() { switch (subcommand) { case "setup": - handleSetup(argv); + await handleSetup(argv); break; case "review": await handleReview(argv); diff --git a/plugin/vendor/codex/scripts/lib/app-server-protocol.d.ts b/plugin/vendor/codex/scripts/lib/app-server-protocol.d.ts index 7553dc8..cc6446d 100644 --- a/plugin/vendor/codex/scripts/lib/app-server-protocol.d.ts +++ b/plugin/vendor/codex/scripts/lib/app-server-protocol.d.ts @@ -51,6 +51,7 @@ export interface CodexAppServerClientOptions { capabilities?: InitializeCapabilities; brokerEndpoint?: string; disableBroker?: boolean; + reuseExistingBroker?: boolean; } export interface AppServerMethodMap { diff --git a/plugin/vendor/codex/scripts/lib/app-server.mjs b/plugin/vendor/codex/scripts/lib/app-server.mjs index fec105c..127c837 100644 --- a/plugin/vendor/codex/scripts/lib/app-server.mjs +++ b/plugin/vendor/codex/scripts/lib/app-server.mjs @@ -13,7 +13,7 @@ import process from "node:process"; import { spawn } from "node:child_process"; import readline from "node:readline"; import { parseBrokerEndpoint } from "./broker-endpoint.mjs"; -import { ensureBrokerSession } from "./broker-lifecycle.mjs"; +import { ensureBrokerSession, loadBrokerSession } from "./broker-lifecycle.mjs"; import { terminateProcessTree } from "./process.mjs"; const PLUGIN_MANIFEST_URL = new URL("../../.claude-plugin/plugin.json", import.meta.url); @@ -188,9 +188,9 @@ class SpawnedCodexAppServerClient extends AppServerClientBase { async initialize() { this.proc = spawn("codex", ["app-server"], { cwd: this.cwd, - env: this.options.env, + env: this.options.env ?? process.env, stdio: ["pipe", "pipe", "pipe"], - shell: process.platform === "win32", + shell: process.platform === "win32" ? (process.env.SHELL || true) : false, windowsHide: true }); @@ -333,7 +333,10 @@ export class CodexAppServerClient { let brokerEndpoint = null; if (!options.disableBroker) { brokerEndpoint = options.brokerEndpoint ?? options.env?.[BROKER_ENDPOINT_ENV] ?? process.env[BROKER_ENDPOINT_ENV] ?? null; - if (!brokerEndpoint) { + if (!brokerEndpoint && options.reuseExistingBroker) { + brokerEndpoint = loadBrokerSession(cwd)?.endpoint ?? null; + } + if (!brokerEndpoint && !options.reuseExistingBroker) { const brokerSession = await ensureBrokerSession(cwd, { env: options.env }); brokerEndpoint = brokerSession?.endpoint ?? null; } diff --git a/plugin/vendor/codex/scripts/lib/codex.mjs b/plugin/vendor/codex/scripts/lib/codex.mjs index bf7e8c8..f2fe88b 100644 --- a/plugin/vendor/codex/scripts/lib/codex.mjs +++ b/plugin/vendor/codex/scripts/lib/codex.mjs @@ -37,7 +37,7 @@ 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, runCommand } from "./process.mjs"; +import { binaryAvailable } from "./process.mjs"; const SERVICE_NAME = "claude_code_codex_plugin"; const TASK_THREAD_PREFIX = "Codex Companion Task"; @@ -639,7 +639,16 @@ async function startThread(client, cwd, options = {}) { const response = await client.request("thread/start", buildThreadParams(cwd, options)); const threadId = response.thread.id; if (options.threadName) { - await client.request("thread/name/set", { threadId, name: 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; + } + } } return response; } @@ -652,6 +661,134 @@ function buildResultStatus(turnState) { return turnState.finalTurn?.status === "completed" ? 0 : 1; } +const BUILTIN_PROVIDER_LABELS = new Map([ + ["openai", "OpenAI"], + ["ollama", "Ollama"], + ["lmstudio", "LM Studio"] +]); + +function normalizeProviderId(value) { + const providerId = typeof value === "string" ? value.trim() : ""; + return providerId || null; +} + +function formatProviderLabel(providerId, providerConfig = null) { + const configuredName = typeof providerConfig?.name === "string" ? providerConfig.name.trim() : ""; + if (configuredName) { + return configuredName; + } + if (!providerId) { + return "The active provider"; + } + return BUILTIN_PROVIDER_LABELS.get(providerId) ?? providerId; +} + +function buildAuthStatus(fields = {}) { + return { + available: true, + loggedIn: false, + detail: "not authenticated", + source: "unknown", + authMethod: null, + verified: null, + requiresOpenaiAuth: null, + provider: null, + ...fields + }; +} + +function resolveProviderConfig(configResponse) { + const config = configResponse?.config; + if (!config || typeof config !== "object") { + return { + providerId: null, + providerConfig: null + }; + } + + const providerId = normalizeProviderId(config.model_provider); + const providers = + config.model_providers && typeof config.model_providers === "object" && !Array.isArray(config.model_providers) + ? config.model_providers + : null; + const providerConfig = + providerId && providers?.[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null; + + return { + providerId, + providerConfig + }; +} + +function buildAppServerAuthStatus(accountResponse, configResponse) { + const account = accountResponse?.account ?? null; + const requiresOpenaiAuth = + typeof accountResponse?.requiresOpenaiAuth === "boolean" ? accountResponse.requiresOpenaiAuth : null; + const { providerId, providerConfig } = resolveProviderConfig(configResponse); + const providerLabel = formatProviderLabel(providerId, providerConfig); + + if (account?.type === "chatgpt") { + const email = typeof account.email === "string" && account.email.trim() ? account.email.trim() : null; + return buildAuthStatus({ + loggedIn: true, + detail: email ? `ChatGPT login active for ${email}` : "ChatGPT login active", + source: "app-server", + authMethod: "chatgpt", + verified: true, + requiresOpenaiAuth, + provider: providerId + }); + } + + if (account?.type === "apiKey") { + return buildAuthStatus({ + loggedIn: true, + detail: "API key configured (unverified)", + source: "app-server", + authMethod: "apiKey", + verified: false, + requiresOpenaiAuth, + provider: providerId + }); + } + + if (requiresOpenaiAuth === false) { + return buildAuthStatus({ + loggedIn: true, + detail: `${providerLabel} is configured and does not require OpenAI authentication`, + source: "app-server", + requiresOpenaiAuth, + provider: providerId + }); + } + + return buildAuthStatus({ + loggedIn: false, + detail: `${providerLabel} requires OpenAI authentication`, + source: "app-server", + requiresOpenaiAuth, + provider: providerId + }); +} + +async function getCodexAuthStatusFromClient(client, cwd) { + try { + const accountResponse = await client.request("account/read", { refreshToken: false }); + const configResponse = await client.request("config/read", { + includeLayers: false, + cwd + }); + + return buildAppServerAuthStatus(accountResponse, configResponse); + } catch (error) { + return buildAuthStatus({ + loggedIn: false, + detail: error instanceof Error ? error.message : String(error), + source: "app-server" + }); + } +} + export function getCodexAvailability(cwd) { const versionStatus = binaryAvailable("codex", ["--version"], { cwd }); if (!versionStatus.available) { @@ -691,38 +828,39 @@ export function getSessionRuntimeStatus(env = process.env, cwd = process.cwd()) }; } -export function getCodexLoginStatus(cwd) { +export async function getCodexAuthStatus(cwd, options = {}) { const availability = getCodexAvailability(cwd); if (!availability.available) { return { available: false, loggedIn: false, - detail: availability.detail + detail: availability.detail, + source: "availability", + authMethod: null, + verified: null, + requiresOpenaiAuth: null, + provider: null }; } - const result = runCommand("codex", ["login", "status"], { cwd }); - if (result.error) { - return { - available: true, + let client = null; + try { + client = await CodexAppServerClient.connect(cwd, { + env: options.env, + reuseExistingBroker: true + }); + return await getCodexAuthStatusFromClient(client, cwd); + } catch (error) { + return buildAuthStatus({ loggedIn: false, - detail: result.error.message - }; + detail: error instanceof Error ? error.message : String(error), + source: "app-server" + }); + } finally { + if (client) { + await client.close().catch(() => {}); + } } - - if (result.status === 0) { - return { - available: true, - loggedIn: true, - detail: result.stdout.trim() || "authenticated" - }; - } - - return { - available: true, - loggedIn: false, - detail: result.stderr.trim() || result.stdout.trim() || "not authenticated" - }; } export async function interruptAppServerTurn(cwd, { threadId, turnId }) { @@ -745,12 +883,9 @@ export async function interruptAppServerTurn(cwd, { threadId, turnId }) { }; } - const brokerEndpoint = process.env[BROKER_ENDPOINT_ENV] ?? loadBrokerSession(cwd)?.endpoint ?? null; let client = null; try { - client = brokerEndpoint - ? await CodexAppServerClient.connect(cwd, { brokerEndpoint }) - : await CodexAppServerClient.connect(cwd, { disableBroker: true }); + client = await CodexAppServerClient.connect(cwd, { reuseExistingBroker: true }); await client.request("turn/interrupt", { threadId, turnId }); return { attempted: true, diff --git a/plugin/vendor/codex/scripts/lib/git.mjs b/plugin/vendor/codex/scripts/lib/git.mjs index 1c0529a..1749cfc 100644 --- a/plugin/vendor/codex/scripts/lib/git.mjs +++ b/plugin/vendor/codex/scripts/lib/git.mjs @@ -2,9 +2,11 @@ import fs from "node:fs"; import path from "node:path"; import { isProbablyText } from "./fs.mjs"; -import { runCommand, runCommandChecked } from "./process.mjs"; +import { formatCommandFailure, 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 }); @@ -14,6 +16,64 @@ 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; @@ -135,12 +195,25 @@ function formatSection(title, body) { function formatUntrackedFile(cwd, relativePath) { const absolutePath = path.join(cwd, relativePath); - const stat = fs.statSync(absolutePath); + let stat; + try { + stat = fs.statSync(absolutePath); + } catch { + return `### ${relativePath}\n(skipped: broken symlink or unreadable file)`; + } + if (stat.isDirectory()) { + return `### ${relativePath}\n(skipped: directory)`; + } if (stat.size > MAX_UNTRACKED_BYTES) { return `### ${relativePath}\n(skipped: ${stat.size} bytes exceeds ${MAX_UNTRACKED_BYTES} byte limit)`; } - const buffer = fs.readFileSync(absolutePath); + let buffer; + try { + buffer = fs.readFileSync(absolutePath); + } catch { + return `### ${relativePath}\n(skipped: broken symlink or unreadable file)`; + } if (!isProbablyText(buffer)) { return `### ${relativePath}\n(skipped: binary file)`; } @@ -148,55 +221,115 @@ function formatUntrackedFile(cwd, relativePath) { return [`### ${relativePath}`, "```", buffer.toString("utf8").trimEnd(), "```"].join("\n"); } -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"); +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); - const parts = [ - formatSection("Git Status", status), - formatSection("Staged Diff", stagedDiff), - formatSection("Unstaged Diff", unstagedDiff), - formatSection("Untracked Files", untrackedBody) - ]; + 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) + ]; + } 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") + content: parts.join("\n"), + changedFiles }; } -function collectBranchContext(cwd, baseRef) { - const mergeBase = gitChecked(cwd, ["merge-base", "HEAD", baseRef]).stdout.trim(); - const commitRange = `${mergeBase}..HEAD`; +function collectBranchContext(cwd, baseRef, options = {}) { + const includeDiff = options.includeDiff !== false; + const comparison = options.comparison ?? buildBranchComparison(cwd, baseRef); const currentBranch = getCurrentBranch(cwd); - 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; + 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(); return { mode: "branch", - 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") + 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 }; } -export function collectReviewContext(cwd, target) { +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 = {}) { const repoRoot = getRepoRoot(cwd); - const state = getWorkingTreeState(cwd); - const currentBranch = getCurrentBranch(cwd); + const currentBranch = getCurrentBranch(repoRoot); + const maxInlineFiles = normalizeMaxInlineFiles(options.maxInlineFiles); + const maxInlineDiffBytes = normalizeMaxInlineDiffBytes(options.maxInlineDiffBytes); let details; + let includeDiff; + let diffBytes; if (target.mode === "working-tree") { - details = collectWorkingTreeContext(repoRoot, state); + 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 }); } else { - details = collectBranchContext(repoRoot, target.baseRef); + 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 }); } return { @@ -204,6 +337,10 @@ export function collectReviewContext(cwd, target) { repoRoot, branch: currentBranch, target, + fileCount: details.changedFiles.length, + diffBytes, + inputMode: includeDiff ? "inline-diff" : "self-collect", + collectionGuidance: buildAdversarialCollectionGuidance({ includeDiff }), ...details }; } diff --git a/plugin/vendor/codex/scripts/lib/job-control.mjs b/plugin/vendor/codex/scripts/lib/job-control.mjs index 74ba7f7..ad152c1 100644 --- a/plugin/vendor/codex/scripts/lib/job-control.mjs +++ b/plugin/vendor/codex/scripts/lib/job-control.mjs @@ -231,7 +231,7 @@ export function buildStatusSnapshot(cwd, options = {}) { return { workspaceRoot, config, - sessionRuntime: getSessionRuntimeStatus(options.env), + sessionRuntime: getSessionRuntimeStatus(options.env, workspaceRoot), running, latestFinished, recent, @@ -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) { +export function resolveCancelableJob(cwd, reference, options = {}) { const workspaceRoot = resolveWorkspaceRoot(cwd); const jobs = sortJobsNewestFirst(listJobs(workspaceRoot)); const activeJobs = jobs.filter((job) => job.status === "queued" || job.status === "running"); @@ -291,12 +291,18 @@ export function resolveCancelableJob(cwd, reference) { return { workspaceRoot, job: selected }; } - if (activeJobs.length === 1) { - return { workspaceRoot, job: activeJobs[0] }; + const sessionScopedActiveJobs = filterJobsForCurrentSession(activeJobs, options); + + if (sessionScopedActiveJobs.length === 1) { + return { workspaceRoot, job: sessionScopedActiveJobs[0] }; } - if (activeJobs.length > 1) { + if (sessionScopedActiveJobs.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."); } diff --git a/plugin/vendor/codex/scripts/lib/process.mjs b/plugin/vendor/codex/scripts/lib/process.mjs index 0948dbd..af28d1c 100644 --- a/plugin/vendor/codex/scripts/lib/process.mjs +++ b/plugin/vendor/codex/scripts/lib/process.mjs @@ -7,8 +7,9 @@ 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", + shell: process.platform === "win32" ? (process.env.SHELL || true) : false, windowsHide: true }); diff --git a/plugin/vendor/codex/scripts/stop-review-gate-hook.mjs b/plugin/vendor/codex/scripts/stop-review-gate-hook.mjs index c22edbd..2346bdc 100644 --- a/plugin/vendor/codex/scripts/stop-review-gate-hook.mjs +++ b/plugin/vendor/codex/scripts/stop-review-gate-hook.mjs @@ -6,7 +6,7 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; -import { getCodexLoginStatus } from "./lib/codex.mjs"; +import { getCodexAvailability } from "./lib/codex.mjs"; import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs"; import { getConfig, listJobs } from "./lib/state.mjs"; import { sortJobsNewestFirst } from "./lib/job-control.mjs"; @@ -57,13 +57,13 @@ function buildStopReviewPrompt(input = {}) { } function buildSetupNote(cwd) { - const authStatus = getCodexLoginStatus(cwd); - if (authStatus.available && authStatus.loggedIn) { + const availability = getCodexAvailability(cwd); + if (availability.available) { return null; } - const detail = authStatus.detail ? ` ${authStatus.detail}.` : ""; - return `Codex is not set up for the review gate.${detail} Run /codex:setup and, if needed, !codex login.`; + const detail = availability.detail ? ` ${availability.detail}.` : ""; + return `Codex is not set up for the review gate.${detail} Run /codex:setup.`; } function parseStopReviewOutput(rawOutput) { @@ -175,4 +175,10 @@ function main() { logNote(runningTaskNote); } -main(); +try { + main(); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; +}