Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: quality/balanced profiles now deliver Opus subagents (#695)
resolveModelInternal was converting every 'opus' result to 'inherit'
before returning it. 'inherit' tells Claude Code's Task tool to use
the parent session's model — which defaults to Sonnet 4.6, silently
running quality/balanced-profile agents on the wrong model.

Pass 'opus' directly; Claude Code's Task tool resolves it to the
current Opus version. Orgs that block Opus will now get a clear error
instead of a silent Sonnet downgrade.

- Remove opus → 'inherit' conversion from all 3 return sites in
  resolveModelInternal (core.cjs)
- Update 7 test assertions and fix validValues set (core.test.cjs)
- Update model-profiles.md, model-profile-resolution.md, and
  adaptive-model-selection.md to reflect the change
  • Loading branch information
Ethan Hurst committed Mar 3, 2026
commit a24282d1059e38b09475c97246561e3896f46480
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed
- **Quality/balanced profiles now deliver Opus subagents** — `resolveModelInternal` previously
converted `opus` to `inherit`, causing agents to silently run on Sonnet when the parent
session used the default Sonnet 4.6 model. Opus is now passed directly to Task calls (#695)

### Added
- **Adaptive model profile** — fourth model profile (`adaptive`) that auto-selects models per-plan based on complexity evaluation (#210)
- `evaluateComplexity()` scores plan metadata (files modified, task count, objective keywords, plan type, dependencies) on 0-10+ scale
Expand Down
6 changes: 3 additions & 3 deletions get-shit-done/bin/lib/core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ function resolveModelInternal(cwd, agentType, context) {
// Check per-agent override first
const override = config.model_overrides?.[agentType];
if (override) {
return override === 'opus' ? 'inherit' : override;
return override;
}

// Fall back to profile lookup
Expand All @@ -523,13 +523,13 @@ function resolveModelInternal(cwd, agentType, context) {
}
}

return resolved === 'opus' ? 'inherit' : resolved;
return resolved;
}

const agentModels = MODEL_PROFILES[agentType];
if (!agentModels) return 'sonnet';
const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
return resolved === 'opus' ? 'inherit' : resolved;
return resolved;
}

// ─── Misc utilities ───────────────────────────────────────────────────────────
Expand Down
4 changes: 2 additions & 2 deletions get-shit-done/references/adaptive-model-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ When adaptive is active but no plan context is available (e.g., during init), de
a. Evaluate complexity from plan context
b. Look up model from ADAPTIVE_TIERS[tier][agentType]
c. Clamp to min_model / max_model bounds
d. Return result (opus → 'inherit')
d. Return result ('opus', 'sonnet', or 'haiku')
3. Non-adaptive: standard profile table lookup
```

Expand All @@ -102,7 +102,7 @@ node gsd-tools.cjs resolve-adaptive-model gsd-executor \
# Complex plan
node gsd-tools.cjs resolve-adaptive-model gsd-planner \
--context '{"files_modified":["a.js","b.js","c.js","d.js","e.js"],"task_count":8,"objective":"architect new integration with external API"}'
# Returns: inherit (opus), complex tier
# Returns: opus, complex tier
```

## Workflow Integration
Expand Down
6 changes: 3 additions & 3 deletions get-shit-done/references/model-profile-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ Look up the agent in the table for the resolved profile. Pass the model paramete
Task(
prompt="...",
subagent_type="gsd-planner",
model="{resolved_model}" # "inherit", "sonnet", or "haiku"
model="{resolved_model}" # "opus", "sonnet", or "haiku"
)
```

**Note:** Opus-tier agents resolve to `"inherit"` (not `"opus"`). This causes the agent to use the parent session's model, avoiding conflicts with organization policies that may block specific opus versions.
**Note:** Opus-tier agents resolve to `"opus"`, which Claude Code's Task tool maps to the current Opus model version. Earlier versions used `"inherit"` to avoid org-policy conflicts, but that silently downgraded agents to Sonnet when the parent session ran on Sonnet (the default). Passing `"opus"` directly ensures quality-profile agents actually run on Opus.

## Adaptive Profile

Expand All @@ -43,4 +43,4 @@ See `references/adaptive-model-selection.md` for full algorithm.
1. Resolve once at orchestration start (or per-plan if adaptive)
2. Store the profile value
3. Look up each agent's model from the table when spawning
4. Pass model parameter to each Task call (values: `"inherit"`, `"sonnet"`, `"haiku"`)
4. Pass model parameter to each Task call (values: `"opus"`, `"sonnet"`, `"haiku"`)
9 changes: 7 additions & 2 deletions get-shit-done/references/model-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,10 @@ Verification requires goal-backward reasoning - checking if code *delivers* what
**Why Haiku for gsd-codebase-mapper?**
Read-only exploration and pattern extraction. No reasoning required, just structured output from file contents.

**Why `inherit` instead of passing `opus` directly?**
Claude Code's `"opus"` alias maps to a specific model version. Organizations may block older opus versions while allowing newer ones. GSD returns `"inherit"` for opus-tier agents, causing them to use whatever opus version the user has configured in their session. This avoids version conflicts and silent fallbacks to Sonnet.
**Why `opus` and not `inherit`?**
GSD passes `"opus"` directly to Claude Code's Task tool, which resolves it to the current
Opus model version. Earlier versions used `"inherit"` to avoid org-policy version conflicts,
but this silently downgraded agents to Sonnet when the parent session ran on Sonnet (the
default). Passing `"opus"` explicitly ensures quality-profile agents actually run on Opus.
If an org policy blocks Opus, the Task call will fail with a clear error rather than
silently running on the wrong model.
24 changes: 12 additions & 12 deletions tests/core.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('resolveModelInternal', () => {
test('all known agents resolve to a valid string for each profile', () => {
const knownAgents = ['gsd-planner', 'gsd-executor', 'gsd-phase-researcher', 'gsd-codebase-mapper'];
const profiles = ['quality', 'balanced', 'budget'];
const validValues = ['inherit', 'sonnet', 'haiku', 'opus'];
const validValues = ['opus', 'sonnet', 'haiku'];

for (const profile of profiles) {
writeConfig({ model_profile: profile });
Expand All @@ -185,20 +185,20 @@ describe('resolveModelInternal', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'haiku');
});

test('opus override resolves to inherit', () => {
test('opus override resolves to opus', () => {
writeConfig({
model_overrides: { 'gsd-executor': 'opus' },
});
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'inherit');
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'opus');
});

test('agents not in override fall back to profile', () => {
writeConfig({
model_profile: 'quality',
model_overrides: { 'gsd-executor': 'haiku' },
});
// gsd-planner not overridden, should use quality profile -> opus -> inherit
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit');
// gsd-planner not overridden, should use quality profile -> opus
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'opus');
});
});

Expand All @@ -210,8 +210,8 @@ describe('resolveModelInternal', () => {

test('defaults to balanced profile when model_profile missing', () => {
writeConfig({});
// balanced profile, gsd-planner -> opus -> inherit
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit');
// balanced profile, gsd-planner -> opus
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'opus');
});
});
});
Expand Down Expand Up @@ -1004,9 +1004,9 @@ describe('resolveModelInternal (adaptive profile)', () => {
// gsd-executor in medium tier = sonnet
const result = resolveModelInternal(tmpDir, 'gsd-executor');
assert.strictEqual(result, 'sonnet');
// gsd-planner in medium tier = opus -> inherit
// gsd-planner in medium tier = opus
const plannerResult = resolveModelInternal(tmpDir, 'gsd-planner');
assert.strictEqual(plannerResult, 'inherit');
assert.strictEqual(plannerResult, 'opus');
});

test('medium tier codebase-mapper resolves to haiku', () => {
Expand All @@ -1023,15 +1023,15 @@ describe('resolveModelInternal (adaptive profile)', () => {
assert.strictEqual(result, 'haiku');
});

test('complex context returns inherit for planner (opus)', () => {
test('complex context returns opus for planner', () => {
writeConfig({ model_profile: 'adaptive' });
const ctx = {
files_modified: ['a.js', 'b.js', 'c.js', 'd.js', 'e.js'],
task_count: 8,
objective: 'architect new integration with external API',
};
const result = resolveModelInternal(tmpDir, 'gsd-planner', ctx);
assert.strictEqual(result, 'inherit'); // opus -> inherit
assert.strictEqual(result, 'opus');
});

test('min_model clamping upgrades haiku to sonnet', () => {
Expand Down Expand Up @@ -1068,7 +1068,7 @@ describe('resolveModelInternal (adaptive profile)', () => {
const ctx = { files_modified: ['a.js'], task_count: 1, objective: 'fix typo' };
// Override should win over adaptive
const result = resolveModelInternal(tmpDir, 'gsd-executor', ctx);
assert.strictEqual(result, 'inherit'); // opus -> inherit
assert.strictEqual(result, 'opus');
});

test('context ignored for non-adaptive profiles', () => {
Expand Down