From 32b57d647adcfb1938224dc0ee8a30d0d63399fe Mon Sep 17 00:00:00 2001 From: jinku Date: Wed, 29 Jul 2026 01:11:11 -0700 Subject: [PATCH] fix: stop defaulting reasoning effort per model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.4.0 gave every friendly alias a `high` default effort. That duplicates a catalog the host CLI owns, exactly like the pinned model IDs that release removed, and it is wrong for `haiku` — Haiku 4.5 is not in the reasoning-effort model tier. Delete `DEFAULT_EFFORT_BY_MODEL` / `resolveDefaultEffort` at the shared boundary so `buildArgs()` is the only thing that emits the flag. `--model` still defaults to `opus`; effort is forwarded only when the user asks for it, and each model keeps whatever Claude Code defaults to. Also gate `--model` / `--effort` on their resolved values. Both resolvers return undefined for whitespace-only input, which previously reached spawn() argv as undefined and threw ERR_INVALID_ARG_TYPE. Co-Authored-By: Claude Opus 5 (1M context) --- .codex-plugin/plugin.json | 2 +- CHANGELOG.md | 7 ++- README.md | 12 ++--- internal-skills/cli-runtime/runtime.md | 2 +- package.json | 2 +- scripts/claude-companion.mjs | 11 ++--- scripts/lib/claude-cli.mjs | 33 ++++++------- skills/adversarial-review/SKILL.md | 6 +-- skills/rescue/SKILL.md | 6 +-- skills/review/SKILL.md | 6 +-- tests/claude-cli.test.mjs | 53 +++++++++------------ tests/integration/claude-companion.test.mjs | 14 +++--- tests/skills-contracts.test.mjs | 23 +++++---- 13 files changed, 86 insertions(+), 91 deletions(-) diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index b011a30..516d818 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cc", - "version": "1.4.0", + "version": "1.4.1", "description": "Claude Code Plugin for Codex. Delegate code reviews, investigations, and tracked tasks to Claude Code from inside Codex.", "author": { "name": "Sendbird, Inc.", diff --git a/CHANGELOG.md b/CHANGELOG.md index edf6d81..93e70f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Changelog +## v1.4.1 + +- Stop defaulting reasoning effort per model. v1.4.0 gave every friendly alias a `high` default, which duplicated a catalog that belongs to the host CLI — exactly like the pinned model IDs removed in that same release — and was wrong for `haiku`, since Haiku 4.5 is not in the reasoning-effort model tier. `--effort` is now forwarded only when you pass it, so each model keeps whatever effort Claude Code defaults to and Claude Code stays the authority on which levels a model supports. `--model` still defaults to `opus`. Users who relied on the v1.2.0 `opus` + `xhigh` behavior should pass `--effort xhigh` explicitly. +- Gate `--model` and `--effort` on their resolved values in `buildArgs()`. Both resolvers return `undefined` for whitespace-only input, so a blank flag value pushed `undefined` into the spawn argv and failed with `ERR_INVALID_ARG_TYPE`. Blank values are now dropped like any other unset flag. + ## v1.4.0 -- Delegate Claude model resolution to Claude Code. `--model` now forwards every value through unchanged, the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` are matched case-insensitively, and the plugin no longer pins `claude-opus-4-7[1m]` / `claude-sonnet-4-6[1m]` or forces the 1M-context variant. Run `/model` in Claude Code to discover what your account and provider actually offer. Every friendly alias now defaults to `high` effort, so `opus` moves from `xhigh` to `high` and `haiku` moves from unset to `high` unless you pass `--effort`. +- Delegate Claude model resolution to Claude Code. `--model` now forwards every value through unchanged, the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` are matched case-insensitively, and the plugin no longer pins `claude-opus-4-7[1m]` / `claude-sonnet-4-6[1m]` or forces the 1M-context variant. Run `/model` in Claude Code to discover what your account and provider actually offer. Effort is also no longer inferred from the model; see v1.4.1, which corrects the per-alias `high` default this release introduced. - Add `fable` as a supported model alias across `review`, `adversarial-review`, and `rescue`, forwarded to Claude Code rather than pinned to a version-specific model ID. - Stop pinning `spawn_agent` routing to a Codex-owned catalog. Codex 0.145 only advertises `agent_type` when custom agents are configured and dropped `gpt-5.4-mini` / `gpt-5.4` from the child-model catalog, which broke every built-in background forwarding spawn. The forwarding children now omit `agent_type` and `model`, inheriting the built-in default agent and the parent model while keeping `reasoning_effort: "medium"`. - Request targeted `sandbox_permissions: "require_escalated"` for the single Claude companion command in the review, adversarial-review, and rescue forwarding paths. Codex's default `workspace-write` sandbox has no outbound network, so the companion previously started Claude Code only for it to fail with `API Error: Unable to connect to API (ENOTFOUND)`. Global `network_access` is still not required. diff --git a/README.md b/README.md index 9e3e4d6..e822a40 100644 --- a/README.md +++ b/README.md @@ -117,20 +117,20 @@ Quick routing rule: Standard read-only review of your current work. ```text -$cc:review # review uncommitted changes (default: opus + high effort) +$cc:review # review uncommitted changes (default: opus) $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 sonnet # switch to sonnet +$cc:review --model fable # use Fable $cc:review --model opus --effort xhigh # explicitly raise opus effort ``` **Flags:** `--base `, `--scope `, `--wait`, `--background`, `--model `, `--effort ` -**Defaults:** model `opus` with `high` effort. After trimming surrounding whitespace, the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` are matched case-insensitively and canonicalized to lowercase; every friendly alias defaults to `high` effort. Every other `--model` value passes through unchanged for Claude Code to resolve, including full model IDs and provider-specific names, and receives no inferred effort. Pass `--model` and `--effort` to override. +**Defaults:** model `opus`, and no effort at all. After trimming surrounding whitespace, the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` are matched case-insensitively and canonicalized to lowercase; every other `--model` value passes through unchanged for Claude Code to resolve, including full model IDs and provider-specific names. `--effort` is forwarded only when you pass it, so each model keeps whatever effort Claude Code defaults to. Claude Code owns which effort levels each model supports, so check `/model` rather than assuming a level applies everywhere. -**Model discovery:** run `/model` in Claude Code to see the models and effort levels available to your current account and provider, then pass the selected alias or full ID to this plugin. The plugin intentionally does not maintain a static model catalog or force `[1m]`; Claude Code owns alias versions, managed restrictions, provider routing, and extended-context eligibility. +**Model discovery:** run `/model` in Claude Code to see the models and effort levels available to your current account and provider, then pass the selected alias or full ID to this plugin. The plugin intentionally does not maintain a static model catalog, a per-model effort table, or force `[1m]`; Claude Code owns alias versions, supported effort levels, managed restrictions, provider routing, and extended-context eligibility. Scope `auto` (the default) inspects `git status` and chooses between working-tree and branch automatically. @@ -175,7 +175,7 @@ $cc:rescue --model sonnet --effort medium investigate the flaky test | `--fresh` | Force a new task (don't resume) | | `--write` | Allow file edits (default) | | `--model ` | Any Claude Code model alias, full model ID, or provider-specific name; defaults to `opus`. Run `/model` in Claude Code to discover options available to your account and provider. | -| `--effort ` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max` (default: `high` for every friendly model alias) | +| `--effort ` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max`. Unset by default, so the model keeps Claude Code's own effort default. Claude Code owns which levels each model supports. | | `--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. diff --git a/internal-skills/cli-runtime/runtime.md b/internal-skills/cli-runtime/runtime.md index 5a7a24f..1171187 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`; fable, opus, sonnet, and haiku each default to `high` effort. +- Leave `--model` and `--effort` unset unless the user explicitly asks for a specific model or effort. The companion command defaults model to `opus` and forwards no `--effort` at all, so `fable`, `opus`, `sonnet`, and `haiku` each keep whatever effort Claude Code defaults to for them. Claude Code owns which effort levels each model supports; this plugin owns no per-model effort table. - Forward an explicit `--model` value unchanged to the companion. The companion trims surrounding whitespace, canonicalizes the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` to lowercase, then forwards every other `--model` value unchanged to Claude Code. Claude Code owns alias resolution; `/model` is the authoritative picker for the current account and provider. - `--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. diff --git a/package.json b/package.json index 2f3a5a9..1f04275 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cc-plugin-codex", - "version": "1.4.0", + "version": "1.4.1", "description": "Claude Code Plugin for Codex by Sendbird", "type": "module", "author": { diff --git a/scripts/claude-companion.mjs b/scripts/claude-companion.mjs index f897f38..0124e33 100644 --- a/scripts/claude-companion.mjs +++ b/scripts/claude-companion.mjs @@ -12,7 +12,7 @@ * - Uses claude-cli.mjs instead of app-server/broker * - Friendly model aliases canonicalize to lowercase; other names pass through * - Default model when --model is unset: opus - * - Default effort for friendly model aliases: high + * - No effort default: --effort is forwarded only when the user passes it * - 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 @@ -45,7 +45,6 @@ import { resolveModel, resolveEffort, resolveDefaultModel, - resolveDefaultEffort, SANDBOX_READ_ONLY_TOOLS, createSandboxSettings, cleanupSandboxSettings, @@ -1512,7 +1511,6 @@ async function handleReviewCommand(argv, config) { const requestedModel = resolveModel(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. @@ -1537,7 +1535,7 @@ async function handleReviewCommand(argv, config) { base: options.base, scope: options.scope, model: resolvedModel, - effort: resolvedEffort, + effort: options.effort, focusText, reviewName: config.reviewName, markViewedOnSuccess @@ -1559,7 +1557,7 @@ async function handleReviewCommand(argv, config) { base: options.base, scope: options.scope, model: resolvedModel, - effort: resolvedEffort, + effort: options.effort, focusText, reviewName: config.reviewName, onProgress: progress, @@ -1613,8 +1611,7 @@ async function handleTask(argv) { const requestedModel = resolveModel(options.model); const model = resolveDefaultModel(requestedModel); - const resolvedEffort = resolveDefaultEffort(model, options.effort); - const effort = resolvedEffort ? resolveEffort(resolvedEffort) : null; + const effort = options.effort ? resolveEffort(options.effort) : null; const prompt = readTaskPrompt(cwd, options, positionals); const markViewedOnSuccess = resolveMarkViewedOnSuccess( options["view-state"], diff --git a/scripts/lib/claude-cli.mjs b/scripts/lib/claude-cli.mjs index e80c684..68ffdbb 100644 --- a/scripts/lib/claude-cli.mjs +++ b/scripts/lib/claude-cli.mjs @@ -648,13 +648,11 @@ export const DEFAULT_MODEL = "opus"; const FRIENDLY_ALIASES = new Set(["fable", "opus", "sonnet", "haiku"]); -export const DEFAULT_EFFORT_BY_MODEL = new Map([ - ["fable", "high"], - ["opus", "high"], - ["sonnet", "high"], - ["haiku", "high"], -]); - +// No per-model effort defaults. Which effort levels a model supports — and what +// it falls back to — is owned by Claude Code, not this plugin: Haiku 4.5 is not +// in the reasoning-effort model tier at all. Any table here would have to track +// the host's model catalog and would rot exactly like the pinned model IDs did, +// so `--effort` is forwarded only when the user asks for it. export function resolveDefaultModel(model) { if (model == null || String(model).trim() === "") { return DEFAULT_MODEL; @@ -662,14 +660,6 @@ export function resolveDefaultModel(model) { return model; } -export function resolveDefaultEffort(model, effort) { - if (effort != null && String(effort).trim() !== "") { - return effort; - } - const key = String(model ?? "").trim().toLowerCase(); - return DEFAULT_EFFORT_BY_MODEL.get(key); -} - export function resolveModel(model) { if (model == null) return undefined; const normalized = String(model).trim(); @@ -719,11 +709,16 @@ export function buildArgs(prompt, options = {}) { if (options.noSessionPersistence) { args.push("--no-session-persistence"); } - if (options.model) { - args.push("--model", resolveModel(options.model)); + // Gate on the resolved value, not the raw input: both resolvers return + // undefined for whitespace-only strings, and pushing undefined into argv + // throws ERR_INVALID_ARG_TYPE in spawn(). + const model = resolveModel(options.model); + if (model) { + args.push("--model", model); } - if (options.effort) { - args.push("--effort", resolveEffort(options.effort)); + const effort = resolveEffort(options.effort); + if (effort) { + args.push("--effort", effort); } if (options.sessionId) { args.push("--session-id", options.sessionId); diff --git a/skills/adversarial-review/SKILL.md b/skills/adversarial-review/SKILL.md index 980d7f0..c5dd7cc 100644 --- a/skills/adversarial-review/SKILL.md +++ b/skills/adversarial-review/SKILL.md @@ -1,6 +1,6 @@ --- name: adversarial-review -description: 'Run a design-challenging Claude Code review of local git changes in this repository. Args: --wait, --background, --base , --scope , --model , --effort , [focus text]. Defaults to opus + high effort. Use only when the user wants stronger scrutiny than a normal review, such as explicit tradeoff challenge, risky-change review, or custom focus text.' +description: 'Run a design-challenging Claude Code review of local git changes in this repository. Args: --wait, --background, --base , --scope , --model , --effort , [focus text]. Defaults to opus with no forced effort. Use only when the user wants stronger scrutiny than a normal review, such as explicit tradeoff challenge, risky-change review, or custom focus text.' --- # Claude Code Adversarial Review @@ -16,9 +16,9 @@ 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; fable, opus, sonnet, and haiku each default to high effort) +Supported arguments: `--wait`, `--background`, `--base `, `--scope auto|working-tree|branch`, `--model `, `--effort `, plus optional focus text after the flags (defaults: model=opus and no effort; `fable`, `opus`, `sonnet`, and `haiku` each keep Claude Code's own effort default, and Claude Code owns which effort levels each model supports) -Forward `--model` unchanged to the companion. The companion trims surrounding whitespace, canonicalizes the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` to lowercase, then forwards every other `--model` value unchanged to Claude Code. Claude Code owns alias resolution; `/model` is the authoritative picker for the current account and provider. +Forward `--model` unchanged to the companion. The companion trims surrounding whitespace, canonicalizes the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` to lowercase, then forwards every other `--model` value unchanged to Claude Code. Claude Code owns alias resolution and supported effort levels; `/model` is the authoritative picker for the current account and provider. Raw slash-command arguments: `$ARGUMENTS` diff --git a/skills/rescue/SKILL.md b/skills/rescue/SKILL.md index e9652de..037e6cd 100644 --- a/skills/rescue/SKILL.md +++ b/skills/rescue/SKILL.md @@ -1,6 +1,6 @@ --- name: rescue -description: 'Delegate a substantial diagnosis, implementation, or follow-up task to Claude Code through the tracked-job runtime. Args: --background, --wait, --resume, --resume-last, --fresh, --write, --model , --effort , --prompt-file , [task text]. Defaults to opus + high effort. Use when Claude should investigate or change things, not when the user only wants review findings.' +description: 'Delegate a substantial diagnosis, implementation, or follow-up task to Claude Code through the tracked-job runtime. Args: --background, --wait, --resume, --resume-last, --fresh, --write, --model , --effort , --prompt-file , [task text]. Defaults to opus with no forced effort. Use when Claude should investigate or change things, not when the user only wants review findings.' --- # Claude Code Rescue @@ -24,9 +24,9 @@ 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; fable, opus, sonnet, and haiku each default to high effort. +Companion defaults: model=opus, and no effort. The companion forwards `--effort` only when the user passes it, so `fable`, `opus`, `sonnet`, and `haiku` each keep Claude Code's own effort default. Claude Code owns which effort levels each model supports. -Forward `--model` unchanged to the companion. The companion trims surrounding whitespace, canonicalizes the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` to lowercase, then forwards every other `--model` value unchanged to Claude Code. Claude Code owns alias resolution; `/model` is the authoritative picker for the current account and provider. +Forward `--model` unchanged to the companion. The companion trims surrounding whitespace, canonicalizes the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` to lowercase, then forwards every other `--model` value unchanged to Claude Code. Claude Code owns alias resolution and supported effort levels; `/model` is the authoritative picker for the current account and provider. 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. diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index f267e19..66c8ecd 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -1,6 +1,6 @@ --- name: review -description: 'Run a standard Claude Code review of local git changes in this repository. Args: --wait, --background, --base , --scope , --model , --effort . Defaults to opus + high effort. Use as the default path for ordinary code-review requests when the user did not explicitly ask for stronger adversarial scrutiny or for Claude to own the implementation work.' +description: 'Run a standard Claude Code review of local git changes in this repository. Args: --wait, --background, --base , --scope , --model , --effort . Defaults to opus with no forced effort. Use as the default path for ordinary code-review requests when the user did not explicitly ask for stronger adversarial scrutiny or for Claude to own the implementation work.' --- # Claude Code Review @@ -16,9 +16,9 @@ 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; fable, opus, sonnet, and haiku each default to high effort) +Supported arguments: `--wait`, `--background`, `--base `, `--scope auto|working-tree|branch`, `--model `, `--effort ` (defaults: model=opus and no effort; `fable`, `opus`, `sonnet`, and `haiku` each keep Claude Code's own effort default, and Claude Code owns which effort levels each model supports) -Forward `--model` unchanged to the companion. The companion trims surrounding whitespace, canonicalizes the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` to lowercase, then forwards every other `--model` value unchanged to Claude Code. Claude Code owns alias resolution; `/model` is the authoritative picker for the current account and provider. +Forward `--model` unchanged to the companion. The companion trims surrounding whitespace, canonicalizes the friendly aliases `fable`, `opus`, `sonnet`, and `haiku` to lowercase, then forwards every other `--model` value unchanged to Claude Code. Claude Code owns alias resolution and supported effort levels; `/model` is the authoritative picker for the current account and provider. Raw slash-command arguments: `$ARGUMENTS` diff --git a/tests/claude-cli.test.mjs b/tests/claude-cli.test.mjs index a3cd9aa..5c37212 100644 --- a/tests/claude-cli.test.mjs +++ b/tests/claude-cli.test.mjs @@ -5,19 +5,18 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; +import * as claudeCli from "../scripts/lib/claude-cli.mjs"; import { StreamParser, validateTurnCompletion, resolveModel, resolveEffort, resolveDefaultModel, - resolveDefaultEffort, resolveClaudeBin, buildArgs, EFFORT_ALIASES, VALID_EFFORTS, DEFAULT_MODEL, - DEFAULT_EFFORT_BY_MODEL, SANDBOX_READ_ONLY_BASH_TOOLS, SANDBOX_READ_ONLY_TOOLS, SANDBOX_TEMP_DIR, @@ -464,7 +463,7 @@ describe("resolveModel", () => { }); // =========================================================================== -// resolveDefaultModel / resolveDefaultEffort +// resolveDefaultModel / effort defaults // =========================================================================== describe("resolveDefaultModel", () => { @@ -487,38 +486,30 @@ describe("resolveDefaultModel", () => { }); }); -describe("resolveDefaultEffort", () => { - it("defaults to high for every friendly model alias", () => { - for (const alias of ["fable", "opus", "sonnet", "haiku"]) { - assert.equal(resolveDefaultEffort(alias, null), "high"); - assert.equal(resolveDefaultEffort(alias.toUpperCase(), undefined), "high"); - } - }); - - it("does not infer effort for full or provider-specific model names", () => { - assert.equal(resolveDefaultEffort("claude-opus-4-7[1m]", null), undefined); - assert.equal(resolveDefaultEffort("claude-sonnet-4-6", null), undefined); - assert.equal(resolveDefaultEffort("us.anthropic.claude-opus-custom", null), undefined); - assert.equal(resolveDefaultEffort("some-future-model", null), undefined); - }); - - it("preserves an explicit effort regardless of model", () => { - assert.equal(resolveDefaultEffort("opus", "low"), "low"); - assert.equal(resolveDefaultEffort("sonnet", "medium"), "medium"); - assert.equal(resolveDefaultEffort("haiku", "high"), "high"); - assert.equal(resolveDefaultEffort(null, "max"), "max"); +describe("effort defaults", () => { + it("does not export a per-model effort table", () => { + assert.equal("DEFAULT_EFFORT_BY_MODEL" in claudeCli, false); + assert.equal("resolveDefaultEffort" in claudeCli, false); }); - it("treats blank effort as missing", () => { - assert.equal(resolveDefaultEffort("opus", ""), "high"); - assert.equal(resolveDefaultEffort("opus", " "), "high"); - }); - - it("DEFAULT_EFFORT_BY_MODEL contains the expected entries", () => { + it("omits --effort for every friendly alias unless the caller asks", () => { + // Which effort levels each alias supports is Claude Code's to decide. + // Emitting nothing lets the host apply its own per-model default. for (const alias of ["fable", "opus", "sonnet", "haiku"]) { - assert.equal(DEFAULT_EFFORT_BY_MODEL.get(alias), "high"); + assert.equal(buildArgs("p", { model: alias }).includes("--effort"), false); } - assert.equal(DEFAULT_EFFORT_BY_MODEL.size, 4); + }); + + it("omits whitespace-only --model / --effort instead of pushing undefined", () => { + // Both resolvers return undefined for blank input; pushing that into argv + // throws ERR_INVALID_ARG_TYPE in spawn(). + const args = buildArgs("p", { model: " ", effort: " " }); + assert.equal(args.includes("--model"), false); + assert.equal(args.includes("--effort"), false); + assert.equal( + args.every((value) => typeof value === "string"), + true + ); }); }); diff --git a/tests/integration/claude-companion.test.mjs b/tests/integration/claude-companion.test.mjs index 0840578..d7b70e9 100644 --- a/tests/integration/claude-companion.test.mjs +++ b/tests/integration/claude-companion.test.mjs @@ -922,7 +922,7 @@ describe("claude-companion integration", () => { testEnv.workspaceDir, "--write", "--model", - "haiku", + "sonnet", "--effort", "high", "--prompt-file", @@ -942,7 +942,7 @@ describe("claude-companion integration", () => { const args = JSON.parse(fs.readFileSync(argsFile, "utf8")); assert.equal(args[0], "-p"); assert.ok(args.includes("--model")); - assert.equal(args[args.indexOf("--model") + 1], "haiku"); + assert.equal(args[args.indexOf("--model") + 1], "sonnet"); assert.ok(args.includes("--effort")); assert.equal(args[args.indexOf("--effort") + 1], "high"); assert.ok(args.includes("--permission-mode")); @@ -954,7 +954,7 @@ describe("claude-companion integration", () => { } }); - it("defaults every friendly model alias to high effort", () => { + it("omits --effort for every friendly model alias by default", () => { const testEnv = createTestEnvironment(); try { @@ -981,14 +981,14 @@ describe("claude-companion integration", () => { const args = JSON.parse(fs.readFileSync(argsFile, "utf8")); assert.equal(args[args.indexOf("--model") + 1], alias); - assert.equal(args[args.indexOf("--effort") + 1], "high"); + assert.equal(args.includes("--effort"), false); } } finally { cleanupTestEnvironment(testEnv); } }); - it("canonicalizes native Fable with high default effort through task and review flows", () => { + it("canonicalizes native Fable without inferring effort through task and review flows", () => { const testEnv = createTestEnvironment(); try { @@ -1013,7 +1013,7 @@ describe("claude-companion integration", () => { const taskArgs = JSON.parse(fs.readFileSync(taskArgsFile, "utf8")); assert.equal(taskArgs[taskArgs.indexOf("--model") + 1], "fable"); - assert.equal(taskArgs[taskArgs.indexOf("--effort") + 1], "high"); + assert.equal(taskArgs.includes("--effort"), false); setupGitWorkspace(testEnv.workspaceDir); seedWorkingTreeDiff(testEnv.workspaceDir); @@ -1044,7 +1044,7 @@ describe("claude-companion integration", () => { 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"); + assert.equal(invocation.args.includes("--effort"), false); } } finally { cleanupTestEnvironment(testEnv); diff --git a/tests/skills-contracts.test.mjs b/tests/skills-contracts.test.mjs index 25105ca..3c681ac 100644 --- a/tests/skills-contracts.test.mjs +++ b/tests/skills-contracts.test.mjs @@ -16,7 +16,7 @@ function read(relativePath) { return fs.readFileSync(path.join(PROJECT_ROOT, relativePath), "utf8"); } -test("public model contracts document native Fable support and alias effort policy", () => { +test("public model contracts document native Fable support and host-owned effort defaults", () => { const contracts = [ "README.md", "skills/review/SKILL.md", @@ -28,13 +28,20 @@ test("public model contracts document native Fable support and alias effort poli for (const contractPath of contracts) { const contract = read(contractPath); assert.match(contract, /fable/i, `${contractPath} must document Fable`); - for (const alias of ["fable", "opus", "sonnet", "haiku"]) { - assert.match( - contract, - new RegExp(`${alias}[^\\n]*high|high[^\\n]*${alias}`, "i"), - `${contractPath} must document ${alias}'s high default effort` - ); - } + // Assert only what the plugin controls: no per-model effort default of its + // own, and effort support attributed to Claude Code. Do not pin a claim + // about how the CLI handles a specific model + effort pair; that is the + // host's behavior and this plugin never observes it. + assert.match( + contract, + /Claude Code (owns|defaults)[^\n]*effort|effort[^\n]*Claude Code/i, + `${contractPath} must attribute effort defaults to Claude Code` + ); + assert.doesNotMatch( + contract, + /default(s)? to `?high`? effort/i, + `${contractPath} must not claim a plugin-owned per-model effort default` + ); } });