Files
oh-my-opencode/src/shared/migration.ts
T
Kenny c698a5b888 fix: remove hardcoded model defaults from categories and agents
BREAKING CHANGE: Model resolution overhauled

- Created centralized model-resolver.ts with priority chain:
  userModel → inheritedModel → systemDefaultModel
- Removed model field from all 7 DEFAULT_CATEGORIES entries
- Removed DEFAULT_MODEL constants from 10 agents
- Removed singleton agent exports (use factories instead)
- Made CategoryConfigSchema.model optional
- CLI no longer generates model overrides
- Empty strings treated as unset (uses fallback)

Users must now:
1. Use factory functions (createOracleAgent, etc.) instead of singletons
2. Provide model explicitly or use systemDefaultModel
3. Configure category models explicitly if needed

Fixes model fallback bug where hardcoded defaults overrode
user's OpenCode configured model.
2026-01-17 12:51:03 -05:00

179 lines
5.1 KiB
TypeScript

import * as fs from "fs"
import { log } from "./logger"
// Migration map: old keys → new keys (for backward compatibility)
export const AGENT_NAME_MAP: Record<string, string> = {
omo: "Sisyphus",
"OmO": "Sisyphus",
sisyphus: "Sisyphus",
"OmO-Plan": "Prometheus (Planner)",
"omo-plan": "Prometheus (Planner)",
"Planner-Sisyphus": "Prometheus (Planner)",
"planner-sisyphus": "Prometheus (Planner)",
prometheus: "Prometheus (Planner)",
"plan-consultant": "Metis (Plan Consultant)",
metis: "Metis (Plan Consultant)",
build: "build",
oracle: "oracle",
librarian: "librarian",
explore: "explore",
"frontend-ui-ux-engineer": "frontend-ui-ux-engineer",
"document-writer": "document-writer",
"multimodal-looker": "multimodal-looker",
"orchestrator-sisyphus": "orchestrator-sisyphus",
}
export const BUILTIN_AGENT_NAMES = new Set([
"Sisyphus",
"oracle",
"librarian",
"explore",
"frontend-ui-ux-engineer",
"document-writer",
"multimodal-looker",
"Metis (Plan Consultant)",
"Momus (Plan Reviewer)",
"Prometheus (Planner)",
"orchestrator-sisyphus",
"build",
])
// Migration map: old hook names → new hook names (for backward compatibility)
export const HOOK_NAME_MAP: Record<string, string> = {
// Legacy names (backward compatibility)
"anthropic-auto-compact": "anthropic-context-window-limit-recovery",
}
/**
* @deprecated LEGACY MIGRATION ONLY
*
* This map exists solely for migrating old configs that used hardcoded model strings.
* It maps legacy model strings to semantic category names, allowing users to migrate
* from explicit model configs to category-based configs.
*
* DO NOT add new entries here. New agents should use:
* - Category-based config (preferred): { category: "most-capable" }
* - Or inherit from OpenCode's config.model
*
* This map will be removed in a future major version once migration period ends.
*/
export const MODEL_TO_CATEGORY_MAP: Record<string, string> = {
"google/gemini-3-pro-preview": "visual-engineering",
"openai/gpt-5.2": "ultrabrain",
"anthropic/claude-haiku-4-5": "quick",
"anthropic/claude-opus-4-5": "most-capable",
"anthropic/claude-sonnet-4-5": "general",
}
export function migrateAgentNames(agents: Record<string, unknown>): { migrated: Record<string, unknown>; changed: boolean } {
const migrated: Record<string, unknown> = {}
let changed = false
for (const [key, value] of Object.entries(agents)) {
const newKey = AGENT_NAME_MAP[key.toLowerCase()] ?? AGENT_NAME_MAP[key] ?? key
if (newKey !== key) {
changed = true
}
migrated[newKey] = value
}
return { migrated, changed }
}
export function migrateHookNames(hooks: string[]): { migrated: string[]; changed: boolean } {
const migrated: string[] = []
let changed = false
for (const hook of hooks) {
const newHook = HOOK_NAME_MAP[hook] ?? hook
if (newHook !== hook) {
changed = true
}
migrated.push(newHook)
}
return { migrated, changed }
}
export function migrateAgentConfigToCategory(config: Record<string, unknown>): {
migrated: Record<string, unknown>
changed: boolean
} {
const { model, ...rest } = config
if (typeof model !== "string") {
return { migrated: config, changed: false }
}
const category = MODEL_TO_CATEGORY_MAP[model]
if (!category) {
return { migrated: config, changed: false }
}
return {
migrated: { category, ...rest },
changed: true,
}
}
export function shouldDeleteAgentConfig(
config: Record<string, unknown>,
category: string
): boolean {
const { DEFAULT_CATEGORIES } = require("../tools/delegate-task/constants")
const defaults = DEFAULT_CATEGORIES[category]
if (!defaults) return false
const keys = Object.keys(config).filter((k) => k !== "category")
if (keys.length === 0) return true
for (const key of keys) {
if (config[key] !== (defaults as Record<string, unknown>)[key]) {
return false
}
}
return true
}
export function migrateConfigFile(configPath: string, rawConfig: Record<string, unknown>): boolean {
let needsWrite = false
if (rawConfig.agents && typeof rawConfig.agents === "object") {
const { migrated, changed } = migrateAgentNames(rawConfig.agents as Record<string, unknown>)
if (changed) {
rawConfig.agents = migrated
needsWrite = true
}
}
if (rawConfig.omo_agent) {
rawConfig.sisyphus_agent = rawConfig.omo_agent
delete rawConfig.omo_agent
needsWrite = true
}
if (rawConfig.disabled_hooks && Array.isArray(rawConfig.disabled_hooks)) {
const { migrated, changed } = migrateHookNames(rawConfig.disabled_hooks as string[])
if (changed) {
rawConfig.disabled_hooks = migrated
needsWrite = true
}
}
if (needsWrite) {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
const backupPath = `${configPath}.bak.${timestamp}`
fs.copyFileSync(configPath, backupPath)
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8")
log(`Migrated config file: ${configPath} (backup: ${backupPath})`)
} catch (err) {
log(`Failed to write migrated config to ${configPath}:`, err)
}
}
return needsWrite
}