From 774d0bd84dc7c183a5b82942d8f0926fc07adcfd Mon Sep 17 00:00:00 2001 From: MoerAI Date: Wed, 25 Mar 2026 16:49:53 +0900 Subject: [PATCH] fix(ralph-loop): restrict semantic completion to DONE promise and assistant entries - Gate semantic detection on promise === 'DONE' in both transcript and session message paths to prevent false positives on VERIFIED promises - Restrict transcript semantic fallback to assistant/text entries only, skipping tool_use/tool_result to avoid matching file content - Add regression test for VERIFIED promise not triggering semantic detection --- .../completion-promise-detector.test.ts | 22 +++++++++++++++++++ .../ralph-loop/completion-promise-detector.ts | 9 ++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/hooks/ralph-loop/completion-promise-detector.test.ts b/src/hooks/ralph-loop/completion-promise-detector.test.ts index 704517732..bbb2206fb 100644 --- a/src/hooks/ralph-loop/completion-promise-detector.test.ts +++ b/src/hooks/ralph-loop/completion-promise-detector.test.ts @@ -273,6 +273,28 @@ describe("detectCompletionInSessionMessages", () => { // #then expect(detected).toBe(true) }) + + test("#when promise is VERIFIED #then semantic completion should NOT trigger", async () => { + // #given + const messages: SessionMessage[] = [ + { + info: { role: "assistant" }, + parts: [{ type: "text", text: "The task is complete. All work has been finished." }], + }, + ] + const ctx = createPluginInput(messages) + + // #when + const detected = await detectCompletionInSessionMessages(ctx, { + sessionID: "session-123", + promise: "VERIFIED", + apiTimeoutMs: 1000, + directory: "/tmp", + }) + + // #then + expect(detected).toBe(false) + }) }) }) diff --git a/src/hooks/ralph-loop/completion-promise-detector.ts b/src/hooks/ralph-loop/completion-promise-detector.ts index 39322e536..cd02ce7f7 100644 --- a/src/hooks/ralph-loop/completion-promise-detector.ts +++ b/src/hooks/ralph-loop/completion-promise-detector.ts @@ -49,8 +49,9 @@ export function detectCompletionInTranscript( if (entry.type === "user") continue if (startedAt && entry.timestamp && entry.timestamp < startedAt) continue if (pattern.test(line)) return true - // Fallback: check for semantic completion - if (detectSemanticCompletion(line)) { + // Fallback: semantic completion only for DONE promise and assistant entries + const isAssistantEntry = entry.type === "assistant" || entry.type === "text" + if (promise === "DONE" && isAssistantEntry && detectSemanticCompletion(line)) { log("[ralph-loop] WARNING: Semantic completion detected in transcript (agent used natural language instead of DONE)") return true } @@ -118,8 +119,8 @@ export async function detectCompletionInSessionMessages( return true } - // Fallback: check for semantic completion - if (detectSemanticCompletion(responseText)) { + // Fallback: semantic completion only for DONE promise + if (options.promise === "DONE" && detectSemanticCompletion(responseText)) { log("[ralph-loop] WARNING: Semantic completion detected (agent used natural language instead of DONE)", { sessionID: options.sessionID, })