Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ they already have.

- `/codex:review` for a normal read-only Codex review
- `/codex:adversarial-review` for a steerable challenge review
- `/codex:rescue`, `/codex:transfer`, `/codex:status`, `/codex:result`, and `/codex:cancel` to delegate work, hand off sessions, and manage background jobs
- `/codex:rescue`, `/codex:expert`, `/codex:transfer`, `/codex:status`, `/codex:result`, and `/codex:cancel` to delegate work, request a user-approved expert pass, hand off sessions, and manage background jobs

## Requirements

Expand Down Expand Up @@ -158,10 +158,16 @@ Ask Codex to redesign the database connection to be more resilient.

**Notes:**

- if you do not pass `--model` or `--effort`, Codex chooses its own defaults.
- for a fresh rescue task, if you do not pass `--model` or `--effort`, the companion explicitly starts `gpt-5.6-luna` at `max` effort; resumed tasks preserve their existing thread routing.
- if you say `spark`, the plugin maps that to `gpt-5.3-codex-spark`
- follow-up rescue requests can continue the latest Codex task in the repo

### `/codex:expert`

The active model can invoke this command when it needs a stronger bounded pass, reaches the Luna Max effort ceiling, or encounters an ambiguous/high-risk decision. The command always asks the user to choose the expert model and reasoning effort before it starts anything; the default is Sol at High effort.

The selected expert runs in a separate persistent, named Codex thread. Its final answer and session ID are returned to the active conversation so the model can continue with the expert's evidence.

### `/codex:transfer`

Creates a persistent Codex thread from the current Claude Code session and prints a `codex resume <session-id>` command.
Expand Down
5 changes: 3 additions & 2 deletions plugins/codex/agents/codex-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ Forwarding rules:
- Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work beyond shaping the forwarded prompt text.
- Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own.
- Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`. This subagent only forwards to `task`.
- Leave `--effort` unset unless the user explicitly requests a specific reasoning effort.
- Leave model unset by default. Only add `--model` when the user explicitly asks for a specific model.
- Leave `--effort` unset unless the user explicitly requests a specific reasoning effort; the runtime defaults fresh bounded tasks to Luna at Max effort.
- Leave model unset unless the user explicitly asks for a specific model; the runtime defaults fresh bounded tasks to `gpt-5.6-luna`.
- Do not add these defaults when continuing a previous task; the runtime preserves the existing thread's routing on resume.
- If the user asks for `spark`, map that to `--model gpt-5.3-codex-spark`.
- If the user asks for a concrete model name such as `gpt-5.4-mini`, pass it through with `--model`.
- Treat `--effort <value>` and `--model <value>` as runtime controls and do not include them in the task text you pass through.
Expand Down
29 changes: 29 additions & 0 deletions plugins/codex/commands/expert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
description: Automatically ask a user-selected named Codex expert when the current model needs a stronger bounded pass
argument-hint: "[--name <name>] [--write] [handoff]"
allowed-tools: Bash(node:*), AskUserQuestion
---

Use the companion expert broker for a deliberate, user-approved escalation when the current model has reached its supported effort ceiling, has failed a bounded task, or identifies an ambiguous/high-risk decision that needs a stronger pass. This command is safe for model invocation because it always pauses for the user's model-and-effort choice before starting an expert, even when raw arguments contain routing flags.

Raw handoff request:
`$ARGUMENTS`

Use `AskUserQuestion` exactly once with these two questions:

1. Which expert model should handle the handoff?
- `Sol (Recommended)` — map to `gpt-5.6-sol` and use it as the default expert.
- `Terra` — map to `gpt-5.6-terra` for a balanced expert pass.
- `Luna` — map to `gpt-5.6-luna` for a lower-cost expert pass.
2. How much reasoning effort should the expert use?
- `High (Recommended)` — the default Sol setting.
- `XHigh` — spend more effort on a difficult bounded problem.
- `Max` — use the maximum effort supported by this harness.

After the choice, invoke exactly one foreground command, adding the selected `--model` and `--effort` values after the raw handoff arguments so the user's choice is authoritative:

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" expert "$ARGUMENTS" --model <selected-model-id> --effort <selected-effort>
Comment thread
MattSyska marked this conversation as resolved.
```

