Merge pull request #1613 from code-yeongyu/fix/1561-dead-migration
fix(migration): remove task_system backup rewrite (#1561)
This commit is contained in:
@@ -1018,6 +1018,40 @@ describe("migrateConfigFile with backup", () => {
|
||||
expect(agents.oracle.category).toBe("ultrabrain")
|
||||
})
|
||||
|
||||
test("does not write or create backups for experimental.task_system", () => {
|
||||
//#given: Config with experimental.task_system enabled
|
||||
const testConfigPath = "/tmp/test-config-task-system.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
experimental: { task_system: true },
|
||||
}
|
||||
|
||||
fs.writeFileSync(testConfigPath, globalThis.JSON.stringify(rawConfig, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
|
||||
const dir = path.dirname(testConfigPath)
|
||||
const basename = path.basename(testConfigPath)
|
||||
const existingFiles = fs.readdirSync(dir)
|
||||
const existingBackups = existingFiles.filter((f) => f.startsWith(`${basename}.bak.`))
|
||||
existingBackups.forEach((f) => {
|
||||
const backupPath = path.join(dir, f)
|
||||
try {
|
||||
fs.unlinkSync(backupPath)
|
||||
cleanupPaths.splice(cleanupPaths.indexOf(backupPath), 1)
|
||||
} catch {
|
||||
}
|
||||
})
|
||||
|
||||
//#when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
//#then: No write or backup should occur
|
||||
expect(needsWrite).toBe(false)
|
||||
|
||||
const files = fs.readdirSync(dir)
|
||||
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
|
||||
expect(backupFiles.length).toBe(0)
|
||||
})
|
||||
|
||||
test("does not write when no migration needed", () => {
|
||||
// given: Config with no migrations needed
|
||||
const testConfigPath = "/tmp/test-config-no-migration.json"
|
||||
|
||||
@@ -1,330 +1,5 @@
|
||||
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> = {
|
||||
// Sisyphus variants → "sisyphus"
|
||||
omo: "sisyphus",
|
||||
OmO: "sisyphus",
|
||||
Sisyphus: "sisyphus",
|
||||
sisyphus: "sisyphus",
|
||||
|
||||
// Prometheus variants → "prometheus"
|
||||
"OmO-Plan": "prometheus",
|
||||
"omo-plan": "prometheus",
|
||||
"Planner-Sisyphus": "prometheus",
|
||||
"planner-sisyphus": "prometheus",
|
||||
"Prometheus (Planner)": "prometheus",
|
||||
prometheus: "prometheus",
|
||||
|
||||
// Atlas variants → "atlas"
|
||||
"orchestrator-sisyphus": "atlas",
|
||||
Atlas: "atlas",
|
||||
atlas: "atlas",
|
||||
|
||||
// Metis variants → "metis"
|
||||
"plan-consultant": "metis",
|
||||
"Metis (Plan Consultant)": "metis",
|
||||
metis: "metis",
|
||||
|
||||
// Momus variants → "momus"
|
||||
"Momus (Plan Reviewer)": "momus",
|
||||
momus: "momus",
|
||||
|
||||
// Sisyphus-Junior → "sisyphus-junior"
|
||||
"Sisyphus-Junior": "sisyphus-junior",
|
||||
"sisyphus-junior": "sisyphus-junior",
|
||||
|
||||
// Already lowercase - passthrough
|
||||
build: "build",
|
||||
oracle: "oracle",
|
||||
librarian: "librarian",
|
||||
explore: "explore",
|
||||
"multimodal-looker": "multimodal-looker",
|
||||
}
|
||||
|
||||
export const BUILTIN_AGENT_NAMES = new Set([
|
||||
"sisyphus", // was "Sisyphus"
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
"multimodal-looker",
|
||||
"metis", // was "Metis (Plan Consultant)"
|
||||
"momus", // was "Momus (Plan Reviewer)"
|
||||
"prometheus", // was "Prometheus (Planner)"
|
||||
"atlas", // was "Atlas"
|
||||
"build",
|
||||
])
|
||||
|
||||
// Migration map: old hook names → new hook names (for backward compatibility)
|
||||
// null means the hook was removed and should be filtered out from disabled_hooks
|
||||
export const HOOK_NAME_MAP: Record<string, string | null> = {
|
||||
// Legacy names (backward compatibility)
|
||||
"anthropic-auto-compact": "anthropic-context-window-limit-recovery",
|
||||
"sisyphus-orchestrator": "atlas",
|
||||
|
||||
// Removed hooks (v3.0.0) - will be filtered out and user warned
|
||||
"empty-message-sanitizer": null,
|
||||
}
|
||||
|
||||
/**
|
||||
* @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: "unspecified-high" }
|
||||
* - 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": "visual-engineering",
|
||||
"google/gemini-3-flash": "writing",
|
||||
"openai/gpt-5.2": "ultrabrain",
|
||||
"anthropic/claude-haiku-4-5": "quick",
|
||||
"anthropic/claude-opus-4-6": "unspecified-high",
|
||||
"anthropic/claude-sonnet-4-5": "unspecified-low",
|
||||
}
|
||||
|
||||
/**
|
||||
* Model version migration map: old full model strings → new full model strings.
|
||||
* Used to auto-upgrade hardcoded model versions in user configs when the plugin
|
||||
* bumps to newer model versions.
|
||||
*
|
||||
* Keys are full "provider/model" strings. Only openai and anthropic entries needed.
|
||||
*/
|
||||
export const MODEL_VERSION_MAP: Record<string, string> = {
|
||||
"openai/gpt-5.2-codex": "openai/gpt-5.3-codex",
|
||||
"anthropic/claude-opus-4-5": "anthropic/claude-opus-4-6",
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a consistent migration key for tracking applied migrations.
|
||||
*/
|
||||
function migrationKey(oldModel: string, newModel: string): string {
|
||||
return `model-version:${oldModel}->${newModel}`
|
||||
}
|
||||
|
||||
export function migrateModelVersions(
|
||||
configs: Record<string, unknown>,
|
||||
appliedMigrations?: Set<string>,
|
||||
): { migrated: Record<string, unknown>; changed: boolean; newMigrations: string[] } {
|
||||
const migrated: Record<string, unknown> = {}
|
||||
let changed = false
|
||||
const newMigrations: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(configs)) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
const config = value as Record<string, unknown>
|
||||
if (typeof config.model === "string" && MODEL_VERSION_MAP[config.model]) {
|
||||
const oldModel = config.model
|
||||
const newModel = MODEL_VERSION_MAP[oldModel]
|
||||
const mKey = migrationKey(oldModel, newModel)
|
||||
|
||||
// Skip if this migration was already applied (user may have reverted)
|
||||
if (appliedMigrations?.has(mKey)) {
|
||||
migrated[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
migrated[key] = { ...config, model: newModel }
|
||||
changed = true
|
||||
newMigrations.push(mKey)
|
||||
continue
|
||||
}
|
||||
}
|
||||
migrated[key] = value
|
||||
}
|
||||
|
||||
return { migrated, changed, newMigrations }
|
||||
}
|
||||
|
||||
export function migrateHookNames(hooks: string[]): { migrated: string[]; changed: boolean; removed: string[] } {
|
||||
const migrated: string[] = []
|
||||
const removed: string[] = []
|
||||
let changed = false
|
||||
|
||||
for (const hook of hooks) {
|
||||
const mapping = HOOK_NAME_MAP[hook]
|
||||
|
||||
if (mapping === null) {
|
||||
removed.push(hook)
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
|
||||
const newHook = mapping ?? hook
|
||||
if (newHook !== hook) {
|
||||
changed = true
|
||||
}
|
||||
migrated.push(newHook)
|
||||
}
|
||||
|
||||
return { migrated, changed, removed }
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Load previously applied migrations
|
||||
const existingMigrations = Array.isArray(rawConfig._migrations)
|
||||
? new Set(rawConfig._migrations as string[])
|
||||
: new Set<string>()
|
||||
const allNewMigrations: string[] = []
|
||||
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
const { migrated, changed } = migrateAgentNames(rawConfig.agents as Record<string, unknown>)
|
||||
if (changed) {
|
||||
rawConfig.agents = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate model versions in agents (skip already-applied migrations)
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(
|
||||
rawConfig.agents as Record<string, unknown>,
|
||||
existingMigrations,
|
||||
)
|
||||
if (changed) {
|
||||
rawConfig.agents = migrated
|
||||
needsWrite = true
|
||||
log(`Migrated model versions in agents config`)
|
||||
}
|
||||
allNewMigrations.push(...newMigrations)
|
||||
}
|
||||
|
||||
// Migrate model versions in categories (skip already-applied migrations)
|
||||
if (rawConfig.categories && typeof rawConfig.categories === "object") {
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(
|
||||
rawConfig.categories as Record<string, unknown>,
|
||||
existingMigrations,
|
||||
)
|
||||
if (changed) {
|
||||
rawConfig.categories = migrated
|
||||
needsWrite = true
|
||||
log(`Migrated model versions in categories config`)
|
||||
}
|
||||
allNewMigrations.push(...newMigrations)
|
||||
}
|
||||
|
||||
// Record newly applied migrations
|
||||
if (allNewMigrations.length > 0) {
|
||||
const updatedMigrations = Array.from(existingMigrations)
|
||||
updatedMigrations.push(...allNewMigrations)
|
||||
rawConfig._migrations = updatedMigrations
|
||||
needsWrite = true
|
||||
}
|
||||
|
||||
if (rawConfig.omo_agent) {
|
||||
rawConfig.sisyphus_agent = rawConfig.omo_agent
|
||||
delete rawConfig.omo_agent
|
||||
needsWrite = true
|
||||
}
|
||||
|
||||
if (rawConfig.disabled_agents && Array.isArray(rawConfig.disabled_agents)) {
|
||||
const migrated: string[] = []
|
||||
let changed = false
|
||||
for (const agent of rawConfig.disabled_agents as string[]) {
|
||||
const newAgent = AGENT_NAME_MAP[agent.toLowerCase()] ?? AGENT_NAME_MAP[agent] ?? agent
|
||||
if (newAgent !== agent) {
|
||||
changed = true
|
||||
}
|
||||
migrated.push(newAgent)
|
||||
}
|
||||
if (changed) {
|
||||
rawConfig.disabled_agents = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
if (rawConfig.disabled_hooks && Array.isArray(rawConfig.disabled_hooks)) {
|
||||
const { migrated, changed, removed } = migrateHookNames(rawConfig.disabled_hooks as string[])
|
||||
if (changed) {
|
||||
rawConfig.disabled_hooks = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
log(`Removed obsolete hooks from disabled_hooks: ${removed.join(", ")} (these hooks no longer exist in v3.0.0)`)
|
||||
}
|
||||
}
|
||||
|
||||
if (rawConfig.experimental && typeof rawConfig.experimental === "object") {
|
||||
const exp = rawConfig.experimental as Record<string, unknown>
|
||||
if ("task_system" in exp && exp.task_system !== undefined) {
|
||||
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
|
||||
}
|
||||
export { AGENT_NAME_MAP, BUILTIN_AGENT_NAMES, migrateAgentNames } from "./migration/agent-names"
|
||||
export { HOOK_NAME_MAP, migrateHookNames } from "./migration/hook-names"
|
||||
export { MODEL_VERSION_MAP, migrateModelVersions } from "./migration/model-versions"
|
||||
export { MODEL_TO_CATEGORY_MAP, migrateAgentConfigToCategory, shouldDeleteAgentConfig } from "./migration/agent-category"
|
||||
export { migrateConfigFile } from "./migration/config-migration"
|
||||
|
||||
60
src/shared/migration/agent-category.ts
Normal file
60
src/shared/migration/agent-category.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @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: "unspecified-high" }
|
||||
* - 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": "visual-engineering",
|
||||
"google/gemini-3-flash": "writing",
|
||||
"openai/gpt-5.2": "ultrabrain",
|
||||
"anthropic/claude-haiku-4-5": "quick",
|
||||
"anthropic/claude-opus-4-6": "unspecified-high",
|
||||
"anthropic/claude-sonnet-4-5": "unspecified-low",
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
70
src/shared/migration/agent-names.ts
Normal file
70
src/shared/migration/agent-names.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export const AGENT_NAME_MAP: Record<string, string> = {
|
||||
// Sisyphus variants → "sisyphus"
|
||||
omo: "sisyphus",
|
||||
OmO: "sisyphus",
|
||||
Sisyphus: "sisyphus",
|
||||
sisyphus: "sisyphus",
|
||||
|
||||
// Prometheus variants → "prometheus"
|
||||
"OmO-Plan": "prometheus",
|
||||
"omo-plan": "prometheus",
|
||||
"Planner-Sisyphus": "prometheus",
|
||||
"planner-sisyphus": "prometheus",
|
||||
"Prometheus (Planner)": "prometheus",
|
||||
prometheus: "prometheus",
|
||||
|
||||
// Atlas variants → "atlas"
|
||||
"orchestrator-sisyphus": "atlas",
|
||||
Atlas: "atlas",
|
||||
atlas: "atlas",
|
||||
|
||||
// Metis variants → "metis"
|
||||
"plan-consultant": "metis",
|
||||
"Metis (Plan Consultant)": "metis",
|
||||
metis: "metis",
|
||||
|
||||
// Momus variants → "momus"
|
||||
"Momus (Plan Reviewer)": "momus",
|
||||
momus: "momus",
|
||||
|
||||
// Sisyphus-Junior → "sisyphus-junior"
|
||||
"Sisyphus-Junior": "sisyphus-junior",
|
||||
"sisyphus-junior": "sisyphus-junior",
|
||||
|
||||
// Already lowercase - passthrough
|
||||
build: "build",
|
||||
oracle: "oracle",
|
||||
librarian: "librarian",
|
||||
explore: "explore",
|
||||
"multimodal-looker": "multimodal-looker",
|
||||
}
|
||||
|
||||
export const BUILTIN_AGENT_NAMES = new Set([
|
||||
"sisyphus", // was "Sisyphus"
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
"multimodal-looker",
|
||||
"metis", // was "Metis (Plan Consultant)"
|
||||
"momus", // was "Momus (Plan Reviewer)"
|
||||
"prometheus", // was "Prometheus (Planner)"
|
||||
"atlas", // was "Atlas"
|
||||
"build",
|
||||
])
|
||||
|
||||
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 }
|
||||
}
|
||||
112
src/shared/migration/config-migration.ts
Normal file
112
src/shared/migration/config-migration.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import * as fs from "fs"
|
||||
import { log } from "../logger"
|
||||
import { AGENT_NAME_MAP, migrateAgentNames } from "./agent-names"
|
||||
import { migrateHookNames } from "./hook-names"
|
||||
import { migrateModelVersions } from "./model-versions"
|
||||
|
||||
export function migrateConfigFile(
|
||||
configPath: string,
|
||||
rawConfig: Record<string, unknown>
|
||||
): boolean {
|
||||
let needsWrite = false
|
||||
|
||||
// Load previously applied migrations
|
||||
const existingMigrations = Array.isArray(rawConfig._migrations)
|
||||
? new Set(rawConfig._migrations as string[])
|
||||
: new Set<string>()
|
||||
const allNewMigrations: string[] = []
|
||||
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
const { migrated, changed } = migrateAgentNames(rawConfig.agents as Record<string, unknown>)
|
||||
if (changed) {
|
||||
rawConfig.agents = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate model versions in agents (skip already-applied migrations)
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(
|
||||
rawConfig.agents as Record<string, unknown>,
|
||||
existingMigrations
|
||||
)
|
||||
if (changed) {
|
||||
rawConfig.agents = migrated
|
||||
needsWrite = true
|
||||
log("Migrated model versions in agents config")
|
||||
}
|
||||
allNewMigrations.push(...newMigrations)
|
||||
}
|
||||
|
||||
// Migrate model versions in categories (skip already-applied migrations)
|
||||
if (rawConfig.categories && typeof rawConfig.categories === "object") {
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(
|
||||
rawConfig.categories as Record<string, unknown>,
|
||||
existingMigrations
|
||||
)
|
||||
if (changed) {
|
||||
rawConfig.categories = migrated
|
||||
needsWrite = true
|
||||
log("Migrated model versions in categories config")
|
||||
}
|
||||
allNewMigrations.push(...newMigrations)
|
||||
}
|
||||
|
||||
// Record newly applied migrations
|
||||
if (allNewMigrations.length > 0) {
|
||||
const updatedMigrations = Array.from(existingMigrations)
|
||||
updatedMigrations.push(...allNewMigrations)
|
||||
rawConfig._migrations = updatedMigrations
|
||||
needsWrite = true
|
||||
}
|
||||
|
||||
if (rawConfig.omo_agent) {
|
||||
rawConfig.sisyphus_agent = rawConfig.omo_agent
|
||||
delete rawConfig.omo_agent
|
||||
needsWrite = true
|
||||
}
|
||||
|
||||
if (rawConfig.disabled_agents && Array.isArray(rawConfig.disabled_agents)) {
|
||||
const migrated: string[] = []
|
||||
let changed = false
|
||||
for (const agent of rawConfig.disabled_agents as string[]) {
|
||||
const newAgent = AGENT_NAME_MAP[agent.toLowerCase()] ?? AGENT_NAME_MAP[agent] ?? agent
|
||||
if (newAgent !== agent) {
|
||||
changed = true
|
||||
}
|
||||
migrated.push(newAgent)
|
||||
}
|
||||
if (changed) {
|
||||
rawConfig.disabled_agents = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
if (rawConfig.disabled_hooks && Array.isArray(rawConfig.disabled_hooks)) {
|
||||
const { migrated, changed, removed } = migrateHookNames(rawConfig.disabled_hooks as string[])
|
||||
if (changed) {
|
||||
rawConfig.disabled_hooks = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
log(
|
||||
`Removed obsolete hooks from disabled_hooks: ${removed.join(", ")} (these hooks no longer exist in v3.0.0)`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
36
src/shared/migration/hook-names.ts
Normal file
36
src/shared/migration/hook-names.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Migration map: old hook names → new hook names (for backward compatibility)
|
||||
// null means the hook was removed and should be filtered out from disabled_hooks
|
||||
export const HOOK_NAME_MAP: Record<string, string | null> = {
|
||||
// Legacy names (backward compatibility)
|
||||
"anthropic-auto-compact": "anthropic-context-window-limit-recovery",
|
||||
"sisyphus-orchestrator": "atlas",
|
||||
|
||||
// Removed hooks (v3.0.0) - will be filtered out and user warned
|
||||
"empty-message-sanitizer": null,
|
||||
}
|
||||
|
||||
export function migrateHookNames(
|
||||
hooks: string[]
|
||||
): { migrated: string[]; changed: boolean; removed: string[] } {
|
||||
const migrated: string[] = []
|
||||
const removed: string[] = []
|
||||
let changed = false
|
||||
|
||||
for (const hook of hooks) {
|
||||
const mapping = HOOK_NAME_MAP[hook]
|
||||
|
||||
if (mapping === null) {
|
||||
removed.push(hook)
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
|
||||
const newHook = mapping ?? hook
|
||||
if (newHook !== hook) {
|
||||
changed = true
|
||||
}
|
||||
migrated.push(newHook)
|
||||
}
|
||||
|
||||
return { migrated, changed, removed }
|
||||
}
|
||||
49
src/shared/migration/model-versions.ts
Normal file
49
src/shared/migration/model-versions.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Model version migration map: old full model strings → new full model strings.
|
||||
* Used to auto-upgrade hardcoded model versions in user configs when the plugin
|
||||
* bumps to newer model versions.
|
||||
*
|
||||
* Keys are full "provider/model" strings. Only openai and anthropic entries needed.
|
||||
*/
|
||||
export const MODEL_VERSION_MAP: Record<string, string> = {
|
||||
"openai/gpt-5.2-codex": "openai/gpt-5.3-codex",
|
||||
"anthropic/claude-opus-4-5": "anthropic/claude-opus-4-6",
|
||||
}
|
||||
|
||||
function migrationKey(oldModel: string, newModel: string): string {
|
||||
return `model-version:${oldModel}->${newModel}`
|
||||
}
|
||||
|
||||
export function migrateModelVersions(
|
||||
configs: Record<string, unknown>,
|
||||
appliedMigrations?: Set<string>
|
||||
): { migrated: Record<string, unknown>; changed: boolean; newMigrations: string[] } {
|
||||
const migrated: Record<string, unknown> = {}
|
||||
let changed = false
|
||||
const newMigrations: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(configs)) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
const config = value as Record<string, unknown>
|
||||
if (typeof config.model === "string" && MODEL_VERSION_MAP[config.model]) {
|
||||
const oldModel = config.model
|
||||
const newModel = MODEL_VERSION_MAP[oldModel]
|
||||
const mKey = migrationKey(oldModel, newModel)
|
||||
|
||||
// Skip if this migration was already applied (user may have reverted)
|
||||
if (appliedMigrations?.has(mKey)) {
|
||||
migrated[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
migrated[key] = { ...config, model: newModel }
|
||||
changed = true
|
||||
newMigrations.push(mKey)
|
||||
continue
|
||||
}
|
||||
}
|
||||
migrated[key] = value
|
||||
}
|
||||
|
||||
return { migrated, changed, newMigrations }
|
||||
}
|
||||
Reference in New Issue
Block a user