@zhang6hk/dsh-codex 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +18 -0
  3. package/README.md +75 -2
  4. package/README.zh.md +78 -0
  5. package/cordis.patch.yml +7 -0
  6. package/lib/args.js +118 -0
  7. package/lib/codex-path.js +31 -0
  8. package/lib/commands.js +242 -0
  9. package/lib/companion.js +177 -0
  10. package/lib/index.js +105 -0
  11. package/lib/render.js +114 -0
  12. package/lib/tool.js +103 -0
  13. package/locale/en.json +6 -0
  14. package/locale/zh.json +6 -0
  15. package/package.json +49 -4
  16. package/vendor/codex/.claude-plugin/plugin.json +8 -0
  17. package/vendor/codex/prompts/adversarial-review.md +84 -0
  18. package/vendor/codex/prompts/stop-review-gate.md +36 -0
  19. package/vendor/codex/schemas/review-output.schema.json +87 -0
  20. package/vendor/codex/scripts/app-server-broker.mjs +252 -0
  21. package/vendor/codex/scripts/codex-companion.mjs +1073 -0
  22. package/vendor/codex/scripts/lib/app-server-protocol.d.ts +75 -0
  23. package/vendor/codex/scripts/lib/app-server.mjs +354 -0
  24. package/vendor/codex/scripts/lib/args.mjs +128 -0
  25. package/vendor/codex/scripts/lib/broker-endpoint.mjs +41 -0
  26. package/vendor/codex/scripts/lib/broker-lifecycle.mjs +209 -0
  27. package/vendor/codex/scripts/lib/claude-session-transfer.mjs +44 -0
  28. package/vendor/codex/scripts/lib/codex.mjs +1219 -0
  29. package/vendor/codex/scripts/lib/fs.mjs +40 -0
  30. package/vendor/codex/scripts/lib/git.mjs +347 -0
  31. package/vendor/codex/scripts/lib/job-control.mjs +308 -0
  32. package/vendor/codex/scripts/lib/process.mjs +135 -0
  33. package/vendor/codex/scripts/lib/prompts.mjs +13 -0
  34. package/vendor/codex/scripts/lib/render.mjs +465 -0
  35. package/vendor/codex/scripts/lib/state.mjs +191 -0
  36. package/vendor/codex/scripts/lib/tracked-jobs.mjs +204 -0
  37. package/vendor/codex/scripts/lib/workspace.mjs +9 -0