Return the command's stdout verbatim. The selected model and effort are authoritative even if raw arguments contained routing flags. Do not silently change the selected model or effort, create a second expert, or modify the handoff text. The command creates a persistent named Codex thread and returns its session ID with the expert's final answer.
7 changes: 4 additions & 3 deletions plugins/codex/commands/rescue.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Delegate investigation, an explicit fix request, or follow-up rescue work to the Codex rescue subagent
argument-hint: "[--background|--wait] [--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [what Codex should investigate, solve, or continue]"
argument-hint: "[--background|--wait] [--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh|max>] [what Codex should investigate, solve, or continue]"
allowed-tools: Bash(node:*), AskUserQuestion, Agent
---

Expand Down Expand Up @@ -42,8 +42,9 @@ Operating rules:
- Return the Codex companion stdout verbatim to the user.
- Do not paraphrase, summarize, rewrite, or add commentary before or after it.
- Do not ask the subagent to inspect files, monitor progress, poll `/codex:status`, fetch `/codex:result`, call `/codex:cancel`, summarize output, or do follow-up work of its own.
- Leave `--effort` unset unless the user explicitly asks for a specific reasoning effort.
- Leave the model unset unless the user explicitly asks for one. If they ask for `spark`, map it to `gpt-5.3-codex-spark`.
- For a fresh bounded task, leave model and effort unset unless the user explicitly chooses them; the runtime then passes `--model gpt-5.6-luna --effort max` explicitly.
- When continuing a previous task, leave model and effort unset so the runtime preserves that thread's routing.
- If the user asks for `spark`, pass `--model gpt-5.3-codex-spark` to the runtime.
- Leave `--resume` and `--fresh` in the forwarded request. The subagent handles that routing when it builds the `task` command.
- If the helper reports that Codex is missing or unauthenticated, stop and tell the user to run `/codex:setup`.
- If the user did not supply a request, ask what Codex should investigate or fix.
121 changes: 109 additions & 12 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";

import { parseArgs, splitRawArgumentString } from "./lib/args.mjs";
import {
buildExpertSelectionOffer,
buildPersistentTaskThreadName,
DEFAULT_CONTINUE_PROMPT,
findLatestTaskThread,
Expand All @@ -19,6 +20,7 @@ import {
parseStructuredOutput,
readOutputSchema,
runAppServerReview,
runExpertHandoff,
runAppServerTurn
} from "./lib/codex.mjs";
import { resolveClaudeSessionPath } from "./lib/claude-session-transfer.mjs";
Expand Down Expand Up @@ -58,6 +60,8 @@ import {
renderReviewResult,
renderStoredJobResult,
renderCancelReport,
renderExpertResult,
renderExpertSelectionOffer,
renderJobStatusReport,
renderSetupReport,
renderStatusReport,
Expand All @@ -68,7 +72,9 @@ 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 DEFAULT_TASK_MODEL = "gpt-5.6-luna";
const DEFAULT_TASK_EFFORT = "max";
const VALID_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
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.";

Expand All @@ -79,7 +85,8 @@ function printUsage() {
" node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]",
" node scripts/codex-companion.mjs review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>]",
" node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [focus text]",
" node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
" node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh|max>] [prompt]",
" node scripts/codex-companion.mjs expert [--name <name>] [--write] [--model <model>] [--effort <none|minimal|low|medium|high|xhigh|max>] [handoff]",
" node scripts/codex-companion.mjs transfer [--source <claude-jsonl>] [--json]",
" node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
" node scripts/codex-companion.mjs result [job-id] [--json]",
Expand Down Expand Up @@ -121,21 +128,37 @@ function normalizeReasoningEffort(effort) {
}
if (!VALID_REASONING_EFFORTS.has(normalized)) {
throw new Error(
`Unsupported reasoning effort "${effort}". Use one of: none, minimal, low, medium, high, xhigh.`
`Unsupported reasoning effort "${effort}". Use one of: none, minimal, low, medium, high, xhigh, max.`
);
}
return normalized;
}

function resolveTaskRouting(options, resumeLast) {
const model = normalizeRequestedModel(options.model);
const effort = normalizeReasoningEffort(options.effort);

if (resumeLast) {
return { model, effort };
}

return {
model: model ?? DEFAULT_TASK_MODEL,
effort: effort ?? DEFAULT_TASK_EFFORT
};
}

function normalizeArgv(argv) {
if (argv.length === 1) {
const [raw] = argv;
if (!raw || !raw.trim()) {
return [];
}
return splitRawArgumentString(raw);
if (argv.length === 0) {
return [];
}
return argv;

const [first, ...rest] = argv;
if (!first || !first.trim()) {
return rest;
}

return [...splitRawArgumentString(first), ...rest];
Comment thread
MattSyska marked this conversation as resolved.
}

function parseCommandInput(argv, config = {}) {
Expand All @@ -148,6 +171,20 @@ function parseCommandInput(argv, config = {}) {
});
}

function parseExpertInput(argv, config = {}) {
const [rawHandoff = "", ...selectedRouting] = argv;
const raw = parseCommandInput([rawHandoff], config);
Comment thread
MattSyska marked this conversation as resolved.
Outdated
Comment thread
MattSyska marked this conversation as resolved.
Outdated
const selected = parseCommandInput(selectedRouting, config);

return {
options: {
...raw.options,
...selected.options
Comment thread
MattSyska marked this conversation as resolved.
Outdated
},
positionals: [...raw.positionals, ...selected.positionals]
Comment thread
MattSyska marked this conversation as resolved.
};
}

function resolveCommandCwd(options = {}) {
return options.cwd ? path.resolve(process.cwd(), options.cwd) : process.cwd();
}
Expand Down Expand Up @@ -759,6 +796,64 @@ async function handleReview(argv) {
});
}

