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', () => {