fix(#3039): clamp max/xhigh effort to high for Claude-runtime skills (#3119)

* 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 <sim@local>
This commit is contained in:
Tom Boucher
2026-08-06 10:20:22 -04:00
committed by GitHub
parent 077028584f
commit b181c2f8c3
9 changed files with 53 additions and 29 deletions

View File

@@ -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)

View File

@@ -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;
}
/**

View File

@@ -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

View File

@@ -2,7 +2,7 @@
name: gsd-execute-phase
description: "Execute all plans in a phase with wave-based parallelization"
argument-hint: "<phase-number> [--wave N] [--gaps-only] [--interactive] [--tdd]"
effort: max
effort: high
allowed-tools:
- Read
- Write

View File

@@ -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 <N>] [--view] [--gaps] [--skip-verify] [--skip-ui] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--bounce] [--skip-bounce] [--chunked] [--tdd] [--mvp] [--granularity <coarse|standard|fine>] [--no-tracer] [--no-reversibility-gates]"
effort: max
effort: high
allowed-tools:
- Read
- Write

View File

@@ -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;
}
/**

View File

@@ -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).",

View File

@@ -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)."
}
}

View File

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