Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cc",
"version": "1.1.0",
"version": "1.2.0",
"description": "Claude Code Plugin for Codex. Delegate code reviews, investigations, and tracked tasks to Claude Code from inside Codex.",
"author": {
"name": "Sendbird, Inc.",
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v1.2.0

- Default the Claude model for `review`, `adversarial-review`, and `rescue`/`task` to `opus` (resolved to the 1M-context variant `claude-opus-4-7[1m]`) with `xhigh` effort. The `sonnet` alias resolves to `claude-sonnet-4-6[1m]` and defaults to `high` effort; `haiku` stays on `claude-haiku-4-5` with effort unset. `--model` and `--effort` remain user-overridable; `xhigh` is now a first-class effort level and `max` is reserved for users who explicitly opt in.
- Isolate `review` and `adversarial-review` from the user repo with a three-layer design instead of the previous Bash-pattern allowlist (which the Claude CLI does not strictly enforce — once `Bash` is in the allowlist with any sub-pattern, the entire `Bash` tool opens up). Reviews now run inside an ephemeral `git worktree` checked out at the branch tip (or the original repo for `working-tree` scope, so staged/unstaged/untracked changes remain visible), use a bundled read-only git MCP server (`mcp-git` subcommand) exposing `diff`/`log`/`show`/`blame`/`status`/`grep`/`ls_files` as structured tools with strict ref/path validation, and tighten the allowlist to `Read`, `Glob`, `Grep`, `WebSearch`, `WebFetch`, and `mcp__gitReview__*` only (no `Bash` entry).
- Leave network unrestricted in the `read-only` sandbox preset so `WebFetch`/`WebSearch` and the Claude CLI's own API path keep working; safety comes from removing `Bash` from the allowlist rather than from blocking network. File writes outside the OS temp dir stay blocked.
- Expose `--effort` on `review` and `adversarial-review` and document the new defaults in `SKILL.md`, `README.md`, and the internal `cli-runtime` reference.
- Sweep stranded `review-worktrees/`, `sandbox/`, and `mcp/` runtime files older than six hours at the start of every review to reclaim resources after `kill -9` or crashed runs.

## v1.1.0

- Restructure the internal Claude runtime and prompt-shaping guidance from pseudo-hidden `SKILL.md` files into plain internal reference documents, while keeping the public `review`, `adversarial-review`, and `rescue` skills self-sufficient on their critical invocation rules.
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,17 @@ Quick routing rule:
Standard read-only review of your current work.

```text
$cc:review # review uncommitted changes
$cc:review # review uncommitted changes (default: opus + xhigh effort)
$cc:review --base main # review branch vs main
$cc:review --scope branch # explicitly compare branch tip to base
$cc:review --background # run in background, check with $cc:status later
$cc:review --model sonnet # use a specific Claude model
$cc:review --model sonnet # switch to sonnet (defaults to high effort)
$cc:review --model opus --effort high # opus with a lighter effort
```

**Flags:** `--base <ref>`, `--scope <auto|working-tree|branch>`, `--wait`, `--background`, `--model <model>`
**Flags:** `--base <ref>`, `--scope <auto|working-tree|branch>`, `--wait`, `--background`, `--model <model>`, `--effort <low|medium|high|max>`

**Defaults:** model `opus` (resolves to `claude-opus-4-7[1m]`, the 1M-context variant) with `xhigh` effort. If you pick `sonnet`, it resolves to `claude-sonnet-4-6[1m]` (also 1M context) and the default effort drops to `high`. `haiku` resolves to `claude-haiku-4-5` and has no effort setting. Pass `--model` and `--effort` to override.

Scope `auto` (the default) inspects `git status` and chooses between working-tree and branch automatically.

Expand Down Expand Up @@ -179,8 +182,8 @@ $cc:rescue --model sonnet --effort medium investigate the flaky test
| `--resume-last` | Alias for `--resume` |
| `--fresh` | Force a new task (don't resume) |
| `--write` | Allow file edits (default) |
| `--model <model>` | Claude model (`sonnet`, `haiku`, or full ID) |
| `--effort <level>` | Reasoning effort: `low`, `medium`, `high`, `max` |
| `--model <model>` | Claude model (`opus`, `sonnet`, `haiku`, or full ID; defaults to `opus`. The `opus` and `sonnet` aliases resolve to their 1M-context variants `claude-opus-4-7[1m]` and `claude-sonnet-4-6[1m]`.) |
| `--effort <level>` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max` (default: `xhigh` for opus, `high` for sonnet, unset for haiku) |
| `--prompt-file <path>` | Read task description from a file |

**Resume behavior:** If you don't pass `--resume` or `--fresh`, rescue checks for a resumable Claude session and asks once whether to continue or start fresh. Your phrasing guides the recommendation — "continue the last run" → resume, "start over" → fresh.
Expand Down
3 changes: 1 addition & 2 deletions internal-skills/cli-runtime/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ Command selection:

Routing controls:
- Treat `--model`, `--effort`, `--resume`, `--resume-last`, `--fresh`, `--prompt-file`, `--view-state`, `--owner-session-id`, and `--job-id` as routing controls, not task text.
- Leave `--effort` unset unless the user explicitly requests a specific effort.
- Leave model unset by default. Add `--model` only when the user explicitly asks for one.
- Leave `--model` and `--effort` unset unless the user explicitly asks for a specific model or effort. The companion command applies these defaults itself: model defaults to `opus`, effort defaults to `xhigh` for opus, `high` for sonnet, and is left unset for haiku.
- `--view-state on-success` means the user will see this companion result in the current turn, so the companion may mark it viewed on success.
- `--view-state defer` means the parent is not waiting, so the companion must leave the result unread until the user explicitly checks it.
- `--owner-session-id <session-id>` is an internal parent-session routing control. Preserve it when present so tracked jobs remain visible to the parent session's `$cc:status` / `$cc:result`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cc-plugin-codex",
"version": "1.1.0",
"version": "1.2.0",
"description": "Claude Code Plugin for Codex by Sendbird",
"type": "module",
"author": {
Expand Down
131 changes: 97 additions & 34 deletions scripts/claude-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
*
* Adapted from codex-companion.mjs:
* - Uses claude-cli.mjs instead of app-server/broker
* - MODEL_ALIASES: sonnet -> claude-sonnet-4-6, haiku -> claude-haiku-4-5
* - Claude CLI effort values: low, medium, high, max
* - Legacy effort aliases: none|minimal -> low, xhigh -> max
* - MODEL_ALIASES: opus -> claude-opus-4-7[1m], sonnet -> claude-sonnet-4-6[1m], haiku -> claude-haiku-4-5
* - Default model when --model is unset: opus
* - Default effort by model: opus -> xhigh, sonnet -> high, haiku -> unset
* - Claude CLI effort values: low, medium, high, xhigh, max
* - Legacy effort aliases: none|minimal -> low
* - Review gate matches upstream setup semantics: Stop hook runs when enabled
*
* Subcommands:
Expand All @@ -37,10 +39,20 @@ import {
cancelClaudeProcess,
MODEL_ALIASES,
resolveEffort,
resolveDefaultModel,
resolveDefaultEffort,
SANDBOX_READ_ONLY_TOOLS,
createSandboxSettings,
cleanupSandboxSettings,
createReviewMcpConfig,
cleanupReviewMcpConfig,
pruneStaleSandboxSettings,
pruneStaleReviewMcpConfigs,
} from "./lib/claude-cli.mjs";
import {
createReviewIsolation,
pruneStaleReviewWorktrees,
} from "./lib/review-worktree.mjs";
import { readStdinIfPiped } from "./lib/fs.mjs";
import {
collectReviewContext,
Expand Down Expand Up @@ -112,9 +124,9 @@ function printUsage() {
[
"Usage:",
" node scripts/claude-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]",
" node scripts/claude-companion.mjs review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>]",
" node scripts/claude-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [focus text]",
" node scripts/claude-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|sonnet|haiku>] [--effort <low|medium|high|max>] [prompt]",
" node scripts/claude-companion.mjs review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [--model <model|opus|sonnet|haiku>] [--effort <low|medium|high|xhigh|max>]",
" node scripts/claude-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [--model <model|opus|sonnet|haiku>] [--effort <low|medium|high|xhigh|max>] [focus text]",
" node scripts/claude-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|opus|sonnet|haiku>] [--effort <low|medium|high|xhigh|max>] [prompt]",
" node scripts/claude-companion.mjs status [job-id] [--all] [--json]",
" node scripts/claude-companion.mjs result [job-id] [--json]",
" node scripts/claude-companion.mjs cancel [job-id] [--json]",
Expand Down Expand Up @@ -462,6 +474,11 @@ async function executeReviewRun(request) {
ensureClaudeReady(request.cwd);
ensureGitRepository(request.cwd);

// Sweep dead resources from previous crashed runs before allocating new ones.
try { pruneStaleReviewWorktrees(request.cwd); } catch {}
try { pruneStaleSandboxSettings(); } catch {}
try { pruneStaleReviewMcpConfigs(); } catch {}

const target = resolveReviewTarget(request.cwd, {
base: request.base,
scope: request.scope
Expand All @@ -470,19 +487,32 @@ async function executeReviewRun(request) {
const reviewName = request.reviewName ?? "Review";

if (reviewName === "Review") {
// Standard review via Claude CLI — read-only sandbox
// Standard review via Claude CLI — read-only sandbox + ephemeral worktree.
const context = collectReviewContext(request.cwd, target);
const prompt = buildReviewPrompt(context);
const sandboxSettingsFile = createSandboxSettings("read-only");
let result;
const sandboxSettingsFile = createSandboxSettings("read-only");
try {
result = await runClaudeReview(request.cwd, prompt, {
model: request.model,
onProgress: request.onProgress,
onSpawn: request.onSpawn,
permissionMode: "dontAsk",
settingsFile: sandboxSettingsFile,
});
const isolation = createReviewIsolation(request.cwd, target, { label: "review" });
try {
const mcpConfigFile = createReviewMcpConfig(isolation.gitRoot);
try {
result = await runClaudeReview(isolation.cwd, prompt, {
model: request.model,
effort: request.effort,
onProgress: request.onProgress,
onSpawn: request.onSpawn,
permissionMode: "dontAsk",
settingsFile: sandboxSettingsFile,
mcpConfigFile,
strictMcpConfig: true,
});
} finally {
cleanupReviewMcpConfig(mcpConfigFile);
}
} finally {
isolation.cleanup();
}
} finally {
cleanupSandboxSettings(sandboxSettingsFile);
}
Expand Down Expand Up @@ -523,25 +553,40 @@ async function executeReviewRun(request) {
};
}

// Adversarial review with structured output — read-only sandbox
// Adversarial review with structured output — read-only sandbox + ephemeral worktree.
const context = collectReviewContext(request.cwd, target);
const prompt = buildAdversarialReviewPrompt(context, focusText);
const schema = readOutputSchema(REVIEW_SCHEMA_PATH);
const sandboxSettingsFile = createSandboxSettings("read-only");
let result;
const sandboxSettingsFile = createSandboxSettings("read-only");
try {
result = await runClaudeAdversarialReview(
context.repoRoot,
prompt,
schema,
{
model: request.model,
onProgress: request.onProgress,
onSpawn: request.onSpawn,
permissionMode: "dontAsk",
settingsFile: sandboxSettingsFile,
const isolation = createReviewIsolation(context.repoRoot, target, {
label: "adversarial-review",
});
try {
const mcpConfigFile = createReviewMcpConfig(isolation.gitRoot);
try {
result = await runClaudeAdversarialReview(
isolation.cwd,
prompt,
schema,
{
model: request.model,
effort: request.effort,
onProgress: request.onProgress,
onSpawn: request.onSpawn,
permissionMode: "dontAsk",
settingsFile: sandboxSettingsFile,
mcpConfigFile,
strictMcpConfig: true,
}
);
} finally {
cleanupReviewMcpConfig(mcpConfigFile);
}
);
} finally {
isolation.cleanup();
}
} finally {
cleanupSandboxSettings(sandboxSettingsFile);
}
Expand Down Expand Up @@ -825,11 +870,12 @@ function buildReviewRequest({
base,
scope,
model,
effort,
focusText,
reviewName,
markViewedOnSuccess
}) {
return { cwd, base, scope, model, focusText, reviewName, markViewedOnSuccess };
return { cwd, base, scope, model, effort, focusText, reviewName, markViewedOnSuccess };
}

function spawnDetachedReviewWorker(cwd, jobId) {
Expand Down Expand Up @@ -1183,7 +1229,7 @@ async function resolveLatestResumableSession(cwd, options = {}) {

async function handleReviewCommand(argv, config) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["base", "scope", "model", "cwd", "view-state", "job-id", "owner-session-id"],
valueOptions: ["base", "scope", "model", "effort", "cwd", "view-state", "job-id", "owner-session-id"],
booleanOptions: ["json", "background", "wait"],
aliasMap: {
m: "model"
Expand All @@ -1205,6 +1251,10 @@ async function handleReviewCommand(argv, config) {
Boolean(options.background)
);

const requestedModel = normalizeRequestedModel(options.model);
const resolvedModel = resolveDefaultModel(requestedModel);
const resolvedEffort = resolveDefaultEffort(resolvedModel, options.effort);

await withReleasedReservation(workspaceRoot, explicitJobId, async () => {
// Validate inside the reservation guard so failures do not leak markers.
config.validateRequest?.(target, focusText);
Expand All @@ -1227,7 +1277,8 @@ async function handleReviewCommand(argv, config) {
cwd,
base: options.base,
scope: options.scope,
model: options.model,
model: resolvedModel,
effort: resolvedEffort,
focusText,
reviewName: config.reviewName,
markViewedOnSuccess
Expand All @@ -1248,7 +1299,8 @@ async function handleReviewCommand(argv, config) {
cwd,
base: options.base,
scope: options.scope,
model: options.model,
model: resolvedModel,
effort: resolvedEffort,
focusText,
reviewName: config.reviewName,
onProgress: progress,
Expand Down Expand Up @@ -1300,8 +1352,10 @@ async function handleTask(argv) {
const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);

const model = normalizeRequestedModel(options.model);
const effort = resolveEffort(options.effort) ?? null;
const requestedModel = normalizeRequestedModel(options.model);
const model = resolveDefaultModel(requestedModel);
const resolvedEffort = resolveDefaultEffort(model, options.effort);
const effort = resolvedEffort ? resolveEffort(resolvedEffort) : null;
const prompt = readTaskPrompt(cwd, options, positionals);
const markViewedOnSuccess = resolveMarkViewedOnSuccess(
options["view-state"],
Expand Down Expand Up @@ -1819,11 +1873,20 @@ async function main() {
case "cancel":
await handleCancel(argv);
break;
case "mcp-git":
await handleMcpGit(argv);
break;
default:
throw new Error(`Unknown subcommand: ${subcommand}`);
}
}

async function handleMcpGit(_argv) {
const { runMcpGitServer } = await import("./lib/mcp-git.mjs");
const exitCode = await runMcpGitServer();
process.exit(exitCode ?? 0);
}

main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
Expand Down
Loading
Loading