From f70829d067573960964b89f29515dde5722be4a2 Mon Sep 17 00:00:00 2001 From: radioflyer28 <9313101+radioflyer28@users.noreply.github.com> Date: Wed, 13 May 2026 18:26:54 -0400 Subject: [PATCH 1/3] feat: transport resolved reasoning effort --- bin/install.js | 4 ++++ get-shit-done/bin/lib/commands.cjs | 4 +++- sdk/src/query/config-query.test.ts | 24 ++++++++++++++++++++++-- sdk/src/query/config-query.ts | 6 +++++- tests/codex-config.test.cjs | 2 ++ tests/commands.test.cjs | 15 +++++++++++++++ 6 files changed, 51 insertions(+), 4 deletions(-) diff --git a/bin/install.js b/bin/install.js index 801a1af25..385529fbc 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2360,6 +2360,10 @@ Direct mapping: GSD embeds the resolved per-agent model directly into each agent's \`.toml\` at install time so \`model_overrides\` from \`.planning/config.json\` and \`~/.gsd/defaults.json\` are honored automatically by Codex's agent router. +- Resolved \`reasoning_effort="low|medium|high|xhigh"\` → pass \`reasoning_effort\` + to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty, + inherited, or unsupported values; do not invent one-off effort literals in + workflow prose. - \`fork_context: false\` by default — GSD agents load their own context via \`\` blocks - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct Codex mapping. Codex \`spawn_agent\` does not create or bind a git worktree automatically. diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 8142bec7d..cc45dc9b7 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execGit, platformWriteSync, platformReadSync, platformEnsureDir } = require('./shell-command-projection.cjs'); -const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs'); +const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveReasoningEffortInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs'); const { planningDir, planningPaths } = require('./planning-workspace.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); @@ -240,11 +240,13 @@ function cmdResolveModel(cwd, agentType, raw) { const config = loadConfig(cwd); const profile = config.model_profile || 'balanced'; const model = resolveModelInternal(cwd, agentType); + const reasoningEffort = resolveReasoningEffortInternal(cwd, agentType); const agentModels = MODEL_PROFILES[agentType]; const result = agentModels ? { model, profile } : { model, profile, unknown_agent: true }; + if (reasoningEffort) result.reasoning_effort = reasoningEffort; output(result, raw, model); } diff --git a/sdk/src/query/config-query.test.ts b/sdk/src/query/config-query.test.ts index ea24b5c3b..0bcc8bbc2 100644 --- a/sdk/src/query/config-query.test.ts +++ b/sdk/src/query/config-query.test.ts @@ -208,8 +208,28 @@ describe('resolveModel', () => { const planner = (await resolveModel(['gsd-planner'], tmpDir)).data as Record; const executor = (await resolveModel(['gsd-executor'], tmpDir)).data as Record; - expect(planner).toMatchObject({ model: 'gpt-5.5', profile: 'balanced' }); - expect(executor).toMatchObject({ model: 'gpt-5.3-codex', profile: 'balanced' }); + expect(planner).toMatchObject({ model: 'gpt-5.5', profile: 'balanced', reasoning_effort: 'high' }); + expect(executor).toMatchObject({ model: 'gpt-5.3-codex', profile: 'balanced', reasoning_effort: 'medium' }); + }); + + it('returns runtime reasoning_effort from the same phase-tier source as model', async () => { + const { resolveModel } = await import('./config-query.js'); + await writeFile( + join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ + model_profile: 'budget', + runtime: 'codex', + models: { execution: 'opus' }, + }), + ); + + const executor = (await resolveModel(['gsd-executor'], tmpDir)).data as Record; + + expect(executor).toMatchObject({ + model: 'gpt-5.4', + profile: 'budget', + reasoning_effort: 'xhigh', + }); }); it('resolveModel uses workstream config when --ws is specified', async () => { diff --git a/sdk/src/query/config-query.ts b/sdk/src/query/config-query.ts index 7c6d7a871..47fbf3795 100644 --- a/sdk/src/query/config-query.ts +++ b/sdk/src/query/config-query.ts @@ -222,7 +222,11 @@ export const resolveModel: QueryHandler = async (args, projectDir, workstream) = const tier = typeof phaseTier === 'string' ? phaseTier : alias; const runtimeTier = resolveRuntimeTier(config as Record, tier); if (runtimeTier?.model) { - return { data: { model: runtimeTier.model, profile } }; + const result: Record = { model: runtimeTier.model, profile }; + if (runtimeTier.reasoning_effort) { + result.reasoning_effort = runtimeTier.reasoning_effort; + } + return { data: result }; } if (resolveModelIds === 'omit') { diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index f27ce2b89..2f005b59b 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -133,6 +133,8 @@ describe('getCodexSkillAdapterHeader', () => { const result = getCodexSkillAdapterHeader('gsd-execute-phase'); assert.ok(result.includes('spawn_agent'), 'maps to spawn_agent'); assert.ok(result.includes('agent_type'), 'maps subagent_type to agent_type'); + assert.ok(result.includes('reasoning_effort'), 'documents reasoning_effort transport'); + assert.ok(result.includes('do not invent one-off effort literals'), 'keeps effort policy centralized'); assert.ok(result.includes('fork_context'), 'documents fork_context default'); assert.ok(result.includes('wait(ids)'), 'documents parallel wait pattern'); assert.ok(result.includes('close_agent'), 'documents close_agent cleanup'); diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index cc84ed57f..03e5eff2c 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -1117,6 +1117,21 @@ describe('resolve-model command', () => { assert.ok(output.model, 'should resolve a model'); }); + test('includes reasoning_effort when selected runtime supports it', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ + model_profile: 'balanced', + runtime: 'codex', + models: { planning: 'opus' }, + })); + const result = runGsdTools('resolve-model gsd-planner', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.model, 'gpt-5.4'); + assert.strictEqual(output.profile, 'balanced'); + assert.strictEqual(output.reasoning_effort, 'xhigh'); + }); + test('fails when no agent-type provided', () => { const result = runGsdTools('resolve-model', tmpDir); assert.ok(!result.success, 'should fail without agent-type'); From c20f714d68b80d8e5f5978ba643d45a27ff8216a Mon Sep 17 00:00:00 2001 From: radioflyer28 <9313101+radioflyer28@users.noreply.github.com> Date: Wed, 13 May 2026 19:25:44 -0400 Subject: [PATCH 2/3] docs: document reasoning effort transport --- .changeset/agent-launch-reasoning-transport.md | 5 +++++ docs/CLI-TOOLS.md | 4 +++- docs/CONFIGURATION.md | 2 ++ sdk/src/query/QUERY-HANDLERS.md | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/agent-launch-reasoning-transport.md diff --git a/.changeset/agent-launch-reasoning-transport.md b/.changeset/agent-launch-reasoning-transport.md new file mode 100644 index 000000000..95bdf4b50 --- /dev/null +++ b/.changeset/agent-launch-reasoning-transport.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 3474 +--- +**Agent launch model resolution now transports reasoning effort** — `resolve-model` JSON output includes supported runtime `reasoning_effort`, and runtime adapter guidance passes it to child-agent launches only when supported. diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index 505b5807d..c2ac438ba 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -207,7 +207,9 @@ node gsd-tools.cjs config-set-model-profile ```bash # Get model for agent based on current profile node gsd-tools.cjs resolve-model -# Returns: opus | sonnet | haiku | inherit +# Raw output returns the selected model ID/tier. +# JSON output also includes profile and, when the active runtime supports it, +# reasoning_effort. ``` Agent names: `gsd-planner`, `gsd-executor`, `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-roadmapper`, `gsd-debugger`, `gsd-codebase-mapper`, `gsd-nyquist-auditor` diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a9354c53a..3480733af 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -980,6 +980,8 @@ The intent is the same as the Claude profile tiers -- use a stronger model for p When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtime-native model IDs instead of Claude aliases. This lets a single shared `.planning/config.json` work cleanly across Claude and Codex. +`resolve-model` JSON output includes `reasoning_effort` when it is defined by the same runtime tier that selected the model. Runtime adapters may pass that value to child-agent launch calls that support it; runtimes without explicit support omit it. + **Built-in tier maps:** | Runtime | `opus` | `sonnet` | `haiku` | reasoning_effort | diff --git a/sdk/src/query/QUERY-HANDLERS.md b/sdk/src/query/QUERY-HANDLERS.md index b6189e3c6..1d2437ef8 100644 --- a/sdk/src/query/QUERY-HANDLERS.md +++ b/sdk/src/query/QUERY-HANDLERS.md @@ -155,7 +155,7 @@ From `read-only-parity.integration.test.ts` (full `toEqual` on this repo): | SDK dispatch (canonical) | Notes | | ------------------------ | ----- | -| `resolve-model` | Args e.g. `gsd-planner`. | +| `resolve-model` | Args e.g. `gsd-planner`; returns `reasoning_effort` when the selected runtime tier defines one. | | `phase-plan-index` | Phase number arg. | | `roadmap.get-phase` | Phase number arg. | | `list.todos` | No args. | From 810778e137185e0648ea6c3ab84fda38976d957f Mon Sep 17 00:00:00 2001 From: radioflyer28 <9313101+radioflyer28@users.noreply.github.com> Date: Wed, 13 May 2026 22:27:15 -0400 Subject: [PATCH 3/3] fix(sdk): gate reasoning effort by runtime allowlist --- ...ransport.md => careful-model-transport.md} | 2 +- bin/install.js | 2 +- docs/CONFIGURATION.md | 2 +- sdk/src/query/config-query.test.ts | 29 ++++++++++++++- sdk/src/query/config-query.ts | 9 ++++- tests/codex-config.test.cjs | 6 +++- tests/commands.test.cjs | 36 +++++++++++++++++++ 7 files changed, 80 insertions(+), 6 deletions(-) rename .changeset/{agent-launch-reasoning-transport.md => careful-model-transport.md} (94%) diff --git a/.changeset/agent-launch-reasoning-transport.md b/.changeset/careful-model-transport.md similarity index 94% rename from .changeset/agent-launch-reasoning-transport.md rename to .changeset/careful-model-transport.md index 95bdf4b50..d2f00b52e 100644 --- a/.changeset/agent-launch-reasoning-transport.md +++ b/.changeset/careful-model-transport.md @@ -1,5 +1,5 @@ --- -type: Added +type: Changed pr: 3474 --- **Agent launch model resolution now transports reasoning effort** — `resolve-model` JSON output includes supported runtime `reasoning_effort`, and runtime adapter guidance passes it to child-agent launches only when supported. diff --git a/bin/install.js b/bin/install.js index 385529fbc..ea0a704d8 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2360,7 +2360,7 @@ Direct mapping: GSD embeds the resolved per-agent model directly into each agent's \`.toml\` at install time so \`model_overrides\` from \`.planning/config.json\` and \`~/.gsd/defaults.json\` are honored automatically by Codex's agent router. -- Resolved \`reasoning_effort="low|medium|high|xhigh"\` → pass \`reasoning_effort\` +- Resolved \`reasoning_effort="low|medium|high|xhigh"\` (\`xhigh\` is a GSD/Codex tier, not a generic runtime enum) → pass \`reasoning_effort\` to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty, inherited, or unsupported values; do not invent one-off effort literals in workflow prose. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3480733af..a427575cd 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -980,7 +980,7 @@ The intent is the same as the Claude profile tiers -- use a stronger model for p When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtime-native model IDs instead of Claude aliases. This lets a single shared `.planning/config.json` work cleanly across Claude and Codex. -`resolve-model` JSON output includes `reasoning_effort` when it is defined by the same runtime tier that selected the model. Runtime adapters may pass that value to child-agent launch calls that support it; runtimes without explicit support omit it. +`resolve-model` JSON output includes `reasoning_effort` when the runtime tier resolved for the agent (after phase-type overrides) defines a `reasoning_effort`. Runtime adapters may pass that value to child-agent launch calls that support it; runtimes without explicit support omit it. **Built-in tier maps:** diff --git a/sdk/src/query/config-query.test.ts b/sdk/src/query/config-query.test.ts index 0bcc8bbc2..5faa8ede6 100644 --- a/sdk/src/query/config-query.test.ts +++ b/sdk/src/query/config-query.test.ts @@ -214,6 +214,8 @@ describe('resolveModel', () => { it('returns runtime reasoning_effort from the same phase-tier source as model', async () => { const { resolveModel } = await import('./config-query.js'); + const { resolveRuntimeTierDefault } = await import('../model-catalog.js'); + const opusCodexTier = resolveRuntimeTierDefault('codex', 'opus'); await writeFile( join(tmpDir, '.planning', 'config.json'), JSON.stringify({ @@ -228,10 +230,35 @@ describe('resolveModel', () => { expect(executor).toMatchObject({ model: 'gpt-5.4', profile: 'budget', - reasoning_effort: 'xhigh', + reasoning_effort: opusCodexTier?.reasoning_effort, }); }); + it('does not leak reasoning_effort from overrides for unsupported runtimes', async () => { + const { resolveModel } = await import('./config-query.js'); + await writeFile( + join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ + model_profile: 'balanced', + runtime: 'opencode', + models: { planning: 'opus' }, + model_profile_overrides: { + opencode: { + opus: { model: 'openrouter/openai/gpt-5.5', reasoning_effort: 'high' }, + }, + }, + }), + ); + + const planner = (await resolveModel(['gsd-planner'], tmpDir)).data as Record; + + expect(planner).toMatchObject({ + model: 'openrouter/openai/gpt-5.5', + profile: 'balanced', + }); + expect(planner).not.toHaveProperty('reasoning_effort'); + }); + it('resolveModel uses workstream config when --ws is specified', async () => { const { resolveModel } = await import('./config-query.js'); // Root config: balanced profile → gsd-executor resolves to 'sonnet' diff --git a/sdk/src/query/config-query.ts b/sdk/src/query/config-query.ts index 47fbf3795..e5b8c1c17 100644 --- a/sdk/src/query/config-query.ts +++ b/sdk/src/query/config-query.ts @@ -30,8 +30,11 @@ import { VALID_PROFILES, getAgentToModelMapForProfile, resolveRuntimeTierDefault, + runtimesWithReasoningEffort, } from '../model-catalog.js'; +const RUNTIMES_WITH_REASONING_EFFORT = runtimesWithReasoningEffort(); + // ─── configGet ────────────────────────────────────────────────────────────── /** @@ -151,7 +154,11 @@ function resolveRuntimeTier(config: Record, tier: string): Runt const userEntry = normalizeRuntimeTierEntry(runtimeOverrides?.[tier]); if (!builtin && !userEntry) return null; - return { ...(builtin ?? {}), ...(userEntry ?? {}) }; + const merged = { ...(builtin ?? {}), ...(userEntry ?? {}) }; + if (!RUNTIMES_WITH_REASONING_EFFORT.has(runtime)) { + delete merged.reasoning_effort; + } + return merged; } /** diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index 2f005b59b..299011eb0 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -133,7 +133,11 @@ describe('getCodexSkillAdapterHeader', () => { const result = getCodexSkillAdapterHeader('gsd-execute-phase'); assert.ok(result.includes('spawn_agent'), 'maps to spawn_agent'); assert.ok(result.includes('agent_type'), 'maps subagent_type to agent_type'); - assert.ok(result.includes('reasoning_effort'), 'documents reasoning_effort transport'); + assert.match( + result, + /Resolved `reasoning_effort="low\|medium\|high\|xhigh"` \(`xhigh` is a GSD\/Codex tier, not a generic runtime enum\) → pass `reasoning_effort`\s+to `spawn_agent` when the runtime\/tool supports it/, + 'documents reasoning_effort transport', + ); assert.ok(result.includes('do not invent one-off effort literals'), 'keeps effort policy centralized'); assert.ok(result.includes('fork_context'), 'documents fork_context default'); assert.ok(result.includes('wait(ids)'), 'documents parallel wait pattern'); diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index 03e5eff2c..1a0f8859a 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -1132,6 +1132,42 @@ describe('resolve-model command', () => { assert.strictEqual(output.reasoning_effort, 'xhigh'); }); + test('does not include reasoning_effort for unsupported runtime overrides', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ + model_profile: 'balanced', + runtime: 'opencode', + models: { planning: 'opus' }, + model_profile_overrides: { + opencode: { + opus: { model: 'openrouter/openai/gpt-5.5', reasoning_effort: 'high' }, + }, + }, + })); + const result = runGsdTools('resolve-model gsd-planner', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.model, 'openrouter/openai/gpt-5.5'); + assert.strictEqual(output.profile, 'balanced'); + assert.ok(!Object.prototype.hasOwnProperty.call(output, 'reasoning_effort')); + }); + + test('does not include reasoning_effort for per-agent model_overrides', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ + model_profile: 'balanced', + runtime: 'codex', + models: { planning: 'opus' }, + model_overrides: { 'gsd-planner': 'gpt-5.5' }, + })); + const result = runGsdTools('resolve-model gsd-planner', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.model, 'gpt-5.5'); + assert.strictEqual(output.profile, 'balanced'); + assert.ok(!Object.prototype.hasOwnProperty.call(output, 'reasoning_effort')); + }); + test('fails when no agent-type provided', () => { const result = runGsdTools('resolve-model', tmpDir); assert.ok(!result.success, 'should fail without agent-type');