@@ -0,0 +1,1073 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import { parseArgs, splitRawArgumentString } from "./lib/args.mjs";
10
+ import {
11
+ buildPersistentTaskThreadName,
12
+ DEFAULT_CONTINUE_PROMPT,
13
+ findLatestTaskThread,
14
+ getCodexAuthStatus,
15
+ getCodexAvailability,
16
+ getSessionRuntimeStatus,
17
+ importExternalAgentSession,
18
+ interruptAppServerTurn,
19
+ parseStructuredOutput,
20
+ readOutputSchema,
21
+ runAppServerReview,
22
+ runAppServerTurn
23
+ } from "./lib/codex.mjs";
24
+ import { resolveClaudeSessionPath } from "./lib/claude-session-transfer.mjs";
25
+ import { readStdinIfPiped } from "./lib/fs.mjs";
26
+ import { collectReviewContext, ensureGitRepository, resolveReviewTarget } from "./lib/git.mjs";
27
+ import { binaryAvailable, terminateProcessTree } from "./lib/process.mjs";
28
+ import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs";
29
+ import {
30
+ generateJobId,
31
+ getConfig,
32
+ listJobs,
33
+ setConfig,
34
+ upsertJob,
35
+ writeJobFile
36
+ } from "./lib/state.mjs";
37
+ import {
38
+ buildSingleJobSnapshot,
39
+ buildStatusSnapshot,
40
+ readStoredJob,
41
+ resolveCancelableJob,
42
+ resolveResultJob,
43
+ sortJobsNewestFirst
44
+ } from "./lib/job-control.mjs";
45
+ import {
46
+ appendLogLine,
47
+ createJobLogFile,
48
+ createJobProgressUpdater,
49
+ createJobRecord,
50
+ createProgressReporter,
51
+ nowIso,
52
+ runTrackedJob,
53
+ SESSION_ID_ENV
54
+ } from "./lib/tracked-jobs.mjs";
55
+ import { resolveWorkspaceRoot } from "./lib/workspace.mjs";
56
+ import {
57
+ renderNativeReviewResult,
58
+ renderReviewResult,
59
+ renderStoredJobResult,
60
+ renderCancelReport,
61
+ renderJobStatusReport,
62
+ renderSetupReport,
63
+ renderStatusReport,
64
+ renderTaskResult
65
+ } from "./lib/render.mjs";
66
+
67
+ const ROOT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
68
+ const REVIEW_SCHEMA = path.join(ROOT_DIR, "schemas", "review-output.schema.json");
69
+ const DEFAULT_STATUS_WAIT_TIMEOUT_MS = 240000;
70
+ const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
71
+ const VALID_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
72
+ const MODEL_ALIASES = new Map([["spark", "gpt-5.3-codex-spark"]]);
73
+ const STOP_REVIEW_TASK_MARKER = "Run a stop-gate review of the previous Claude turn.";
74
+
75
+ function printUsage() {
76
+ console.log(
77
+ [
78
+ "Usage:",
79
+ " node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]",
80
+ " node scripts/codex-companion.mjs review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>]",
81
+ " node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [focus text]",
82
+ " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
83
+ " node scripts/codex-companion.mjs transfer [--source <claude-jsonl>] [--json]",
84
+ " node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
85
+ " node scripts/codex-companion.mjs result [job-id] [--json]",
86
+ " node scripts/codex-companion.mjs cancel [job-id] [--json]"
87
+ ].join("\n")
88
+ );
89
+ }
90
+
91
+ function outputResult(value, asJson) {
92
+ if (asJson) {
93
+ console.log(JSON.stringify(value, null, 2));
94
+ } else {
95
+ process.stdout.write(value);
96
+ }
97
+ }
98
+
99
+ function outputCommandResult(payload, rendered, asJson) {
100
+ outputResult(asJson ? payload : rendered, asJson);
101
+ }
102
+
103
+ function normalizeRequestedModel(model) {
104
+ if (model == null) {
105
+ return null;
106
+ }
107
+ const normalized = String(model).trim();
108
+ if (!normalized) {
109
+ return null;
110
+ }
111
+ return MODEL_ALIASES.get(normalized.toLowerCase()) ?? normalized;
112
+ }
113
+
114
+ function normalizeReasoningEffort(effort) {
115
+ if (effort == null) {
116
+ return null;
117
+ }
118
+ const normalized = String(effort).trim().toLowerCase();
119
+ if (!normalized) {
120
+ return null;
121
+ }
122
+ if (!VALID_REASONING_EFFORTS.has(normalized)) {
123
+ throw new Error(
124
+ `Unsupported reasoning effort "${effort}". Use one of: none, minimal, low, medium, high, xhigh.`
125
+ );
126
+ }
127
+ return normalized;
128
+ }
129
+
130
+ function normalizeArgv(argv) {
131
+ if (argv.length === 1) {
132
+ const [raw] = argv;
133
+ if (!raw || !raw.trim()) {
134
+ return [];
135
+ }
136
+ return splitRawArgumentString(raw);
137
+ }
138
+ return argv;
139
+ }
140
+
141
+ function parseCommandInput(argv, config = {}) {
142
+ return parseArgs(normalizeArgv(argv), {
143
+ ...config,
144
+ aliasMap: {
145
+ C: "cwd",
146
+ ...(config.aliasMap ?? {})
147
+ }
148
+ });
149
+ }
150
+
151
+ function resolveCommandCwd(options = {}) {
152
+ return options.cwd ? path.resolve(process.cwd(), options.cwd) : process.cwd();
153
+ }
154
+
155
+ function resolveCommandWorkspace(options = {}) {
156
+ return resolveWorkspaceRoot(resolveCommandCwd(options));
157
+ }
158
+
159
+ function sleep(ms) {
160
+ return new Promise((resolve) => setTimeout(resolve, ms));
161
+ }
162
+
163
+ function shorten(text, limit = 96) {
164
+ const normalized = String(text ?? "").trim().replace(/\s+/g, " ");
165
+ if (!normalized) {
166
+ return "";
167
+ }
168
+ if (normalized.length <= limit) {
169
+ return normalized;
170
+ }
171
+ return `${normalized.slice(0, limit - 3)}...`;
172
+ }
173
+
174
+ function firstMeaningfulLine(text, fallback) {
175
+ const line = String(text ?? "")
176
+ .split(/\r?\n/)
177
+ .map((value) => value.trim())
178
+ .find(Boolean);
179
+ return line ?? fallback;
180
+ }
181
+
182
+ async function buildSetupReport(cwd, actionsTaken = []) {
183
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
184
+ const nodeStatus = binaryAvailable("node", ["--version"], { cwd });
185
+ const npmStatus = binaryAvailable("npm", ["--version"], { cwd });
186
+ const codexStatus = getCodexAvailability(cwd);
187
+ const authStatus = await getCodexAuthStatus(cwd);
188
+ const config = getConfig(workspaceRoot);
189
+
190
+ const nextSteps = [];
191
+ if (!codexStatus.available) {
192
+ nextSteps.push("Install Codex with `npm install -g @openai/codex`.");
193
+ }
194
+ if (codexStatus.available && !authStatus.loggedIn && authStatus.requiresOpenaiAuth) {
195
+ nextSteps.push("Run `!codex login`.");
196
+ nextSteps.push("If browser login is blocked, retry with `!codex login --device-auth` or `!codex login --with-api-key`.");
197
+ }
198
+ if (!config.stopReviewGate) {
199
+ nextSteps.push("Optional: run `/codex:setup --enable-review-gate` to require a fresh review before stop.");
200
+ }
201
+
202
+ return {
203
+ ready: nodeStatus.available && codexStatus.available && authStatus.loggedIn,
204
+ node: nodeStatus,
205
+ npm: npmStatus,
206
+ codex: codexStatus,
207
+ auth: authStatus,
208
+ sessionRuntime: getSessionRuntimeStatus(process.env, workspaceRoot),
209
+ reviewGateEnabled: Boolean(config.stopReviewGate),
210
+ actionsTaken,
211
+ nextSteps
212
+ };
213
+ }
214
+
215
+ async function handleSetup(argv) {
216
+ const { options } = parseCommandInput(argv, {
217
+ valueOptions: ["cwd"],
218
+ booleanOptions: ["json", "enable-review-gate", "disable-review-gate"]
219
+ });
220
+
221
+ if (options["enable-review-gate"] && options["disable-review-gate"]) {
222
+ throw new Error("Choose either --enable-review-gate or --disable-review-gate.");
223
+ }
224
+
225
+ const cwd = resolveCommandCwd(options);
226
+ const workspaceRoot = resolveCommandWorkspace(options);
227
+ const actionsTaken = [];
228
+
229
+ if (options["enable-review-gate"]) {
230
+ setConfig(workspaceRoot, "stopReviewGate", true);
231
+ actionsTaken.push(`Enabled the stop-time review gate for ${workspaceRoot}.`);
232
+ } else if (options["disable-review-gate"]) {
233
+ setConfig(workspaceRoot, "stopReviewGate", false);
234
+ actionsTaken.push(`Disabled the stop-time review gate for ${workspaceRoot}.`);
235
+ }
236
+
237
+ const finalReport = await buildSetupReport(cwd, actionsTaken);
238
+ outputResult(options.json ? finalReport : renderSetupReport(finalReport), options.json);
239
+ }
240
+
241
+ function buildAdversarialReviewPrompt(context, focusText) {
242
+ const template = loadPromptTemplate(ROOT_DIR, "adversarial-review");
243
+ return interpolateTemplate(template, {
244
+ REVIEW_KIND: "Adversarial Review",
245
+ TARGET_LABEL: context.target.label,
246
+ USER_FOCUS: focusText || "No extra focus provided.",
247
+ REVIEW_COLLECTION_GUIDANCE: context.collectionGuidance,
248
+ REVIEW_INPUT: context.content
249
+ });
250
+ }
251
+
252
+ function ensureCodexAvailable(cwd) {
253
+ const availability = getCodexAvailability(cwd);
254
+ if (!availability.available) {
255
+ 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`.");
256
+ }
257
+ }
258
+
259
+ function buildNativeReviewTarget(target) {
260
+ if (target.mode === "working-tree") {
261
+ return { type: "uncommittedChanges" };
262
+ }
263
+
264
+ if (target.mode === "branch") {
265
+ return { type: "baseBranch", branch: target.baseRef };
266
+ }
267
+
268
+ return null;
269
+ }
270
+
271
+ function validateNativeReviewRequest(target, focusText) {
272
+ if (focusText.trim()) {
273
+ throw new Error(
274
+ `\`/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.`
275
+ );
276
+ }
277
+
278
+ const nativeTarget = buildNativeReviewTarget(target);
279
+ if (!nativeTarget) {
280
+ throw new Error("This `/codex:review` target is not supported by the built-in reviewer. Retry with `/codex:adversarial-review` for custom targeting.");
281
+ }
282
+
283
+ return nativeTarget;
284
+ }
285
+
286
+ function renderStatusPayload(report, asJson) {
287
+ return asJson ? report : renderStatusReport(report);
288
+ }
289
+
290
+ function isActiveJobStatus(status) {
291
+ return status === "queued" || status === "running";
292
+ }
293
+
294
+ function getCurrentClaudeSessionId() {
295
+ return process.env[SESSION_ID_ENV] ?? null;
296
+ }
297
+
298
+ function filterJobsForCurrentClaudeSession(jobs) {
299
+ const sessionId = getCurrentClaudeSessionId();
300
+ if (!sessionId) {
301
+ return jobs;
302
+ }
303
+ return jobs.filter((job) => job.sessionId === sessionId);
304
+ }
305
+
306
+ function findLatestResumableTaskJob(jobs) {
307
+ return (
308
+ jobs.find(
309
+ (job) =>
310
+ job.jobClass === "task" &&
311
+ job.threadId &&
312
+ job.status !== "queued" &&
313
+ job.status !== "running"
314
+ ) ?? null
315
+ );
316
+ }
317
+
318
+ async function waitForSingleJobSnapshot(cwd, reference, options = {}) {
319
+ const timeoutMs = Math.max(0, Number(options.timeoutMs) || DEFAULT_STATUS_WAIT_TIMEOUT_MS);
320
+ const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || DEFAULT_STATUS_POLL_INTERVAL_MS);
321
+ const deadline = Date.now() + timeoutMs;
322
+ let snapshot = buildSingleJobSnapshot(cwd, reference);
323
+
324
+ while (isActiveJobStatus(snapshot.job.status) && Date.now() < deadline) {
325
+ await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
326
+ snapshot = buildSingleJobSnapshot(cwd, reference);
327
+ }
328
+
329
+ return {
330
+ ...snapshot,
331
+ waitTimedOut: isActiveJobStatus(snapshot.job.status),
332
+ timeoutMs
333
+ };
334
+ }
335
+
336
+ async function resolveLatestTrackedTaskThread(cwd, options = {}) {
337
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
338
+ const sessionId = getCurrentClaudeSessionId();
339
+ const jobs = sortJobsNewestFirst(listJobs(workspaceRoot)).filter((job) => job.id !== options.excludeJobId);
340
+ const visibleJobs = filterJobsForCurrentClaudeSession(jobs);
341
+ const activeTask = visibleJobs.find((job) => job.jobClass === "task" && (job.status === "queued" || job.status === "running"));
342
+ if (activeTask) {
343
+ throw new Error(`Task ${activeTask.id} is still running. Use /codex:status before continuing it.`);
344
+ }
345
+
346
+ const trackedTask = findLatestResumableTaskJob(visibleJobs);
347
+ if (trackedTask) {
348
+ return { id: trackedTask.threadId };
349
+ }
350
+
351
+ if (sessionId) {
352
+ return null;
353
+ }
354
+
355
+ return findLatestTaskThread(workspaceRoot);
356
+ }
357
+
358
+ async function executeReviewRun(request) {
359
+ ensureCodexAvailable(request.cwd);
360
+ ensureGitRepository(request.cwd);
361
+
362
+ const target = resolveReviewTarget(request.cwd, {
363
+ base: request.base,
364
+ scope: request.scope
365
+ });
366
+ const focusText = request.focusText?.trim() ?? "";
367
+ const reviewName = request.reviewName ?? "Review";
368
+ if (reviewName === "Review") {
369
+ const reviewTarget = validateNativeReviewRequest(target, focusText);
370
+ const result = await runAppServerReview(request.cwd, {
371
+ target: reviewTarget,
372
+ model: request.model,
373
+ onProgress: request.onProgress
374
+ });
375
+ const payload = {
376
+ review: reviewName,
377
+ target,
378
+ threadId: result.threadId,
379
+ sourceThreadId: result.sourceThreadId,
380
+ codex: {
381
+ status: result.status,
382
+ stderr: result.stderr,
383
+ stdout: result.reviewText,
384
+ reasoning: result.reasoningSummary
385
+ }
386
+ };
387
+ const rendered = renderNativeReviewResult(
388
+ {
389
+ status: result.status,
390
+ stdout: result.reviewText,
391
+ stderr: result.stderr
392
+ },
393
+ { reviewLabel: reviewName, targetLabel: target.label, reasoningSummary: result.reasoningSummary }
394
+ );
395
+
396
+ return {
397
+ exitStatus: result.status,
398
+ threadId: result.threadId,
399
+ turnId: result.turnId,
400
+ payload,
401
+ rendered,
402
+ summary: firstMeaningfulLine(result.reviewText, `${reviewName} completed.`),
403
+ jobTitle: `Codex ${reviewName}`,
404
+ jobClass: "review",
405
+ targetLabel: target.label
406
+ };
407
+ }
408
+
409
+ const context = collectReviewContext(request.cwd, target);
410
+ const prompt = buildAdversarialReviewPrompt(context, focusText);
411
+ const result = await runAppServerTurn(context.repoRoot, {
412
+ prompt,
413
+ model: request.model,
414
+ sandbox: "read-only",
415
+ outputSchema: readOutputSchema(REVIEW_SCHEMA),
416
+ onProgress: request.onProgress
417
+ });
418
+ const parsed = parseStructuredOutput(result.finalMessage, {
419
+ status: result.status,
420
+ failureMessage: result.error?.message ?? result.stderr
421
+ });
422
+ const payload = {
423
+ review: reviewName,
424
+ target,
425
+ threadId: result.threadId,
426
+ context: {
427
+ repoRoot: context.repoRoot,
428
+ branch: context.branch,
429
+ summary: context.summary
430
+ },
431
+ codex: {
432
+ status: result.status,
433
+ stderr: result.stderr,
434
+ stdout: result.finalMessage,
435
+ reasoning: result.reasoningSummary
436
+ },
437
+ result: parsed.parsed,
438
+ rawOutput: parsed.rawOutput,
439
+ parseError: parsed.parseError,
440
+ reasoningSummary: result.reasoningSummary
441
+ };
442
+
443
+ return {
444
+ exitStatus: result.status,
445
+ threadId: result.threadId,
446
+ turnId: result.turnId,
447
+ payload,
448
+ rendered: renderReviewResult(parsed, {
449
+ reviewLabel: reviewName,
450
+ targetLabel: context.target.label,
451
+ reasoningSummary: result.reasoningSummary
452
+ }),
453
+ summary: parsed.parsed?.summary ?? parsed.parseError ?? firstMeaningfulLine(result.finalMessage, `${reviewName} finished.`),
454
+ jobTitle: `Codex ${reviewName}`,
455
+ jobClass: "review",
456
+ targetLabel: context.target.label
457
+ };
458
+ }
459
+
460
+
461
+ async function executeTaskRun(request) {
462
+ const workspaceRoot = resolveWorkspaceRoot(request.cwd);
463
+ ensureCodexAvailable(request.cwd);
464
+
465
+ const taskMetadata = buildTaskRunMetadata({
466
+ prompt: request.prompt,
467
+ resumeLast: request.resumeLast
468
+ });
469
+
470
+ let resumeThreadId = null;
471
+ if (request.resumeLast) {
472
+ const latestThread = await resolveLatestTrackedTaskThread(workspaceRoot, {
473
+ excludeJobId: request.jobId
474
+ });
475
+ if (!latestThread) {
476
+ throw new Error("No previous Codex task thread was found for this repository.");
477
+ }
478
+ resumeThreadId = latestThread.id;
479
+ }
480
+
481
+ if (!request.prompt && !resumeThreadId) {
482
+ throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last.");
483
+ }
484
+
485
+ const result = await runAppServerTurn(workspaceRoot, {
486
+ resumeThreadId,
487
+ prompt: request.prompt,
488
+ defaultPrompt: resumeThreadId ? DEFAULT_CONTINUE_PROMPT : "",
489
+ model: request.model,
490
+ effort: request.effort,
491
+ sandbox: request.write ? "workspace-write" : "read-only",
492
+ onProgress: request.onProgress,
493
+ persistThread: true,
494
+ threadName: resumeThreadId ? null : buildPersistentTaskThreadName(request.prompt || DEFAULT_CONTINUE_PROMPT)
495
+ });
496
+
497
+ const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : "";
498
+ const failureMessage = result.error?.message ?? result.stderr ?? "";
499
+ const rendered = renderTaskResult(
500
+ {
501
+ rawOutput,
502
+ failureMessage,
503
+ reasoningSummary: result.reasoningSummary
504
+ },
505
+ {
506
+ title: taskMetadata.title,
507
+ jobId: request.jobId ?? null,
508
+ write: Boolean(request.write)
509
+ }
510
+ );
511
+ const payload = {
512
+ status: result.status,
513
+ threadId: result.threadId,
514
+ rawOutput,
515
+ touchedFiles: result.touchedFiles,
516
+ reasoningSummary: result.reasoningSummary
517
+ };
518
+
519
+ return {
520
+ exitStatus: result.status,
521
+ threadId: result.threadId,
522
+ turnId: result.turnId,
523
+ payload,
524
+ rendered,
525
+ summary: firstMeaningfulLine(rawOutput, firstMeaningfulLine(failureMessage, `${taskMetadata.title} finished.`)),
526
+ jobTitle: taskMetadata.title,
527
+ jobClass: "task",
528
+ write: Boolean(request.write)
529
+ };
530
+ }
531
+
532
+ function buildReviewJobMetadata(reviewName, target) {
533
+ return {
534
+ kind: reviewName === "Adversarial Review" ? "adversarial-review" : "review",
535
+ title: reviewName === "Review" ? "Codex Review" : `Codex ${reviewName}`,
536
+ summary: `${reviewName} ${target.label}`
537
+ };
538
+ }
539
+
540
+ function buildTaskRunMetadata({ prompt, resumeLast = false }) {
541
+ if (!resumeLast && String(prompt ?? "").includes(STOP_REVIEW_TASK_MARKER)) {
542
+ return {
543
+ title: "Codex Stop Gate Review",
544
+ summary: "Stop-gate review of previous Claude turn"
545
+ };
546
+ }
547
+
548
+ const title = resumeLast ? "Codex Resume" : "Codex Task";
549
+ const fallbackSummary = resumeLast ? DEFAULT_CONTINUE_PROMPT : "Task";
550
+ return {
551
+ title,
552
+ summary: shorten(prompt || fallbackSummary)
553
+ };
554
+ }
555
+
556
+ function renderQueuedTaskLaunch(payload) {
557
+ return `${payload.title} started in the background as ${payload.jobId}. Check /codex:status ${payload.jobId} for progress.\n`;
558
+ }
559
+
560
+ function getJobKindLabel(kind, jobClass) {
561
+ if (kind === "adversarial-review") {
562
+ return "adversarial-review";
563
+ }
564
+ return jobClass === "review" ? "review" : "rescue";
565
+ }
566
+
567
+ function createCompanionJob({ prefix, kind, title, workspaceRoot, jobClass, summary, write = false }) {
568
+ return createJobRecord({
569
+ id: generateJobId(prefix),
570
+ kind,
571
+ kindLabel: getJobKindLabel(kind, jobClass),
572
+ title,
573
+ workspaceRoot,
574
+ jobClass,
575
+ summary,
576
+ write
577
+ });
578
+ }
579
+
580
+ function createTrackedProgress(job, options = {}) {
581
+ const logFile = options.logFile ?? createJobLogFile(job.workspaceRoot, job.id, job.title);
582
+ return {
583
+ logFile,
584
+ progress: createProgressReporter({
585
+ stderr: Boolean(options.stderr),
586
+ logFile,
587
+ onEvent: createJobProgressUpdater(job.workspaceRoot, job.id)
588
+ })
589
+ };
590
+ }
591
+
592
+ function buildTaskJob(workspaceRoot, taskMetadata, write) {
593
+ return createCompanionJob({
594
+ prefix: "task",
595
+ kind: "task",
596
+ title: taskMetadata.title,
597
+ workspaceRoot,
598
+ jobClass: "task",
599
+ summary: taskMetadata.summary,
600
+ write
601
+ });
602
+ }
603
+
604
+ function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, jobId }) {
605
+ return {
606
+ cwd,
607
+ model,
608
+ effort,
609
+ prompt,
610
+ write,
611
+ resumeLast,
612
+ jobId
613
+ };
614
+ }
615
+
616
+ function renderTransferResult(payload) {
617
+ const lines = [
618
+ "Transferred the Claude session into a Codex thread with visible turn history.",
619
+ `Codex session ID: ${payload.threadId}`,
620
+ `Resume in Codex: ${payload.resumeCommand}`
621
+ ];
622
+ return `${lines.join("\n")}\n`;
623
+ }
624
+
625
+ async function executeTransfer(cwd, options = {}) {
626
+ const sourcePath = resolveClaudeSessionPath(cwd, {
627
+ source: options.source
628
+ });
629
+ const result = await importExternalAgentSession(cwd, { sourcePath });
630
+ const payload = {
631
+ threadId: result.threadId,
632
+ resumeCommand: `codex resume ${result.threadId}`,
633
+ sourcePath,
634
+ sessionId: path.basename(sourcePath, ".jsonl")
635
+ };
636
+
637
+ return {
638
+ payload,
639
+ rendered: renderTransferResult(payload)
640
+ };
641
+ }
642
+
643
+ function readTaskPrompt(cwd, options, positionals) {
644
+ if (options["prompt-file"]) {
645
+ return fs.readFileSync(path.resolve(cwd, options["prompt-file"]), "utf8");
646
+ }
647
+
648
+ const positionalPrompt = positionals.join(" ");
649
+ return positionalPrompt || readStdinIfPiped();
650
+ }
651
+
652
+ function requireTaskRequest(prompt, resumeLast) {
653
+ if (!prompt && !resumeLast) {
654
+ throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last.");
655
+ }
656
+ }
657
+
658
+ async function runForegroundCommand(job, runner, options = {}) {
659
+ const { logFile, progress } = createTrackedProgress(job, {
660
+ logFile: options.logFile,
661
+ stderr: !options.json
662
+ });
663
+ const execution = await runTrackedJob(job, () => runner(progress), { logFile });
664
+ outputResult(options.json ? execution.payload : execution.rendered, options.json);
665
+ if (execution.exitStatus !== 0) {
666
+ process.exitCode = execution.exitStatus;
667
+ }
668
+ return execution;
669
+ }
670
+
671
+ function spawnDetachedTaskWorker(cwd, jobId) {
672
+ const scriptPath = path.join(ROOT_DIR, "scripts", "codex-companion.mjs");
673
+ const child = spawn(process.execPath, [scriptPath, "task-worker", "--cwd", cwd, "--job-id", jobId], {
674
+ cwd,
675
+ env: process.env,
676
+ detached: true,
677
+ stdio: "ignore",
678
+ windowsHide: true
679
+ });
680
+ child.unref();
681
+ return child;
682
+ }
683
+
684
+ function enqueueBackgroundTask(cwd, job, request) {
685
+ const { logFile } = createTrackedProgress(job);
686
+ appendLogLine(logFile, "Queued for background execution.");
687
+
688
+ const child = spawnDetachedTaskWorker(cwd, job.id);
689
+ const queuedRecord = {
690
+ ...job,
691
+ status: "queued",
692
+ phase: "queued",
693
+ pid: child.pid ?? null,
694
+ logFile,
695
+ request
696
+ };
697
+ writeJobFile(job.workspaceRoot, job.id, queuedRecord);
698
+ upsertJob(job.workspaceRoot, queuedRecord);
699
+
700
+ return {
701
+ payload: {
702
+ jobId: job.id,
703
+ status: "queued",
704
+ title: job.title,
705
+ summary: job.summary,
706
+ logFile
707
+ },
708
+ logFile
709
+ };
710
+ }
711
+
712
+ async function handleReviewCommand(argv, config) {
713
+ const { options, positionals } = parseCommandInput(argv, {
714
+ valueOptions: ["base", "scope", "model", "cwd"],
715
+ booleanOptions: ["json", "background", "wait"],
716
+ aliasMap: {
717
+ m: "model"
718
+ }
719
+ });
720
+
721
+ const cwd = resolveCommandCwd(options);
722
+ const workspaceRoot = resolveCommandWorkspace(options);
723
+ const focusText = positionals.join(" ").trim();
724
+ const target = resolveReviewTarget(cwd, {
725
+ base: options.base,
726
+ scope: options.scope
727
+ });
728
+
729
+ config.validateRequest?.(target, focusText);
730
+ const metadata = buildReviewJobMetadata(config.reviewName, target);
731
+ const job = createCompanionJob({
732
+ prefix: "review",
733
+ kind: metadata.kind,
734
+ title: metadata.title,
735
+ workspaceRoot,
736
+ jobClass: "review",
737
+ summary: metadata.summary
738
+ });
739
+ await runForegroundCommand(
740
+ job,
741
+ (progress) =>
742
+ executeReviewRun({
743
+ cwd,
744
+ base: options.base,
745
+ scope: options.scope,
746
+ model: options.model,
747
+ focusText,
748
+ reviewName: config.reviewName,
749
+ onProgress: progress
750
+ }),
751
+ { json: options.json }
752
+ );
753
+ }
754
+
755
+ async function handleReview(argv) {
756
+ return handleReviewCommand(argv, {
757
+ reviewName: "Review",
758
+ validateRequest: validateNativeReviewRequest
759
+ });
760
+ }
761
+
762
+ async function handleTask(argv) {
763
+ const { options, positionals } = parseCommandInput(argv, {
764
+ valueOptions: ["model", "effort", "cwd", "prompt-file"],
765
+ booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"],
766
+ aliasMap: {
767
+ m: "model"
768
+ }
769
+ });
770
+
771
+ const cwd = resolveCommandCwd(options);
772
+ const workspaceRoot = resolveCommandWorkspace(options);
773
+ const model = normalizeRequestedModel(options.model);
774
+ const effort = normalizeReasoningEffort(options.effort);
775
+ const prompt = readTaskPrompt(cwd, options, positionals);
776
+
777
+ const resumeLast = Boolean(options["resume-last"] || options.resume);
778
+ const fresh = Boolean(options.fresh);
779
+ if (resumeLast && fresh) {
780
+ throw new Error("Choose either --resume/--resume-last or --fresh.");
781
+ }
782
+ const write = Boolean(options.write);
783
+ const taskMetadata = buildTaskRunMetadata({
784
+ prompt,
785
+ resumeLast
786
+ });
787
+
788
+ if (options.background) {
789
+ ensureCodexAvailable(cwd);
790
+ requireTaskRequest(prompt, resumeLast);
791
+
792
+ const job = buildTaskJob(workspaceRoot, taskMetadata, write);
793
+ const request = buildTaskRequest({
794
+ cwd,
795
+ model,
796
+ effort,
797
+ prompt,
798
+ write,
799
+ resumeLast,
800
+ jobId: job.id
801
+ });
802
+ const { payload } = enqueueBackgroundTask(cwd, job, request);
803
+ outputCommandResult(payload, renderQueuedTaskLaunch(payload), options.json);
804
+ return;
805
+ }
806
+
807
+ const job = buildTaskJob(workspaceRoot, taskMetadata, write);
808
+ await runForegroundCommand(
809
+ job,
810
+ (progress) =>
811
+ executeTaskRun({
812
+ cwd,
813
+ model,
814
+ effort,
815
+ prompt,
816
+ write,
817
+ resumeLast,
818
+ jobId: job.id,
819
+ onProgress: progress
820
+ }),
821
+ { json: options.json }
822
+ );
823
+ }
824
+
825
+ async function handleTransfer(argv) {
826
+ const { options } = parseCommandInput(argv, {
827
+ valueOptions: ["cwd", "source"],
828
+ booleanOptions: ["json"]
829
+ });
830
+
831
+ const cwd = resolveCommandCwd(options);
832
+ const { payload, rendered } = await executeTransfer(cwd, {
833
+ source: options.source
834
+ });
835
+ outputCommandResult(payload, rendered, options.json);
836
+ }
837
+
838
+ async function handleTaskWorker(argv) {
839
+ const { options } = parseCommandInput(argv, {
840
+ valueOptions: ["cwd", "job-id"]
841
+ });
842
+
843
+ if (!options["job-id"]) {
844
+ throw new Error("Missing required --job-id for task-worker.");
845
+ }
846
+
847
+ const cwd = resolveCommandCwd(options);
848
+ const workspaceRoot = resolveCommandWorkspace(options);
849
+ const storedJob = readStoredJob(workspaceRoot, options["job-id"]);
850
+ if (!storedJob) {
851
+ throw new Error(`No stored job found for ${options["job-id"]}.`);
852
+ }
853
+
854
+ const request = storedJob.request;
855
+ if (!request || typeof request !== "object") {
856
+ throw new Error(`Stored job ${options["job-id"]} is missing its task request payload.`);
857
+ }
858
+
859
+ const { logFile, progress } = createTrackedProgress(
860
+ {
861
+ ...storedJob,
862
+ workspaceRoot
863
+ },
864
+ {
865
+ logFile: storedJob.logFile ?? null
866
+ }
867
+ );
868
+ await runTrackedJob(
869
+ {
870
+ ...storedJob,
871
+ workspaceRoot,
872
+ logFile
873
+ },
874
+ () =>
875
+ executeTaskRun({
876
+ ...request,
877
+ onProgress: progress
878
+ }),
879
+ { logFile }
880
+ );
881
+ }
882
+
883
+ async function handleStatus(argv) {
884
+ const { options, positionals } = parseCommandInput(argv, {
885
+ valueOptions: ["cwd", "timeout-ms", "poll-interval-ms"],
886
+ booleanOptions: ["json", "all", "wait"]
887
+ });
888
+
889
+ const cwd = resolveCommandCwd(options);
890
+ const reference = positionals[0] ?? "";
891
+ if (reference) {
892
+ const snapshot = options.wait
893
+ ? await waitForSingleJobSnapshot(cwd, reference, {
894
+ timeoutMs: options["timeout-ms"],
895
+ pollIntervalMs: options["poll-interval-ms"]
896
+ })
897
+ : buildSingleJobSnapshot(cwd, reference);
898
+ outputCommandResult(snapshot, renderJobStatusReport(snapshot.job), options.json);
899
+ return;
900
+ }
901
+
902
+ if (options.wait) {
903
+ throw new Error("`status --wait` requires a job id.");
904
+ }
905
+
906
+ const report = buildStatusSnapshot(cwd, { all: options.all });
907
+ outputResult(renderStatusPayload(report, options.json), options.json);
908
+ }
909
+
910
+ function handleResult(argv) {
911
+ const { options, positionals } = parseCommandInput(argv, {
912
+ valueOptions: ["cwd"],
913
+ booleanOptions: ["json"]
914
+ });
915
+
916
+ const cwd = resolveCommandCwd(options);
917
+ const reference = positionals[0] ?? "";
918
+ const { workspaceRoot, job } = resolveResultJob(cwd, reference);
919
+ const storedJob = readStoredJob(workspaceRoot, job.id);
920
+ const payload = {
921
+ job,
922
+ storedJob
923
+ };
924
+
925
+ outputCommandResult(payload, renderStoredJobResult(job, storedJob), options.json);
926
+ }
927
+
928
+ function handleTaskResumeCandidate(argv) {
929
+ const { options } = parseCommandInput(argv, {
930
+ valueOptions: ["cwd"],
931
+ booleanOptions: ["json"]
932
+ });
933
+
934
+ const cwd = resolveCommandCwd(options);
935
+ const workspaceRoot = resolveCommandWorkspace(options);
936
+ const sessionId = getCurrentClaudeSessionId();
937
+ const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(listJobs(workspaceRoot)));
938
+ const candidate = findLatestResumableTaskJob(jobs);
939
+
940
+ const payload = {
941
+ available: Boolean(candidate),
942
+ sessionId,
943
+ candidate:
944
+ candidate == null
945
+ ? null
946
+ : {
947
+ id: candidate.id,
948
+ status: candidate.status,
949
+ title: candidate.title ?? null,
950
+ summary: candidate.summary ?? null,
951
+ threadId: candidate.threadId,
952
+ completedAt: candidate.completedAt ?? null,
953
+ updatedAt: candidate.updatedAt ?? null
954
+ }
955
+ };
956
+
957
+ const rendered = candidate
958
+ ? `Resumable task found: ${candidate.id} (${candidate.status}).\n`
959
+ : "No resumable task found for this session.\n";
960
+ outputCommandResult(payload, rendered, options.json);
961
+ }
962
+
963
+ async function handleCancel(argv) {
964
+ const { options, positionals } = parseCommandInput(argv, {
965
+ valueOptions: ["cwd"],
966
+ booleanOptions: ["json"]
967
+ });
968
+
969
+ const cwd = resolveCommandCwd(options);
970
+ const reference = positionals[0] ?? "";
971
+ const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env });
972
+ const existing = readStoredJob(workspaceRoot, job.id) ?? {};
973
+ const threadId = existing.threadId ?? job.threadId ?? null;
974
+ const turnId = existing.turnId ?? job.turnId ?? null;
975
+
976
+ const interrupt = await interruptAppServerTurn(cwd, { threadId, turnId });
977
+ if (interrupt.attempted) {
978
+ appendLogLine(
979
+ job.logFile,
980
+ interrupt.interrupted
981
+ ? `Requested Codex turn interrupt for ${turnId} on ${threadId}.`
982
+ : `Codex turn interrupt failed${interrupt.detail ? `: ${interrupt.detail}` : "."}`
983
+ );
984
+ }
985
+
986
+ terminateProcessTree(job.pid ?? Number.NaN);
987
+ appendLogLine(job.logFile, "Cancelled by user.");
988
+
989
+ const completedAt = nowIso();
990
+ const nextJob = {
991
+ ...job,
992
+ status: "cancelled",
993
+ phase: "cancelled",
994
+ pid: null,
995
+ completedAt,
996
+ errorMessage: "Cancelled by user."
997
+ };
998
+
999
+ writeJobFile(workspaceRoot, job.id, {
1000
+ ...existing,
1001
+ ...nextJob,
1002
+ cancelledAt: completedAt
1003
+ });
1004
+ upsertJob(workspaceRoot, {
1005
+ id: job.id,
1006
+ status: "cancelled",
1007
+ phase: "cancelled",
1008
+ pid: null,
1009
+ errorMessage: "Cancelled by user.",
1010
+ completedAt
1011
+ });
1012
+
1013
+ const payload = {
1014
+ jobId: job.id,
1015
+ status: "cancelled",
1016
+ title: job.title,
1017
+ turnInterruptAttempted: interrupt.attempted,
1018
+ turnInterrupted: interrupt.interrupted
1019
+ };
1020
+
1021
+ outputCommandResult(payload, renderCancelReport(nextJob), options.json);
1022
+ }
1023
+
1024
+ async function main() {
1025
+ const [subcommand, ...argv] = process.argv.slice(2);
1026
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
1027
+ printUsage();
1028
+ return;
1029
+ }
1030
+
1031
+ switch (subcommand) {
1032
+ case "setup":
1033
+ await handleSetup(argv);
1034
+ break;
1035
+ case "review":
1036
+ await handleReview(argv);
1037
+ break;
1038
+ case "adversarial-review":
1039
+ await handleReviewCommand(argv, {
1040
+ reviewName: "Adversarial Review"
1041
+ });
1042
+ break;
1043
+ case "task":
1044
+ await handleTask(argv);
1045
+ break;
1046
+ case "transfer":
1047
+ await handleTransfer(argv);
1048
+ break;
1049
+ case "task-worker":
1050
+ await handleTaskWorker(argv);
1051
+ break;
1052
+ case "status":
1053
+ await handleStatus(argv);
1054
+ break;
1055
+ case "result":
1056
+ handleResult(argv);
1057
+ break;
1058
+ case "task-resume-candidate":
1059
+ handleTaskResumeCandidate(argv);
1060
+ break;
1061
+ case "cancel":
1062
+ await handleCancel(argv);
1063
+ break;
1064
+ default:
1065
+ throw new Error(`Unknown subcommand: ${subcommand}`);
1066
+ }
1067
+ }
1068
+
1069
+ main().catch((error) => {
1070
+ const message = error instanceof Error ? error.message : String(error);
1071
+ process.stderr.write(`${message}\n`);
1072
+ process.exitCode = 1;
1073
+ });