From 52b2afb6b0308bd10d985aad5fce6a39559ba389 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 20 Feb 2026 11:12:33 +0900 Subject: [PATCH] fix(config): promote hashline_edit to top-level flag Move hashline_edit out of experimental so it is a stable top-level config with default-on runtime behavior and explicit disable support. Add migration and tests to preserve existing experimental.hashline_edit users without breaking configs. --- assets/oh-my-opencode.schema.json | 6 +- src/config/schema.test.ts | 101 ++++++++++---------- src/config/schema/experimental.ts | 2 - src/config/schema/oh-my-opencode-config.ts | 2 + src/plugin/hooks/create-tool-guard-hooks.ts | 4 +- src/plugin/tool-registry.ts | 2 +- src/shared/migration.test.ts | 46 +++++++++ src/shared/migration/config-migration.ts | 14 +++ 8 files changed, 117 insertions(+), 60 deletions(-) diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 9327d59c..06a3811a 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -128,6 +128,9 @@ "type": "string" } }, + "hashline_edit": { + "type": "boolean" + }, "agents": { "type": "object", "properties": { @@ -3141,9 +3144,6 @@ }, "disable_omo_env": { "type": "boolean" - }, - "hashline_edit": { - "type": "boolean" } }, "additionalProperties": false diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 53d10c6d..4a64cfaf 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -644,6 +644,55 @@ describe("OhMyOpenCodeConfigSchema - browser_automation_engine", () => { }) }) +describe("OhMyOpenCodeConfigSchema - hashline_edit", () => { + test("accepts hashline_edit as true", () => { + //#given + const input = { hashline_edit: true } + + //#when + const result = OhMyOpenCodeConfigSchema.safeParse(input) + + //#then + expect(result.success).toBe(true) + expect(result.data?.hashline_edit).toBe(true) + }) + + test("accepts hashline_edit as false", () => { + //#given + const input = { hashline_edit: false } + + //#when + const result = OhMyOpenCodeConfigSchema.safeParse(input) + + //#then + expect(result.success).toBe(true) + expect(result.data?.hashline_edit).toBe(false) + }) + + test("hashline_edit is optional", () => { + //#given + const input = { auto_update: true } + + //#when + const result = OhMyOpenCodeConfigSchema.safeParse(input) + + //#then + expect(result.success).toBe(true) + expect(result.data?.hashline_edit).toBeUndefined() + }) + + test("rejects non-boolean hashline_edit", () => { + //#given + const input = { hashline_edit: "true" } + + //#when + const result = OhMyOpenCodeConfigSchema.safeParse(input) + + //#then + expect(result.success).toBe(false) + }) +}) + describe("ExperimentalConfigSchema feature flags", () => { test("accepts plugin_load_timeout_ms as number", () => { //#given @@ -699,48 +748,6 @@ describe("ExperimentalConfigSchema feature flags", () => { } }) - test("accepts hashline_edit as true", () => { - //#given - const config = { hashline_edit: true } - - //#when - const result = ExperimentalConfigSchema.safeParse(config) - - //#then - expect(result.success).toBe(true) - if (result.success) { - expect(result.data.hashline_edit).toBe(true) - } - }) - - test("accepts hashline_edit as false", () => { - //#given - const config = { hashline_edit: false } - - //#when - const result = ExperimentalConfigSchema.safeParse(config) - - //#then - expect(result.success).toBe(true) - if (result.success) { - expect(result.data.hashline_edit).toBe(false) - } - }) - - test("hashline_edit is optional", () => { - //#given - const config = { safe_hook_creation: true } - - //#when - const result = ExperimentalConfigSchema.safeParse(config) - - //#then - expect(result.success).toBe(true) - if (result.success) { - expect(result.data.hashline_edit).toBeUndefined() - } - }) - test("accepts disable_omo_env as true", () => { //#given const config = { disable_omo_env: true } @@ -794,16 +801,6 @@ describe("ExperimentalConfigSchema feature flags", () => { expect(result.success).toBe(false) }) - test("rejects non-boolean hashline_edit", () => { - //#given - const config = { hashline_edit: "true" } - - //#when - const result = ExperimentalConfigSchema.safeParse(config) - - //#then - expect(result.success).toBe(false) - }) }) describe("GitMasterConfigSchema", () => { diff --git a/src/config/schema/experimental.ts b/src/config/schema/experimental.ts index 4fbcdf18..cccb052c 100644 --- a/src/config/schema/experimental.ts +++ b/src/config/schema/experimental.ts @@ -17,8 +17,6 @@ export const ExperimentalConfigSchema = z.object({ safe_hook_creation: z.boolean().optional(), /** Disable auto-injected context in prompts (experimental) */ disable_omo_env: z.boolean().optional(), - /** Enable hashline_edit tool for improved file editing with hash-based line anchors */ - hashline_edit: z.boolean().optional(), }) export type ExperimentalConfig = z.infer diff --git a/src/config/schema/oh-my-opencode-config.ts b/src/config/schema/oh-my-opencode-config.ts index be0ebd91..dbeedc37 100644 --- a/src/config/schema/oh-my-opencode-config.ts +++ b/src/config/schema/oh-my-opencode-config.ts @@ -33,6 +33,8 @@ export const OhMyOpenCodeConfigSchema = z.object({ disabled_commands: z.array(BuiltinCommandNameSchema).optional(), /** Disable specific tools by name (e.g., ["todowrite", "todoread"]) */ disabled_tools: z.array(z.string()).optional(), + /** Enable hashline_edit tool/hook integrations (default: true at call site) */ + hashline_edit: z.boolean().optional(), agents: AgentOverridesSchema.optional(), categories: CategoriesConfigSchema.optional(), claude_code: ClaudeCodeConfigSchema.optional(), diff --git a/src/plugin/hooks/create-tool-guard-hooks.ts b/src/plugin/hooks/create-tool-guard-hooks.ts index 8f1e7f81..521e3675 100644 --- a/src/plugin/hooks/create-tool-guard-hooks.ts +++ b/src/plugin/hooks/create-tool-guard-hooks.ts @@ -98,11 +98,11 @@ export function createToolGuardHooks(args: { : null const hashlineReadEnhancer = isHookEnabled("hashline-read-enhancer") - ? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.experimental?.hashline_edit ?? true } })) + ? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.hashline_edit ?? true } })) : null const hashlineEditDiffEnhancer = isHookEnabled("hashline-edit-diff-enhancer") - ? safeHook("hashline-edit-diff-enhancer", () => createHashlineEditDiffEnhancerHook({ hashline_edit: { enabled: pluginConfig.experimental?.hashline_edit ?? true } })) + ? safeHook("hashline-edit-diff-enhancer", () => createHashlineEditDiffEnhancerHook({ hashline_edit: { enabled: pluginConfig.hashline_edit ?? true } })) : null return { diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 590e28c7..21d7901f 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -113,7 +113,7 @@ export function createToolRegistry(args: { } : {} - const hashlineEnabled = pluginConfig.experimental?.hashline_edit ?? true + const hashlineEnabled = pluginConfig.hashline_edit ?? true const hashlineToolsRecord: Record = hashlineEnabled ? { edit: createHashlineEditTool() } : {} diff --git a/src/shared/migration.test.ts b/src/shared/migration.test.ts index 1955ce99..42ebd202 100644 --- a/src/shared/migration.test.ts +++ b/src/shared/migration.test.ts @@ -306,6 +306,52 @@ describe("migrateHookNames", () => { describe("migrateConfigFile", () => { const testConfigPath = "/tmp/nonexistent-path-for-test.json" + test("migrates experimental.hashline_edit to top-level hashline_edit", () => { + // given: Config with legacy experimental.hashline_edit + const rawConfig: Record = { + experimental: { hashline_edit: false, safe_hook_creation: true }, + } + + // when: Migrate config file + const needsWrite = migrateConfigFile(testConfigPath, rawConfig) + + // then: hashline_edit should move to top-level and be removed from experimental + expect(needsWrite).toBe(true) + expect(rawConfig.hashline_edit).toBe(false) + expect(rawConfig.experimental).toEqual({ safe_hook_creation: true }) + }) + + test("migrates and removes empty experimental object", () => { + // given: Config with only experimental.hashline_edit + const rawConfig: Record = { + experimental: { hashline_edit: true }, + } + + // when: Migrate config file + const needsWrite = migrateConfigFile(testConfigPath, rawConfig) + + // then: hashline_edit moves top-level and empty experimental is removed + expect(needsWrite).toBe(true) + expect(rawConfig.hashline_edit).toBe(true) + expect(rawConfig.experimental).toBeUndefined() + }) + + test("does not overwrite top-level hashline_edit when already set", () => { + // given: Config with both top-level and legacy location + const rawConfig: Record = { + hashline_edit: false, + experimental: { hashline_edit: true }, + } + + // when: Migrate config file + const needsWrite = migrateConfigFile(testConfigPath, rawConfig) + + // then: top-level value wins, legacy key removed + expect(needsWrite).toBe(true) + expect(rawConfig.hashline_edit).toBe(false) + expect(rawConfig.experimental).toBeUndefined() + }) + test("migrates omo_agent to sisyphus_agent", () => { // given: Config with legacy omo_agent key const rawConfig: Record = { diff --git a/src/shared/migration/config-migration.ts b/src/shared/migration/config-migration.ts index b3051efb..aae93724 100644 --- a/src/shared/migration/config-migration.ts +++ b/src/shared/migration/config-migration.ts @@ -67,6 +67,20 @@ export function migrateConfigFile( needsWrite = true } + if (copy.experimental && typeof copy.experimental === "object") { + const experimental = copy.experimental as Record + if ("hashline_edit" in experimental) { + if (copy.hashline_edit === undefined) { + copy.hashline_edit = experimental.hashline_edit + } + delete experimental.hashline_edit + if (Object.keys(experimental).length === 0) { + delete copy.experimental + } + needsWrite = true + } + } + if (copy.disabled_agents && Array.isArray(copy.disabled_agents)) { const migrated: string[] = [] let changed = false