From 5c4f4fc6558a48326656f24d82c6b61781da1cdc Mon Sep 17 00:00:00 2001 From: popododo0720 Date: Wed, 14 Jan 2026 21:36:32 +0900 Subject: [PATCH] fix: ulw keyword word boundary and skill_mcp parseArguments object handling - Add word boundary to ulw/ultrawork regex to prevent false matches on substrings like 'StatefulWidget' (fixes #779) - Handle object type in parseArguments to prevent [object Object] JSON parse error (fixes #747) - Add test cases for word boundary behavior --- src/hooks/keyword-detector/constants.ts | 2 +- src/hooks/keyword-detector/index.test.ts | 95 ++++++++++++++++++++++++ src/tools/skill-mcp/tools.ts | 8 +- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/hooks/keyword-detector/constants.ts b/src/hooks/keyword-detector/constants.ts index 1043caa95..2430ddcdb 100644 --- a/src/hooks/keyword-detector/constants.ts +++ b/src/hooks/keyword-detector/constants.ts @@ -192,7 +192,7 @@ THE USER ASKED FOR X. DELIVER EXACTLY X. NOT A SUBSET. NOT A DEMO. NOT A STARTIN export const KEYWORD_DETECTORS: Array<{ pattern: RegExp; message: string | ((agentName?: string) => string) }> = [ { - pattern: /(ultrawork|ulw)/i, + pattern: /\b(ultrawork|ulw)\b/i, message: getUltraworkMessage, }, // SEARCH: EN/KO/JP/CN/VN diff --git a/src/hooks/keyword-detector/index.test.ts b/src/hooks/keyword-detector/index.test.ts index 83dc08b27..15bb5046f 100644 --- a/src/hooks/keyword-detector/index.test.ts +++ b/src/hooks/keyword-detector/index.test.ts @@ -123,3 +123,98 @@ describe("keyword-detector session filtering", () => { expect(toastCalls).toContain("Ultrawork Mode Activated") }) }) + +describe("keyword-detector word boundary", () => { + let logCalls: Array<{ msg: string; data?: unknown }> + + beforeEach(() => { + setMainSession(undefined) + logCalls = [] + spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => { + logCalls.push({ msg, data }) + }) + }) + + afterEach(() => { + setMainSession(undefined) + }) + + function createMockPluginInput(options: { toastCalls?: string[] } = {}) { + const toastCalls = options.toastCalls ?? [] + return { + client: { + tui: { + showToast: async (opts: any) => { + toastCalls.push(opts.body.title) + }, + }, + }, + } as any + } + + test("should NOT trigger ultrawork on partial matches like 'StatefulWidget' containing 'ulw'", async () => { + // #given - text contains 'ulw' as part of another word (StatefulWidget) + setMainSession(undefined) + + const toastCalls: string[] = [] + const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls })) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "refactor the StatefulWidget component" }], + } + + // #when - message with partial 'ulw' match is processed + await hook["chat.message"]( + { sessionID: "any-session" }, + output + ) + + // #then - ultrawork should NOT be triggered + expect(output.message.variant).toBeUndefined() + expect(toastCalls).not.toContain("Ultrawork Mode Activated") + }) + + test("should trigger ultrawork on standalone 'ulw' keyword", async () => { + // #given - text contains standalone 'ulw' + setMainSession(undefined) + + const toastCalls: string[] = [] + const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls })) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "ulw do this task" }], + } + + // #when - message with standalone 'ulw' is processed + await hook["chat.message"]( + { sessionID: "any-session" }, + output + ) + + // #then - ultrawork should be triggered + expect(output.message.variant).toBe("max") + expect(toastCalls).toContain("Ultrawork Mode Activated") + }) + + test("should NOT trigger ultrawork on file references containing 'ulw' substring", async () => { + // #given - file reference contains 'ulw' as substring + setMainSession(undefined) + + const toastCalls: string[] = [] + const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls })) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "@StatefulWidget.tsx please review this file" }], + } + + // #when - message referencing file with 'ulw' substring is processed + await hook["chat.message"]( + { sessionID: "any-session" }, + output + ) + + // #then - ultrawork should NOT be triggered + expect(output.message.variant).toBeUndefined() + expect(toastCalls).not.toContain("Ultrawork Mode Activated") + }) +}) diff --git a/src/tools/skill-mcp/tools.ts b/src/tools/skill-mcp/tools.ts index b678c9960..ee71db131 100644 --- a/src/tools/skill-mcp/tools.ts +++ b/src/tools/skill-mcp/tools.ts @@ -69,8 +69,14 @@ function formatAvailableMcps(skills: LoadedSkill[]): string { return mcps.length > 0 ? mcps.join("\n") : " (none found)" } -function parseArguments(argsJson: string | undefined): Record { +function parseArguments(argsJson: string | Record | undefined): Record { if (!argsJson) return {} + + // Handle case when argsJson is already an object (from tool calling pipeline) + if (typeof argsJson === "object" && argsJson !== null) { + return argsJson + } + try { const parsed = JSON.parse(argsJson) if (typeof parsed !== "object" || parsed === null) {