feat: add inherit model profile for OpenCode /model (#951)

This commit is contained in:
jorge g
2026-03-15 13:40:08 -04:00
committed by GitHub
parent 789cac127d
commit 698985feb1
14 changed files with 111 additions and 50 deletions

View File

@@ -528,7 +528,7 @@ You're never locked in. The system adapts.
| Command | What it does |
|---------|--------------|
| `/gsd:settings` | Configure model profile and workflow agents |
| `/gsd:set-profile <profile>` | Switch model profile (quality/balanced/budget) |
| `/gsd:set-profile <profile>` | Switch model profile (quality/balanced/budget/inherit) |
| `/gsd:add-todo [desc]` | Capture idea for later |
| `/gsd:check-todos` | List pending todos |
| `/gsd:debug [desc]` | Systematic debugging with persistent state |
@@ -560,12 +560,15 @@ Control which Claude model each agent uses. Balance quality vs token spend.
| `quality` | Opus | Opus | Sonnet |
| `balanced` (default) | Opus | Sonnet | Sonnet |
| `budget` | Sonnet | Sonnet | Haiku |
| `inherit` | Inherit | Inherit | Inherit |
Switch profiles:
```
/gsd:set-profile budget
```
Use `inherit` to follow the current runtime model selection (for example OpenCode `/model`).
Or configure via `/gsd:settings`.
### Workflow Agents

View File

@@ -1,6 +1,6 @@
---
name: gsd:set-profile
description: Switch model profile for GSD agents (quality/balanced/budget)
description: Switch model profile for GSD agents (quality/balanced/budget/inherit)
argument-hint: <profile>
allowed-tools:
- Read
@@ -12,7 +12,7 @@ allowed-tools:
Switch the model profile used by GSD agents. Controls which Claude model each agent uses, balancing quality vs token spend.
Routes to the set-profile workflow which handles:
- Argument validation (quality/balanced/budget)
- Argument validation (quality/balanced/budget/inherit)
- Config file creation if missing
- Profile update in config.json
- Confirmation with model table display

View File

@@ -363,7 +363,7 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n
|---------|---------|---------|------------------|
| `mode` | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step |
| `granularity` | `coarse`, `standard`, `fine` | `standard` | Phase granularity: how finely scope is sliced (3-5, 5-8, or 8-12 phases) |
| `model_profile` | `quality`, `balanced`, `budget` | `balanced` | Model tier for each agent (see table below) |
| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) |
### Planning Settings
@@ -407,24 +407,25 @@ Disable these to speed up phases in familiar domains or when conserving tokens.
### Model Profiles (Per-Agent Breakdown)
| Agent | `quality` | `balanced` | `budget` |
|-------|-----------|------------|----------|
| gsd-planner | Opus | Opus | Sonnet |
| gsd-roadmapper | Opus | Sonnet | Sonnet |
| gsd-executor | Opus | Sonnet | Sonnet |
| gsd-phase-researcher | Opus | Sonnet | Haiku |
| gsd-project-researcher | Opus | Sonnet | Haiku |
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku |
| gsd-debugger | Opus | Sonnet | Sonnet |
| gsd-codebase-mapper | Sonnet | Haiku | Haiku |
| gsd-verifier | Sonnet | Sonnet | Haiku |
| gsd-plan-checker | Sonnet | Sonnet | Haiku |
| gsd-integration-checker | Sonnet | Sonnet | Haiku |
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|-------|-----------|------------|----------|-----------|
| gsd-planner | Opus | Opus | Sonnet | Inherit |
| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit |
| gsd-executor | Opus | Sonnet | Sonnet | Inherit |
| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit |
| gsd-debugger | Opus | Sonnet | Sonnet | Inherit |
| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit |
| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit |
| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit |
| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit |
**Profile philosophy:**
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`).
---

View File

@@ -405,9 +405,10 @@ function resolveModelInternal(cwd, agentType) {
}
// Fall back to profile lookup
const profile = config.model_profile || 'balanced';
const profile = String(config.model_profile || 'balanced').toLowerCase();
const agentModels = MODEL_PROFILES[agentType];
if (!agentModels) return 'sonnet';
if (profile === 'inherit') return 'inherit';
const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
return resolved === 'opus' ? 'inherit' : resolved;
}

View File

@@ -607,7 +607,7 @@ function cmdValidateHealth(cwd, options, raw) {
const raw = fs.readFileSync(configPath, 'utf-8');
const parsed = JSON.parse(raw);
// Validate known fields
const validProfiles = ['quality', 'balanced', 'budget'];
const validProfiles = ['quality', 'balanced', 'budget', 'inherit'];
if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
}

View File

@@ -26,6 +26,8 @@ Task(
**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.
If `model_profile` is `"inherit"`, all agents resolve to `"inherit"` (useful for OpenCode `/model`).
## Usage
1. Resolve once at orchestration start

View File

@@ -1,23 +1,23 @@
# Model Profiles
Model profiles control which Claude model each GSD agent uses. This allows balancing quality vs token spend.
Model profiles control which Claude model each GSD agent uses. This allows balancing quality vs token spend, or inheriting the currently selected session model.
## Profile Definitions
| Agent | `quality` | `balanced` | `budget` |
|-------|-----------|------------|----------|
| gsd-planner | opus | opus | sonnet |
| gsd-roadmapper | opus | sonnet | sonnet |
| gsd-executor | opus | sonnet | sonnet |
| gsd-phase-researcher | opus | sonnet | haiku |
| gsd-project-researcher | opus | sonnet | haiku |
| gsd-research-synthesizer | sonnet | sonnet | haiku |
| gsd-debugger | opus | sonnet | sonnet |
| gsd-codebase-mapper | sonnet | haiku | haiku |
| gsd-verifier | sonnet | sonnet | haiku |
| gsd-plan-checker | sonnet | sonnet | haiku |
| gsd-integration-checker | sonnet | sonnet | haiku |
| gsd-nyquist-auditor | sonnet | sonnet | haiku |
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|-------|-----------|------------|----------|-----------|
| gsd-planner | opus | opus | sonnet | inherit |
| gsd-roadmapper | opus | sonnet | sonnet | inherit |
| gsd-executor | opus | sonnet | sonnet | inherit |
| gsd-phase-researcher | opus | sonnet | haiku | inherit |
| gsd-project-researcher | opus | sonnet | haiku | inherit |
| gsd-research-synthesizer | sonnet | sonnet | haiku | inherit |
| gsd-debugger | opus | sonnet | sonnet | inherit |
| gsd-codebase-mapper | sonnet | haiku | haiku | inherit |
| gsd-verifier | sonnet | sonnet | haiku | inherit |
| gsd-plan-checker | sonnet | sonnet | haiku | inherit |
| gsd-integration-checker | sonnet | sonnet | haiku | inherit |
| gsd-nyquist-auditor | sonnet | sonnet | haiku | inherit |
## Profile Philosophy
@@ -37,6 +37,11 @@ Model profiles control which Claude model each GSD agent uses. This allows balan
- Haiku for research and verification
- Use when: conserving quota, high-volume work, less critical phases
**inherit** - Follow the current session model
- All agents resolve to `inherit`
- Best when you switch models interactively (for example OpenCode `/model`)
- Use when: you want GSD to follow your currently selected runtime model
## Resolution Logic
Orchestrators resolve model before spawning:
@@ -62,7 +67,7 @@ Override specific agents without changing the entire profile:
}
```
Overrides take precedence over the profile. Valid values: `opus`, `sonnet`, `haiku`.
Overrides take precedence over the profile. Valid values: `opus`, `sonnet`, `haiku`, `inherit`.
## Switching Profiles
@@ -91,3 +96,6 @@ Read-only exploration and pattern extraction. No reasoning required, just struct
**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 `inherit` profile?**
Some runtimes (including OpenCode) let users switch models at runtime (`/model`). The `inherit` profile keeps all GSD subagents aligned to that live selection.

