diff --git a/README.md b/README.md index ac4d1d8..4a3fa87 100644 --- a/README.md +++ b/README.md @@ -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 `, `--scope `, `--wait`, `--background`, `--model `, `--effort ` +**Flags:** `--base `, `--scope `, `--wait`, `--background`, `--model `, `--effort ` -**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. @@ -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 ` | 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 ` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max` (default: `xhigh` for opus, `high` for sonnet, unset for haiku) | +| `--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 ` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max` (default: `xhigh` for opus, `high` for sonnet and fable, unset for haiku) | | `--prompt-file ` | 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. @@ -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 diff --git a/internal-skills/cli-runtime/runtime.md b/internal-skills/cli-runtime/runtime.md index d797090..7eff100 100644 --- a/internal-skills/cli-runtime/runtime.md +++ b/internal-skills/cli-runtime/runtime.md @@ -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 ` 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`. diff --git a/scripts/claude-companion.mjs b/scripts/claude-companion.mjs index af7ed83..1ea9534 100644 --- a/scripts/claude-companion.mjs +++ b/scripts/claude-companion.mjs @@ -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 @@ -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 ] [--scope ] [--model ] [--effort ]", - " node scripts/claude-companion.mjs adversarial-review [--wait|--background] [--base ] [--scope ] [--model ] [--effort ] [focus text]", - " node scripts/claude-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [prompt]", + " node scripts/claude-companion.mjs review [--wait|--background] [--base ] [--scope ] [--model ] [--effort ]", + " node scripts/claude-companion.mjs adversarial-review [--wait|--background] [--base ] [--scope ] [--model ] [--effort ] [focus text]", + " node scripts/claude-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [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]", diff --git a/scripts/lib/claude-cli.mjs b/scripts/lib/claude-cli.mjs index 6f76da1..9e4d073 100644 --- a/scripts/lib/claude-cli.mjs +++ b/scripts/lib/claude-cli.mjs @@ -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) { diff --git a/skills/adversarial-review/SKILL.md b/skills/adversarial-review/SKILL.md index ab48bb3..e0a9711 100644 --- a/skills/adversarial-review/SKILL.md +++ b/skills/adversarial-review/SKILL.md @@ -16,7 +16,7 @@ Unlike `$cc:review`, this skill accepts custom focus text after the flags. The m Resolve `` as two directories above this `SKILL.md` file. Always run the companion from that active plugin root: `node "/scripts/claude-companion.mjs" adversarial-review ...` -Supported arguments: `--wait`, `--background`, `--base `, `--scope auto|working-tree|branch`, `--model `, `--effort `, 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 `, `--scope auto|working-tree|branch`, `--model `, `--effort `, 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` diff --git a/skills/rescue/SKILL.md b/skills/rescue/SKILL.md index b3cfacd..73936cc 100644 --- a/skills/rescue/SKILL.md +++ b/skills/rescue/SKILL.md @@ -24,6 +24,8 @@ Raw slash-command arguments: Supported arguments: `--background`, `--wait`, `--resume`, `--resume-last`, `--fresh`, `--write`, `--model `, `--effort `, `--prompt-file `, 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. diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index 6d2c409..ee70a02 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -16,7 +16,7 @@ If the overall request is "you review it too, also ask Claude to review in the b Resolve `` as two directories above this `SKILL.md` file. Always run the companion from that active plugin root: `node "/scripts/claude-companion.mjs" review ...` -Supported arguments: `--wait`, `--background`, `--base `, `--scope auto|working-tree|branch`, `--model `, `--effort ` (defaults: model=opus, effort=xhigh; sonnet defaults to high; haiku has no effort) +Supported arguments: `--wait`, `--background`, `--base `, `--scope auto|working-tree|branch`, `--model `, `--effort ` (defaults: model=opus, effort=xhigh; sonnet and fable default to high; haiku has no effort) Raw slash-command arguments: `$ARGUMENTS` diff --git a/tests/claude-cli.test.mjs b/tests/claude-cli.test.mjs index 58d73a8..bf5c2fc 100644 --- a/tests/claude-cli.test.mjs +++ b/tests/claude-cli.test.mjs @@ -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); @@ -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); }); }); @@ -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"); }); @@ -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); @@ -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); }); }); @@ -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"); diff --git a/tests/integration/claude-companion.test.mjs b/tests/integration/claude-companion.test.mjs index 3f09d55..523614b 100644 --- a/tests/integration/claude-companion.test.mjs +++ b/tests/integration/claude-companion.test.mjs @@ -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 = { diff --git a/tests/skills-contracts.test.mjs b/tests/skills-contracts.test.mjs index b34d2f9..01e75ed 100644 --- a/tests/skills-contracts.test.mjs +++ b/tests/skills-contracts.test.mjs @@ -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"],