From 6a31e911d8e921226a3e9e1ca3d666a73976afea Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 21 Feb 2026 17:07:39 +0900 Subject: [PATCH] feat(hooks): add task-reminder hook for task tool usage tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Injects a reminder after 10 tool turns without task tool usage. Tracks per-session counters and cleans up on session deletion. 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/hooks/task-reminder/hook.ts | 59 ++++++++++ src/hooks/task-reminder/index.test.ts | 150 ++++++++++++++++++++++++++ src/hooks/task-reminder/index.ts | 1 + 3 files changed, 210 insertions(+) create mode 100644 src/hooks/task-reminder/hook.ts create mode 100644 src/hooks/task-reminder/index.test.ts create mode 100644 src/hooks/task-reminder/index.ts diff --git a/src/hooks/task-reminder/hook.ts b/src/hooks/task-reminder/hook.ts new file mode 100644 index 00000000..4e795018 --- /dev/null +++ b/src/hooks/task-reminder/hook.ts @@ -0,0 +1,59 @@ +import type { PluginInput } from "@opencode-ai/plugin" + +const TASK_TOOLS = new Set([ + "task", + "task_create", + "task_list", + "task_get", + "task_update", + "task_delete", +]) +const TURN_THRESHOLD = 10 +const REMINDER_MESSAGE = ` + +The task tools haven't been used recently. If you're tracking work, use task with action=create/update (or task_create/task_update) to record progress.` + +interface ToolExecuteInput { + tool: string + sessionID: string + callID: string +} + +interface ToolExecuteOutput { + output: string +} + +export function createTaskReminderHook(_ctx: PluginInput) { + const sessionCounters = new Map() + + const toolExecuteAfter = async (input: ToolExecuteInput, output: ToolExecuteOutput) => { + const { tool, sessionID } = input + const toolLower = tool.toLowerCase() + + if (TASK_TOOLS.has(toolLower)) { + sessionCounters.set(sessionID, 0) + return + } + + const currentCount = sessionCounters.get(sessionID) ?? 0 + const newCount = currentCount + 1 + + if (newCount >= TURN_THRESHOLD) { + output.output += REMINDER_MESSAGE + sessionCounters.set(sessionID, 0) + } else { + sessionCounters.set(sessionID, newCount) + } + } + + return { + "tool.execute.after": toolExecuteAfter, + event: async ({ event }: { event: { type: string; properties?: unknown } }) => { + if (event.type !== "session.deleted") return + const props = event.properties as { info?: { id?: string } } | undefined + const sessionId = props?.info?.id + if (!sessionId) return + sessionCounters.delete(sessionId) + }, + } +} diff --git a/src/hooks/task-reminder/index.test.ts b/src/hooks/task-reminder/index.test.ts new file mode 100644 index 00000000..db43ac58 --- /dev/null +++ b/src/hooks/task-reminder/index.test.ts @@ -0,0 +1,150 @@ +import { describe, test, expect, beforeEach } from "bun:test" +import { createTaskReminderHook } from "./index" +import type { PluginInput } from "@opencode-ai/plugin" + +const mockCtx = {} as PluginInput + +describe("TaskReminderHook", () => { + let hook: ReturnType + + beforeEach(() => { + hook = createTaskReminderHook(mockCtx) + }) + + test("does not inject reminder before 10 turns", async () => { + //#given + const sessionID = "test-session" + const output = { output: "Result" } + + //#when + for (let i = 0; i < 9; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-${i}` }, + output + ) + } + + //#then + expect(output.output).not.toContain("task tools haven't been used") + }) + + test("injects reminder after 10 turns without task tool usage", async () => { + //#given + const sessionID = "test-session" + const output = { output: "Result" } + + //#when + for (let i = 0; i < 10; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-${i}` }, + output + ) + } + + //#then + expect(output.output).toContain("task tools haven't been used") + }) + + test("resets counter when task tool is used", async () => { + //#given + const sessionID = "test-session" + const output = { output: "Result" } + + //#when + for (let i = 0; i < 5; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-${i}` }, + output + ) + } + await hook["tool.execute.after"]?.( + { tool: "task", sessionID, callID: "call-task" }, + output + ) + for (let i = 0; i < 9; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-after-${i}` }, + output + ) + } + + //#then + expect(output.output).not.toContain("task tools haven't been used") + }) + + test("resets counter after injecting reminder", async () => { + //#given + const sessionID = "test-session" + const output1 = { output: "Result 1" } + const output2 = { output: "Result 2" } + + //#when + for (let i = 0; i < 10; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-1-${i}` }, + output1 + ) + } + for (let i = 0; i < 9; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-2-${i}` }, + output2 + ) + } + + //#then + expect(output1.output).toContain("task tools haven't been used") + expect(output2.output).not.toContain("task tools haven't been used") + }) + + test("tracks separate counters per session", async () => { + //#given + const session1 = "session-1" + const session2 = "session-2" + const output1 = { output: "Result 1" } + const output2 = { output: "Result 2" } + + //#when + for (let i = 0; i < 10; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID: session1, callID: `call-${i}` }, + output1 + ) + } + for (let i = 0; i < 5; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID: session2, callID: `call-${i}` }, + output2 + ) + } + + //#then + expect(output1.output).toContain("task tools haven't been used") + expect(output2.output).not.toContain("task tools haven't been used") + }) + + test("cleans up counters on session.deleted", async () => { + //#given + const sessionID = "test-session" + const output = { output: "Result" } + + //#when + for (let i = 0; i < 10; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-${i}` }, + output + ) + } + await hook.event?.({ event: { type: "session.deleted", properties: { info: { id: sessionID } } } }) + const outputAfterDelete = { output: "Result" } + for (let i = 0; i < 9; i++) { + await hook["tool.execute.after"]?.( + { tool: "bash", sessionID, callID: `call-after-${i}` }, + outputAfterDelete + ) + } + + //#then + expect(outputAfterDelete.output).not.toContain("task tools haven't been used") + }) +}) diff --git a/src/hooks/task-reminder/index.ts b/src/hooks/task-reminder/index.ts new file mode 100644 index 00000000..194a4261 --- /dev/null +++ b/src/hooks/task-reminder/index.ts @@ -0,0 +1 @@ +export { createTaskReminderHook } from "./hook";