diff --git a/src/hooks/keyword-detector/constants.ts b/src/hooks/keyword-detector/constants.ts index 6c9bec4a9..254bb4fa0 100644 --- a/src/hooks/keyword-detector/constants.ts +++ b/src/hooks/keyword-detector/constants.ts @@ -2,7 +2,7 @@ export const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g export const INLINE_CODE_PATTERN = /`[^`]+`/g // Re-export from submodules -export { isPlannerAgent, getUltraworkMessage } from "./ultrawork" +export { isPlannerAgent, isNonOmoAgent, getUltraworkMessage } from "./ultrawork" export { SEARCH_PATTERN, SEARCH_MESSAGE } from "./search" export { ANALYZE_PATTERN, ANALYZE_MESSAGE } from "./analyze" diff --git a/src/hooks/keyword-detector/hook.ts b/src/hooks/keyword-detector/hook.ts index 8d7fcb15d..86e4eda73 100644 --- a/src/hooks/keyword-detector/hook.ts +++ b/src/hooks/keyword-detector/hook.ts @@ -1,6 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" import { detectKeywordsWithType, extractPromptText } from "./detector" -import { isPlannerAgent } from "./constants" +import { isPlannerAgent, isNonOmoAgent } from "./constants" import { log } from "../../shared" import { isSystemDirective, @@ -45,6 +45,12 @@ export function createKeywordDetectorHook(ctx: PluginInput, _collector?: Context const currentAgent = getSessionAgent(input.sessionID) ?? input.agent + // Skip all keyword injection for non-OMO agents (e.g., OpenCode-Builder, Plan) + if (isNonOmoAgent(currentAgent)) { + log(`[keyword-detector] Skipping keyword injection for non-OMO agent`, { sessionID: input.sessionID, agent: currentAgent }) + return + } + // Remove system-reminder content to prevent automated system messages from triggering mode keywords const cleanText = removeSystemReminders(promptText) const modelID = input.model?.modelID diff --git a/src/hooks/keyword-detector/index.test.ts b/src/hooks/keyword-detector/index.test.ts index bf162ddeb..083c6b4f3 100644 --- a/src/hooks/keyword-detector/index.test.ts +++ b/src/hooks/keyword-detector/index.test.ts @@ -746,3 +746,109 @@ describe("keyword-detector agent-specific ultrawork messages", () => { expect(textPart!.text).not.toContain("YOU ARE A PLANNER, NOT AN IMPLEMENTER") }) }) + +describe("keyword-detector non-OMO agent skipping", () => { + let logCalls: Array<{ msg: string; data?: unknown }> + let logSpy: ReturnType + + beforeEach(() => { + _resetForTesting() + logCalls = [] + logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => { + logCalls.push({ msg, data }) + }) + }) + + afterEach(() => { + logSpy?.mockRestore() + _resetForTesting() + }) + + function createMockPluginInput() { + return { + client: { + tui: { + showToast: async () => {}, + }, + }, + } as any + } + + test("should skip all keyword injection for OpenCode-Builder agent", async () => { + // given - keyword-detector hook with Builder agent + const collector = new ContextCollector() + const hook = createKeywordDetectorHook(createMockPluginInput(), collector) + const sessionID = "builder-session" + const output = { + message: {} as Record, + parts: [{ type: "text", text: "ultrawork search and analyze this code" }], + } + + // when - keyword detection runs with OpenCode-Builder agent + await hook["chat.message"]({ sessionID, agent: "OpenCode-Builder" }, output) + + // then - no keywords should be injected + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toBe("ultrawork search and analyze this code") + }) + + test("should skip all keyword injection for Plan agent", async () => { + // given - keyword-detector hook with Plan agent + const collector = new ContextCollector() + const hook = createKeywordDetectorHook(createMockPluginInput(), collector) + const sessionID = "plan-session" + const output = { + message: {} as Record, + parts: [{ type: "text", text: "search mode analyze mode ultrawork" }], + } + + // when - keyword detection runs with Plan agent + await hook["chat.message"]({ sessionID, agent: "Plan" }, output) + + // then - no keywords should be injected for non-OMO Plan agent + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toBe("search mode analyze mode ultrawork") + }) + + test("should still inject keywords for OMO agents like Sisyphus", async () => { + // given - keyword-detector hook with Sisyphus agent + const collector = new ContextCollector() + const hook = createKeywordDetectorHook(createMockPluginInput(), collector) + const sessionID = "sisyphus-session-omo" + const output = { + message: {} as Record, + parts: [{ type: "text", text: "ultrawork implement this" }], + } + + // when - keyword detection runs with Sisyphus (OMO agent) + await hook["chat.message"]({ sessionID, agent: "sisyphus" }, output) + + // then - keywords should be injected normally + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toContain("YOU MUST LEVERAGE ALL AVAILABLE AGENTS") + expect(textPart!.text).toContain("implement this") + }) + + test("should skip keyword injection for agent names containing 'builder'", async () => { + // given - keyword-detector hook with a builder-variant agent name + const collector = new ContextCollector() + const hook = createKeywordDetectorHook(createMockPluginInput(), collector) + const sessionID = "custom-builder-session" + const output = { + message: {} as Record, + parts: [{ type: "text", text: "search this codebase" }], + } + + // when - keyword detection runs with a builder-type agent + await hook["chat.message"]({ sessionID, agent: "Custom-Builder" }, output) + + // then - search-mode should NOT be injected + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toBe("search this codebase") + expect(textPart!.text).not.toContain("[search-mode]") + }) +}) diff --git a/src/hooks/keyword-detector/ultrawork/index.ts b/src/hooks/keyword-detector/ultrawork/index.ts index f0f208fb3..3b732cafe 100644 --- a/src/hooks/keyword-detector/ultrawork/index.ts +++ b/src/hooks/keyword-detector/ultrawork/index.ts @@ -10,6 +10,7 @@ export { isPlannerAgent, + isNonOmoAgent, isGptModel, isGeminiModel, getUltraworkSource, diff --git a/src/hooks/keyword-detector/ultrawork/source-detector.ts b/src/hooks/keyword-detector/ultrawork/source-detector.ts index ad1ff4755..2ee01a6de 100644 --- a/src/hooks/keyword-detector/ultrawork/source-detector.ts +++ b/src/hooks/keyword-detector/ultrawork/source-detector.ts @@ -23,6 +23,16 @@ export function isPlannerAgent(agentName?: string): boolean { return /\bplan\b/.test(normalized) } +/** + * Checks if agent is a non-OMO agent (e.g., OpenCode's built-in Builder/Plan). + * Non-OMO agents should not receive keyword injection (search-mode, analyze-mode, etc.). + */ +export function isNonOmoAgent(agentName?: string): boolean { + if (!agentName) return false + const lowerName = agentName.toLowerCase() + return lowerName.includes("builder") || lowerName === "plan" +} + export { isGptModel, isGeminiModel } /** Ultrawork message source type */