fix(migration): add hook rename and removal mappings for v3.0.0 upgrade

- Add sisyphus-orchestrator → atlas hook rename mapping
- Add null mappings for removed hooks (preemptive-compaction, empty-message-sanitizer)
- Update migrateHookNames() to filter out removed hooks and return removed list
- Log warning when obsolete hooks are removed from disabled_hooks
- Add tests for new migration scenarios
This commit is contained in:
justsisyphus 2026-01-23 01:24:07 +09:00
parent 45b2782d55
commit e15677efd5
2 changed files with 74 additions and 8 deletions

View File

@ -118,13 +118,14 @@ describe("migrateHookNames", () => {
const hooks = ["anthropic-auto-compact", "comment-checker"] const hooks = ["anthropic-auto-compact", "comment-checker"]
// #when: Migrate hook names // #when: Migrate hook names
const { migrated, changed } = migrateHookNames(hooks) const { migrated, changed, removed } = migrateHookNames(hooks)
// #then: Legacy hook name should be migrated // #then: Legacy hook name should be migrated
expect(changed).toBe(true) expect(changed).toBe(true)
expect(migrated).toContain("anthropic-context-window-limit-recovery") expect(migrated).toContain("anthropic-context-window-limit-recovery")
expect(migrated).toContain("comment-checker") expect(migrated).toContain("comment-checker")
expect(migrated).not.toContain("anthropic-auto-compact") expect(migrated).not.toContain("anthropic-auto-compact")
expect(removed).toEqual([])
}) })
test("preserves current hook names unchanged", () => { test("preserves current hook names unchanged", () => {
@ -136,11 +137,12 @@ describe("migrateHookNames", () => {
] ]
// #when: Migrate hook names // #when: Migrate hook names
const { migrated, changed } = migrateHookNames(hooks) const { migrated, changed, removed } = migrateHookNames(hooks)
// #then: Current names should remain unchanged // #then: Current names should remain unchanged
expect(changed).toBe(false) expect(changed).toBe(false)
expect(migrated).toEqual(hooks) expect(migrated).toEqual(hooks)
expect(removed).toEqual([])
}) })
test("handles empty hooks array", () => { test("handles empty hooks array", () => {
@ -148,11 +150,12 @@ describe("migrateHookNames", () => {
const hooks: string[] = [] const hooks: string[] = []
// #when: Migrate hook names // #when: Migrate hook names
const { migrated, changed } = migrateHookNames(hooks) const { migrated, changed, removed } = migrateHookNames(hooks)
// #then: Should return empty array with no changes // #then: Should return empty array with no changes
expect(changed).toBe(false) expect(changed).toBe(false)
expect(migrated).toEqual([]) expect(migrated).toEqual([])
expect(removed).toEqual([])
}) })
test("migrates multiple legacy hook names", () => { test("migrates multiple legacy hook names", () => {
@ -166,6 +169,51 @@ describe("migrateHookNames", () => {
expect(changed).toBe(true) expect(changed).toBe(true)
expect(migrated).toEqual(["anthropic-context-window-limit-recovery"]) expect(migrated).toEqual(["anthropic-context-window-limit-recovery"])
}) })
test("migrates sisyphus-orchestrator to atlas", () => {
// #given: Config with legacy sisyphus-orchestrator hook
const hooks = ["sisyphus-orchestrator", "comment-checker"]
// #when: Migrate hook names
const { migrated, changed, removed } = migrateHookNames(hooks)
// #then: sisyphus-orchestrator should be migrated to atlas
expect(changed).toBe(true)
expect(migrated).toContain("atlas")
expect(migrated).toContain("comment-checker")
expect(migrated).not.toContain("sisyphus-orchestrator")
expect(removed).toEqual([])
})
test("removes obsolete hooks and returns them in removed array", () => {
// #given: Config with removed hooks from v3.0.0
const hooks = ["preemptive-compaction", "empty-message-sanitizer", "comment-checker"]
// #when: Migrate hook names
const { migrated, changed, removed } = migrateHookNames(hooks)
// #then: Removed hooks should be filtered out
expect(changed).toBe(true)
expect(migrated).toEqual(["comment-checker"])
expect(removed).toContain("preemptive-compaction")
expect(removed).toContain("empty-message-sanitizer")
expect(removed).toHaveLength(2)
})
test("handles mixed migration and removal", () => {
// #given: Config with both legacy rename and removed hooks
const hooks = ["anthropic-auto-compact", "preemptive-compaction", "sisyphus-orchestrator"]
// #when: Migrate hook names
const { migrated, changed, removed } = migrateHookNames(hooks)
// #then: Legacy should be renamed, removed should be filtered
expect(changed).toBe(true)
expect(migrated).toContain("anthropic-context-window-limit-recovery")
expect(migrated).toContain("atlas")
expect(migrated).not.toContain("preemptive-compaction")
expect(removed).toEqual(["preemptive-compaction"])
})
}) })
describe("migrateConfigFile", () => { describe("migrateConfigFile", () => {

View File

@ -36,9 +36,15 @@ export const BUILTIN_AGENT_NAMES = new Set([
]) ])
// Migration map: old hook names → new hook names (for backward compatibility) // Migration map: old hook names → new hook names (for backward compatibility)
export const HOOK_NAME_MAP: Record<string, string> = { // 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) // Legacy names (backward compatibility)
"anthropic-auto-compact": "anthropic-context-window-limit-recovery", "anthropic-auto-compact": "anthropic-context-window-limit-recovery",
"sisyphus-orchestrator": "atlas",
// Removed hooks (v3.0.0) - will be filtered out and user warned
"preemptive-compaction": null,
"empty-message-sanitizer": null,
} }
/** /**
@ -77,19 +83,28 @@ export function migrateAgentNames(agents: Record<string, unknown>): { migrated:
return { migrated, changed } return { migrated, changed }
} }
export function migrateHookNames(hooks: string[]): { migrated: string[]; changed: boolean } { export function migrateHookNames(hooks: string[]): { migrated: string[]; changed: boolean; removed: string[] } {
const migrated: string[] = [] const migrated: string[] = []
const removed: string[] = []
let changed = false let changed = false
for (const hook of hooks) { for (const hook of hooks) {
const newHook = HOOK_NAME_MAP[hook] ?? hook const mapping = HOOK_NAME_MAP[hook]
if (mapping === null) {
removed.push(hook)
changed = true
continue
}
const newHook = mapping ?? hook
if (newHook !== hook) { if (newHook !== hook) {
changed = true changed = true
} }
migrated.push(newHook) migrated.push(newHook)
} }
return { migrated, changed } return { migrated, changed, removed }
} }
export function migrateAgentConfigToCategory(config: Record<string, unknown>): { export function migrateAgentConfigToCategory(config: Record<string, unknown>): {
@ -167,11 +182,14 @@ export function migrateConfigFile(configPath: string, rawConfig: Record<string,
} }
if (rawConfig.disabled_hooks && Array.isArray(rawConfig.disabled_hooks)) { if (rawConfig.disabled_hooks && Array.isArray(rawConfig.disabled_hooks)) {
const { migrated, changed } = migrateHookNames(rawConfig.disabled_hooks as string[]) const { migrated, changed, removed } = migrateHookNames(rawConfig.disabled_hooks as string[])
if (changed) { if (changed) {
rawConfig.disabled_hooks = migrated rawConfig.disabled_hooks = migrated
needsWrite = true 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) { if (needsWrite) {