From 4743dd9660a0df7c6e51c1950f7513e7c83a765f Mon Sep 17 00:00:00 2001 From: Kirill Korikov Date: Sun, 6 Sep 2026 17:32:22 +0400 Subject: [PATCH 1/7] Remove hardcoded codex role models; add role_model config --- README.md | 2 ++ agents/architect.md | 2 -- agents/builder.md | 2 -- agents/researcher.md | 2 -- agents/reviewer.md | 2 -- agents/tester.md | 2 -- cmd/dotagents/agents.go | 18 ++++------------- cmd/dotagents/agents_test.go | 38 ++++++++++++++++++++++++------------ cmd/dotagents/main.go | 1 + 9 files changed, 32 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 1ee7b2b..bebd207 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ mcp_servers: A role is a Markdown file in `~/.agents/agents/` with frontmatter (`name`, `description`, `model`, `effort`, `tools`, optional per-harness overrides) and the system prompt as body. dotagents renders it into each harness's native format — e.g. TOML for Codex. Five generic starter roles ship with the tool: `architect` `builder` `researcher` `reviewer` `tester`. A same-name file in your `~/.agents/agents/` always wins over the starter. +Roles are model-neutral: a role without `model` renders without one and the harness uses its own default. To pin one model for all rendered roles, set `role_model` on the agent entry in `dotagents.yaml` — that YAML entry is the single place to update model names. + ## Hooks and memory Hooks are lifecycle commands (session start/end, stop) registered per harness in `dotagents.yaml`. dotagents ships a memory integration built on them — pick a tier during setup: diff --git a/agents/architect.md b/agents/architect.md index b430c87..98dec79 100644 --- a/agents/architect.md +++ b/agents/architect.md @@ -1,8 +1,6 @@ --- name: architect description: Designs system architecture, telemetry schemas, and technical plans. Use for design docs, architecture reviews, and API surface decisions. Delegates implementation to builders. -model: opus -effort: high tools: [Read, Glob, Grep, Bash, Write, Edit] color: blue codex: diff --git a/agents/builder.md b/agents/builder.md index 24eed1e..bdc8702 100644 --- a/agents/builder.md +++ b/agents/builder.md @@ -1,8 +1,6 @@ --- name: builder description: Implements code changes following specs or architect designs. Use for feature implementation, bug fixes, and script writing. Focused on writing correct, minimal code. -model: opus -effort: high tools: [Read, Glob, Grep, Bash, Write, Edit] color: yellow codex: diff --git a/agents/researcher.md b/agents/researcher.md index 09ea9e3..43dfa8a 100644 --- a/agents/researcher.md +++ b/agents/researcher.md @@ -1,8 +1,6 @@ --- name: researcher description: Investigates codebases, APIs, repos, and web sources to produce findings reports. Use for technical research, competitive analysis, and feasibility studies. -model: opus -effort: high tools: [Read, Glob, Grep, Bash, WebFetch, WebSearch, Write] color: green codex: diff --git a/agents/reviewer.md b/agents/reviewer.md index eed01b5..a047b8e 100644 --- a/agents/reviewer.md +++ b/agents/reviewer.md @@ -1,8 +1,6 @@ --- name: reviewer description: Reviews code changes, PRs, and implementations against specs and best practices. Use for code review, quality gates, and pre-merge checks. Read-only. -model: opus -effort: high tools: [Read, Glob, Grep, Bash] color: purple codex: diff --git a/agents/tester.md b/agents/tester.md index a576710..aa47c8e 100644 --- a/agents/tester.md +++ b/agents/tester.md @@ -1,8 +1,6 @@ --- name: tester description: Runs end-to-end tests against a prepared environment. Executes golden routes and user scenarios, records logic/UX/behavior problems. Never fixes code or builds — report only. -model: opus -effort: medium tools: [Read, Glob, Grep, Bash, Write] color: green --- diff --git a/cmd/dotagents/agents.go b/cmd/dotagents/agents.go index 95528aa..d71420b 100644 --- a/cmd/dotagents/agents.go +++ b/cmd/dotagents/agents.go @@ -372,6 +372,9 @@ func renderAgentRole(role agentRole, agent agentConfig) (string, string, bool) { if h == nil || h.Roles == nil { return "", "", false } + if role.Model == "" && agent.RoleModel != "" { + role.Model = agent.RoleModel + } target := filepath.Join(agent.AgentRoot, role.Name+h.Roles.Extension) content := h.Roles.Render(role) return target, content, true @@ -404,7 +407,7 @@ func renderClaudeAgentRole(role agentRole) string { func renderCodexAgentRole(role agentRole) string { model := strings.TrimSpace(role.Codex.Model) if model == "" { - model = codexModelFor(role.Model) + model = strings.TrimSpace(role.Model) } effort := strings.TrimSpace(role.Codex.ModelReasoningEffort) if effort == "" { @@ -508,19 +511,6 @@ func writeTOMLMultiline(b *strings.Builder, key string, value string) { b.WriteString("\n") } -func codexModelFor(model string) string { - switch strings.ToLower(strings.TrimSpace(model)) { - case "haiku": - return "gpt-5.4-mini" - case "sonnet", "opus": - return "gpt-5.4" - default: - if strings.TrimSpace(model) == "" { - return "gpt-5.4" - } - return strings.TrimSpace(model) - } -} func droidModelFor(model string) string { model = strings.TrimSpace(model) diff --git a/cmd/dotagents/agents_test.go b/cmd/dotagents/agents_test.go index 7e4b02d..06d5944 100644 --- a/cmd/dotagents/agents_test.go +++ b/cmd/dotagents/agents_test.go @@ -42,7 +42,7 @@ func TestRenderCodexAgentRoleEscapesControlCharacters(t *testing.T) { Effort: "high", Instructions: "Line one\nLine two\tTabbed\rReturn", Codex: codexRoleOptions{ - Model: "gpt-5.4-mini", + Model: "test-model-mini", ModelReasoningEffort: "medium", }, } @@ -50,8 +50,7 @@ func TestRenderCodexAgentRoleEscapesControlCharacters(t *testing.T) { got := renderCodexAgentRole(role) for _, want := range []string{ `name = "researcher"`, - `description = "Find \"facts\""`, - `model = "gpt-5.4-mini"`, + `model = "test-model-mini"`, `model_reasoning_effort = "medium"`, `developer_instructions = "Line one\nLine two\tTabbed\rReturn"`, generatedAgentMarker, @@ -95,22 +94,35 @@ func TestRenderDroidAgentRoleMapsModelAndTools(t *testing.T) { } } -func TestCodexModelFor(t *testing.T) { - tests := map[string]string{ - "": "gpt-5.4", - "sonnet": "gpt-5.4", - "opus": "gpt-5.4", - "haiku": "gpt-5.4-mini", - "gpt-custom": "gpt-custom", +func TestRenderCodexAgentRoleOmitsMissingModel(t *testing.T) { + role := agentRole{ + Name: "builder", + Description: "Builds features", + Instructions: "Implement the change.", } - for input, want := range tests { - if got := codexModelFor(input); got != want { - t.Fatalf("codexModelFor(%q) = %q, want %q", input, got, want) + got := renderCodexAgentRole(role) + for _, absent := range []string{"model =", "model_reasoning_effort ="} { + if strings.Contains(got, absent) { + t.Fatalf("model-neutral codex role should omit %q:\n%s", absent, got) } } } +func TestRenderAgentRolePrefillsConfiguredModel(t *testing.T) { + role := agentRole{Name: "builder", Description: "Builds features", Instructions: "Implement."} + path, content, ok := renderAgentRole(role, agentConfig{Name: agentClaudeCode, AgentRoot: t.TempDir(), RoleModel: "configured-model"}) + if !ok { + t.Fatal("claude role was not rendered") + } + if !strings.Contains(content, `model: "configured-model"`) { + t.Fatalf("role missing configured role_model:\n%s", content) + } + if path == "" { + t.Fatal("empty target path") + } +} + func TestDroidModelFor(t *testing.T) { tests := map[string]string{ "": "inherit", diff --git a/cmd/dotagents/main.go b/cmd/dotagents/main.go index 7b98383..f206d74 100644 --- a/cmd/dotagents/main.go +++ b/cmd/dotagents/main.go @@ -36,6 +36,7 @@ type agentConfig struct { SkillRoot string `yaml:"skill_root"` AgentRoot string `yaml:"agent_root,omitempty"` Detect string `yaml:"detect,omitempty"` + RoleModel string `yaml:"role_model,omitempty"` } type repoLinkReport struct { From 0bc696a611e24098ded2f41a50984810bca5f6aa Mon Sep 17 00:00:00 2001 From: Kirill Korikov Date: Sun, 6 Sep 2026 17:37:33 +0400 Subject: [PATCH 2/7] Neutralize legacy model aliases for codex roles --- README.md | 2 +- cmd/dotagents/agents.go | 15 +++++++++++++-- cmd/dotagents/agents_test.go | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bebd207..75c0a19 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ mcp_servers: A role is a Markdown file in `~/.agents/agents/` with frontmatter (`name`, `description`, `model`, `effort`, `tools`, optional per-harness overrides) and the system prompt as body. dotagents renders it into each harness's native format — e.g. TOML for Codex. Five generic starter roles ship with the tool: `architect` `builder` `researcher` `reviewer` `tester`. A same-name file in your `~/.agents/agents/` always wins over the starter. -Roles are model-neutral: a role without `model` renders without one and the harness uses its own default. To pin one model for all rendered roles, set `role_model` on the agent entry in `dotagents.yaml` — that YAML entry is the single place to update model names. +Roles are model-neutral: a role without `model` renders without one and the harness uses its own default. To pin one model for all rendered roles, set `role_model` on the agent entry in `dotagents.yaml` — that YAML entry is the single place to update model names. Legacy Claude-family aliases (`opus`, `sonnet`, `haiku`) still render for Claude Code but are neutralized for Codex, which uses its own default instead. ## Hooks and memory diff --git a/cmd/dotagents/agents.go b/cmd/dotagents/agents.go index d71420b..6a58012 100644 --- a/cmd/dotagents/agents.go +++ b/cmd/dotagents/agents.go @@ -407,7 +407,7 @@ func renderClaudeAgentRole(role agentRole) string { func renderCodexAgentRole(role agentRole) string { model := strings.TrimSpace(role.Codex.Model) if model == "" { - model = strings.TrimSpace(role.Model) + model = codexModelFor(role.Model) } effort := strings.TrimSpace(role.Codex.ModelReasoningEffort) if effort == "" { @@ -428,6 +428,18 @@ func renderCodexAgentRole(role agentRole) string { return b.String() } +// codexModelFor resolves legacy canonical model aliases. Claude-family names +// (opus/sonnet/haiku) are not valid Codex identifiers, so they render without a +// model and Codex uses its own default; anything else passes through verbatim. +func codexModelFor(model string) string { + switch strings.ToLower(strings.TrimSpace(model)) { + case "", "haiku", "sonnet", "opus": + return "" + default: + return strings.TrimSpace(model) + } +} + func renderDroidAgentRole(role agentRole) string { model := strings.TrimSpace(role.Droid.Model) if model == "" { @@ -511,7 +523,6 @@ func writeTOMLMultiline(b *strings.Builder, key string, value string) { b.WriteString("\n") } - func droidModelFor(model string) string { model = strings.TrimSpace(model) switch strings.ToLower(model) { diff --git a/cmd/dotagents/agents_test.go b/cmd/dotagents/agents_test.go index 06d5944..1540369 100644 --- a/cmd/dotagents/agents_test.go +++ b/cmd/dotagents/agents_test.go @@ -108,6 +108,21 @@ func TestRenderCodexAgentRoleOmitsMissingModel(t *testing.T) { } } } +func TestCodexModelForNeutralizesAliases(t *testing.T) { + tests := map[string]string{ + "": "", + "sonnet": "", + "opus": "", + "haiku": "", + "gpt-custom": "gpt-custom", + } + + for input, want := range tests { + if got := codexModelFor(input); got != want { + t.Fatalf("codexModelFor(%q) = %q, want %q", input, got, want) + } + } +} func TestRenderAgentRolePrefillsConfiguredModel(t *testing.T) { role := agentRole{Name: "builder", Description: "Builds features", Instructions: "Implement."} From fb78b13f5a179ef7679aef2a4f20b6f3f0f17dc4 Mon Sep 17 00:00:00 2001 From: Kirill Korikov Date: Sun, 6 Sep 2026 17:38:49 +0400 Subject: [PATCH 3/7] Make starter roles fully model-neutral --- agents/architect.md | 3 --- agents/builder.md | 3 --- agents/researcher.md | 3 --- agents/reviewer.md | 3 --- 4 files changed, 12 deletions(-) diff --git a/agents/architect.md b/agents/architect.md index 98dec79..fae01b4 100644 --- a/agents/architect.md +++ b/agents/architect.md @@ -3,9 +3,6 @@ name: architect description: Designs system architecture, telemetry schemas, and technical plans. Use for design docs, architecture reviews, and API surface decisions. Delegates implementation to builders. tools: [Read, Glob, Grep, Bash, Write, Edit] color: blue -codex: - model: gpt-5.6-sol - model_reasoning_effort: high --- You are a senior software architect. Your job is to design, not build. diff --git a/agents/builder.md b/agents/builder.md index bdc8702..3d2cbc8 100644 --- a/agents/builder.md +++ b/agents/builder.md @@ -3,9 +3,6 @@ name: builder description: Implements code changes following specs or architect designs. Use for feature implementation, bug fixes, and script writing. Focused on writing correct, minimal code. tools: [Read, Glob, Grep, Bash, Write, Edit] color: yellow -codex: - model: gpt-5.6-sol - model_reasoning_effort: high --- You are a senior developer. Your job is to implement exactly what was specified. diff --git a/agents/researcher.md b/agents/researcher.md index 43dfa8a..3a20bef 100644 --- a/agents/researcher.md +++ b/agents/researcher.md @@ -3,9 +3,6 @@ name: researcher description: Investigates codebases, APIs, repos, and web sources to produce findings reports. Use for technical research, competitive analysis, and feasibility studies. tools: [Read, Glob, Grep, Bash, WebFetch, WebSearch, Write] color: green -codex: - model: gpt-5.6-luna - model_reasoning_effort: medium --- You are a technical researcher. Your job is to investigate and report, not implement. diff --git a/agents/reviewer.md b/agents/reviewer.md index a047b8e..6b5e186 100644 --- a/agents/reviewer.md +++ b/agents/reviewer.md @@ -3,9 +3,6 @@ name: reviewer description: Reviews code changes, PRs, and implementations against specs and best practices. Use for code review, quality gates, and pre-merge checks. Read-only. tools: [Read, Glob, Grep, Bash] color: purple -codex: - model: gpt-5.6-sol - model_reasoning_effort: high --- You are a senior code reviewer. Your job is to find bugs, security issues, and spec violations. From cce64d4937147437ba7a9c3f8df031543b65fe2c Mon Sep 17 00:00:00 2001 From: Kirill Korikov Date: Sun, 6 Sep 2026 17:50:00 +0400 Subject: [PATCH 4/7] Keep researcher harness pins; document role_model and tier behavior --- agents/researcher.md | 8 ++++++++ docs/roles.md | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/agents/researcher.md b/agents/researcher.md index 3a20bef..a7f4b6e 100644 --- a/agents/researcher.md +++ b/agents/researcher.md @@ -1,8 +1,16 @@ --- name: researcher description: Investigates codebases, APIs, repos, and web sources to produce findings reports. Use for technical research, competitive analysis, and feasibility studies. +model: opus +effort: high tools: [Read, Glob, Grep, Bash, WebFetch, WebSearch, Write] color: green +codex: + model: gpt-5.6-luna + model_reasoning_effort: max +omp: + model: gpt-5.6-luna + thinking-level: max --- You are a technical researcher. Your job is to investigate and report, not implement. diff --git a/docs/roles.md b/docs/roles.md index 16e3a0e..0721201 100644 --- a/docs/roles.md +++ b/docs/roles.md @@ -4,7 +4,7 @@ A role is a Markdown file in `~/.agents/agents/` with frontmatter (`name`, `desc ## Model tiers and overrides -The generic `model` value is a capability tier (`haiku`, `sonnet`, or `opus`). Claude Code, Codex, and Droid render it natively in their own model family. Harnesses without a native tier concept use native inheritance or an exact per-harness override: +Claude Code and Droid render the tier natively in their own model family. Codex neutralizes the tier and uses its own default unless an exact per-harness `codex.model` override is set. Harnesses without a tier concept use native inheritance or an exact per-harness override: ```yaml model: opus @@ -19,7 +19,10 @@ opencode: qwen: model: qwen3-coder-plus approval_mode: plan -``` + +### Centralized model pin + +To pin one model for all rendered roles that have no explicit model, set `role_model` on the agent entry in `dotagents.yaml`. Roles (or per-harness overrides) that declare a model always win over `role_model`. Exact model names belong in role frontmatter or `role_model` — never in tool code. ## Rendering targets From 1c2e3e6c3e02a7cdc8930f07de4bfe19d4a8faf3 Mon Sep 17 00:00:00 2001 From: Kirill Korikov Date: Sun, 6 Sep 2026 17:54:09 +0400 Subject: [PATCH 5/7] Apply role_model to opencode and qwen role renderers --- cmd/dotagents/opencode.go | 2 ++ cmd/dotagents/opencode_test.go | 13 +++++++++++++ cmd/dotagents/qwen.go | 6 +++++- cmd/dotagents/qwen_test.go | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/cmd/dotagents/opencode.go b/cmd/dotagents/opencode.go index 1c004af..9736ea3 100644 --- a/cmd/dotagents/opencode.go +++ b/cmd/dotagents/opencode.go @@ -52,6 +52,8 @@ func renderOpenCodeAgentRole(role agentRole) string { b.WriteString("\n") if model := strings.TrimSpace(role.Opencode.Model); model != "" { writeYAMLScalar(&b, "model", model) + } else if model := strings.TrimSpace(role.Model); model != "" && !canonicalModelTier(model) { + writeYAMLScalar(&b, "model", model) } if temperature := strings.TrimSpace(role.Opencode.Temperature); temperature != "" { b.WriteString("temperature: ") diff --git a/cmd/dotagents/opencode_test.go b/cmd/dotagents/opencode_test.go index ad2caaf..fec9d67 100644 --- a/cmd/dotagents/opencode_test.go +++ b/cmd/dotagents/opencode_test.go @@ -387,3 +387,16 @@ Be helpful. t.Fatalf("converted opencode role dropped body:\n%s", data) } } + +func TestOpenCodeRoleRenderUsesConfiguredModel(t *testing.T) { + role := agentRole{ + Name: "builder", + Description: "Builds features", + Model: "openai/gpt-5.6", + Instructions: "Implement.", + } + content := renderOpenCodeAgentRole(role) + if !strings.Contains(content, "model: \"openai/gpt-5.6\"") { + t.Fatalf("configured role_model not applied to OpenCode role:\n%s", content) + } +} diff --git a/cmd/dotagents/qwen.go b/cmd/dotagents/qwen.go index 3b6cc43..63de12a 100644 --- a/cmd/dotagents/qwen.go +++ b/cmd/dotagents/qwen.go @@ -154,7 +154,11 @@ var qwenToolMapping = map[string]string{ func renderQwenAgentRole(role agentRole) string { model := strings.TrimSpace(role.Qwen.Model) if model == "" { - model = "inherit" + if generic := strings.TrimSpace(role.Model); generic != "" && !canonicalModelTier(generic) { + model = generic + } else { + model = "inherit" + } } tools := role.Qwen.Tools if len(tools) == 0 { diff --git a/cmd/dotagents/qwen_test.go b/cmd/dotagents/qwen_test.go index c8b71b3..f11ab00 100644 --- a/cmd/dotagents/qwen_test.go +++ b/cmd/dotagents/qwen_test.go @@ -247,3 +247,17 @@ hooks: t.Fatalf("config-driven skills should not create a Qwen mirror: %v", err) } } + +func TestQwenRoleRenderUsesConfiguredModel(t *testing.T) { + role := agentRole{Name: "builder", Description: "Builds features", Model: "qwen3-coder-plus", Instructions: "Implement."} + content := renderQwenAgentRole(role) + if !strings.Contains(content, "model: \"qwen3-coder-plus\"") { + t.Fatalf("configured role_model not applied to Qwen role:\n%s", content) + } + + alias := agentRole{Name: "reviewer", Description: "Reviews code", Model: "opus", Instructions: "Review."} + content = renderQwenAgentRole(alias) + if strings.Contains(content, "model: opus") { + t.Fatalf("tier alias leaked into Qwen role:\n%s", content) + } +} From d736ad4d18dda497072739b10fb088a9654979d7 Mon Sep 17 00:00:00 2001 From: Kirill Korikov Date: Sun, 6 Sep 2026 17:56:33 +0400 Subject: [PATCH 6/7] Add canonicalModelTier helper --- cmd/dotagents/agents.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmd/dotagents/agents.go b/cmd/dotagents/agents.go index 3bafbbe..f42f229 100644 --- a/cmd/dotagents/agents.go +++ b/cmd/dotagents/agents.go @@ -512,6 +512,17 @@ func renderDroidAgentRole(role agentRole) string { return b.String() } +// canonicalModelTier reports whether the value is a legacy tier alias rather +// than an exact model identifier. Harnesses without a tier concept must not +// emit these values verbatim. +func canonicalModelTier(model string) bool { + switch strings.ToLower(strings.TrimSpace(model)) { + case "haiku", "sonnet", "opus": + return true + } + return false +} + func agentRoleSourceLabel(role agentRole) string { if role.Source == "" { return "agents/" + role.Name + agentRoleMarkdownExt From 9b975ba3afec54d7a1182162aee287ea5161a51c Mon Sep 17 00:00:00 2001 From: Kirill Korikov Date: Sun, 6 Sep 2026 18:27:03 +0400 Subject: [PATCH 7/7] Restore yaml fence; align README tiers wording --- README.md | 2 +- docs/roles.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 875867b..7148654 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Candidates are inert until you promote them into durable instructions — consol ## Roles -Markdown role definitions in `~/.agents/agents/`, rendered to each harness's native format (Claude Markdown, Codex TOML, Qwen Markdown, Droid). Generic `model` tiers (`haiku`/`sonnet`/`opus`) render natively per family; per-harness overrides pin exact ids. Six starter roles ship with the tool; yours win on name collision. Details in [docs/roles.md](docs/roles.md). +Markdown role definitions in `~/.agents/agents/`, rendered to each harness's native format (Claude Markdown, Codex TOML, Qwen Markdown, Droid). Generic `model` tiers (`haiku`/`sonnet`/`opus`) render natively for Claude and Droid; Codex omits them and uses its own default unless a per-harness override pins an exact id. Six starter roles ship with the tool; yours win on name collision. Details in [docs/roles.md](docs/roles.md). ## Commands diff --git a/docs/roles.md b/docs/roles.md index 0721201..8d98968 100644 --- a/docs/roles.md +++ b/docs/roles.md @@ -19,6 +19,7 @@ opencode: qwen: model: qwen3-coder-plus approval_mode: plan +``` ### Centralized model pin