refactor(hashline): override native edit tool instead of separate tool + disabler hook

Replace 3-component hashline system (separate hashline_edit tool + edit
disabler hook + OpenAI-exempted read enhancer) with 2-component system
that directly overrides the native edit tool key, matching the
delegate_task pattern.

- Register hashline tool as 'edit' key to override native edit
- Delete hashline-edit-disabler hook (no longer needed)
- Delete hashline-provider-state module (no remaining consumers)
- Remove OpenAI exemption from read enhancer (explicit opt-in means all providers)
- Remove setProvider wiring from chat-params
This commit is contained in:
YeonGyu-Kim
2026-02-16 22:02:52 +09:00
parent 9eb786debd
commit af7b1ee620
15 changed files with 5 additions and 416 deletions
@@ -1,3 +0,0 @@
export const HOOK_NAME = "hashline-edit-disabler"
export const EDIT_DISABLED_MESSAGE = `The 'edit' tool is disabled. Use 'hashline_edit' tool instead. Read the file first to get LINE:HASH anchors, then use hashline_edit with set_line, replace_lines, or insert_after operations.`
-37
View File
@@ -1,37 +0,0 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { getProvider } from "../../features/hashline-provider-state"
import { EDIT_DISABLED_MESSAGE } from "./constants"
export interface HashlineEditDisablerConfig {
experimental?: {
hashline_edit?: boolean
}
}
export function createHashlineEditDisablerHook(
config: HashlineEditDisablerConfig,
): Hooks {
const isHashlineEnabled = config.experimental?.hashline_edit ?? false
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string },
) => {
if (!isHashlineEnabled) {
return
}
const toolName = input.tool.toLowerCase()
if (toolName !== "edit") {
return
}
const providerID = getProvider(input.sessionID)
if (providerID === "openai") {
return
}
throw new Error(EDIT_DISABLED_MESSAGE)
},
}
}
@@ -1,168 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
import { createHashlineEditDisablerHook } from "./index"
import { setProvider, clearProvider } from "../../features/hashline-provider-state"
describe("hashline-edit-disabler hook", () => {
const sessionID = "test-session-123"
beforeEach(() => {
clearProvider(sessionID)
})
afterEach(() => {
clearProvider(sessionID)
})
it("blocks edit tool when hashline enabled + non-OpenAI provider", async () => {
//#given
setProvider(sessionID, "anthropic")
const hook = createHashlineEditDisablerHook({
experimental: { hashline_edit: true },
})
const input = { tool: "edit", sessionID }
const output = { args: {} }
//#when
const executeBeforeHandler = hook["tool.execute.before"]
if (!executeBeforeHandler) {
throw new Error("tool.execute.before handler not found")
}
//#then
await expect(executeBeforeHandler(input, output)).rejects.toThrow(
/hashline_edit/,
)
})
it("passes through edit tool when hashline disabled", async () => {
//#given
setProvider(sessionID, "anthropic")
const hook = createHashlineEditDisablerHook({
experimental: { hashline_edit: false },
})
const input = { tool: "edit", sessionID }
const output = { args: {} }
//#when
const executeBeforeHandler = hook["tool.execute.before"]
if (!executeBeforeHandler) {
throw new Error("tool.execute.before handler not found")
}
//#then
const result = await executeBeforeHandler(input, output)
expect(result).toBeUndefined()
})
it("passes through edit tool when OpenAI provider (even if hashline enabled)", async () => {
//#given
setProvider(sessionID, "openai")
const hook = createHashlineEditDisablerHook({
experimental: { hashline_edit: true },
})
const input = { tool: "edit", sessionID }
const output = { args: {} }
//#when
const executeBeforeHandler = hook["tool.execute.before"]
if (!executeBeforeHandler) {
throw new Error("tool.execute.before handler not found")
}
//#then
const result = await executeBeforeHandler(input, output)
expect(result).toBeUndefined()
})
it("passes through non-edit tools", async () => {
//#given
setProvider(sessionID, "anthropic")
const hook = createHashlineEditDisablerHook({
experimental: { hashline_edit: true },
})
const input = { tool: "write", sessionID }
const output = { args: {} }
//#when
const executeBeforeHandler = hook["tool.execute.before"]
if (!executeBeforeHandler) {
throw new Error("tool.execute.before handler not found")
}
//#then
const result = await executeBeforeHandler(input, output)
expect(result).toBeUndefined()
})
it("blocks case-insensitive edit tool names", async () => {
//#given
setProvider(sessionID, "anthropic")
const hook = createHashlineEditDisablerHook({
experimental: { hashline_edit: true },
})
//#when
const executeBeforeHandler = hook["tool.execute.before"]
if (!executeBeforeHandler) {
throw new Error("tool.execute.before handler not found")
}
//#then
for (const toolName of ["Edit", "EDIT", "edit", "EdIt"]) {
const input = { tool: toolName, sessionID }
const output = { args: {} }
await expect(executeBeforeHandler(input, output)).rejects.toThrow(
/hashline_edit/,
)
}
})
it("passes through when hashline config is undefined", async () => {
//#given
setProvider(sessionID, "anthropic")
const hook = createHashlineEditDisablerHook({
experimental: {},
})
const input = { tool: "edit", sessionID }
const output = { args: {} }
//#when
const executeBeforeHandler = hook["tool.execute.before"]
if (!executeBeforeHandler) {
throw new Error("tool.execute.before handler not found")
}
//#then
const result = await executeBeforeHandler(input, output)
expect(result).toBeUndefined()
})
it("error message includes hashline_edit tool guidance", async () => {
//#given
setProvider(sessionID, "anthropic")
const hook = createHashlineEditDisablerHook({
experimental: { hashline_edit: true },
})
const input = { tool: "edit", sessionID }
const output = { args: {} }
//#when
const executeBeforeHandler = hook["tool.execute.before"]
if (!executeBeforeHandler) {
throw new Error("tool.execute.before handler not found")
}
//#then
try {
await executeBeforeHandler(input, output)
throw new Error("Expected error to be thrown")
} catch (error) {
if (error instanceof Error) {
expect(error.message).toContain("hashline_edit")
expect(error.message).toContain("set_line")
expect(error.message).toContain("replace_lines")
expect(error.message).toContain("insert_after")
}
}
})
})
@@ -1,2 +0,0 @@
export { createHashlineEditDisablerHook } from "./hook"
export { HOOK_NAME, EDIT_DISABLED_MESSAGE } from "./constants"
+3 -11
View File
@@ -1,5 +1,4 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { getProvider } from "../../features/hashline-provider-state"
import { computeLineHash } from "../../tools/hashline-edit/hash-computation"
interface HashlineReadEnhancerConfig {
@@ -12,15 +11,8 @@ function isReadTool(toolName: string): boolean {
return toolName.toLowerCase() === "read"
}
function shouldProcess(sessionID: string, config: HashlineReadEnhancerConfig): boolean {
if (!config.hashline_edit?.enabled) {
return false
}
const providerID = getProvider(sessionID)
if (providerID === "openai") {
return false
}
return true
function shouldProcess(config: HashlineReadEnhancerConfig): boolean {
return config.hashline_edit?.enabled ?? false
}
function isTextFile(output: string): boolean {
@@ -65,7 +57,7 @@ export function createHashlineReadEnhancerHook(
if (typeof output.output !== "string") {
return
}
if (!shouldProcess(input.sessionID, config)) {
if (!shouldProcess(config)) {
return
}
output.output = transformOutput(output.output)
+1 -52
View File
@@ -1,7 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
import { describe, it, expect, beforeEach } from "bun:test"
import { createHashlineReadEnhancerHook } from "./hook"
import type { PluginInput } from "@opencode-ai/plugin"
import { setProvider, clearProvider } from "../../features/hashline-provider-state"
//#given - Test setup helpers
function createMockContext(): PluginInput {
@@ -27,11 +26,6 @@ describe("createHashlineReadEnhancerHook", () => {
beforeEach(() => {
mockCtx = createMockContext()
clearProvider(sessionID)
})
afterEach(() => {
clearProvider(sessionID)
})
describe("tool name matching", () => {
@@ -120,51 +114,6 @@ describe("createHashlineReadEnhancerHook", () => {
})
})
describe("provider check", () => {
it("should skip when provider is OpenAI", async () => {
//#given
setProvider(sessionID, "openai")
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const originalOutput = "1: hello\n2: world"
const output = { title: "Read", output: originalOutput, metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBe(originalOutput)
})
it("should process when provider is Claude", async () => {
//#given
setProvider(sessionID, "anthropic")
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: hello\n2: world", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toContain("|")
})
it("should process when provider is unknown (undefined)", async () => {
//#given
// Provider not set, getProvider returns undefined
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: hello\n2: world", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toContain("|")
})
})
describe("output transformation", () => {
it("should transform 'N: content' format to 'N:HASH|content'", async () => {
//#given
-1
View File
@@ -43,5 +43,4 @@ export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter";
export { createPreemptiveCompactionHook } from "./preemptive-compaction";
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
export { createWriteExistingFileGuardHook } from "./write-existing-file-guard";
export { createHashlineEditDisablerHook } from "./hashline-edit-disabler";
export { createHashlineReadEnhancerHook } from "./hashline-read-enhancer";