async function handleExpert(argv) {
const { options, positionals } = parseExpertInput(argv, {
valueOptions: ["model", "effort", "cwd", "prompt-file", "name"],
Comment thread
MattSyska marked this conversation as resolved.
booleanOptions: ["json", "write"],
aliasMap: {
m: "model"
}
});

const cwd = resolveCommandCwd(options);
const prompt = readTaskPrompt(cwd, options, positionals);
requireTaskRequest(prompt, false);

const model = normalizeRequestedModel(options.model);
const effort = normalizeReasoningEffort(options.effort);
const expertName = options.name === undefined ? "Expert" : String(options.name).trim();
if (!expertName) {
throw new Error("Provide a non-empty expert name with --name.");
}

if (!model || !effort) {
const offer = buildExpertSelectionOffer({
expertName,
model,
effort,
prompt
});
outputCommandResult(offer, renderExpertSelectionOffer(offer), options.json);
return;
}

ensureCodexAvailable(cwd);
const result = await runExpertHandoff(cwd, {
expertName,
model,
effort,
prompt,
sandbox: options.write ? "workspace-write" : "read-only"
});
const payload = {
status: result.status === 0 ? "completed" : "failed",
expert: {
name: result.expertName,
model: result.model,
effort: result.effort,
threadId: result.threadId,
turnId: result.turnId
},
finalMessage: result.finalMessage,
reasoningSummary: result.reasoningSummary,
error: result.error?.message ?? result.error ?? result.stderr ?? null
};
outputCommandResult(payload, renderExpertResult(payload), options.json);
if (result.status !== 0) {
process.exitCode = result.status;
}
}

async function handleTask(argv) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["model", "effort", "cwd", "prompt-file"],
Expand All @@ -770,15 +865,14 @@ async function handleTask(argv) {

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 { model, effort } = resolveTaskRouting(options, resumeLast);
const write = Boolean(options.write);
const taskMetadata = buildTaskRunMetadata({
prompt,
Expand Down Expand Up @@ -1043,6 +1137,9 @@ async function main() {
case "task":
await handleTask(argv);
break;
case "expert":
await handleExpert(argv);
break;
case "transfer":
await handleTransfer(argv);
break;
Expand Down
Loading