diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 6226ef4..4933620 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -40,6 +40,18 @@ }, "category": "developer-tools" }, + { + "name": "codex-advisor", + "source": { + "source": "local", + "path": "./plugins/codex-advisor" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "developer-tools" + }, { "name": "adhd-output-style", "source": { diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 410c540..36af72f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -53,6 +53,21 @@ "category": "developer-tools", "tags": ["review", "agent"] }, + { + "name": "codex-advisor", + "source": "./plugins/codex-advisor", + "description": "Get a second opinion from GPT via Codex CLI before big decisions.", + "version": "1.0.0", + "author": { + "name": "Fatih C. Akyon" + }, + "homepage": "https://github.com/fcakyon/claude-codex-settings#plugins", + "repository": "https://github.com/fcakyon/claude-codex-settings", + "license": "Apache-2.0", + "keywords": ["advisor", "second-opinion", "cross-model", "plan-review", "code-review"], + "category": "developer-tools", + "tags": ["review", "agent"] + }, { "name": "adhd-output-style", "source": "./plugins/adhd-output-style", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index fdeb104..b2814fb 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -23,6 +23,13 @@ "version": "1.2.0", "license": "Apache-2.0" }, + { + "name": "codex-advisor", + "source": "./plugins/codex-advisor", + "description": "Get a second opinion from GPT via Codex CLI before big decisions.", + "version": "1.0.0", + "license": "Apache-2.0" + }, { "name": "adhd-output-style", "source": "./plugins/adhd-output-style", diff --git a/README.md b/README.md index ac7d3d8..f4d483d 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,25 @@ Ask a Fable 5 reviewer to challenge a plan or conclusion before you commit, a dr +
+codex-advisor - Get a second opinion from GPT via Codex CLI before big decisions + +| Claude Code | Codex CLI | Gemini CLI | +| ----------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------- | +| `claude plugin install codex-advisor@claude-settings` | `codex plugin add codex-advisor@claude-settings` | `gemini extensions install --path ./plugins/codex-advisor` | + +Ask GPT to challenge a plan before you build it, the cross-model check the built-in advisor cannot give you. Claude Code attaches the recent conversation, so "ask the codex advisor" is enough. Codex, Cursor, and Gemini reach the same reviewer. Needs the Codex CLI signed in. + +**Agents:** + +- [`codex-advisor`](./plugins/codex-advisor/claude-agents/codex-advisor.md) - Sends the recent conversation to GPT and returns the verdict verbatim + +**Skills:** + +- [`codex-advisor`](./plugins/codex-advisor/skills/codex-advisor/SKILL.md) - Routes Claude Code to its native agent and other tools to `codex exec` + +
+
adhd-output-style - Use fewer output tokens with short steps, quick insights, and clear next actions @@ -908,7 +927,7 @@ Configuration in [`.claude/settings.json`](./.claude/settings.json): - **Environment**: bash working directory, telemetry disabled, MCP output limits - **Permissions**: bash commands, git operations, MCP tools - **Auto mode**: `auto` permission mode with a custom `autoMode` classifier block in [`.claude/settings.json`](./.claude/settings.json) - see the [auto mode config reference](https://code.claude.com/docs/en/auto-mode-config) for what each rule section does, and run `claude auto-mode defaults` to print the current built-in block and allow rules -- **Advisor**: a stronger model reviews key decisions via the [advisor tool](https://code.claude.com/docs/en/advisor), paired as Opus main plus Opus advisor. Fable 5 as advisor currently fails with a bare "unavailable" ([#73365](https://github.com/anthropics/claude-code/issues/73365)), so for an on-demand Fable second opinion use the standalone [`fable-advisor`](./plugins/fable-advisor) plugin +- **Advisor**: a stronger model reviews key decisions via the [advisor tool](https://code.claude.com/docs/en/advisor), paired as Opus main plus Opus advisor. Fable 5 as advisor currently fails with a bare "unavailable" ([#73365](https://github.com/anthropics/claude-code/issues/73365)), so for an on-demand Fable second opinion use the standalone [`fable-advisor`](./plugins/fable-advisor) plugin. The advisor only pairs Claude with Claude, so for a review from outside that family use [`codex-advisor`](./plugins/codex-advisor) - **Plugins**: All plugins enabled
diff --git a/plugins/codex-advisor/.claude-plugin/plugin.json b/plugins/codex-advisor/.claude-plugin/plugin.json new file mode 100644 index 0000000..e7ffa98 --- /dev/null +++ b/plugins/codex-advisor/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "codex-advisor", + "version": "1.0.0", + "description": "Get a second opinion from GPT via Codex CLI before big decisions.", + "author": { + "name": "Fatih C. Akyon" + }, + "agents": "./claude-agents/codex-advisor.md", + "hooks": "./claude-hooks/hooks.json", + "license": "Apache-2.0" +} diff --git a/plugins/codex-advisor/.codex-plugin/plugin.json b/plugins/codex-advisor/.codex-plugin/plugin.json new file mode 100644 index 0000000..8158f60 --- /dev/null +++ b/plugins/codex-advisor/.codex-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "codex-advisor", + "version": "1.0.0", + "description": "Get a second opinion from GPT via Codex CLI before big decisions.", + "author": { + "name": "Fatih C. Akyon" + }, + "license": "Apache-2.0" +} diff --git a/plugins/codex-advisor/.cursor-plugin/plugin.json b/plugins/codex-advisor/.cursor-plugin/plugin.json new file mode 100644 index 0000000..8158f60 --- /dev/null +++ b/plugins/codex-advisor/.cursor-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "codex-advisor", + "version": "1.0.0", + "description": "Get a second opinion from GPT via Codex CLI before big decisions.", + "author": { + "name": "Fatih C. Akyon" + }, + "license": "Apache-2.0" +} diff --git a/plugins/codex-advisor/claude-agents/codex-advisor.md b/plugins/codex-advisor/claude-agents/codex-advisor.md new file mode 100644 index 0000000..2e8e2ef --- /dev/null +++ b/plugins/codex-advisor/claude-agents/codex-advisor.md @@ -0,0 +1,22 @@ +--- +name: codex-advisor +description: |- + Second-opinion reviewer backed by GPT through the Codex CLI, a cross-model stand-in for the built-in advisor. Consult it before suggesting or implementing a plan, when an error keeps recurring, or before declaring a task done. It receives the recent conversation automatically and returns a verdict without running commands. +model: haiku +color: cyan +tools: [Bash] +--- + +You are a relay, not the reviewer. GPT reviews through the Codex CLI, and your only job is to carry the request over and hand back the answer untouched. + +1. A hook gives you the exact `node ...` command to run, and stages the recent conversation for it. Run that command, passing the caller's question on standard input: + + ``` + node "/path/from/the/hook/ask_codex.mjs" --context "/path/from/the/hook/conversation.md" <<'ASK' + the caller's question, verbatim + ASK + ``` + +2. Return the reviewer's answer verbatim, with nothing added. The review is the deliverable, not your reading of it. + +Never answer the question yourself, and never soften or summarize the verdict. If the command fails, report the error and say the review did not happen. A missing or unauthenticated `codex` binary means the user needs to install it or sign in, so say that plainly rather than reviewing in its place. diff --git a/plugins/codex-advisor/claude-hooks/hooks.json b/plugins/codex-advisor/claude-hooks/hooks.json new file mode 100644 index 0000000..7c6e702 --- /dev/null +++ b/plugins/codex-advisor/claude-hooks/hooks.json @@ -0,0 +1,19 @@ +{ + "description": "Stages the real conversation for the codex-advisor reviewer instead of only the caller's prompt", + "hooks": { + "SubagentStart": [ + { + "matcher": "^(codex-advisor:)?codex-advisor$", + "hooks": [ + { + "type": "command", + "command": "node", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/claude-hooks/scripts/stage_transcript.mjs" + ] + } + ] + } + ] + } +} diff --git a/plugins/codex-advisor/claude-hooks/scripts/stage_transcript.mjs b/plugins/codex-advisor/claude-hooks/scripts/stage_transcript.mjs new file mode 100644 index 0000000..2944507 --- /dev/null +++ b/plugins/codex-advisor/claude-hooks/scripts/stage_transcript.mjs @@ -0,0 +1,116 @@ +#!/usr/bin/env node +// SubagentStart hook: stages the recent conversation to a file and hands the codex-advisor +// relay the exact command to run. Staging beats inlining here because the reviewer is a +// separate process, so the relay never has to retype the history. +// Node-only, cross-platform, exits 0 on any failure. + +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const script = fileURLToPath( + new URL("../../skills/codex-advisor/scripts/ask_codex.mjs", import.meta.url), +); +const run = (contextFile) => + contextFile + ? `node "${script}" --context "${contextFile}"` + : `node "${script}"`; + +const emit = (contextFile) => + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SubagentStart", + additionalContext: contextFile + ? `The recent conversation has been staged for the reviewer, the same history the built-in advisor tool would see. Send the caller's question to the reviewer on standard input with exactly this command:\n\n${run(contextFile)}` + : `The recent conversation could not be reconstructed, so the reviewer will see only your question. Say so in your reply. Run:\n\n${run()}`, + }, + }), + ); + +const clip = (s, n) => (s.length > n ? s.slice(0, n) + " …[truncated]" : s); + +const renderBlock = (role, b) => { + switch (b.type) { + case "text": + return `${role}: ${clip(b.text ?? "", 4000)}`; + case "thinking": + return `assistant (thinking): ${clip(b.thinking ?? "", 1200)}`; + case "tool_use": + return `assistant → ${b.name ?? "?"}(${clip(JSON.stringify(b.input ?? {}), 280)})`; + case "tool_result": { + const c = b.content; + const text = + typeof c === "string" + ? c + : Array.isArray(c) + ? c.map((x) => x.text ?? "").join(" ") + : JSON.stringify(c ?? ""); + return ` ↳ result: ${clip(text, 700)}`; + } + default: + return ""; + } +}; + +// Any failure below leaves contextFile unset, which downgrades the review to the caller's +// prompt instead of losing the turn. +let contextFile; +try { + const lines = readFileSync( + JSON.parse(readFileSync(0, "utf8")).transcript_path, + "utf8", + ).split("\n"); + + // Scan newest-first and stop at 80 records, so a multi-MB transcript never parses the head it would discard. + const records = []; + for (let i = lines.length - 1; i >= 0 && records.length < 80; i--) { + if (!lines[i]) continue; + let r; + try { + r = JSON.parse(lines[i]); // skip a half-written or malformed line + } catch { + continue; + } + if (r.isSidechain === true || r.isMeta === true) continue; + if (r.type !== "user" && r.type !== "assistant") continue; + const content = r.message?.content; + let rendered; + if (typeof content === "string") + rendered = `${r.type}: ${clip(content, 4000)}`; + else if (Array.isArray(content)) + rendered = content + .map((b) => renderBlock(r.type, b)) + .filter(Boolean) + .join("\n"); + else continue; + if (rendered) records.push(rendered); + } + records.reverse(); + + if (records.length) { + // Keep the newest characters so a very chatty session cannot flood the reviewer. + let recent = records.join("\n\n---\n\n"); + const MAX = 160000; + if (recent.length > MAX) + recent = "…[older turns truncated for length]\n" + recent.slice(-MAX); + + const staged = join( + mkdtempSync(join(tmpdir(), "codex-advisor-")), + "conversation.md", + ); + writeFileSync( + staged, + `You are reviewing an in-progress Claude Code session. Below is the recent conversation, most recent last, the same history the built-in advisor tool would see. Weigh the question against this actual context, not just the caller's summary of it. + + +${recent} + +`, + ); + contextFile = staged; + } +} catch {} + +emit(contextFile); diff --git a/plugins/codex-advisor/gemini-extension.json b/plugins/codex-advisor/gemini-extension.json new file mode 100644 index 0000000..47882d2 --- /dev/null +++ b/plugins/codex-advisor/gemini-extension.json @@ -0,0 +1,5 @@ +{ + "name": "codex-advisor", + "version": "1.0.0", + "description": "Get a second opinion from GPT via Codex CLI before big decisions." +} diff --git a/plugins/codex-advisor/skills/codex-advisor/SKILL.md b/plugins/codex-advisor/skills/codex-advisor/SKILL.md new file mode 100644 index 0000000..4e1b77c --- /dev/null +++ b/plugins/codex-advisor/skills/codex-advisor/SKILL.md @@ -0,0 +1,36 @@ +--- +name: codex-advisor +description: This skill should be used when the user asks for a "GPT second opinion", wants a "cross-model review", needs to "check a plan before committing", wants another view after repeated failures, or explicitly invokes "codex-advisor". +--- + +# Codex Advisor + +Get a focused second opinion from GPT through the Codex CLI, without handing the +work over to it. + +## Route by tool + +1. In Claude Code, delegate to the native `codex-advisor` agent. It receives the + recent conversation automatically, so do not paste the history yourself. +2. In Codex, Cursor, or Gemini CLI, locate this skill's directory and run + `node scripts/ask_codex.mjs`. Pass the review request on standard input. + +The request must include the decision or conclusion, the relevant recent +context and evidence, and any constraints or alternatives that affect the +verdict. Include only facts that matter to the decision. + +Return the reviewer's answer without rewriting it. If `codex` is missing or not +authenticated, surface the command error and ask the user to install the Codex +CLI or sign in. Do not fall back to the host model. + +## Picking the reviewer + +The review runs on whatever model `~/.codex/config.toml` sets. In Codex itself +that is the same model already doing the work, which is a weak second opinion, +so set `CODEX_ADVISOR_MODEL` to a different one before calling. + +## Weighing the answer + +Give the verdict serious weight. If a step it recommends fails when tried, or a +file contradicts a specific claim, surface the conflict instead of following the +review blindly. diff --git a/plugins/codex-advisor/skills/codex-advisor/reviewer-prompt.md b/plugins/codex-advisor/skills/codex-advisor/reviewer-prompt.md new file mode 100644 index 0000000..46dc6ae --- /dev/null +++ b/plugins/codex-advisor/skills/codex-advisor/reviewer-prompt.md @@ -0,0 +1,17 @@ +You are a second opinion, the same role as Claude Code's built-in advisor, consulted at a decision point: a plan about to be committed to, a recurring error, or a task about to be declared done. Another model did the work, not you. + +You usually receive the recent conversation in a `` block. Treat it as the primary context and the `` block as the specific ask. If the conversation is missing, or a load-bearing detail is absent, say what you need instead of guessing. + +Open your reply with one marker line: "context: recent-conversation received" or "context: caller prompt only". + +Do not investigate: no commands, tests, shell, file reads, or exploring. Answer from what you were given. + +Challenge the plan or conclusion, do not rewrite it. Look for what makes it wrong: unstated assumptions, reasoning gaps, missed edge cases, a cheaper or safer alternative, evidence pointing the other way. You are a different model than the one under review, so say plainly where you would have gone another way. + +Return: + +- A one-word verdict: proceed, proceed-with-changes, or reconsider. +- The risks or gaps that matter, most important first, each with the failure it causes and the fix. +- If you disagree, cite the exact evidence that breaks the conclusion. + +Say nothing about what is already sound. Be concrete, not encouraging. diff --git a/plugins/codex-advisor/skills/codex-advisor/scripts/ask_codex.mjs b/plugins/codex-advisor/skills/codex-advisor/scripts/ask_codex.mjs new file mode 100644 index 0000000..7358ac2 --- /dev/null +++ b/plugins/codex-advisor/skills/codex-advisor/scripts/ask_codex.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +// Sends a review request to the Codex CLI and prints only the reviewer's answer. +// The question arrives on standard input. --context prepends a staged conversation. +// Set CODEX_ADVISOR_MODEL to review with a model other than the Codex default. + +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; + +const args = process.argv.slice(2); +const contextIndex = args.indexOf("--context"); +const question = readFileSync(0, "utf8").trim(); +if (!question) + throw new Error("ask_codex: pass the review request on standard input"); + +const prompt = [ + readFileSync( + new URL("../reviewer-prompt.md", import.meta.url), + "utf8", + ).trim(), + contextIndex === -1 + ? "" + : readFileSync(args[contextIndex + 1], "utf8").trim(), + `\n${question}\n`, +] + .filter(Boolean) + .join("\n\n"); + +const answerFile = join( + mkdtempSync(join(tmpdir(), "codex-advisor-")), + "answer.md", +); +const result = spawnSync( + "codex", + [ + "exec", + ...(process.env.CODEX_ADVISOR_MODEL + ? ["--model", process.env.CODEX_ADVISOR_MODEL] + : []), + "--sandbox", + "read-only", + "--ephemeral", + "--skip-git-repo-check", + "--color", + "never", + "--output-last-message", + answerFile, + "-", + ], + { input: prompt, stdio: ["pipe", "ignore", "pipe"] }, +); + +if (result.error) throw result.error; +if (result.signal) throw new Error(`codex exited from signal ${result.signal}`); +// Codex narrates the run on stderr, so it only becomes useful once something failed. +if (result.status !== 0) { + process.stderr.write(result.stderr ?? ""); + process.exit(result.status ?? 1); +} + +process.stdout.write(readFileSync(answerFile, "utf8")); + +// Both temp trees hold a copy of the conversation, so drop them once the review is through. +// A failed run exits above with them intact, which keeps a retry cheap. +rmSync(dirname(answerFile), { recursive: true, force: true }); +if (contextIndex !== -1) + rmSync(dirname(args[contextIndex + 1]), { recursive: true, force: true });