View File

@@ -301,7 +301,7 @@ Usage: `/gsd:plan-milestone-gaps`
Configure workflow toggles and model profile interactively.
- Toggle researcher, plan checker, verifier agents
- Select model profile (quality/balanced/budget)
- Select model profile (quality/balanced/budget/inherit)
- Updates `.planning/config.json`
Usage: `/gsd:settings`
@@ -312,6 +312,7 @@ Quick switch model profile for GSD agents.
- `quality` — Opus everywhere except verification
- `balanced` — Opus for planning, Sonnet for execution (default)
- `budget` — Sonnet for writing, Haiku for research/verification
- `inherit` — Use current session model for all agents (OpenCode `/model`)
Usage: `/gsd:set-profile budget`

View File

@@ -159,7 +159,8 @@ AskUserQuestion([
options: [
{ label: "Balanced (Recommended)", description: "Sonnet for most agents — good quality/cost ratio" },
{ label: "Quality", description: "Opus for research/roadmap — higher cost, deeper analysis" },
{ label: "Budget", description: "Haiku where possible — fastest, lowest cost" }
{ label: "Budget", description: "Haiku where possible — fastest, lowest cost" },
{ label: "Inherit", description: "Use the current session model for all agents (OpenCode /model)" }
]
}
])
@@ -173,7 +174,7 @@ Create `.planning/config.json` with mode set to "yolo":
"granularity": "[selected]",
"parallelization": true|false,
"commit_docs": true|false,
"model_profile": "quality|balanced|budget",
"model_profile": "quality|balanced|budget|inherit",
"workflow": {
"research": true|false,
"plan_check": true|false,
@@ -459,7 +460,8 @@ questions: [
options: [
{ label: "Balanced (Recommended)", description: "Sonnet for most agents — good quality/cost ratio" },
{ label: "Quality", description: "Opus for research/roadmap — higher cost, deeper analysis" },
{ label: "Budget", description: "Haiku where possible — fastest, lowest cost" }
{ label: "Budget", description: "Haiku where possible — fastest, lowest cost" },
{ label: "Inherit", description: "Use the current session model for all agents (OpenCode /model)" }
]
}
]
@@ -473,7 +475,7 @@ Create `.planning/config.json` with all settings:
"granularity": "coarse|standard|fine",
"parallelization": true|false,
"commit_docs": true|false,
"model_profile": "quality|balanced|budget",
"model_profile": "quality|balanced|budget|inherit",
"workflow": {
"research": true|false,
"plan_check": true|false,

View File

@@ -98,7 +98,7 @@ Present:
# [Project Name]
**Progress:** {PROGRESS_BAR}
**Profile:** [quality/balanced/budget]
**Profile:** [quality/balanced/budget/inherit]
## Recent Work
- [Phase X, Plan Y]: [what was accomplished - 1 line from summary-extract]

View File

@@ -12,9 +12,9 @@ Read all files referenced by the invoking prompt's execution_context before star
Validate argument:
```
if $ARGUMENTS.profile not in ["quality", "balanced", "budget"]:
if $ARGUMENTS.profile not in ["quality", "balanced", "budget", "inherit"]:
Error: Invalid profile "$ARGUMENTS.profile"
Valid profiles: quality, balanced, budget
Valid profiles: quality, balanced, budget, inherit
EXIT
```
</step>
@@ -69,6 +69,7 @@ Map profile names:
- quality: use "quality" column from MODEL_PROFILES
- balanced: use "balanced" column from MODEL_PROFILES
- budget: use "budget" column from MODEL_PROFILES
- inherit: use "inherit" for all agents
</step>
</process>

View File

@@ -48,7 +48,8 @@ AskUserQuestion([
options: [
{ label: "Quality", description: "Opus everywhere except verification (highest cost)" },
{ label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for execution/verification" },
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" }
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" },
{ label: "Inherit", description: "Use current session model for all agents (best for OpenCode /model)" }
]
},
{
@@ -136,7 +137,7 @@ Merge new settings into existing config.json:
```json
{
...existing_config,
"model_profile": "quality" | "balanced" | "budget",
"model_profile": "quality" | "balanced" | "budget" | "inherit",
"workflow": {
"research": true/false,
"plan_check": true/false,
@@ -210,7 +211,7 @@ Display:
| Setting | Value |
|----------------------|-------|
| Model Profile | {quality/balanced/budget} |
| Model Profile | {quality/balanced/budget/inherit} |
| Plan Researcher | {On/Off} |
| Plan Checker | {On/Off} |
| Execution Verifier | {On/Off} |

View File

@@ -147,7 +147,7 @@ describe('resolveModelInternal', () => {
describe('model profile structural validation', () => {
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 profiles = ['quality', 'balanced', 'budget', 'inherit'];
const validValues = ['inherit', 'sonnet', 'haiku', 'opus'];
for (const profile of profiles) {
@@ -161,6 +161,14 @@ describe('resolveModelInternal', () => {
}
}
});
test('inherit profile forces all known agents to inherit model', () => {
const knownAgents = ['gsd-planner', 'gsd-executor', 'gsd-phase-researcher', 'gsd-codebase-mapper'];
writeConfig({ model_profile: 'inherit' });
for (const agent of knownAgents) {
assert.strictEqual(resolveModelInternal(tmpDir, agent), 'inherit');
}
});
});
describe('override precedence', () => {
@@ -195,6 +203,11 @@ describe('resolveModelInternal', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-nonexistent'), 'sonnet');
});
test('returns sonnet for unknown agent type even with inherit profile', () => {
writeConfig({ model_profile: 'inherit' });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-nonexistent'), 'sonnet');
});
test('defaults to balanced profile when model_profile missing', () => {
writeConfig({});
// balanced profile, gsd-planner -> opus -> inherit

View File

@@ -255,6 +255,34 @@ describe('validate health command', () => {
);
});
test('accepts inherit model_profile as valid', () => {
writeMinimalProjectMd(tmpDir);
writeMinimalRoadmap(tmpDir, ['1']);
writeMinimalStateMd(tmpDir);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({
model_profile: 'inherit',
workflow: {
research: true,
plan_check: true,
verifier: true,
nyquist_validation: true,
},
})
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
const result = runGsdTools('validate health', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
!output.warnings.some(w => w.code === 'W004'),
`Should not warn for inherit model_profile: ${JSON.stringify(output.warnings)}`
);
});
// ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
test('warns about incorrectly named phase directories', () => {