From cfb93a12d0ce160977adb6ea694ced83296785aa Mon Sep 17 00:00:00 2001 From: bong Date: Thu, 12 Feb 2026 04:29:40 +0900 Subject: [PATCH 1/4] feat(schema): add model_candidates field to agent config - Added model_candidates: z.array(z.string()).optional() to AgentOverrideConfigSchema - Updated JSON schema generation (build-schema.ts uses correct Zod 4 method) - Maintains backward compatibility (field is optional) - JSON schema now includes model_candidates field Preparation for Task 3: Runtime state management module --- assets/oh-my-opencode.schema.json | 3277 +++++++++++++++++++++++++- script/build-schema.ts | 6 +- src/config/schema/agent-overrides.ts | 2 + 3 files changed, 3281 insertions(+), 4 deletions(-) diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 343c3c0785..4a60d3022f 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -2,5 +2,3280 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", "title": "Oh My OpenCode Configuration", - "description": "Configuration schema for oh-my-opencode plugin" + "description": "Configuration schema for oh-my-opencode plugin", + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "new_task_system_enabled": { + "type": "boolean" + }, + "default_run_agent": { + "type": "string" + }, + "disabled_mcps": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "disabled_agents": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "sisyphus", + "hephaestus", + "prometheus", + "oracle", + "librarian", + "explore", + "multimodal-looker", + "metis", + "momus", + "atlas" + ] + } + }, + "disabled_skills": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "playwright", + "agent-browser", + "dev-browser", + "frontend-ui-ux", + "git-master" + ] + } + }, + "disabled_hooks": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "todo-continuation-enforcer", + "context-window-monitor", + "session-recovery", + "session-notification", + "comment-checker", + "grep-output-truncator", + "tool-output-truncator", + "question-label-truncator", + "directory-agents-injector", + "directory-readme-injector", + "empty-task-response-detector", + "think-mode", + "subagent-question-blocker", + "anthropic-context-window-limit-recovery", + "preemptive-compaction", + "rules-injector", + "background-notification", + "auto-update-checker", + "startup-toast", + "keyword-detector", + "agent-usage-reminder", + "non-interactive-env", + "interactive-bash-session", + "thinking-block-validator", + "ralph-loop", + "category-skill-reminder", + "compaction-context-injector", + "compaction-todo-preserver", + "claude-code-hooks", + "auto-slash-command", + "edit-error-recovery", + "delegate-task-retry", + "prometheus-md-only", + "sisyphus-junior-notepad", + "start-work", + "atlas", + "unstable-agent-babysitter", + "task-reminder", + "task-resume-info", + "stop-continuation-guard", + "tasks-todowrite-disabler", + "write-existing-file-guard", + "anthropic-effort" + ] + } + }, + "disabled_commands": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "init-deep", + "ralph-loop", + "ulw-loop", + "cancel-ralph", + "refactor", + "start-work", + "stop-continuation" + ] + } + }, + "disabled_tools": { + "type": "array", + "items": { + "type": "string" + } + }, + "agents": { + "type": "object", + "properties": { + "build": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "plan": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "sisyphus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "hephaestus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "sisyphus-junior": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "OpenCode-Builder": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "prometheus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "metis": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "momus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "oracle": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "librarian": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "explore": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "multimodal-looker": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "atlas": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "model_candidates": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "categories": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "prompt_append": { + "type": "string" + }, + "is_unstable_agent": { + "type": "boolean" + }, + "disable": { + "type": "boolean" + } + } + } + }, + "claude_code": { + "type": "object", + "properties": { + "mcp": { + "type": "boolean" + }, + "commands": { + "type": "boolean" + }, + "skills": { + "type": "boolean" + }, + "agents": { + "type": "boolean" + }, + "hooks": { + "type": "boolean" + }, + "plugins": { + "type": "boolean" + }, + "plugins_override": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + } + } + }, + "sisyphus_agent": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean" + }, + "default_builder_enabled": { + "type": "boolean" + }, + "planner_enabled": { + "type": "boolean" + }, + "replace_plan": { + "type": "boolean" + } + } + }, + "comment_checker": { + "type": "object", + "properties": { + "custom_prompt": { + "type": "string" + } + } + }, + "experimental": { + "type": "object", + "properties": { + "aggressive_truncation": { + "type": "boolean" + }, + "auto_resume": { + "type": "boolean" + }, + "preemptive_compaction": { + "type": "boolean" + }, + "truncate_all_tool_outputs": { + "type": "boolean" + }, + "dynamic_context_pruning": { + "type": "object", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "notification": { + "default": "detailed", + "type": "string", + "enum": [ + "off", + "minimal", + "detailed" + ] + }, + "turn_protection": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "turns": { + "default": 3, + "type": "number", + "minimum": 1, + "maximum": 10 + } + } + }, + "protected_tools": { + "default": [ + "task", + "todowrite", + "todoread", + "lsp_rename", + "session_read", + "session_write", + "session_search" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "strategies": { + "type": "object", + "properties": { + "deduplication": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + } + }, + "supersede_writes": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "aggressive": { + "default": false, + "type": "boolean" + } + } + }, + "purge_errors": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "turns": { + "default": 5, + "type": "number", + "minimum": 1, + "maximum": 20 + } + } + } + } + } + } + }, + "task_system": { + "type": "boolean" + }, + "plugin_load_timeout_ms": { + "type": "number", + "minimum": 1000 + }, + "safe_hook_creation": { + "type": "boolean" + } + } + }, + "auto_update": { + "type": "boolean" + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "allOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "template": { + "type": "string" + }, + "from": { + "type": "string" + }, + "model": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "subtask": { + "type": "boolean" + }, + "argument-hint": { + "type": "string" + }, + "license": { + "type": "string" + }, + "compatibility": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "allowed-tools": { + "type": "array", + "items": { + "type": "string" + } + }, + "disable": { + "type": "boolean" + } + } + } + ] + } + }, + { + "type": "object", + "properties": { + "sources": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "recursive": { + "type": "boolean" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "path" + ] + } + ] + } + }, + "enable": { + "type": "array", + "items": { + "type": "string" + } + }, + "disable": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + ] + }, + "ralph_loop": { + "type": "object", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "default_max_iterations": { + "default": 100, + "type": "number", + "minimum": 1, + "maximum": 1000 + }, + "state_dir": { + "type": "string" + } + } + }, + "background_task": { + "type": "object", + "properties": { + "defaultConcurrency": { + "type": "number", + "minimum": 1 + }, + "providerConcurrency": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "number", + "minimum": 0 + } + }, + "modelConcurrency": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "number", + "minimum": 0 + } + }, + "staleTimeoutMs": { + "type": "number", + "minimum": 60000 + } + } + }, + "notification": { + "type": "object", + "properties": { + "force_enable": { + "type": "boolean" + } + } + }, + "babysitting": { + "type": "object", + "properties": { + "timeout_ms": { + "default": 120000, + "type": "number" + } + } + }, + "git_master": { + "type": "object", + "properties": { + "commit_footer": { + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "include_co_authored_by": { + "default": true, + "type": "boolean" + } + } + }, + "browser_automation_engine": { + "type": "object", + "properties": { + "provider": { + "default": "playwright", + "type": "string", + "enum": [ + "playwright", + "agent-browser", + "dev-browser" + ] + } + } + }, + "websearch": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "exa", + "tavily" + ] + } + } + }, + "tmux": { + "type": "object", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "layout": { + "default": "main-vertical", + "type": "string", + "enum": [ + "main-horizontal", + "main-vertical", + "tiled", + "even-horizontal", + "even-vertical" + ] + }, + "main_pane_size": { + "default": 60, + "type": "number", + "minimum": 20, + "maximum": 80 + }, + "main_pane_min_width": { + "default": 120, + "type": "number", + "minimum": 40 + }, + "agent_pane_min_width": { + "default": 40, + "type": "number", + "minimum": 20 + } + } + }, + "sisyphus": { + "type": "object", + "properties": { + "tasks": { + "type": "object", + "properties": { + "storage_path": { + "type": "string" + }, + "task_list_id": { + "type": "string" + }, + "claude_code_compat": { + "default": false, + "type": "boolean" + } + } + } + } + }, + "_migrations": { + "type": "array", + "items": { + "type": "string" + } + } + } } \ No newline at end of file diff --git a/script/build-schema.ts b/script/build-schema.ts index d0ed70d3ae..e2b0aa6636 100644 --- a/script/build-schema.ts +++ b/script/build-schema.ts @@ -1,6 +1,5 @@ #!/usr/bin/env bun import * as z from "zod" -import { zodToJsonSchema } from "zod-to-json-schema" import { OhMyOpenCodeConfigSchema } from "../src/config/schema" const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json" @@ -8,8 +7,9 @@ const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json" async function main() { console.log("Generating JSON Schema...") - const jsonSchema = zodToJsonSchema(OhMyOpenCodeConfigSchema, { - target: "draft7", + const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, { + io: "input", + target: "draft-7", }) const finalSchema = { diff --git a/src/config/schema/agent-overrides.ts b/src/config/schema/agent-overrides.ts index 8fd48e330c..cfef7898b0 100644 --- a/src/config/schema/agent-overrides.ts +++ b/src/config/schema/agent-overrides.ts @@ -37,6 +37,8 @@ export const AgentOverrideConfigSchema = z.object({ textVerbosity: z.enum(["low", "medium", "high"]).optional(), /** Provider-specific options. Passed directly to OpenCode SDK. */ providerOptions: z.record(z.string(), z.unknown()).optional(), + /** Model candidates for fallback selection. */ + model_candidates: z.array(z.string()).optional(), }) export const AgentOverridesSchema = z.object({ From 7ac1f467cf01b27dbdb85ebdcf02f5056d14ba7b Mon Sep 17 00:00:00 2001 From: bong Date: Thu, 12 Feb 2026 05:27:19 +0900 Subject: [PATCH 2/4] feat(model-switcher): add agentName parameter to core agents for model override - createSisyphusAgent: add agentName?: string parameter - createHephaestusAgent: add agentName?: string parameter - createAtlasAgent: add agentName to OrchestratorContext - Each create function uses getActiveModel(agentName) for override - Builtin-agents pass agentName when creating agents This enables runtime model switching for Sisyphus, Hephaestus, and Atlas through getActiveModel() integration with the model-switcher state module. --- bun.lock | 28 +- src/agents/agent-builder.ts | 9 +- src/agents/atlas/agent.ts | 7 +- src/agents/builtin-agents.ts | 3 + src/agents/builtin-agents/atlas-agent.ts | 1 + src/agents/builtin-agents/general-agents.ts | 2 +- src/agents/builtin-agents/hephaestus-agent.ts | 3 +- src/agents/builtin-agents/sisyphus-agent.ts | 3 +- src/agents/hephaestus.ts | 7 +- src/agents/sisyphus.ts | 7 +- src/features/model-switcher/index.ts | 9 + src/features/model-switcher/state.ts | 141 ++++ test-direct.ts | 11 + test-schema.ts | 7 + test-schema2.ts | 5 + test-zod4.ts | 15 + test_output.txt | 722 ++++++++++++++++++ 17 files changed, 956 insertions(+), 24 deletions(-) create mode 100644 src/features/model-switcher/index.ts create mode 100644 src/features/model-switcher/state.ts create mode 100644 test-direct.ts create mode 100644 test-schema.ts create mode 100644 test-schema2.ts create mode 100644 test-zod4.ts create mode 100644 test_output.txt diff --git a/bun.lock b/bun.lock index 4a416c88d2..0175f65a56 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,13 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.3.1", - "oh-my-opencode-darwin-x64": "3.3.1", - "oh-my-opencode-linux-arm64": "3.3.1", - "oh-my-opencode-linux-arm64-musl": "3.3.1", - "oh-my-opencode-linux-x64": "3.3.1", - "oh-my-opencode-linux-x64-musl": "3.3.1", - "oh-my-opencode-windows-x64": "3.3.1", + "oh-my-opencode-darwin-arm64": "3.5.2", + "oh-my-opencode-darwin-x64": "3.5.2", + "oh-my-opencode-linux-arm64": "3.5.2", + "oh-my-opencode-linux-arm64-musl": "3.5.2", + "oh-my-opencode-linux-x64": "3.5.2", + "oh-my-opencode-linux-x64-musl": "3.5.2", + "oh-my-opencode-windows-x64": "3.5.2", }, }, }, @@ -226,19 +226,19 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.3.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-R+o42Km6bsIaW6D3I8uu2HCF3BjIWqa/fg38W5y4hJEOw4mL0Q7uV4R+0vtrXRHo9crXTK9ag0fqVQUm+Y6iAQ=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.5.2", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oIS3lB2F9/N+3mF5wCKk6/EPVSz516XWN+mNdquSSeddw+xqMxGdhKY6K/XeYbHJzeN2Z8IOikNEJ6psR2/a8g=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.3.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7VTbpR1vH3OEkoJxBKtYuxFPX8M3IbJKoeHWME9iK6FpT11W1ASsjyuhvzB1jcxSeqF8ddMnjitlG5ub6h5EVw=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.5.2", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OAdXo4ZCCYO4kRWtnyz3tdmaGYPUB3WcXimXAxp+/sEZxAnh7n1RQkpLn6UxWX4AIAdRT9dfrOfRic6VoCYv2g=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-BZ/r/CFlvbOxkdZZrRoT16xFOjibRZHuwQnaE4f0JvOzgK6/HWp3zJI1+2/aX/oK5GA6lZxNWRrJC/SKUi8LEg=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.5.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-5XXNMFhp1VsyrGNRBoXcOyoaUeVkbrWkBRPDGZfpiq+kRXH3aaSWdR5G7Pl/TadOQv9Bl8/8YaxsuHRTFT1aXw=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-U90Wruf21h+CJbtcrS7MeTAc/5VOF6RI+5jr7qj/cCxjXNJtjhyJdz/maehArjtgf304+lYCM/Mh1i+G2D3YFQ=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.5.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-/woIpqvEI85MgJvEVnz4g5FBLeiQNK7srRsueIFPBmtTahh42HFleCDaIltOl/ndjsE5nCHacQVJHkC9W9/F3Q=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-sYzohSNdwsAhivbXcbhPdF1qqQi2CCI7FSgbmvvfBOMyZ8HAgqOFqYW2r3GPdmtywzkjOTvCzTG56FZwEjx15w=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.5.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vTL2A+6zzGhi+m7sC8peLDq5OAp2dRR0UEb4RbZAOHtlEruF7qFEmcK3ccWxwc3+Z3G/ITfwn5VNa72ZS4pNTg=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aG5pZ4eWS0YSGUicOnjMkUPrIqQV4poYF+d9SIvrfvlaMcK6WlQn7jXzgNCwJsfGn5lyhSmjshZBEU+v79Ua3w=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.5.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-bOAA55snLsK2QB00IkQy8le0Oqh/GJ7pxEHtm1oUezlQrW/nX5SS/hJ7dPHMmOd9FoiqnqyqWZxNkLmFoG463A=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.3.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-FGH7cnzBqNwjSkzCDglMsVttaq+MsykAxa7ehaFK+0dnBZArvllS3W13a3dGaANHMZzfK0vz8hNDUdVi7Z63cA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.5.2", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-fnHiAPYglw3unPckmQBoCT6+VqjSWCE3S3J551mRo0ZFrxuEP2ZKyHZeFMMOtKwDepCvmKgd1W040+KmuVUXOA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/agents/agent-builder.ts b/src/agents/agent-builder.ts index f60f8137ba..0edaf1e20c 100644 --- a/src/agents/agent-builder.ts +++ b/src/agents/agent-builder.ts @@ -4,6 +4,7 @@ import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../confi import type { BrowserAutomationProvider } from "../config/schema" import { mergeCategories } from "../shared/merge-categories" import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content" +import { getActiveModel } from "../features/model-switcher" export type AgentSource = AgentFactory | AgentConfig @@ -17,9 +18,13 @@ export function buildAgent( categories?: CategoriesConfig, gitMasterConfig?: GitMasterConfig, browserProvider?: BrowserAutomationProvider, - disabledSkills?: Set + disabledSkills?: Set, + agentName?: string ): AgentConfig { - const base = isFactory(source) ? source(model) : { ...source } + const activeModel = agentName ? getActiveModel(agentName) : undefined + const effectiveModel = activeModel ?? model + + const base = isFactory(source) ? source(effectiveModel) : { ...source } const categoryConfigs: Record = mergeCategories(categories) const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string } diff --git a/src/agents/atlas/agent.ts b/src/agents/atlas/agent.ts index c4aa65f7c0..13931e905f 100644 --- a/src/agents/atlas/agent.ts +++ b/src/agents/atlas/agent.ts @@ -17,6 +17,7 @@ import { buildCategorySkillsDelegationGuide } from "../dynamic-agent-prompt-buil import type { CategoryConfig } from "../../config/schema" import { mergeCategories } from "../../shared/merge-categories" import { createAgentToolRestrictions } from "../../shared/permission-compat" +import { getActiveModel } from "../../features/model-switcher" import { getDefaultAtlasPrompt } from "./default" import { getGptAtlasPrompt } from "./gpt" @@ -47,6 +48,7 @@ export interface OrchestratorContext { availableAgents?: AvailableAgent[] availableSkills?: AvailableSkill[] userCategories?: Record + agentName?: string } /** @@ -93,6 +95,9 @@ function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string { } export function createAtlasAgent(ctx: OrchestratorContext): AgentConfig { + const activeModel = ctx.agentName ? getActiveModel(ctx.agentName) : undefined + const effectiveModel = activeModel ?? ctx.model + const restrictions = createAgentToolRestrictions([ "task", "call_omo_agent", @@ -102,7 +107,7 @@ export function createAtlasAgent(ctx: OrchestratorContext): AgentConfig { description: "Orchestrates work via task() to complete ALL tasks in a todo list until fully done. (Atlas - OhMyOpenCode)", mode: MODE, - ...(ctx.model ? { model: ctx.model } : {}), + ...(effectiveModel ? { model: effectiveModel } : {}), temperature: 0.1, prompt: buildDynamicOrchestratorPrompt(ctx), color: "#10B981", diff --git a/src/agents/builtin-agents.ts b/src/agents/builtin-agents.ts index b20c166ef2..c6b80075e5 100644 --- a/src/agents/builtin-agents.ts +++ b/src/agents/builtin-agents.ts @@ -22,6 +22,7 @@ import { maybeCreateSisyphusConfig } from "./builtin-agents/sisyphus-agent" import { maybeCreateHephaestusConfig } from "./builtin-agents/hephaestus-agent" import { maybeCreateAtlasConfig } from "./builtin-agents/atlas-agent" import { buildCustomAgentMetadata, parseRegisteredAgentSummaries } from "./custom-agent-summaries" +import { loadCandidates } from "../features/model-switcher" type AgentSource = AgentFactory | AgentConfig @@ -67,6 +68,8 @@ export async function createBuiltinAgents( disabledSkills?: Set, useTaskSystem = false ): Promise> { + loadCandidates(agentOverrides) + const connectedProviders = readConnectedProvidersCache() // IMPORTANT: Do NOT call OpenCode client APIs during plugin initialization. // This function is called from config handler, and calling client API causes deadlock. diff --git a/src/agents/builtin-agents/atlas-agent.ts b/src/agents/builtin-agents/atlas-agent.ts index e9c3d17ef6..98b9a0afb2 100644 --- a/src/agents/builtin-agents/atlas-agent.ts +++ b/src/agents/builtin-agents/atlas-agent.ts @@ -52,6 +52,7 @@ export function maybeCreateAtlasConfig(input: { availableAgents, availableSkills, userCategories, + agentName: "atlas", }) if (atlasResolvedVariant) { diff --git a/src/agents/builtin-agents/general-agents.ts b/src/agents/builtin-agents/general-agents.ts index 491e7be119..2dcd230229 100644 --- a/src/agents/builtin-agents/general-agents.ts +++ b/src/agents/builtin-agents/general-agents.ts @@ -73,7 +73,7 @@ export function collectPendingBuiltinAgents(input: { if (!resolution) continue const { model, variant: resolvedVariant } = resolution - let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider, disabledSkills) + let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider, disabledSkills, agentName) // Apply resolved variant from model fallback chain if (resolvedVariant) { diff --git a/src/agents/builtin-agents/hephaestus-agent.ts b/src/agents/builtin-agents/hephaestus-agent.ts index 649ee293d1..78dd78f31b 100644 --- a/src/agents/builtin-agents/hephaestus-agent.ts +++ b/src/agents/builtin-agents/hephaestus-agent.ts @@ -69,7 +69,8 @@ export function maybeCreateHephaestusConfig(input: { undefined, availableSkills, availableCategories, - useTaskSystem + useTaskSystem, + "hephaestus" ) hephaestusConfig = { ...hephaestusConfig, variant: hephaestusResolvedVariant ?? "medium" } diff --git a/src/agents/builtin-agents/sisyphus-agent.ts b/src/agents/builtin-agents/sisyphus-agent.ts index 11e34e22ac..2e844265a5 100644 --- a/src/agents/builtin-agents/sisyphus-agent.ts +++ b/src/agents/builtin-agents/sisyphus-agent.ts @@ -70,7 +70,8 @@ export function maybeCreateSisyphusConfig(input: { undefined, availableSkills, availableCategories, - useTaskSystem + useTaskSystem, + "sisyphus" ) if (sisyphusResolvedVariant) { diff --git a/src/agents/hephaestus.ts b/src/agents/hephaestus.ts index e1c9a43600..201b404cdb 100644 --- a/src/agents/hephaestus.ts +++ b/src/agents/hephaestus.ts @@ -13,6 +13,7 @@ import { buildAntiPatternsSection, categorizeTools, } from "./dynamic-agent-prompt-builder" +import { getActiveModel } from "../features/model-switcher" const MODE: AgentMode = "primary" @@ -600,8 +601,10 @@ export function createHephaestusAgent( availableToolNames?: string[], availableSkills?: AvailableSkill[], availableCategories?: AvailableCategory[], - useTaskSystem = false + useTaskSystem = false, + agentName?: string ): AgentConfig { + const effectiveModel = agentName ? getActiveModel(agentName) ?? model : model const tools = availableToolNames ? categorizeTools(availableToolNames) : [] const skills = availableSkills ?? [] const categories = availableCategories ?? [] @@ -613,7 +616,7 @@ export function createHephaestusAgent( description: "Autonomous Deep Worker - goal-oriented execution with GPT 5.2 Codex. Explores thoroughly before acting, uses explore/librarian agents for comprehensive context, completes tasks end-to-end. Inspired by AmpCode deep mode. (Hephaestus - OhMyOpenCode)", mode: MODE, - model, + model: effectiveModel, maxTokens: 32000, prompt, color: "#D97706", // Forged Amber - Golden heated metal, divine craftsman diff --git a/src/agents/sisyphus.ts b/src/agents/sisyphus.ts index 1a6fac448e..1eac4776e7 100644 --- a/src/agents/sisyphus.ts +++ b/src/agents/sisyphus.ts @@ -1,6 +1,7 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentMode, AgentPromptMetadata } from "./types" import { isGptModel } from "./types" +import { getActiveModel } from "../features/model-switcher" const MODE: AgentMode = "primary" export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = { @@ -506,8 +507,10 @@ export function createSisyphusAgent( availableToolNames?: string[], availableSkills?: AvailableSkill[], availableCategories?: AvailableCategory[], - useTaskSystem = false + useTaskSystem = false, + agentName?: string ): AgentConfig { + const effectiveModel = agentName ? getActiveModel(agentName) ?? model : model const tools = availableToolNames ? categorizeTools(availableToolNames) : [] const skills = availableSkills ?? [] const categories = availableCategories ?? [] @@ -520,7 +523,7 @@ export function createSisyphusAgent( description: "Powerful AI orchestrator. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically via category+skills combinations. Uses explore for internal code (parallel-friendly), librarian for external docs. (Sisyphus - OhMyOpenCode)", mode: MODE, - model, + model: effectiveModel, maxTokens: 64000, prompt, color: "#00CED1", diff --git a/src/features/model-switcher/index.ts b/src/features/model-switcher/index.ts new file mode 100644 index 0000000000..4f51cc3e2e --- /dev/null +++ b/src/features/model-switcher/index.ts @@ -0,0 +1,9 @@ +export type { ModelSwitcherState } from "./state" +export { + getModelSwitcherState, + getActiveModel, + setActiveModel, + loadCandidates, + getCandidates, + getCurrentModelInfo, +} from "./state" diff --git a/src/features/model-switcher/state.ts b/src/features/model-switcher/state.ts new file mode 100644 index 0000000000..dd311298b2 --- /dev/null +++ b/src/features/model-switcher/state.ts @@ -0,0 +1,141 @@ +import type { AgentOverrides } from "../../config/schema" + +/** + * Runtime state for model switcher functionality. + * Uses in-memory storage only (no filesystem persistence). + */ +export interface ModelSwitcherState { + /** + * Agent name -> active model override + * When an agent is switched to a specific model, it's stored here. + */ + activeOverrides: Map + + /** + * Agent name -> list of candidate models from config + * Loaded from config's model_candidates field. + */ + candidates: Map + + /** + * Get the active model for a specific agent. + * Returns undefined if no override is set. + */ + getActiveModel(agentName: string): string | undefined + + /** + * Set an active model override for a specific agent. + * This will be used instead of the default fallback chain. + */ + setActiveModel(agentName: string, model: string): void + + /** + * Load model candidates from config. + * Called once during agent initialization. + */ + loadCandidates(agentOverrides: AgentOverrides): void + + /** + * Get candidate models for a specific agent. + * Returns empty array if no candidates defined. + */ + getCandidates(agentName: string): string[] + + /** + * Get current model info for all agents with overrides/candidates. + * Useful for debugging and status display. + */ + getCurrentModelInfo(): Record +} + +/** + * Internal implementation of ModelSwitcherState. + * Uses singleton pattern to ensure a single instance across the application. + */ +class ModelSwitcherStateImpl implements ModelSwitcherState { + activeOverrides: Map = new Map() + candidates: Map = new Map() + + getActiveModel(agentName: string): string | undefined { + return this.activeOverrides.get(agentName) + } + + setActiveModel(agentName: string, model: string): void { + this.activeOverrides.set(agentName, model) + } + + loadCandidates(agentOverrides: AgentOverrides): void { + for (const [agentName, config] of Object.entries(agentOverrides)) { + if (config?.model_candidates && config.model_candidates.length > 0) { + this.candidates.set(agentName, config.model_candidates) + } + } + } + + getCandidates(agentName: string): string[] { + return this.candidates.get(agentName) ?? [] + } + + getCurrentModelInfo(): Record { + const info: Record = {} + + // Include agents with active overrides + for (const [agentName, model] of this.activeOverrides.entries()) { + info[agentName] = { + active: model, + candidates: this.candidates.get(agentName) ?? [], + } + } + + // Include agents with candidates but no active override + for (const [agentName, models] of this.candidates.entries()) { + if (!info[agentName]) { + info[agentName] = { + active: undefined, + candidates: models, + } + } + } + + return info + } +} + +// Singleton instance +let instance: ModelSwitcherState | null = null + +/** + * Get the singleton instance of ModelSwitcherState. + * Creates a new instance if one doesn't exist. + */ +export function getModelSwitcherState(): ModelSwitcherState { + if (!instance) { + instance = new ModelSwitcherStateImpl() + } + return instance +} + +/** + * Export convenience functions that delegate to the singleton. + * This allows consumers to import these functions directly without + * dealing with the getInstance() pattern. + */ +export function getActiveModel(agentName: string): string | undefined { + return getModelSwitcherState().getActiveModel(agentName) +} + +export function setActiveModel(agentName: string, model: string): void { + getModelSwitcherState().setActiveModel(agentName, model) +} + +export function loadCandidates(agentOverrides: AgentOverrides): void { + getModelSwitcherState().loadCandidates(agentOverrides) +} + +export function getCandidates(agentName: string): string[] { + return getModelSwitcherState().getCandidates(agentName) +} + +export function getCurrentModelInfo(): Record { + return getModelSwitcherState().getCurrentModelInfo() +} diff --git a/test-direct.ts b/test-direct.ts new file mode 100644 index 0000000000..9f153e7642 --- /dev/null +++ b/test-direct.ts @@ -0,0 +1,11 @@ +import { zodToJsonSchema } from "zod-to-json-schema" +import { z } from "zod" + +// Simple test schema +const testSchema = z.object({ + name: z.string(), + age: z.number().optional() +}) + +const result = zodToJsonSchema(testSchema, { target: "draft7" }) +console.log(JSON.stringify(result, null, 2)) diff --git a/test-schema.ts b/test-schema.ts new file mode 100644 index 0000000000..f0fecd8980 --- /dev/null +++ b/test-schema.ts @@ -0,0 +1,7 @@ +import { OhMyOpenCodeConfigSchema } from "./src/config/schema" +import { zodToJsonSchema } from "zod-to-json-schema" + +const jsonSchema = zodToJsonSchema(OhMyOpenCodeConfigSchema, { target: "draft7" }) +console.log("Schema keys:", Object.keys(jsonSchema)) +console.log("Type:", jsonSchema.type) +console.log("Properties keys:", Object.keys(jsonSchema.properties || {})) diff --git a/test-schema2.ts b/test-schema2.ts new file mode 100644 index 0000000000..e8dd6e0d8e --- /dev/null +++ b/test-schema2.ts @@ -0,0 +1,5 @@ +import { OhMyOpenCodeConfigSchema } from "./src/config/schema" + +console.log("Schema:", OhMyOpenCodeConfigSchema) +console.log("Schema type:", typeof OhMyOpenCodeConfigSchema) +console.log("Schema._def:", OhMyOpenCodeConfigSchema._def?.typeName) diff --git a/test-zod4.ts b/test-zod4.ts new file mode 100644 index 0000000000..d79d75f413 --- /dev/null +++ b/test-zod4.ts @@ -0,0 +1,15 @@ +import { z } from "zod" + +const testSchema = z.object({ + name: z.string(), + age: z.number().optional() +}) + +// Zod 4 has built-in JSON schema generation +if (typeof testSchema.jsonSchema === 'function') { + console.log("Zod 4 jsonSchema method exists") + console.log(JSON.stringify(testSchema.jsonSchema(), null, 2)) +} else { + console.log("No jsonSchema method") + console.log("Available methods:", Object.keys(testSchema).filter(k => typeof testSchema[k] === 'function')) +} diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000000..9d4a652e54 --- /dev/null +++ b/test_output.txt @@ -0,0 +1,722 @@ +bun test v1.3.9 (cf6cdbbb) + +script/build-binaries.test.ts: +🔨 Building oh-my-opencode platform binaries + Entry point: src/cli/index.ts + Platforms: 11 + +📦 Building macOS ARM64... + Target: bun-darwin-arm64 + Output: packages/darwin-arm64/bin/oh-my-opencode + [6ms] minify -0.76 MB (estimate) + [89ms] bundle 316 modules + [260ms] compile packages/darwin-arm64/bin/oh-my-opencode bun-darwin-aarch64-v1.3.9 + +src/hooks/session-notification.test.ts: + ✓ packages/darwin-arm64/bin/oh-my-opencode: Mach-O 64-bit arm64 executable, flags: + +📦 Building macOS x64... + Target: bun-darwin-x64 + Output: packages/darwin-x64/bin/oh-my-opencode + [14ms] minify -0.76 MB (estimate) + [103ms] bundle 316 modules + [141ms] compile packages/darwin-x64/bin/oh-my-opencode bun-darwin-x64-v1.3.9 + ✓ packages/darwin-x64/bin/oh-my-opencode: Mach-O 64-bit x86_64 executable, flags: + +📦 Building macOS x64 (no AVX2)... + Target: bun-darwin-x64-baseline + Output: packages/darwin-x64-baseline/bin/oh-my-opencode + [10ms] minify -0.76 MB (estimate) + [96ms] bundle 316 modules + [130ms] compile packages/darwin-x64-baseline/bin/oh-my-opencode bun-darwin-x64-baseline-v1.3.9 + +src/agents/dynamic-agent-prompt-builder.test.ts: + ✓ packages/darwin-x64-baseline/bin/oh-my-opencode: Mach-O 64-bit x86_64 executable, flags: + +📦 Building Linux x64 (glibc)... + Target: bun-linux-x64 + Output: packages/linux-x64/bin/oh-my-opencode + [15ms] minify -15.67 MB (estimate) + [92ms] bundle 318 modules + [111ms] compile packages/linux-x64/bin/oh-my-opencode + +src/plugin/event.test.ts: + ✓ packages/linux-x64/bin/oh-my-opencode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=db04d9fbd1eaf9f5deaf68dde4e71e03ac240201, not stripped + +📦 Building Linux x64 (glibc, no AVX2)... + Target: bun-linux-x64-baseline + Output: packages/linux-x64-baseline/bin/oh-my-opencode + [13ms] minify -15.67 MB (estimate) + [88ms] bundle 318 modules + [38ms] compile packages/linux-x64-baseline/bin/oh-my-opencode bun-linux-x64-baseline-v1.3.9 + ✓ packages/linux-x64-baseline/bin/oh-my-opencode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=320abce7c606bf1eb1b0e8252a71de87f146b170, not stripped + +📦 Building Linux ARM64 (glibc)... + Target: bun-linux-arm64 + Output: packages/linux-arm64/bin/oh-my-opencode + [6ms] minify -0.76 MB (estimate) + [89ms] bundle 316 modules + [32ms] compile packages/linux-arm64/bin/oh-my-opencode bun-linux-aarch64-v1.3.9 + ✓ packages/linux-arm64/bin/oh-my-opencode: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=11c4ef3be017497488463327d9f186036cd2bebd, not stripped + +📦 Building Linux x64 (musl)... + Target: bun-linux-x64-musl + Output: packages/linux-x64-musl/bin/oh-my-opencode + [14ms] minify -15.67 MB (estimate) + [88ms] bundle 318 modules + [37ms] compile packages/linux-x64-musl/bin/oh-my-opencode bun-linux-x64-musl-v1.3.9 + ✓ packages/linux-x64-musl/bin/oh-my-opencode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, BuildID[sha1]=f56141b6441f47af99ad55f15294d0579ae45f48, not stripped + +📦 Building Linux x64 (musl, no AVX2)... + Target: bun-linux-x64-musl-baseline + Output: packages/linux-x64-musl-baseline/bin/oh-my-opencode + [12ms] minify -15.67 MB (estimate) + [88ms] bundle 318 modules + [49ms] compile packages/linux-x64-musl-baseline/bin/oh-my-opencode bun-linux-x64-musl-baseline-v1.3.9 + ✓ packages/linux-x64-musl-baseline/bin/oh-my-opencode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, BuildID[sha1]=4d4a1a315527b0d814d6c38f55f99b9f59634f60, not stripped + +📦 Building Linux ARM64 (musl)... + Target: bun-linux-arm64-musl + Output: packages/linux-arm64-musl/bin/oh-my-opencode + [7ms] minify -0.76 MB (estimate) + [87ms] bundle 316 modules + [63ms] compile packages/linux-arm64-musl/bin/oh-my-opencode bun-linux-aarch64-musl-v1.3.9 + ✓ packages/linux-arm64-musl/bin/oh-my-opencode: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-aarch64.so.1, BuildID[sha1]=ece8722580c5fa25d527b2acf719e76469775e94, not stripped + +📦 Building Windows x64... + Target: bun-windows-x64 + Output: packages/windows-x64/bin/oh-my-opencode.exe + [7ms] minify -0.76 MB (estimate) + [87ms] bundle 316 modules + [276ms] compile packages/windows-x64/bin/oh-my-opencode.exe bun-windows-x64-v1.3.9 + ✓ packages/windows-x64/bin/oh-my-opencode.exe: PE32+ executable (console) x86-64, for MS Windows, 12 sections + +📦 Building Windows x64 (no AVX2)... + Target: bun-windows-x64-baseline + Output: packages/windows-x64-baseline/bin/oh-my-opencode.exe + [7ms] minify -0.76 MB (estimate) + [87ms] bundle 316 modules + +src/shared/model-resolver.test.ts: +128 | +129 | // when +130 | const result = resolveModelWithFallback(input) +131 | +132 | // then +133 | expect(result!.model).toBe("opencode/glm-4.7-free") + ^ +error: expect(received).toBe(expected) + +Expected: "opencode/glm-4.7-free" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:133:29) +(fail) resolveModelWithFallback > Step 1: UI Selection (highest priority) > returns uiSelectedModel with override source when provided +146 | +147 | // when +148 | const result = resolveModelWithFallback(input) +149 | +150 | // then +151 | expect(result!.model).toBe("opencode/glm-4.7-free") + ^ +error: expect(received).toBe(expected) + +Expected: "opencode/glm-4.7-free" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:151:29) +(fail) resolveModelWithFallback > Step 1: UI Selection (highest priority) > UI selection takes priority over config override +164 | // when +165 | const result = resolveModelWithFallback(input) +166 | +167 | // then +168 | expect(result!.model).toBe("anthropic/claude-opus-4-6") +169 | expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-6" }) + ^ +error: expect(received).toHaveBeenCalledWith(...expected) + +Expected: [ "Model resolved via config override", { + model: "anthropic/claude-opus-4-6", + } ] +But it was not called. + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:169:22) +(fail) resolveModelWithFallback > Step 1: UI Selection (highest priority) > whitespace-only uiSelectedModel is treated as not provided +201 | // when +202 | const result = resolveModelWithFallback(input) +203 | +204 | // then +205 | expect(result!.model).toBe("anthropic/claude-opus-4-6") +206 | expect(result!.source).toBe("override") + ^ +error: expect(received).toBe(expected) + +Expected: "override" +Received: "provider-fallback" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:206:30) +(fail) resolveModelWithFallback > Step 2: Config Override > returns userModel with override source when userModel is provided +220 | +221 | // when +222 | const result = resolveModelWithFallback(input) +223 | +224 | // then +225 | expect(result!.model).toBe("custom/my-model") + ^ +error: expect(received).toBe(expected) + +Expected: "custom/my-model" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:225:29) +(fail) resolveModelWithFallback > Step 2: Config Override > override takes priority even if model not in availableModels +276 | +277 | // when +278 | const result = resolveModelWithFallback(input) +279 | +280 | // then +281 | expect(result!.model).toBe("github-copilot/claude-opus-4-6-preview") + ^ +error: expect(received).toBe(expected) + +Expected: "github-copilot/claude-opus-4-6-preview" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:281:29) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > tries providers in order within entry and returns first match +300 | +301 | // when +302 | const result = resolveModelWithFallback(input) +303 | +304 | // then +305 | expect(result!.model).toBe("openai/gpt-5.2") + ^ +error: expect(received).toBe(expected) + +Expected: "openai/gpt-5.2" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:305:29) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > respects provider priority order within entry +318 | +319 | // when +320 | const result = resolveModelWithFallback(input) +321 | +322 | // then +323 | expect(result!.model).toBe("opencode/gpt-5-nano") + ^ +error: expect(received).toBe(expected) + +Expected: "opencode/gpt-5-nano" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:323:29) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > tries next provider when first provider has no match +351 | +352 | // when +353 | const result = resolveModelWithFallback(input) +354 | +355 | // then +356 | expect(result!.source).toBe("system-default") + ^ +error: expect(received).toBe(expected) + +Expected: "system-default" +Received: "provider-fallback" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:356:30) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > skips fallback chain when not provided +366 | +367 | // when +368 | const result = resolveModelWithFallback(input) +369 | +370 | // then +371 | expect(result!.source).toBe("system-default") + ^ +error: expect(received).toBe(expected) + +Expected: "system-default" +Received: "provider-fallback" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:371:30) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > skips fallback chain when empty [1.00ms] +402 | +403 | // when +404 | const result = resolveModelWithFallback(input) +405 | +406 | // then - should find glm-4.7 from opencode via cross-provider fuzzy match +407 | expect(result!.model).toBe("opencode/glm-4.7") + ^ +error: expect(received).toBe(expected) + +Expected: "opencode/glm-4.7" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:407:29) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > cross-provider fuzzy match when preferred provider unavailable (librarian scenario) +425 | +426 | // when +427 | const result = resolveModelWithFallback(input) +428 | +429 | // then - should prefer zai-coding-plan (specified provider) over opencode +430 | expect(result!.model).toBe("zai-coding-plan/glm-4.7") + ^ +error: expect(received).toBe(expected) + +Expected: "zai-coding-plan/glm-4.7" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:430:29) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > prefers specified provider over cross-provider match +443 | +444 | // when +445 | const result = resolveModelWithFallback(input) +446 | +447 | // then - variant should be preserved +448 | expect(result!.model).toBe("opencode/glm-4.7") + ^ +error: expect(received).toBe(expected) + +Expected: "opencode/glm-4.7" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:448:29) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > cross-provider match preserves variant from entry +462 | +463 | // when +464 | const result = resolveModelWithFallback(input) +465 | +466 | // then - should fall through to second entry +467 | expect(result!.model).toBe("anthropic/claude-sonnet-4-5") + ^ +error: expect(received).toBe(expected) + +Expected: "anthropic/claude-sonnet-4-5" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:467:29) +(fail) resolveModelWithFallback > Step 3: Provider fallback chain > cross-provider match tries next entry if no match found anywhere +482 | +483 | // when +484 | const result = resolveModelWithFallback(input) +485 | +486 | // then +487 | expect(result!.model).toBe("google/gemini-3-pro") + ^ +error: expect(received).toBe(expected) + +Expected: "google/gemini-3-pro" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:487:29) +(fail) resolveModelWithFallback > Step 4: System default fallback (no availability match) > returns system default when no availability match found in fallback chain +502 | +503 | // when +504 | const result = resolveModelWithFallback(input) +505 | +506 | // then - should return undefined to let OpenCode use Provider.defaultModel() +507 | expect(result).toBeUndefined() + ^ +error: expect(received).toBeUndefined() + +Received: { + model: "anthropic/claude-opus-4-6", + source: "provider-fallback", + variant: "max", +} + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:507:22) +(fail) resolveModelWithFallback > Step 4: System default fallback (no availability match) > returns undefined when availableModels empty and no connected providers cache exists +521 | +522 | // when +523 | const result = resolveModelWithFallback(input) +524 | +525 | // then - should use connected provider (openai) from fallback chain +526 | expect(result!.model).toBe("openai/claude-opus-4-6") + ^ +error: expect(received).toBe(expected) + +Expected: "openai/claude-opus-4-6" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:526:29) +(fail) resolveModelWithFallback > Step 4: System default fallback (no availability match) > uses connected provider from fallback when availableModels empty but cache exists +541 | +542 | // when +543 | const result = resolveModelWithFallback(input) +544 | +545 | // then - should use github-copilot (second provider) since google not connected +546 | expect(result!.model).toBe("github-copilot/gemini-3-pro") + ^ +error: expect(received).toBe(expected) + +Expected: "github-copilot/gemini-3-pro" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:546:29) +(fail) resolveModelWithFallback > Step 4: System default fallback (no availability match) > uses github-copilot when google not connected (visual-engineering scenario) +561 | +562 | // when +563 | const result = resolveModelWithFallback(input) +564 | +565 | // then - no provider in fallback is connected, fall through to system default +566 | expect(result!.model).toBe("quotio/claude-opus-4-6-20251101") + ^ +error: expect(received).toBe(expected) + +Expected: "quotio/claude-opus-4-6-20251101" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:566:29) +(fail) resolveModelWithFallback > Step 4: System default fallback (no availability match) > falls through to system default when no provider in fallback is connected +581 | +582 | // when +583 | const result = resolveModelWithFallback(input) +584 | +585 | // then - should fall through to system default +586 | expect(result!.model).toBe("google/gemini-3-pro") + ^ +error: expect(received).toBe(expected) + +Expected: "google/gemini-3-pro" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:586:29) +(fail) resolveModelWithFallback > Step 4: System default fallback (no availability match) > falls through to system default when no cache and systemDefaultModel is provided +597 | +598 | // when +599 | const result = resolveModelWithFallback(input) +600 | +601 | // then +602 | expect(result!.model).toBe("google/gemini-3-pro") + ^ +error: expect(received).toBe(expected) + +Expected: "google/gemini-3-pro" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:602:29) +(fail) resolveModelWithFallback > Step 4: System default fallback (no availability match) > returns system default when fallbackChain is not provided +637 | availableModels, +638 | systemDefaultModel: "system/default", +639 | }) +640 | +641 | // then +642 | expect(result!.model).toBe("google/gemini-3-pro") + ^ +error: expect(received).toBe(expected) + +Expected: "google/gemini-3-pro" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:642:29) +(fail) resolveModelWithFallback > Multi-entry fallbackChain > tries all providers in first entry before moving to second entry +659 | availableModels, +660 | systemDefaultModel: "system/default", +661 | }) +662 | +663 | // then +664 | expect(result!.model).toBe("openai/gpt-5.2") + ^ +error: expect(received).toBe(expected) + +Expected: "openai/gpt-5.2" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:664:29) +(fail) resolveModelWithFallback > Multi-entry fallbackChain > returns first matching entry even if later entries have better matches +679 | availableModels, +680 | systemDefaultModel: "system/default", +681 | }) +682 | +683 | // then +684 | expect(result!.model).toBe("system/default") + ^ +error: expect(received).toBe(expected) + +Expected: "system/default" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:684:29) +(fail) resolveModelWithFallback > Multi-entry fallbackChain > falls through to system default when none match availability +719 | +720 | // when +721 | const result = resolveModelWithFallback(input) +722 | +723 | // then - should fuzzy match gemini-3-pro → gemini-3-pro-preview +724 | expect(result!.model).toBe("google/gemini-3-pro-preview") + ^ +error: expect(received).toBe(expected) + +Expected: "google/gemini-3-pro-preview" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:724:29) +(fail) resolveModelWithFallback > categoryDefaultModel (fuzzy matching for category defaults) > applies fuzzy matching to categoryDefaultModel when userModel not provided +738 | +739 | // when +740 | const result = resolveModelWithFallback(input) +741 | +742 | // then - should use exact match +743 | expect(result!.model).toBe("google/gemini-3-pro") + ^ +error: expect(received).toBe(expected) + +Expected: "google/gemini-3-pro" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:743:29) +(fail) resolveModelWithFallback > categoryDefaultModel (fuzzy matching for category defaults) > categoryDefaultModel uses exact match when available +778 | // when +779 | const result = resolveModelWithFallback(input) +780 | +781 | // then - userModel wins +782 | expect(result!.model).toBe("anthropic/claude-opus-4-6") +783 | expect(result!.source).toBe("override") + ^ +error: expect(received).toBe(expected) + +Expected: "override" +Received: "provider-fallback" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:783:30) +(fail) resolveModelWithFallback > categoryDefaultModel (fuzzy matching for category defaults) > userModel takes priority over categoryDefaultModel +794 | +795 | // when +796 | const result = resolveModelWithFallback(input) +797 | +798 | // then - should use categoryDefaultModel since google is connected +799 | expect(result!.model).toBe("google/gemini-3-pro") + ^ +error: expect(received).toBe(expected) + +Expected: "google/gemini-3-pro" +Received: "anthropic/claude-opus-4-6" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:799:29) +(fail) resolveModelWithFallback > categoryDefaultModel (fuzzy matching for category defaults) > categoryDefaultModel works when availableModels is empty but connected provider exists +815 | +816 | // when +817 | const result = resolveModelWithFallback(input) +818 | +819 | // then +820 | expect(result).toBeUndefined() + ^ +error: expect(received).toBeUndefined() + +Received: { + model: "anthropic/claude-opus-4-6", + source: "provider-fallback", + variant: "max", +} + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:820:22) +(fail) resolveModelWithFallback > Optional systemDefaultModel > returns undefined when systemDefaultModel is undefined and no fallback found +829 | +830 | // when +831 | const result = resolveModelWithFallback(input) +832 | +833 | // then +834 | expect(result).toBeUndefined() + ^ +error: expect(received).toBeUndefined() + +Received: { + model: "anthropic/claude-opus-4-6", + source: "provider-fallback", + variant: "max", +} + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:834:22) +(fail) resolveModelWithFallback > Optional systemDefaultModel > returns undefined when no fallbackChain and systemDefaultModel is undefined +846 | const result = resolveModelWithFallback(input) +847 | +848 | // then +849 | expect(result).toBeDefined() +850 | expect(result!.model).toBe("anthropic/claude-opus-4-6") +851 | expect(result!.source).toBe("override") + ^ +error: expect(received).toBe(expected) + +Expected: "override" +Received: "provider-fallback" + + at (/home/bong/oh-my-opencode/src/shared/model-resolver.test.ts:851:30) +(fail) resolveModelWithFallback > Optional systemDefaultModel > still returns override when userModel provided even if systemDefaultModel undefined + +src/shared/opencode-server-auth.test.ts: +[opencode-server-auth] Failed to inject server auth: [opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client structure is incompatible. This may indicate an OpenCode SDK version mismatch. +[opencode-server-auth] Failed to inject server auth: [opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client._client.setConfig is not a function. This may indicate an OpenCode SDK version mismatch. + [272ms] compile packages/windows-x64-baseline/bin/oh-my-opencode.exe bun-windows-x64-baseline-v1.3.9 + +src/hooks/prometheus-md-only/index.test.ts: +357 | +358 | // when +359 | await hook["tool.execute.before"](input, output) +360 | +361 | // then +362 | expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX) + ^ +error: expect(received).toContain(expected) + +Expected to contain: "[SYSTEM DIRECTIVE: OH-MY-OPENCODE" +Received: "\n\n---\n\n[DIRECTIVE:PROMETHEUS READ-ONLY]\n\nYou are being invoked by Prometheus (Plan Builder), a READ-ONLY planning agent.\n\n**CRITICAL CONSTRAINTS:**\n- DO NOT modify any files (no Write, Edit, or any file mutations)\n- DO NOT execute commands that change system state\n- DO NOT create, delete, or rename files\n- ONLY provide analysis, recommendations, and information\n\n**YOUR ROLE**: Provide consultation, research, and analysis to assist with planning.\nReturn your findings and recommendations. The actual implementation will be handled separately after planning is complete.\n\n---\n\nAnalyze this codebase" + + at (/home/bong/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts:362:34) +(fail) prometheus-md-only > with Prometheus agent in message storage > should inject read-only warning when Prometheus calls task +377 | +378 | // when +379 | await hook["tool.execute.before"](input, output) +380 | +381 | // then +382 | expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX) + ^ +error: expect(received).toContain(expected) + +Expected to contain: "[SYSTEM DIRECTIVE: OH-MY-OPENCODE" +Received: "\n\n---\n\n[DIRECTIVE:PROMETHEUS READ-ONLY]\n\nYou are being invoked by Prometheus (Plan Builder), a READ-ONLY planning agent.\n\n**CRITICAL CONSTRAINTS:**\n- DO NOT modify any files (no Write, Edit, or any file mutations)\n- DO NOT execute commands that change system state\n- DO NOT create, delete, or rename files\n- ONLY provide analysis, recommendations, and information\n\n**YOUR ROLE**: Provide consultation, research, and analysis to assist with planning.\nReturn your findings and recommendations. The actual implementation will be handled separately after planning is complete.\n\n---\n\nResearch this library" + + at (/home/bong/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts:382:34) +(fail) prometheus-md-only > with Prometheus agent in message storage > should inject read-only warning when Prometheus calls task +396 | +397 | // when +398 | await hook["tool.execute.before"](input, output) +399 | +400 | // then +401 | expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX) + ^ +error: expect(received).toContain(expected) + +Expected to contain: "[SYSTEM DIRECTIVE: OH-MY-OPENCODE" +Received: "\n\n---\n\n[DIRECTIVE:PROMETHEUS READ-ONLY]\n\nYou are being invoked by Prometheus (Plan Builder), a READ-ONLY planning agent.\n\n**CRITICAL CONSTRAINTS:**\n- DO NOT modify any files (no Write, Edit, or any file mutations)\n- DO NOT execute commands that change system state\n- DO NOT create, delete, or rename files\n- ONLY provide analysis, recommendations, and information\n\n**YOUR ROLE**: Provide consultation, research, and analysis to assist with planning.\nReturn your findings and recommendations. The actual implementation will be handled separately after planning is complete.\n\n---\n\nFind implementation examples" + + at (/home/bong/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts:401:34) +(fail) prometheus-md-only > with Prometheus agent in message storage > should inject read-only warning when Prometheus calls call_omo_agent + +src/hooks/keyword-detector/index.test.ts: +439 | await hook["chat.message"]({ sessionID }, output) +440 | +441 | // then - should trigger search mode from user text only +442 | const textPart = output.parts.find(p => p.type === "text") +443 | expect(textPart).toBeDefined() +444 | expect(textPart!.text).toContain("[search-mode]") + ^ +error: expect(received).toContain(expected) + +Expected to contain: "[search-mode]" +Received: "\nSystem will find and locate files.\n\n\nPlease search for the bug in the code." + + at (/home/bong/oh-my-opencode/src/hooks/keyword-detector/index.test.ts:444:28) +(fail) keyword-detector system-reminder filtering > should detect keywords in user text even when system-reminder is present + +src/hooks/unstable-agent-babysitter/index.test.ts: +85 | +86 | // #when +87 | await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } }) +88 | +89 | // #then +90 | expect(promptCalls.length).toBe(1) + ^ +error: expect(received).toBe(expected) + +Expected: 1 +Received: 0 + + at (/home/bong/oh-my-opencode/src/hooks/unstable-agent-babysitter/index.test.ts:90:32) +(fail) unstable-agent-babysitter hook > fires reminder for hung gemini task [1.00ms] +120 | +121 | // #when +122 | await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } }) +123 | +124 | // #then +125 | expect(promptCalls.length).toBe(1) + ^ +error: expect(received).toBe(expected) + +Expected: 1 +Received: 0 + + at (/home/bong/oh-my-opencode/src/hooks/unstable-agent-babysitter/index.test.ts:125:32) +(fail) unstable-agent-babysitter hook > fires reminder for hung minimax task +173 | // #when +174 | await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } }) +175 | await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } }) +176 | +177 | // #then +178 | expect(promptCalls.length).toBe(1) + ^ +error: expect(received).toBe(expected) + +Expected: 1 +Received: 0 + + at (/home/bong/oh-my-opencode/src/hooks/unstable-agent-babysitter/index.test.ts:178:32) +(fail) unstable-agent-babysitter hook > respects per-task cooldown + +src/features/tmux-subagent/manager.test.ts: +(fail) TmuxSessionManager > onSessionCreated > replaces oldest agent when unsplittable (small window) [5000.21ms] + ^ this test timed out after 5000ms. +(fail) TmuxSessionManager > cleanup > closes all tracked panes [5000.21ms] + ^ this test timed out after 5000ms. + +src/features/background-agent/manager.test.ts: +1894 | expect(pendingTask?.startedAt).toBeUndefined() +1895 | +1896 | // Verify TTL would use queuedAt (implementation detail check) +1897 | const now = Date.now() +1898 | const age = now - pendingTask!.queuedAt!.getTime() +1899 | expect(age).toBeGreaterThanOrEqual(0) + ^ +error: expect(received).toBeGreaterThanOrEqual(expected) + +Expected: >= 0 +Received: -10662 + + at (/home/bong/oh-my-opencode/src/features/background-agent/manager.test.ts:1899:19) +(fail) BackgroundManager - Non-blocking Queue Integration > TTL uses queuedAt for pending, startedAt for running > should use queuedAt for pending task TTL [55.00ms] +1925 | expect(runningTask?.startedAt).toBeInstanceOf(Date) +1926 | +1927 | // Verify TTL would use startedAt (implementation detail check) +1928 | const now = Date.now() +1929 | const age = now - runningTask!.startedAt!.getTime() +1930 | expect(age).toBeGreaterThanOrEqual(0) + ^ +error: expect(received).toBeGreaterThanOrEqual(expected) + +Expected: >= 0 +Received: -10717 + + at (/home/bong/oh-my-opencode/src/features/background-agent/manager.test.ts:1930:19) +(fail) BackgroundManager - Non-blocking Queue Integration > TTL uses queuedAt for pending, startedAt for running > should use startedAt for running task TTL [58.00ms] + +src/features/claude-code-session-state/state.test.ts: + 96 | + 97 | test("should return undefined when not set", () => { + 98 | // given - explicit reset to ensure clean state (parallel test isolation) + 99 | _resetForTesting() +100 | // then +101 | expect(getMainSessionID()).toBeUndefined() + ^ +error: expect(received).toBeUndefined() + +Received: "main-session-123" + + at (/home/bong/oh-my-opencode/src/features/claude-code-session-state/state.test.ts:101:34) +(fail) claude-code-session-state > mainSessionID > should return undefined when not set [1.00ms] + +src/tools/delegate-task/tools.test.ts: +(fail) sisyphus-task > session_id with background parameter > sync continuation preserves variant from previous session message [10000.43ms] + ^ this test timed out after 10000ms. +(fail) sisyphus-task > unstable agent forced background mode > gemini model with run_in_background=false should force background but wait for result [20000.86ms] + ^ this test timed out after 20000ms. +(fail) sisyphus-task > unstable agent forced background mode > minimax model with run_in_background=false should force background but wait for result [20002.86ms] + ^ this test timed out after 20000ms. +(fail) sisyphus-task > unstable agent forced background mode > artistry category (gemini) with run_in_background=false should force background but wait for result [20000.87ms] + ^ this test timed out after 20000ms. +(fail) sisyphus-task > unstable agent forced background mode > writing category (gemini-flash) with run_in_background=false should force background but wait for result [20000.88ms] + ^ this test timed out after 20000ms. +(fail) sisyphus-task > unstable agent forced background mode > is_unstable_agent=true should force background but wait for result [20000.88ms] + ^ this test timed out after 20000ms. + +src/tools/delegate-task/sync-session-poller.test.ts: +(fail) pollSyncSession > timeout handling > returns error string on timeout [5000.22ms] + ^ this test timed out after 5000ms. From f14753682ccb38d4de727b5ff879b4456e28c77a Mon Sep 17 00:00:00 2001 From: bong Date: Thu, 12 Feb 2026 05:38:11 +0900 Subject: [PATCH 3/4] feat(commands): add /switch-model builtin command - Add switch-model command template with model switching instructions - Update BuiltinCommandName type to include 'switch-model' - Update BuiltinCommandNameSchema enum to include 'switch-model' - Add switch-model to BUILTIN_COMMAND_DEFINITIONS with argumentHint The command provides: - Current model assignments display for all agents - Model candidates listing for specific agents - Model switching instructions - Validation for model candidates - Clear error messages for invalid models Template includes detailed instructions for parsing arguments, getting current model info, validating inputs, and performing model switches using model-switcher APIs. --- assets/oh-my-opencode.schema.json | 4 +- src/config/schema/commands.ts | 2 + src/features/builtin-commands/commands.ts | 17 +++ .../templates/switch-model.ts | 129 ++++++++++++++++++ src/features/builtin-commands/types.ts | 2 +- 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/features/builtin-commands/templates/switch-model.ts diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 4a60d3022f..e69f141bf4 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -114,7 +114,9 @@ "cancel-ralph", "refactor", "start-work", - "stop-continuation" + "stop-continuation", + "handoff", + "switch-model" ] } }, diff --git a/src/config/schema/commands.ts b/src/config/schema/commands.ts index 967254538a..5fec3d0472 100644 --- a/src/config/schema/commands.ts +++ b/src/config/schema/commands.ts @@ -8,6 +8,8 @@ export const BuiltinCommandNameSchema = z.enum([ "refactor", "start-work", "stop-continuation", + "handoff", + "switch-model", ]) export type BuiltinCommandName = z.infer diff --git a/src/features/builtin-commands/commands.ts b/src/features/builtin-commands/commands.ts index aee5dc28a2..8e8967d5f8 100644 --- a/src/features/builtin-commands/commands.ts +++ b/src/features/builtin-commands/commands.ts @@ -6,6 +6,7 @@ import { STOP_CONTINUATION_TEMPLATE } from "./templates/stop-continuation" import { REFACTOR_TEMPLATE } from "./templates/refactor" import { START_WORK_TEMPLATE } from "./templates/start-work" import { HANDOFF_TEMPLATE } from "./templates/handoff" +import { SWITCH_MODEL_TEMPLATE } from "./templates/switch-model" const BUILTIN_COMMAND_DEFINITIONS: Record> = { "init-deep": { @@ -94,6 +95,22 @@ $ARGUMENTS `, argumentHint: "[goal]", }, + "switch-model": { + description: "(builtin) Switch the active LLM model for a specific agent in the current session", + template: ` +${SWITCH_MODEL_TEMPLATE} + + + +Session ID: $SESSION_ID +Timestamp: $TIMESTAMP + + + +$ARGUMENTS +`, + argumentHint: "[agent] [model]", + }, } export function loadBuiltinCommands( diff --git a/src/features/builtin-commands/templates/switch-model.ts b/src/features/builtin-commands/templates/switch-model.ts new file mode 100644 index 0000000000..a93edc5f32 --- /dev/null +++ b/src/features/builtin-commands/templates/switch-model.ts @@ -0,0 +1,129 @@ +export const SWITCH_MODEL_TEMPLATE = `You are helping the user switch the active LLM model for a specific agent. + +## WHAT TO DO + +1. **Parse the arguments**: + - No arguments: Show current model assignments for all agents + - One argument (agent name): Show available model_candidates for that agent + - Two arguments (agent name + model): Switch the agent to the specified model + +2. **Get current model information**: + - Use \`getCurrentModelInfo()\` from \`src/features/model-switcher\` to get all agent model states + - This returns: \`{ [agentName]: { active: string | undefined, candidates: string[] } }\` + +3. **Validate agent name**: + - Must be a valid builtin agent name (sisyphus, hephaestus, oracle, explore, librarian, build, plan, etc.) + - If invalid, show error and list valid agent names + +4. **Validate model choice**: + - When switching, ensure the model is in the agent's \`model_candidates\` list + - If model not in candidates, show error and display available candidates + - If \`model_candidates\` is empty or not configured, show appropriate message + +5. **Perform the switch**: + - Call \`setActiveModel(agentName, model)\` from \`src/features/model-switcher\` + - Confirm the switch with a success message + - The new model will take effect for the next message in this session + +## OUTPUT FORMAT + +### When no arguments provided: +\`\`\` +Current Model Assignments + +Session ID: $SESSION_ID +Timestamp: $TIMESTAMP + +Agent | Active Model | Candidates Available +-----------------|----------------------------------|--------------------- +sisyphus | zai-coding-plan/glm-4.7 | 3 models +hephaestus | google/antigravity-claude-opus | 2 models +oracle | (default from config) | 0 models (not configured) + +To view candidates for an agent: /switch-model +To switch model: /switch-model +\`\`\` + +### When agent name provided: +\`\`\` +Model Candidates for Agent: sisyphus + +Current Active: zai-coding-plan/glm-4.7 + +Available Candidates: +1. zai-coding-plan/glm-4.7 (current) +2. google/antigravity-claude-opus-4-6-thinking +3. cursor-acp/gpt-5.2 + +To switch: /switch-model sisyphus +Example: /switch-model sisyphus google/antigravity-claude-opus-4-6-thinking +\`\`\` + +### When switching model: +\`\`\` +Model Switch Successful + +Agent: sisyphus +Previous: zai-coding-plan/glm-4.7 +New: google/antigravity-claude-opus-4-6-thinking + +The new model will be used for sisyphus starting with the next message. +\`\`\` + +### When model not in candidates: +\`\`\` +Error: Invalid Model + +Agent: sisyphus +Requested: some-invalid/model + +This model is not in the configured candidates for sisyphus. + +Available candidates: +- zai-coding-plan/glm-4.7 +- google/antigravity-claude-opus-4-6-thinking +- cursor-acp/gpt-5.2 + +Please choose from the available candidates. +\`\`\` + +### When agent has no candidates configured: +\`\`\` +Error: No Model Candidates Configured + +Agent: oracle + +This agent does not have model_candidates configured in oh-my-opencode.json. +It will use the default model specified in the config. + +To enable model switching for this agent, add model_candidates: + +{ + "agents": { + "oracle": { + "model": "default-model-id", + "model_candidates": [ + "default-model-id", + "alternative-model-id" + ] + } + } +} +\`\`\` + +## IMPORTANT NOTES + +- The switch is **session-scoped** (in-memory only) +- Changes do NOT modify oh-my-opencode.json +- The new model takes effect from the next message +- Always validate before calling \`setActiveModel()\` +- Use clear, actionable error messages + +## AVAILABLE FUNCTIONS + +Import from \`src/features/model-switcher\`: +- \`getCurrentModelInfo()\`: Get all agent model states +- \`setActiveModel(agentName: string, model: string)\`: Switch agent model +- \`getCandidates(agentName: string)\`: Get model candidates for specific agent +- \`getActiveModel(agentName: string)\`: Get current active model for agent +` diff --git a/src/features/builtin-commands/types.ts b/src/features/builtin-commands/types.ts index 0c2624f12b..9f1b212775 100644 --- a/src/features/builtin-commands/types.ts +++ b/src/features/builtin-commands/types.ts @@ -1,6 +1,6 @@ import type { CommandDefinition } from "../claude-code-command-loader" -export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" +export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" | "switch-model" export interface BuiltinCommandConfig { disabled_commands?: BuiltinCommandName[] From 530a57113b62703db14d726b8e99988bb4c686cf Mon Sep 17 00:00:00 2001 From: bong Date: Thu, 12 Feb 2026 05:53:17 +0900 Subject: [PATCH 4/4] test(model-switcher): add unit tests for model switching - Test loadCandidates() parsing with/without/empty model_candidates - Test setActiveModel() and getActiveModel() override functionality - Test getCandidates() and getCurrentModelInfo() state queries - Test backward compatibility (config without model_candidates) - Test invalid agent name handling - 12 test cases all passing Tests use unique agent names to avoid singleton state interference between test cases. --- .../model-switcher/model-switcher.test.ts | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/features/model-switcher/model-switcher.test.ts diff --git a/src/features/model-switcher/model-switcher.test.ts b/src/features/model-switcher/model-switcher.test.ts new file mode 100644 index 0000000000..3107553d72 --- /dev/null +++ b/src/features/model-switcher/model-switcher.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, test } from "bun:test" +import type { AgentOverrides } from "../../agents/types" + +import { + getCurrentModelInfo, + getCandidates, + getActiveModel, + setActiveModel, + loadCandidates, +} from "./index" + +describe("ModelSwitcher", () => { + describe("Initial state", () => { + test("should load candidates into state", () => { + const agentOverrides: AgentOverrides = {} + loadCandidates(agentOverrides) + + expect(getCandidates("test_agent_1")).toEqual([]) + expect(getCandidates("test_agent_2")).toEqual([]) + }) + + test("should parse model_candidates from config", () => { + const agentOverrides: AgentOverrides = { + test_agent_3: { + model: "default-model", + model_candidates: ["model1", "model2", "model3"], + }, + test_agent_4: { + model_candidates: ["modelA", "modelB"], + }, + } + + loadCandidates(agentOverrides) + + expect(getCandidates("test_agent_3")).toEqual(["model1", "model2", "model3"]) + expect(getCandidates("test_agent_4")).toEqual(["modelA", "modelB"]) + expect(getCandidates("oracle")).toEqual([]) + }) + + test("should handle empty model_candidates array", () => { + const agentOverrides: AgentOverrides = { + test_agent_5: { + model: "default-model", + model_candidates: [], + }, + } + + loadCandidates(agentOverrides) + + expect(getCandidates("test_agent_5")).toEqual([]) + }) + }) + + describe("Model overrides", () => { + test("should set active model override", () => { + loadCandidates({ + test_agent_6: { + model: "default-model", + model_candidates: ["model1", "model2", "model3"], + }, + }) + + setActiveModel("test_agent_6", "model2") + + const activeModel = getActiveModel("test_agent_6") + expect(activeModel).toBe("model2") + }) + + test("should return overridden model", () => { + setActiveModel("test_agent_7", "model1") + + const activeModel = getActiveModel("test_agent_7") + + expect(activeModel).toBe("model1") + }) + }) + + describe("Candidate retrieval", () => { + test("should return candidates array for agent with candidates", () => { + const candidates = getCandidates("test_agent_3") + + expect(candidates).toEqual(["model1", "model2", "model3"]) + }) + + test("should return empty array for agent without candidates", () => { + const candidates = getCandidates("test_agent_8") + + expect(candidates).toEqual([]) + }) + }) + + describe("Multiple model switches", () => { + test("should update active model each time", () => { + loadCandidates({ + test_agent_9: { + model: "default-model", + model_candidates: ["model1", "model2", "model3"], + }, + }) + + setActiveModel("test_agent_9", "model1") + expect(getActiveModel("test_agent_9")).toBe("model1") + + setActiveModel("test_agent_9", "model2") + expect(getActiveModel("test_agent_9")).toBe("model2") + + setActiveModel("test_agent_9", "model3") + expect(getActiveModel("test_agent_9")).toBe("model3") + }) + }) + + describe("Current model info", () => { + test("should return comprehensive state snapshot", () => { + loadCandidates({ + test_agent_10: { + model: "default-model", + model_candidates: ["model1", "model2", "model3"], + }, + test_agent_11: { + model_candidates: ["modelA", "modelB"], + }, + }) + + setActiveModel("test_agent_10", "model2") + + const info = getCurrentModelInfo() + + expect(info).toMatchObject({ + test_agent_10: { + active: "model2", + candidates: ["model1", "model2", "model3"], + }, + test_agent_11: { + active: undefined, + candidates: ["modelA", "modelB"], + }, + }) + }) + }) + + describe("Invalid agent name handling", () => { + test("should return empty array for non-existent agent", () => { + const candidates = getCandidates("non-existent-agent") + + expect(candidates).toEqual([]) + }) + + test("should return undefined for non-existent agent", () => { + const activeModel = getActiveModel("non-existent-agent") + + expect(activeModel).toBeUndefined() + }) + }) + + describe("Backward compatibility", () => { + test("should handle config without model_candidates field", () => { + const agentOverrides: AgentOverrides = { + test_agent_12: { + model: "default-model", + }, + } + + loadCandidates(agentOverrides) + + expect(getCandidates("test_agent_12")).toEqual([]) + expect(getActiveModel("test_agent_12")).toBeUndefined() + }) + }) +})