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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,13 @@ $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 # switch to sonnet (defaults to high effort)
$cc:review --model fable # use Fable (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>`, `--effort <low|medium|high|max>`
**Flags:** `--base <ref>`, `--scope <auto|working-tree|branch>`, `--wait`, `--background`, `--model <model>`, `--effort <low|medium|high|xhigh|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.
**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`. `fable` passes through to Claude Code and also defaults 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 @@ -171,8 +172,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 (`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) |
| `--model <model>` | Claude model (`fable`, `opus`, `sonnet`, `haiku`, or full ID; defaults to `opus`. `fable` passes through to Claude Code. 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 and fable, 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 Expand Up @@ -278,7 +279,7 @@ The review gate is an **optional turn-end hook**. When enabled, Codex runs a Cla
| **Runtime** | Codex app-server + broker | Fresh `claude -p` subprocess per invocation |
| **Review gate trigger** | End of Claude Code turn | End of Codex turn |
| **Review gate target** | Reviews previous Claude response | Reviews previous Codex response |
| **Model flags** | Codex model names and effort controls | Claude model names and effort values (`low` / `medium` / `high` / `max`) |
| **Model flags** | Codex model names and effort controls | Claude model names and effort values (`low` / `medium` / `high` / `xhigh` / `max`) |

### Where This Goes Further

Expand Down
2 changes: 1 addition & 1 deletion internal-skills/cli-runtime/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +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 `--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.
- 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 fable, 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
8 changes: 4 additions & 4 deletions scripts/claude-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* - Uses claude-cli.mjs instead of app-server/broker
* - 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
* - Default effort by model: opus -> xhigh, sonnet|fable -> 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
Expand Down Expand Up @@ -137,9 +137,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>] [--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 review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [--model <model|fable|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|fable|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|fable|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
1 change: 1 addition & 0 deletions scripts/lib/claude-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ export const DEFAULT_EFFORT_BY_MODEL = new Map([
["sonnet", "high"],
["claude-sonnet-4-6", "high"],
["claude-sonnet-4-6[1m]", "high"],
["fable", "high"],
]);

export function resolveDefaultModel(model) {
Expand Down
2 changes: 1 addition & 1 deletion skills/adversarial-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Unlike `$cc:review`, this skill accepts custom focus text after the flags. The m
Resolve `<plugin-root>` as two directories above this `SKILL.md` file. Always run the companion from that active plugin root:
`node "<plugin-root>/scripts/claude-companion.mjs" adversarial-review ...`

Supported arguments: `--wait`, `--background`, `--base <ref>`, `--scope auto|working-tree|branch`, `--model <model>`, `--effort <low|medium|high|xhigh|max>`, plus optional focus text after the flags (defaults: model=opus, effort=xhigh; sonnet defaults to high; haiku has no effort)
Supported arguments: `--wait`, `--background`, `--base <ref>`, `--scope auto|working-tree|branch`, `--model <model>`, `--effort <low|medium|high|xhigh|max>`, plus optional focus text after the flags (defaults: model=opus, effort=xhigh; sonnet and fable default to high; haiku has no effort)

Raw slash-command arguments:
`$ARGUMENTS`
Expand Down
2 changes: 2 additions & 0 deletions skills/rescue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Raw slash-command arguments:

Supported arguments: `--background`, `--wait`, `--resume`, `--resume-last`, `--fresh`, `--write`, `--model <model>`, `--effort <low|medium|high|xhigh|max>`, `--prompt-file <path>`, plus free-text task text

Companion defaults: model=opus, effort=xhigh; sonnet and fable default to high; haiku has no effort.

Main-thread routing rules:
- If the user explicitly invoked `$cc:rescue` or `Claude Code Rescue`, do not keep the work in the main Codex thread. Delegate it.
- If the user did not supply a task, ask what Claude Code should investigate or fix.
Expand Down
2 changes: 1 addition & 1 deletion skills/review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ If the overall request is "you review it too, also ask Claude to review in the b
Resolve `<plugin-root>` as two directories above this `SKILL.md` file. Always run the companion from that active plugin root:
`node "<plugin-root>/scripts/claude-companion.mjs" review ...`

Supported arguments: `--wait`, `--background`, `--base <ref>`, `--scope auto|working-tree|branch`, `--model <model>`, `--effort <low|medium|high|xhigh|max>` (defaults: model=opus, effort=xhigh; sonnet defaults to high; haiku has no effort)
Supported arguments: `--wait`, `--background`, `--base <ref>`, `--scope auto|working-tree|branch`, `--model <model>`, `--effort <low|medium|high|xhigh|max>` (defaults: model=opus, effort=xhigh; sonnet and fable default to high; haiku has no effort)

Raw slash-command arguments:
`$ARGUMENTS`
Expand Down
19 changes: 19 additions & 0 deletions tests/claude-cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,10 @@ describe("resolveModel", () => {
assert.equal(resolveModel("claude-3-opus-20240229"), "claude-3-opus-20240229");
});

it("passes the native 'fable' alias through unchanged", () => {
assert.equal(resolveModel("fable"), "fable");
});

it("returns undefined for null/undefined input", () => {
assert.equal(resolveModel(null), undefined);
assert.equal(resolveModel(undefined), undefined);
Expand All @@ -458,6 +462,7 @@ describe("resolveModel", () => {
assert.ok(MODEL_ALIASES.has("opus"));
assert.ok(MODEL_ALIASES.has("sonnet"));
assert.ok(MODEL_ALIASES.has("haiku"));
assert.equal(MODEL_ALIASES.has("fable"), false);
});
});

Expand All @@ -475,6 +480,7 @@ describe("resolveDefaultModel", () => {

it("passes through an explicit model", () => {
assert.equal(resolveDefaultModel("sonnet"), "sonnet");
assert.equal(resolveDefaultModel("fable"), "fable");
assert.equal(resolveDefaultModel("haiku"), "haiku");
assert.equal(resolveDefaultModel("claude-opus-4-7"), "claude-opus-4-7");
});
Expand All @@ -498,6 +504,11 @@ describe("resolveDefaultEffort", () => {
assert.equal(resolveDefaultEffort("claude-sonnet-4-6[1m]", null), "high");
});

it("defaults to high for the native fable alias", () => {
assert.equal(resolveDefaultEffort("fable", null), "high");
assert.equal(resolveDefaultEffort("FABLE", undefined), "high");
});

it("returns undefined for haiku (no effort default)", () => {
assert.equal(resolveDefaultEffort("haiku", null), undefined);
assert.equal(resolveDefaultEffort("claude-haiku-4-5", undefined), undefined);
Expand Down Expand Up @@ -526,6 +537,7 @@ describe("resolveDefaultEffort", () => {
assert.equal(DEFAULT_EFFORT_BY_MODEL.get("sonnet"), "high");
assert.equal(DEFAULT_EFFORT_BY_MODEL.get("claude-sonnet-4-6"), "high");
assert.equal(DEFAULT_EFFORT_BY_MODEL.get("claude-sonnet-4-6[1m]"), "high");
assert.equal(DEFAULT_EFFORT_BY_MODEL.get("fable"), "high");
assert.equal(DEFAULT_EFFORT_BY_MODEL.has("haiku"), false);
});
});
Expand Down Expand Up @@ -735,6 +747,13 @@ describe("buildArgs", () => {
assert.equal(args[idx + 1], "claude-sonnet-4-6[1m]");
});

it("includes the native fable alias unchanged", () => {
const args = buildArgs("p", { model: "fable" });
const idx = args.indexOf("--model");
assert.ok(idx >= 0);
assert.equal(args[idx + 1], "fable");
});

it("includes --effort with resolved effort", () => {
const args = buildArgs("p", { effort: "xhigh" });
const idx = args.indexOf("--effort");
Expand Down
63 changes: 63 additions & 0 deletions tests/integration/claude-companion.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,69 @@ describe("claude-companion integration", () => {
}
});

it("passes native Fable with high default effort through task and review flows", () => {
const testEnv = createTestEnvironment();

try {
const taskArgsFile = path.join(testEnv.rootDir, "fable-task-args.json");
runCompanion(
[
"task",
"--cwd",
testEnv.workspaceDir,
"--model",
"fable",
"--quiet-progress",
"fable task delay=20",
],
{
env: {
...testEnv.env,
CLAUDE_ARGS_FILE: taskArgsFile,
},
}
);

const taskArgs = JSON.parse(fs.readFileSync(taskArgsFile, "utf8"));
assert.equal(taskArgs[taskArgs.indexOf("--model") + 1], "fable");
assert.equal(taskArgs[taskArgs.indexOf("--effort") + 1], "high");

setupGitWorkspace(testEnv.workspaceDir);
seedWorkingTreeDiff(testEnv.workspaceDir);

for (const [command, focusText] of [
["review", []],
["adversarial-review", ["focus on model routing"]],
]) {
const invocationFile = path.join(testEnv.rootDir, `fable-${command}-invocation.json`);
runCompanion(
[
command,
"--cwd",
testEnv.workspaceDir,
"--scope",
"working-tree",
"--model",
"fable",
...focusText,
],
{
env: {
...testEnv.env,
CLAUDE_INVOCATION_FILE: invocationFile,
},
}
);

const invocation = JSON.parse(fs.readFileSync(invocationFile, "utf8"));
assert.equal(invocation.args[invocation.args.indexOf("--model") + 1], "fable");
assert.equal(invocation.args[invocation.args.indexOf("--effort") + 1], "high");
}
} finally {
cleanupTestEnvironment(testEnv);
}
});

it("uses --resume to continue the latest session and keeps --fresh from injecting a resume id", async () => {
const testEnv = createTestEnvironment();
const sessionEnv = {
Expand Down
16 changes: 16 additions & 0 deletions tests/skills-contracts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ function read(relativePath) {
return fs.readFileSync(path.join(PROJECT_ROOT, relativePath), "utf8");
}

test("public model contracts document native Fable support", () => {
const contracts = [
"README.md",
"skills/review/SKILL.md",
"skills/adversarial-review/SKILL.md",
"skills/rescue/SKILL.md",
"internal-skills/cli-runtime/runtime.md",
];

for (const contractPath of contracts) {
const contract = read(contractPath);
assert.match(contract, /fable/i, `${contractPath} must document Fable`);
assert.match(contract, /fable[^\n]*high|high[^\n]*fable/i, `${contractPath} must document Fable's high default effort`);
}
});

test("built-in child commands preserve the workspace for reserved job ids", () => {
const skills = [
["rescue", "skills/rescue/SKILL.md"],
Expand Down