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.
This commit is contained in:
YeonGyu-Kim 2026-02-20 11:12:33 +09:00
parent b8a6f10f70
commit 52b2afb6b0
8 changed files with 117 additions and 60 deletions

View File

@ -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

View File

@ -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", () => {

View File

@ -17,8 +17,6 @@ export const ExperimentalConfigSchema = z.object({
safe_hook_creation: z.boolean().optional(),
/** Disable auto-injected <omo-env> 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<typeof ExperimentalConfigSchema>

View File

@ -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(),

View File

@ -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 {

View File

@ -113,7 +113,7 @@ export function createToolRegistry(args: {
}
: {}
const hashlineEnabled = pluginConfig.experimental?.hashline_edit ?? true
const hashlineEnabled = pluginConfig.hashline_edit ?? true
const hashlineToolsRecord: Record<string, ToolDefinition> = hashlineEnabled
? { edit: createHashlineEditTool() }
: {}

View File

@ -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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {

View File

@ -67,6 +67,20 @@ export function migrateConfigFile(
needsWrite = true
}
if (copy.experimental && typeof copy.experimental === "object") {
const experimental = copy.experimental as Record<string, unknown>
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