diff --git a/src/hooks/.sisyphus/ralph-loop.local.md b/src/hooks/.sisyphus/ralph-loop.local.md new file mode 100644 index 000000000..fd670d82b --- /dev/null +++ b/src/hooks/.sisyphus/ralph-loop.local.md @@ -0,0 +1,12 @@ +--- +active: true +iteration: 2 +max_iterations: 100 +completion_promise: "DONE" +initial_completion_promise: "DONE" +started_at: "2026-03-14T04:20:58.486Z" +session_id: "new-session-1" +strategy: "reset" +message_count_at_start: 0 +--- +Build feature diff --git a/src/hooks/keyword-detector/hook.ts b/src/hooks/keyword-detector/hook.ts index ea6348419..2a1462970 100644 --- a/src/hooks/keyword-detector/hook.ts +++ b/src/hooks/keyword-detector/hook.ts @@ -17,13 +17,18 @@ import { parseRalphLoopArguments } from "../ralph-loop/command-arguments" const ULTRAWORK_KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/i const LEADING_ULTRAWORK_PATTERN = /^\s*(ultrawork|ulw)\b/i +const TRAILING_GREETING_ULTRAWORK_PATTERN = /^\s*(?:hi|hello|hey|hiya|greetings)(?:\s+there)?\s+(ultrawork|ulw)\s*$/i function extractUltraworkTask(cleanText: string): string { + if (TRAILING_GREETING_ULTRAWORK_PATTERN.test(cleanText)) { + return "" + } + return cleanText.replace(ULTRAWORK_KEYWORD_PATTERN, "").trim() } -function hasLeadingUltraworkKeyword(cleanText: string): boolean { - return LEADING_ULTRAWORK_PATTERN.test(cleanText) +function hasEdgeUltraworkKeyword(cleanText: string): boolean { + return LEADING_ULTRAWORK_PATTERN.test(cleanText) || TRAILING_GREETING_ULTRAWORK_PATTERN.test(cleanText) } export function createKeywordDetectorHook( @@ -81,11 +86,11 @@ export function createKeywordDetectorHook( } } - if (!hasLeadingUltraworkKeyword(cleanText)) { + if (!hasEdgeUltraworkKeyword(cleanText)) { const preFilterCount = detectedKeywords.length detectedKeywords = detectedKeywords.filter((k) => k.type !== "ultrawork") if (preFilterCount > detectedKeywords.length) { - log(`[keyword-detector] Filtered non-leading ultrawork keyword`, { + log(`[keyword-detector] Filtered non-edge 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 new file mode 100644 index 000000000..2ec21dec3 --- /dev/null +++ b/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts @@ -0,0 +1,122 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test" + +import { createKeywordDetectorHook } from "./index" +import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" + +type StartLoopCall = { + sessionID: string + prompt: string + options: Record +} + +function createMockPluginInput(toastCalls: string[] = []) { + return { + client: { + tui: { + showToast: async (opts: { body: { title: string } }) => { + toastCalls.push(opts.body.title) + }, + }, + }, + } as any +} + +function createMockRalphLoop(startLoopCalls: StartLoopCall[]) { + return { + startLoop: (sessionID: string, prompt: string, options?: Record): boolean => { + startLoopCalls.push({ sessionID, prompt, options: options ?? {} }) + return true + }, + } +} + +describe("keyword-detector ultrawork edge trigger", () => { + beforeEach(() => { + _resetForTesting() + setMainSession("main-session") + }) + + afterEach(() => { + _resetForTesting() + }) + + test("#given greeting text before ulw and surrounding whitespace #when chat.message fires #then ultrawork still activates", async () => { + // given + const toastCalls: string[] = [] + const startLoopCalls: StartLoopCall[] = [] + const hook = createKeywordDetectorHook( + createMockPluginInput(toastCalls), + undefined, + createMockRalphLoop(startLoopCalls), + ) + const output = { + message: {} as Record, + parts: [{ type: "text", text: " hi there ulw " }], + } + + // when + await hook["chat.message"]({ sessionID: "main-session", agent: "sisyphus" }, output) + + // then + expect(toastCalls).toContain("Ultrawork Mode Activated") + expect(startLoopCalls).toHaveLength(1) + expect(startLoopCalls[0]).toEqual({ + sessionID: "main-session", + prompt: "Complete the task as instructed", + options: { + ultrawork: true, + maxIterations: undefined, + completionPromise: undefined, + strategy: undefined, + }, + }) + expect(output.parts[0]?.text).toContain("ULTRAWORK MODE ENABLED!") + expect(output.parts[0]?.text).toContain(" hi there ulw ") + }) + + test("#given ulw mentioned in the middle of a sentence #when chat.message fires #then ultrawork stays disabled", async () => { + // given + const toastCalls: string[] = [] + const startLoopCalls: StartLoopCall[] = [] + const hook = createKeywordDetectorHook( + createMockPluginInput(toastCalls), + undefined, + createMockRalphLoop(startLoopCalls), + ) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "I think ulw is cool" }], + } + + // 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") + }) + + test("#given trailing ultrawork reference without punctuation #when chat.message fires #then ultrawork stays disabled", async () => { + // given + const toastCalls: string[] = [] + const startLoopCalls: StartLoopCall[] = [] + const hook = createKeywordDetectorHook( + createMockPluginInput(toastCalls), + undefined, + createMockRalphLoop(startLoopCalls), + ) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "what is ultrawork" }], + } + + // 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("what is ultrawork") + }) +})