From f36231a8a6b5c5213623dc87fdb1f1dd213d22f3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 10 Apr 2026 11:12:56 +0900 Subject: [PATCH] fix(keyword-detector): trigger ulw shorthand anywhere --- .../keyword-detector/hook-ralph-loop.test.ts | 9 ++++--- src/hooks/keyword-detector/hook.ts | 25 +++++++++++++------ .../ultrawork-edge-trigger.test.ts | 11 ++++---- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/hooks/keyword-detector/hook-ralph-loop.test.ts b/src/hooks/keyword-detector/hook-ralph-loop.test.ts index ce0a6f066..93b066b18 100644 --- a/src/hooks/keyword-detector/hook-ralph-loop.test.ts +++ b/src/hooks/keyword-detector/hook-ralph-loop.test.ts @@ -86,7 +86,7 @@ describe("keyword-detector ralph-loop activation", () => { expect(startLoopCalls[0].options.ultrawork).toBe(true) }) - test("#given ulw mentioned mid-sentence #when chat.message fires #then ralph-loop startLoop is not invoked", async () => { + test("#given ulw mentioned mid-sentence #when chat.message fires #then ralph-loop startLoop is invoked", async () => { // given setMainSession("main-session") const startLoopCalls: StartLoopCall[] = [] @@ -94,15 +94,16 @@ describe("keyword-detector ralph-loop activation", () => { const hook = createKeywordDetectorHook(createMockPluginInput(), undefined, ralphLoop) const output = { message: {} as Record, - parts: [{ type: "text", text: "I think ulw is cool" }], + parts: [{ type: "text", text: "please ulw fix the flaky keyword tests" }], } // when await hook["chat.message"]({ sessionID: "main-session", agent: "sisyphus" }, output) // then - expect(startLoopCalls).toHaveLength(0) - expect(output.parts[0]?.text).toBe("I think ulw is cool") + expect(startLoopCalls).toHaveLength(1) + expect(startLoopCalls[0].prompt).toBe("please fix the flaky keyword tests") + expect(output.parts[0]?.text).toContain("please ulw fix the flaky keyword tests") }) test("#given question about ultrawork #when chat.message fires #then ralph-loop startLoop is not invoked", async () => { diff --git a/src/hooks/keyword-detector/hook.ts b/src/hooks/keyword-detector/hook.ts index 52a5a292b..5623bb964 100644 --- a/src/hooks/keyword-detector/hook.ts +++ b/src/hooks/keyword-detector/hook.ts @@ -15,22 +15,33 @@ import type { ContextCollector } from "../../features/context-injector" import type { RalphLoopHook } from "../ralph-loop" import { parseRalphLoopArguments } from "../ralph-loop/command-arguments" -const ULTRAWORK_KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/i +const ULTRAWORK_LONGHAND_PATTERN = /\bultrawork\b/i +const ULW_SHORTHAND_PATTERN = /\bulw\b/i const LEADING_ULTRAWORK_PATTERN = /^\s*(ultrawork|ulw)\b/i const GREETING_PREFIX_ULTRAWORK_PATTERN = /^\s*(?:hi|hello|hey|hiya|greetings)(?:\s+there)?(?:[!,.:;-]+\s*|\s+)(ultrawork|ulw)\b/i +function normalizeUltraworkTask(taskText: string): string { + return taskText.replace(/\s+/g, " ").trim() +} + function extractUltraworkTask(cleanText: string): string { const greetingPrefixedMatch = cleanText.match(GREETING_PREFIX_ULTRAWORK_PATTERN) if (greetingPrefixedMatch) { - return cleanText.slice(greetingPrefixedMatch[0].length).trim() + return normalizeUltraworkTask(cleanText.slice(greetingPrefixedMatch[0].length)) } - return cleanText.replace(ULTRAWORK_KEYWORD_PATTERN, "").trim() + if (ULW_SHORTHAND_PATTERN.test(cleanText)) { + return normalizeUltraworkTask(cleanText.replace(ULW_SHORTHAND_PATTERN, " ")) + } + + return normalizeUltraworkTask(cleanText.replace(ULTRAWORK_LONGHAND_PATTERN, " ")) } -function hasEdgeUltraworkKeyword(cleanText: string): boolean { - return LEADING_ULTRAWORK_PATTERN.test(cleanText) || GREETING_PREFIX_ULTRAWORK_PATTERN.test(cleanText) +function shouldAllowUltraworkKeyword(cleanText: string): boolean { + return ULW_SHORTHAND_PATTERN.test(cleanText) + || LEADING_ULTRAWORK_PATTERN.test(cleanText) + || GREETING_PREFIX_ULTRAWORK_PATTERN.test(cleanText) } export function createKeywordDetectorHook( @@ -88,11 +99,11 @@ export function createKeywordDetectorHook( } } - if (!hasEdgeUltraworkKeyword(cleanText)) { + if (!shouldAllowUltraworkKeyword(cleanText)) { const preFilterCount = detectedKeywords.length detectedKeywords = detectedKeywords.filter((k) => k.type !== "ultrawork") if (preFilterCount > detectedKeywords.length) { - log(`[keyword-detector] Filtered non-edge ultrawork keyword`, { + log(`[keyword-detector] Filtered disallowed ultrawork keyword`, { sessionID: input.sessionID, }) } diff --git a/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts b/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts index 0fc7e8f30..9287fc225 100644 --- a/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts +++ b/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts @@ -108,7 +108,7 @@ describe("keyword-detector ultrawork edge trigger", () => { expect(output.parts[0]?.text).toContain("hey ulw fix the flaky keyword tests") }) - test("#given ulw mentioned in the middle of a sentence #when chat.message fires #then ultrawork stays disabled", async () => { + test("#given ulw mentioned in the middle of a sentence #when chat.message fires #then ultrawork still activates", async () => { // given const toastCalls: string[] = [] const startLoopCalls: StartLoopCall[] = [] @@ -119,16 +119,17 @@ describe("keyword-detector ultrawork edge trigger", () => { ) const output = { message: {} as Record, - parts: [{ type: "text", text: "I think ulw is cool" }], + parts: [{ type: "text", text: "please ulw fix the flaky keyword tests" }], } // when await hook["chat.message"]({ sessionID: "main-session", agent: "sisyphus" }, output) // then - expect(toastCalls).not.toContain("Ultrawork Mode Activated") - expect(startLoopCalls).toHaveLength(0) - expect(output.parts[0]?.text).toBe("I think ulw is cool") + expect(toastCalls).toContain("Ultrawork Mode Activated") + expect(startLoopCalls).toHaveLength(1) + expect(startLoopCalls[0]?.prompt).toBe("please fix the flaky keyword tests") + expect(output.parts[0]?.text).toContain("please ulw fix the flaky keyword tests") }) test("#given trailing ultrawork reference without punctuation #when chat.message fires #then ultrawork stays disabled", async () => {