From b181c2f8c3d9be03f361864891f4cfd3c4bae001 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 6 Aug 2026 10:20:22 -0400 Subject: [PATCH] fix(#3039): clamp max/xhigh effort to high for Claude-runtime skills (#3119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#3039): clamp max/xhigh effort to high for Claude-runtime skills effort: max in plan-phase, execute-phase, and autonomous SKILL.md frontmatter was passed through as output_config.effort, which the Anthropic API rejects when extended thinking is disabled (400: effort 'max' is not supported when thinking is disabled on this model). The frontmatter is static at install time and the installer cannot know whether thinking will be on or off at invocation. normalizeClaudeSkillEffort now clamps both 'max' and 'xhigh' to 'high' — the maximum value that works in both thinking states on all supported models. Applied in both src/runtime-artifact-conversion.cts and bin/install.js. * chore(#3039): backfill changeset PR number 3119 * fix(#3039): regenerate skills with clamped effort: high --------- Co-authored-by: sim --- .changeset/quick-foxes-caper.md | 5 +++ bin/install.js | 4 +- skills/gsd-autonomous/SKILL.md | 2 +- skills/gsd-execute-phase/SKILL.md | 2 +- skills/gsd-plan-phase/SKILL.md | 2 +- src/runtime-artifact-conversion.cts | 8 +++- .../2812-cursor-single-menu-surface.json | 6 +-- .../3039-effort-clamp-max-to-high.json | 11 +++++ tests/install-runtime-artifacts.test.cjs | 42 +++++++++---------- 9 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 .changeset/quick-foxes-caper.md create mode 100644 tests/emitted-drift-acks/3039-effort-clamp-max-to-high.json diff --git a/.changeset/quick-foxes-caper.md b/.changeset/quick-foxes-caper.md new file mode 100644 index 000000000..87de52e85 --- /dev/null +++ b/.changeset/quick-foxes-caper.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3119 +--- +**Heavy workflow skills no longer fail on Claude with thinking disabled** — `effort: max` in plan-phase, execute-phase, and autonomous SKILL.md frontmatter was rejected by the Anthropic API (`400: effort 'max' is not supported when thinking is disabled`). The installer now clamps `max`/`xhigh` to `high` for Claude-runtime skills, the maximum value that works in both thinking states on all supported models. (#3039) diff --git a/bin/install.js b/bin/install.js index 6e6bfc685..a57df30af 100755 --- a/bin/install.js +++ b/bin/install.js @@ -1983,7 +1983,9 @@ function skillFrontmatterName(skillDirName) { } function normalizeClaudeSkillEffort(effort) { - return effort === 'xhigh' ? 'max' : effort; + // #3039: `max` is rejected by Anthropic models when extended thinking is disabled. + if (effort === 'xhigh' || effort === 'max') return 'high'; + return effort; } /** diff --git a/skills/gsd-autonomous/SKILL.md b/skills/gsd-autonomous/SKILL.md index 6007e530b..9d1c2932e 100644 --- a/skills/gsd-autonomous/SKILL.md +++ b/skills/gsd-autonomous/SKILL.md @@ -2,7 +2,7 @@ name: gsd-autonomous description: "Run all remaining phases autonomously — discuss→plan→execute per phase" argument-hint: "[--from N] [--to N] [--only N] [--interactive] [--converge]" -effort: max +effort: high allowed-tools: - Read - Write diff --git a/skills/gsd-execute-phase/SKILL.md b/skills/gsd-execute-phase/SKILL.md index 670a7c2a2..34f9b2393 100644 --- a/skills/gsd-execute-phase/SKILL.md +++ b/skills/gsd-execute-phase/SKILL.md @@ -2,7 +2,7 @@ name: gsd-execute-phase description: "Execute all plans in a phase with wave-based parallelization" argument-hint: " [--wave N] [--gaps-only] [--interactive] [--tdd]" -effort: max +effort: high allowed-tools: - Read - Write diff --git a/skills/gsd-plan-phase/SKILL.md b/skills/gsd-plan-phase/SKILL.md index 73bf6fa65..4f7d151a8 100644 --- a/skills/gsd-plan-phase/SKILL.md +++ b/skills/gsd-plan-phase/SKILL.md @@ -2,7 +2,7 @@ name: gsd-plan-phase description: "Create detailed phase plan (PLAN.md) with verification loop" argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase ] [--view] [--gaps] [--skip-verify] [--skip-ui] [--prd ] [--ingest ] [--ingest-format ] [--reviews] [--text] [--bounce] [--skip-bounce] [--chunked] [--tdd] [--mvp] [--granularity ] [--no-tracer] [--no-reversibility-gates]" -effort: max +effort: high allowed-tools: - Read - Write diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index b7df89d51..c7e085b3a 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -391,7 +391,13 @@ function skillFrontmatterName(skillDirName) { } function normalizeClaudeSkillEffort(effort) { - return effort === 'xhigh' ? 'max' : effort; + // #3039: `max` is rejected by Anthropic models when extended thinking is + // disabled (400: output_config.effort 'max' is not supported when thinking + // is disabled). The frontmatter is static at install time and the installer + // cannot know whether thinking will be on or off at invocation. `high` is the + // maximum value that works in both states on all supported models. + if (effort === 'xhigh' || effort === 'max') return 'high'; + return effort; } /** diff --git a/tests/emitted-drift-acks/2812-cursor-single-menu-surface.json b/tests/emitted-drift-acks/2812-cursor-single-menu-surface.json index 3eac2f2a7..72964b303 100644 --- a/tests/emitted-drift-acks/2812-cursor-single-menu-surface.json +++ b/tests/emitted-drift-acks/2812-cursor-single-menu-surface.json @@ -77,7 +77,7 @@ "skills/gsd-audit-fix/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-audit-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-audit-uat/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-autonomous/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", + "skills/gsd-autonomous/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644). #3039: normalizeClaudeSkillEffort clamps max to high \u2014 this Claude-runtime SKILL.md now carries effort: high instead of effort: max.", "skills/gsd-capture/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-cleanup/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-code-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", @@ -87,7 +87,7 @@ "skills/gsd-discuss-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-docs-update/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-eval-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-execute-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", + "skills/gsd-execute-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644). #3039: normalizeClaudeSkillEffort clamps max to high \u2014 this Claude-runtime SKILL.md now carries effort: high instead of effort: max.", "skills/gsd-explore/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-extract-learnings/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-fast/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", @@ -116,7 +116,7 @@ "skills/gsd-onboard/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-pause-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-plan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", + "skills/gsd-plan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644). #3039: normalizeClaudeSkillEffort clamps max to high \u2014 this Claude-runtime SKILL.md now carries effort: high instead of effort: max.", "skills/gsd-plan-review-convergence/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-pr-branch/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", "skills/gsd-profile-user/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", diff --git a/tests/emitted-drift-acks/3039-effort-clamp-max-to-high.json b/tests/emitted-drift-acks/3039-effort-clamp-max-to-high.json new file mode 100644 index 000000000..03d199ae8 --- /dev/null +++ b/tests/emitted-drift-acks/3039-effort-clamp-max-to-high.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "paths": { + "skills/gsd-ns-workflow/skills/autonomous/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", + "skills/gsd-ns-workflow/skills/execute-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", + "skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", + "skills/gsd/gsd-ns-workflow/skills/autonomous/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", + "skills/gsd/gsd-ns-workflow/skills/execute-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", + "skills/gsd/gsd-ns-workflow/skills/plan-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled)." + } +} diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index c95f3cf1c..310e55e8e 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -3205,7 +3205,7 @@ describe('#778 (b) Qwen skills priority', () => { * Context: context:fork was added by #769 to protect context budget, but * plan-phase, execute-phase, and autonomous are spawning orchestrators — a * forked subagent has no Agent/Task tool, breaking their core function. - * effort: max is preserved; context: fork is removed from these three. + * effort: high is clamped from max (thinking-disabled safety, #3039); context: fork is removed from these three. * The converter still passes context: fork through if a source file has it * (for any future leaf skill that legitimately needs isolation). * @@ -3215,9 +3215,9 @@ describe('#778 (b) Qwen skills priority', () => { * 3. Source commands/gsd/plan-phase.md does NOT have context: fork, has effort: max * 4. Source commands/gsd/progress.md has effort: low * 5. Source commands/gsd/stats.md has effort: low - * 6. Claude global install: SKILL.md for autonomous has effort: max, NOT context: fork - * 7. Claude global install: SKILL.md for execute-phase has effort: max, NOT context: fork - * 8. Claude global install: SKILL.md for plan-phase has effort: max, NOT context: fork + * 6. Claude global install: SKILL.md for autonomous has effort: high (clamped), NOT context: fork + * 7. Claude global install: SKILL.md for execute-phase has effort: high (clamped), NOT context: fork + * 8. Claude global install: SKILL.md for plan-phase has effort: high (clamped), NOT context: fork * 9. Claude global install: SKILL.md for progress has effort: low * 10. Claude global install: SKILL.md for stats has effort: low * 11. convertClaudeCommandToClaudeSkill still passes context: fork through (for non-orchestrator skills) @@ -3302,7 +3302,7 @@ function runClaudeGlobalInstall(claudeHome) { // #921/#922: spawning orchestrators must NOT carry context: fork — a forked // subagent has no Agent/Task tool, making it impossible for orchestrators to // spawn their required subagents. context: fork is appropriate only for leaf -// skills that do not themselves dispatch agents. effort: max is portable across Claude Code models. +// skills that do not themselves dispatch agents. effort: max in source; clamped to high in emitted SKILL.md (#3039: max rejected when thinking disabled). describe('#769/#921/#1319 source commands: spawning orchestrators have effort: max but NOT context: fork', () => { test('commands/gsd/autonomous.md does NOT have context: fork (#921)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'autonomous.md')); @@ -3313,7 +3313,7 @@ describe('#769/#921/#1319 source commands: spawning orchestrators have effort: m test('commands/gsd/autonomous.md has effort: max (#1319)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'autonomous.md')); assert.match(fm, /^effort:[ \t]*max$/m, - `autonomous.md frontmatter must have effort: max\nActual:\n${fm}`); + `autonomous.md SOURCE frontmatter must have effort: max (#1319)\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, `autonomous.md frontmatter must not have rejected effort: xhigh (#1319)\nActual:\n${fm}`); }); @@ -3387,7 +3387,7 @@ describe('#769/#1319 convertClaudeCommandToClaudeSkill: preserves context and em `SKILL.md frontmatter must include context: fork\nActual frontmatter:\n${fm}`); }); - test('normalizes effort: xhigh to effort: max in emitted SKILL.md frontmatter (#1319)', () => { + test('normalizes effort: xhigh/max to effort: high in emitted SKILL.md frontmatter (#1319/#3039)', () => { const input = [ '---', 'name: gsd:test-heavy', @@ -3406,10 +3406,10 @@ describe('#769/#1319 convertClaudeCommandToClaudeSkill: preserves context and em const end = result.indexOf('---', 3); const fm = result.substring(3, end); - assert.match(fm, /^effort:[ \t]*max$/m, - `SKILL.md frontmatter must include portable effort: max\nActual frontmatter:\n${fm}`); - assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, - `SKILL.md frontmatter must not include rejected effort: xhigh (#1319)\nActual frontmatter:\n${fm}`); + assert.match(fm, /^effort:[ \t]*high$/m, + `SKILL.md frontmatter must include portable effort: high (#3039)\nActual frontmatter:\n${fm}`); + assert.doesNotMatch(fm, /^effort:[ \t]*(xhigh|max)$/m, + `SKILL.md frontmatter must not include rejected effort: xhigh or max (#1319/#3039)\nActual frontmatter:\n${fm}`); }); test('preserves effort: low in emitted SKILL.md frontmatter', () => { @@ -3460,7 +3460,7 @@ describe('#769/#1319 convertClaudeCommandToClaudeSkill: preserves context and em // #921/#922: after install, spawning orchestrators must NOT carry context: fork // in their emitted SKILL.md. #1319: heavyweight skills must use portable max effort. -describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md files have effort: max but NOT context: fork', () => { +describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md files have effort: high (clamped from max) but NOT context: fork', () => { let tmpDir; let claudeHome; @@ -3486,10 +3486,10 @@ describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'autonomous'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*max$/m, - `gsd-autonomous SKILL.md must have effort: max\nActual:\n${fm}`); + assert.match(fm, /^effort:[ \t]*high$/m, + `gsd-autonomous SKILL.md must have effort: high\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, - `gsd-autonomous SKILL.md must not have rejected effort: xhigh (#1319)\nActual:\n${fm}`); + `gsd-autonomous SKILL.md must not have rejected effort: xhigh or max (#1319/#3039)\nActual:\n${fm}`); }); test('gsd-execute-phase SKILL.md does NOT have context: fork after global install (#921)', () => { @@ -3504,10 +3504,10 @@ describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'execute-phase'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*max$/m, - `gsd-execute-phase SKILL.md must have effort: max\nActual:\n${fm}`); + assert.match(fm, /^effort:[ \t]*high$/m, + `gsd-execute-phase SKILL.md must have effort: high\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, - `gsd-execute-phase SKILL.md must not have rejected effort: xhigh (#1319)\nActual:\n${fm}`); + `gsd-execute-phase SKILL.md must not have rejected effort: xhigh or max (#1319/#3039)\nActual:\n${fm}`); }); test('gsd-plan-phase SKILL.md does NOT have context: fork after global install (#921)', () => { @@ -3522,10 +3522,10 @@ describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'plan-phase'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*max$/m, - `gsd-plan-phase SKILL.md must have effort: max\nActual:\n${fm}`); + assert.match(fm, /^effort:[ \t]*high$/m, + `gsd-plan-phase SKILL.md must have effort: high\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, - `gsd-plan-phase SKILL.md must not have rejected effort: xhigh (#1319)\nActual:\n${fm}`); + `gsd-plan-phase SKILL.md must not have rejected effort: xhigh or max (#1319/#3039)\nActual:\n${fm}`); }); test('gsd-progress SKILL.md has effort: low after global install', () => {