#!/usr/bin/env node import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { parseArgs, splitRawArgumentString } from "./lib/args.mjs"; import { buildPersistentTaskThreadName, DEFAULT_CONTINUE_PROMPT, findLatestTaskThread, getCodexAuthStatus, getCodexAvailability, getSessionRuntimeStatus, interruptAppServerTurn, parseStructuredOutput, readOutputSchema, runAppServerReview, runAppServerTurn } from "./lib/codex.mjs"; import { readStdinIfPiped } from "./lib/fs.mjs"; import { collectReviewContext, ensureGitRepository, resolveReviewTarget } from "./lib/git.mjs"; import { binaryAvailable, terminateProcessTree } from "./lib/process.mjs"; import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs"; import { generateJobId, getConfig, listJobs, setConfig, upsertJob, writeJobFile } from "./lib/state.mjs"; import { buildSingleJobSnapshot, buildStatusSnapshot, readStoredJob, resolveCancelableJob, resolveResultJob, sortJobsNewestFirst } from "./lib/job-control.mjs"; import { appendLogLine, createJobLogFile, createJobProgressUpdater, createJobRecord, createProgressReporter, nowIso, runTrackedJob, SESSION_ID_ENV } from "./lib/tracked-jobs.mjs"; import { resolveWorkspaceRoot } from "./lib/workspace.mjs"; import { renderNativeReviewResult, renderReviewResult, renderStoredJobResult, renderCancelReport, renderJobStatusReport, renderSetupReport, renderStatusReport, renderTaskResult } from "./lib/render.mjs"; const ROOT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url))); const REVIEW_SCHEMA = path.join(ROOT_DIR, "schemas", "review-output.schema.json"); const DEFAULT_STATUS_WAIT_TIMEOUT_MS = 240000; 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."; function printUsage() { console.log( [ "Usage:", " node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]", " node scripts/codex-companion.mjs review [--wait|--background] [--base ] [--scope ]", " node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base ] [--scope ] [focus text]", " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [prompt]", " node scripts/codex-companion.mjs status [job-id] [--all] [--json]", " node scripts/codex-companion.mjs result [job-id] [--json]", " node scripts/codex-companion.mjs cancel [job-id] [--json]" ].join("\n") ); } function outputResult(value, asJson) { if (asJson) { console.log(JSON.stringify(value, null, 2)); } else { process.stdout.write(value); } } function outputCommandResult(payload, rendered, asJson) { outputResult(asJson ? payload : rendered, asJson); } function normalizeRequestedModel(model) { if (model == null) { return null; } const normalized = String(model).trim(); if (!normalized) { return null; } return MODEL_ALIASES.get(normalized.toLowerCase()) ?? normalized; } function normalizeReasoningEffort(effort) { if (effort == null) { return null; } const normalized = String(effort).trim().toLowerCase(); if (!normalized) { return null; } if (!VALID_REASONING_EFFORTS.has(normalized)) { throw new Error( `Unsupported reasoning effort "${effort}". Use one of: none, minimal, low, medium, high, xhigh.` ); } return normalized; } function normalizeArgv(argv) { if (argv.length === 1) { const [raw] = argv; if (!raw || !raw.trim()) { return []; } return splitRawArgumentString(raw); } return argv; } function parseCommandInput(argv, config = {}) { return parseArgs(normalizeArgv(argv), { ...config, aliasMap: { C: "cwd", ...(config.aliasMap ?? {}) } }); } function resolveCommandCwd(options = {}) { return options.cwd ? path.resolve(process.cwd(), options.cwd) : process.cwd(); } function resolveCommandWorkspace(options = {}) { return resolveWorkspaceRoot(resolveCommandCwd(options)); } 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/) .map((value) => value.trim()) .find(Boolean); return line ?? fallback; } 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 = 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 && 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`."); } if (!config.stopReviewGate) { nextSteps.push("Optional: run `/codex:setup --enable-review-gate` to require a fresh review before stop."); } return { ready: nodeStatus.available && codexStatus.available && authStatus.loggedIn, node: nodeStatus, npm: npmStatus, codex: codexStatus, auth: authStatus, sessionRuntime: getSessionRuntimeStatus(process.env, workspaceRoot), reviewGateEnabled: Boolean(config.stopReviewGate), actionsTaken, nextSteps }; } async function handleSetup(argv) { const { options } = parseCommandInput(argv, { valueOptions: ["cwd"], booleanOptions: ["json", "enable-review-gate", "disable-review-gate"] }); if (options["enable-review-gate"] && options["disable-review-gate"]) { throw new Error("Choose either --enable-review-gate or --disable-review-gate."); } const cwd = resolveCommandCwd(options); const workspaceRoot = resolveCommandWorkspace(options); const actionsTaken = []; if (options["enable-review-gate"]) { setConfig(workspaceRoot, "stopReviewGate", true); actionsTaken.push(`Enabled the stop-time review gate for ${workspaceRoot}.`); } else if (options["disable-review-gate"]) { setConfig(workspaceRoot, "stopReviewGate", false); actionsTaken.push(`Disabled the stop-time review gate for ${workspaceRoot}.`); } const finalReport = await buildSetupReport(cwd, actionsTaken); outputResult(options.json ? finalReport : renderSetupReport(finalReport), options.json); } function buildAdversarialReviewPrompt(context, focusText) { const template = loadPromptTemplate(ROOT_DIR, "adversarial-review"); 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 }); } 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`."); } } function buildNativeReviewTarget(target) { if (target.mode === "working-tree") { return { type: "uncommittedChanges" }; } if (target.mode === "branch") { return { type: "baseBranch", branch: target.baseRef }; } return null; } function validateNativeReviewRequest(target, focusText) { if (focusText.trim()) { throw new Error( `\`/codex:review\` now maps directly to the built-in reviewer and does not support custom focus text. Retry with \`/codex:adversarial-review ${focusText.trim()}\` for focused review instructions.` ); } const nativeTarget = buildNativeReviewTarget(target); if (!nativeTarget) { throw new Error("This `/codex:review` target is not supported by the built-in reviewer. Retry with `/codex:adversarial-review` for custom targeting."); } return nativeTarget; } 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" ) ?? 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); const deadline = Date.now() + timeoutMs; let snapshot = buildSingleJobSnapshot(cwd, reference); while (isActiveJobStatus(snapshot.job.status) && Date.now() < deadline) { await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))); snapshot = buildSingleJobSnapshot(cwd, reference); } return { ...snapshot, waitTimedOut: isActiveJobStatus(snapshot.job.status), timeoutMs }; } 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")); if (activeTask) { throw new Error(`Task ${activeTask.id} is still running. Use /codex:status before continuing it.`); } const trackedTask = findLatestResumableTaskJob(visibleJobs); if (trackedTask) { return { id: trackedTask.threadId }; } if (sessionId) { return null; } return findLatestTaskThread(workspaceRoot); } async function executeReviewRun(request) { ensureCodexAvailable(request.cwd); ensureGitRepository(request.cwd); const target = resolveReviewTarget(request.cwd, { base: request.base, scope: request.scope }); const focusText = request.focusText?.trim() ?? ""; const reviewName = request.reviewName ?? "Review"; if (reviewName === "Review") { const reviewTarget = validateNativeReviewRequest(target, focusText); const result = await runAppServerReview(request.cwd, { target: reviewTarget, model: request.model, onProgress: request.onProgress }); const payload = { review: reviewName, target, threadId: result.threadId, sourceThreadId: result.sourceThreadId, codex: { status: result.status, stderr: result.stderr, stdout: result.reviewText, reasoning: result.reasoningSummary } }; const rendered = renderNativeReviewResult( { status: result.status, stdout: result.reviewText, stderr: result.stderr }, { reviewLabel: reviewName, targetLabel: target.label, reasoningSummary: result.reasoningSummary } ); return { exitStatus: result.status, threadId: result.threadId, turnId: result.turnId, payload, rendered, summary: firstMeaningfulLine(result.reviewText, `${reviewName} completed.`), jobTitle: `Codex ${reviewName}`, jobClass: "review", targetLabel: target.label }; } const context = collectReviewContext(request.cwd, target); const prompt = buildAdversarialReviewPrompt(context, focusText); const result = await runAppServerTurn(context.repoRoot, { prompt, model: request.model, sandbox: "read-only", outputSchema: readOutputSchema(REVIEW_SCHEMA), onProgress: request.onProgress }); const parsed = parseStructuredOutput(result.finalMessage, { status: result.status, failureMessage: result.error?.message ?? result.stderr }); const payload = { review: reviewName, target, threadId: result.threadId, context: { repoRoot: context.repoRoot, branch: context.branch, summary: context.summary }, codex: { status: result.status, stderr: result.stderr, stdout: result.finalMessage, reasoning: result.reasoningSummary }, result: parsed.parsed, rawOutput: parsed.rawOutput, parseError: parsed.parseError, reasoningSummary: result.reasoningSummary }; return { exitStatus: result.status, threadId: result.threadId, turnId: result.turnId, payload, rendered: renderReviewResult(parsed, { reviewLabel: reviewName, targetLabel: context.target.label, reasoningSummary: result.reasoningSummary }), summary: parsed.parsed?.summary ?? parsed.parseError ?? firstMeaningfulLine(result.finalMessage, `${reviewName} finished.`), jobTitle: `Codex ${reviewName}`, jobClass: "review", targetLabel: context.target.label }; } async function executeTaskRun(request) { const workspaceRoot = resolveWorkspaceRoot(request.cwd); ensureCodexAvailable(request.cwd); const taskMetadata = buildTaskRunMetadata({ prompt: request.prompt, resumeLast: request.resumeLast }); let resumeThreadId = null; if (request.resumeLast) { const latestThread = await resolveLatestTrackedTaskThread(workspaceRoot, { excludeJobId: request.jobId }); if (!latestThread) { throw new Error("No previous Codex task thread was found for this repository."); } resumeThreadId = latestThread.id; } if (!request.prompt && !resumeThreadId) { throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last."); } const result = await runAppServerTurn(workspaceRoot, { resumeThreadId, prompt: request.prompt, defaultPrompt: resumeThreadId ? DEFAULT_CONTINUE_PROMPT : "", model: request.model, effort: request.effort, sandbox: request.write ? "workspace-write" : "read-only", onProgress: request.onProgress, persistThread: true, threadName: resumeThreadId ? null : buildPersistentTaskThreadName(request.prompt || DEFAULT_CONTINUE_PROMPT) }); const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : ""; const failureMessage = result.error?.message ?? result.stderr ?? ""; const rendered = renderTaskResult( { rawOutput, failureMessage, reasoningSummary: result.reasoningSummary }, { title: taskMetadata.title, jobId: request.jobId ?? null, write: Boolean(request.write) } ); const payload = { status: result.status, threadId: result.threadId, rawOutput, touchedFiles: result.touchedFiles, reasoningSummary: result.reasoningSummary }; return { exitStatus: result.status, threadId: result.threadId, turnId: result.turnId, payload, rendered, summary: firstMeaningfulLine(rawOutput, firstMeaningfulLine(failureMessage, `${taskMetadata.title} finished.`)), jobTitle: taskMetadata.title, jobClass: "task", write: Boolean(request.write) }; } function buildReviewJobMetadata(reviewName, target) { return { kind: reviewName === "Adversarial Review" ? "adversarial-review" : "review", title: reviewName === "Review" ? "Codex Review" : `Codex ${reviewName}`, summary: `${reviewName} ${target.label}` }; } function buildTaskRunMetadata({ prompt, resumeLast = false }) { if (!resumeLast && String(prompt ?? "").includes(STOP_REVIEW_TASK_MARKER)) { return { title: "Codex Stop Gate Review", summary: "Stop-gate review of previous Claude turn" }; } const title = resumeLast ? "Codex Resume" : "Codex Task"; const fallbackSummary = resumeLast ? DEFAULT_CONTINUE_PROMPT : "Task"; return { title, summary: shorten(prompt || fallbackSummary) }; } function renderQueuedTaskLaunch(payload) { return `${payload.title} started in the background as ${payload.jobId}. Check /codex:status ${payload.jobId} for progress.\n`; } function getJobKindLabel(kind, jobClass) { if (kind === "adversarial-review") { return "adversarial-review"; } return jobClass === "review" ? "review" : "rescue"; } function createCompanionJob({ prefix, kind, title, workspaceRoot, jobClass, summary, write = false }) { return createJobRecord({ id: generateJobId(prefix), kind, kindLabel: getJobKindLabel(kind, jobClass), title, workspaceRoot, jobClass, summary, write }); } function createTrackedProgress(job, options = {}) { const logFile = options.logFile ?? createJobLogFile(job.workspaceRoot, job.id, job.title); return { logFile, progress: createProgressReporter({ stderr: Boolean(options.stderr), logFile, onEvent: createJobProgressUpdater(job.workspaceRoot, job.id) }) }; } function buildTaskJob(workspaceRoot, taskMetadata, write) { return createCompanionJob({ prefix: "task", kind: "task", title: taskMetadata.title, workspaceRoot, jobClass: "task", summary: taskMetadata.summary, write }); } function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, jobId }) { return { cwd, model, effort, prompt, write, resumeLast, jobId }; } function readTaskPrompt(cwd, options, positionals) { if (options["prompt-file"]) { return fs.readFileSync(path.resolve(cwd, options["prompt-file"]), "utf8"); } const positionalPrompt = positionals.join(" "); return positionalPrompt || readStdinIfPiped(); } function requireTaskRequest(prompt, resumeLast) { if (!prompt && !resumeLast) { throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last."); } } async function runForegroundCommand(job, runner, options = {}) { const { logFile, progress } = createTrackedProgress(job, { logFile: options.logFile, stderr: !options.json }); const execution = await runTrackedJob(job, () => runner(progress), { logFile }); outputResult(options.json ? execution.payload : execution.rendered, options.json); if (execution.exitStatus !== 0) { process.exitCode = execution.exitStatus; } return execution; } function spawnDetachedTaskWorker(cwd, jobId) { const scriptPath = path.join(ROOT_DIR, "scripts", "codex-companion.mjs"); const child = spawn(process.execPath, [scriptPath, "task-worker", "--cwd", cwd, "--job-id", jobId], { cwd, env: process.env, detached: true, stdio: "ignore", windowsHide: true }); child.unref(); return child; } function enqueueBackgroundTask(cwd, job, request) { const { logFile } = createTrackedProgress(job); appendLogLine(logFile, "Queued for background execution."); const child = spawnDetachedTaskWorker(cwd, job.id); const queuedRecord = { ...job, status: "queued", phase: "queued", pid: child.pid ?? null, logFile, request }; writeJobFile(job.workspaceRoot, job.id, queuedRecord); upsertJob(job.workspaceRoot, queuedRecord); return { payload: { jobId: job.id, status: "queued", title: job.title, summary: job.summary, logFile }, logFile }; } async function handleReviewCommand(argv, config) { const { options, positionals } = parseCommandInput(argv, { valueOptions: ["base", "scope", "model", "cwd"], booleanOptions: ["json", "background", "wait"], aliasMap: { m: "model" } }); const cwd = resolveCommandCwd(options); const workspaceRoot = resolveCommandWorkspace(options); const focusText = positionals.join(" ").trim(); const target = resolveReviewTarget(cwd, { base: options.base, scope: options.scope }); config.validateRequest?.(target, focusText); const metadata = buildReviewJobMetadata(config.reviewName, target); const job = createCompanionJob({ prefix: "review", kind: metadata.kind, title: metadata.title, workspaceRoot, jobClass: "review", summary: metadata.summary }); await runForegroundCommand( job, (progress) => executeReviewRun({ cwd, base: options.base, scope: options.scope, model: options.model, focusText, reviewName: config.reviewName, onProgress: progress }), { json: options.json } ); } async function handleReview(argv) { return handleReviewCommand(argv, { reviewName: "Review", validateRequest: validateNativeReviewRequest }); } async function handleTask(argv) { const { options, positionals } = parseCommandInput(argv, { valueOptions: ["model", "effort", "cwd", "prompt-file"], booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"], aliasMap: { m: "model" } }); const cwd = resolveCommandCwd(options); const workspaceRoot = resolveCommandWorkspace(options); const model = normalizeRequestedModel(options.model); const effort = normalizeReasoningEffort(options.effort); const prompt = readTaskPrompt(cwd, options, positionals); const resumeLast = Boolean(options["resume-last"] || options.resume); const fresh = Boolean(options.fresh); if (resumeLast && fresh) { throw new Error("Choose either --resume/--resume-last or --fresh."); } const write = Boolean(options.write); const taskMetadata = buildTaskRunMetadata({ prompt, resumeLast }); if (options.background) { ensureCodexAvailable(cwd); requireTaskRequest(prompt, resumeLast); const job = buildTaskJob(workspaceRoot, taskMetadata, write); const request = buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, jobId: job.id }); const { payload } = enqueueBackgroundTask(cwd, job, request); outputCommandResult(payload, renderQueuedTaskLaunch(payload), options.json); return; } const job = buildTaskJob(workspaceRoot, taskMetadata, write); await runForegroundCommand( job, (progress) => executeTaskRun({ cwd, model, effort, prompt, write, resumeLast, jobId: job.id, onProgress: progress }), { json: options.json } ); } async function handleTaskWorker(argv) { const { options } = parseCommandInput(argv, { valueOptions: ["cwd", "job-id"] }); if (!options["job-id"]) { 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) { throw new Error(`No stored job found for ${options["job-id"]}.`); } const request = storedJob.request; if (!request || typeof request !== "object") { throw new Error(`Stored job ${options["job-id"]} is missing its task request payload.`); } const { logFile, progress } = createTrackedProgress( { ...storedJob, workspaceRoot }, { logFile: storedJob.logFile ?? null } ); await runTrackedJob( { ...storedJob, workspaceRoot, logFile }, () => executeTaskRun({ ...request, onProgress: progress }), { logFile } ); } async function handleStatus(argv) { const { options, positionals } = parseCommandInput(argv, { valueOptions: ["cwd", "timeout-ms", "poll-interval-ms"], booleanOptions: ["json", "all", "wait"] }); const cwd = resolveCommandCwd(options); const reference = positionals[0] ?? ""; if (reference) { const snapshot = options.wait ? await waitForSingleJobSnapshot(cwd, reference, { timeoutMs: options["timeout-ms"], pollIntervalMs: options["poll-interval-ms"] }) : buildSingleJobSnapshot(cwd, reference); outputCommandResult(snapshot, renderJobStatusReport(snapshot.job), options.json); return; } if (options.wait) { throw new Error("`status --wait` requires a job id."); } const report = buildStatusSnapshot(cwd, { all: options.all }); outputResult(renderStatusPayload(report, options.json), options.json); } function handleResult(argv) { const { options, positionals } = parseCommandInput(argv, { valueOptions: ["cwd"], booleanOptions: ["json"] }); const cwd = resolveCommandCwd(options); const reference = positionals[0] ?? ""; const { workspaceRoot, job } = resolveResultJob(cwd, reference); const storedJob = readStoredJob(workspaceRoot, job.id); const payload = { job, storedJob }; outputCommandResult(payload, renderStoredJobResult(job, storedJob), options.json); } function handleTaskResumeCandidate(argv) { const { options } = parseCommandInput(argv, { valueOptions: ["cwd"], booleanOptions: ["json"] }); const cwd = resolveCommandCwd(options); const workspaceRoot = resolveCommandWorkspace(options); const sessionId = getCurrentClaudeSessionId(); const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(listJobs(workspaceRoot))); const candidate = findLatestResumableTaskJob(jobs); const payload = { available: Boolean(candidate), sessionId, candidate: candidate == null ? null : { id: candidate.id, status: candidate.status, title: candidate.title ?? null, summary: candidate.summary ?? null, threadId: candidate.threadId, completedAt: candidate.completedAt ?? null, updatedAt: candidate.updatedAt ?? null } }; const rendered = candidate ? `Resumable task found: ${candidate.id} (${candidate.status}).\n` : "No resumable task found for this session.\n"; outputCommandResult(payload, rendered, options.json); } async function handleCancel(argv) { const { options, positionals } = parseCommandInput(argv, { valueOptions: ["cwd"], booleanOptions: ["json"] }); const cwd = resolveCommandCwd(options); const reference = positionals[0] ?? ""; 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; const interrupt = await interruptAppServerTurn(cwd, { threadId, turnId }); if (interrupt.attempted) { appendLogLine( job.logFile, interrupt.interrupted ? `Requested Codex turn interrupt for ${turnId} on ${threadId}.` : `Codex turn interrupt failed${interrupt.detail ? `: ${interrupt.detail}` : "."}` ); } terminateProcessTree(job.pid ?? Number.NaN); appendLogLine(job.logFile, "Cancelled by user."); const completedAt = nowIso(); const nextJob = { ...job, status: "cancelled", phase: "cancelled", pid: null, completedAt, errorMessage: "Cancelled by user." }; writeJobFile(workspaceRoot, job.id, { ...existing, ...nextJob, cancelledAt: completedAt }); upsertJob(workspaceRoot, { id: job.id, status: "cancelled", phase: "cancelled", pid: null, errorMessage: "Cancelled by user.", completedAt }); const payload = { jobId: job.id, status: "cancelled", title: job.title, turnInterruptAttempted: interrupt.attempted, turnInterrupted: interrupt.interrupted }; outputCommandResult(payload, renderCancelReport(nextJob), options.json); } async function main() { const [subcommand, ...argv] = process.argv.slice(2); if (!subcommand || subcommand === "help" || subcommand === "--help") { printUsage(); return; } switch (subcommand) { case "setup": await handleSetup(argv); break; case "review": await handleReview(argv); break; case "adversarial-review": await handleReviewCommand(argv, { reviewName: "Adversarial Review" }); break; case "task": await handleTask(argv); break; case "task-worker": await handleTaskWorker(argv); break; case "status": await handleStatus(argv); break; case "result": handleResult(argv); break; case "task-resume-candidate": handleTaskResumeCandidate(argv); break; case "cancel": await handleCancel(argv); break; default: throw new Error(`Unknown subcommand: ${subcommand}`); } } main().catch((error) => { const message = error instanceof Error ? error.message : String(error); process.stderr.write(`${message}\n`); process.exitCode = 1; });