From ce8957e1e168b491db5e12d9fe84065e58122af4 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 18 Mar 2026 17:45:59 +0900 Subject: [PATCH] fix(ralph-loop): harden oracle verification flow Capture oracle verification sessions more reliably and accept parent-session VERIFIED evidence so ULW loops do not retry after successful review. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../ralph-loop/continuation-prompt-builder.ts | 3 ++ .../ralph-loop/ralph-loop-event-handler.ts | 10 ++++- .../ralph-loop/ulw-loop-verification.test.ts | 12 ++---- src/plugin/tool-execute-after.ts | 17 ++++++-- src/plugin/tool-execute-before.ts | 26 +++++++++++- .../tool-execute-before.ulw-loop.test.ts | 41 +++++++++++++++++++ 6 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/hooks/ralph-loop/continuation-prompt-builder.ts b/src/hooks/ralph-loop/continuation-prompt-builder.ts index 8d807fe39..e709caa23 100644 --- a/src/hooks/ralph-loop/continuation-prompt-builder.ts +++ b/src/hooks/ralph-loop/continuation-prompt-builder.ts @@ -25,6 +25,8 @@ You already emitted {{INITIAL_PROMISE}}. This does NOT finish REQUIRED NOW: - Call Oracle using task(subagent_type="oracle", load_skills=[], run_in_background=false, ...) - Ask Oracle to verify whether the original task is actually complete +- Include the original task in the Oracle request +- Explicitly tell Oracle to review skeptically and critically, and to look for reasons the task may still be incomplete or wrong - The system will inspect the Oracle session directly for the verification result - If Oracle does not verify, continue fixing the task and do not consider it complete @@ -40,6 +42,7 @@ REQUIRED NOW: - Oracle does not lie. Treat the verification result as ground truth - Do not claim completion early or argue with the failed verification - After fixing the remaining issues, request Oracle review again using task(subagent_type="oracle", load_skills=[], run_in_background=false, ...) +- Include the original task in the Oracle request and tell Oracle to review skeptically and critically - Only when the work is ready for review again, output: {{PROMISE}} Original task: diff --git a/src/hooks/ralph-loop/ralph-loop-event-handler.ts b/src/hooks/ralph-loop/ralph-loop-event-handler.ts index 73c6260be..0093e890a 100644 --- a/src/hooks/ralph-loop/ralph-loop-event-handler.ts +++ b/src/hooks/ralph-loop/ralph-loop-event-handler.ts @@ -87,7 +87,7 @@ export function createRalphLoopEventHandler( return } - const completionSessionID = verificationSessionID ?? (state.verification_pending ? undefined : sessionID) + const completionSessionID = verificationSessionID ?? sessionID const transcriptPath = completionSessionID ? options.getTranscriptPath(completionSessionID) : undefined const completionViaTranscript = completionSessionID ? detectCompletionInTranscript( @@ -107,7 +107,13 @@ export function createRalphLoopEventHandler( sinceMessageIndex: undefined, }) : state.verification_pending - ? false + ? await detectCompletionInSessionMessages(ctx, { + sessionID, + promise: state.completion_promise, + apiTimeoutMs: options.apiTimeoutMs, + directory: options.directory, + sinceMessageIndex: state.message_count_at_start, + }) : await detectCompletionInSessionMessages(ctx, { sessionID, promise: state.completion_promise, diff --git a/src/hooks/ralph-loop/ulw-loop-verification.test.ts b/src/hooks/ralph-loop/ulw-loop-verification.test.ts index abed0ad76..8366c56d6 100644 --- a/src/hooks/ralph-loop/ulw-loop-verification.test.ts +++ b/src/hooks/ralph-loop/ulw-loop-verification.test.ts @@ -366,7 +366,7 @@ describe("ulw-loop verification", () => { expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(false) }) - test("#given parent session emits VERIFIED #when oracle session is not tracked #then ulw loop continues instead of completing", async () => { + test("#given parent session emits VERIFIED #when oracle session is not tracked #then ulw loop completes from parent session evidence", async () => { const hook = createRalphLoopHook(createMockPluginInput(), { getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath, }) @@ -379,17 +379,13 @@ describe("ulw-loop verification", () => { await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } }) writeFileSync( parentTranscriptPath, - `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: `bad parent leak ${ULTRAWORK_VERIFICATION_PROMISE}` } })}\n`, + `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: `verified ${ULTRAWORK_VERIFICATION_PROMISE}` } })}\n`, ) await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } }) - expect(hook.getState()).not.toBeNull() - expect(hook.getState()?.iteration).toBe(2) - expect(hook.getState()?.completion_promise).toBe("DONE") - expect(hook.getState()?.verification_pending).toBeUndefined() - expect(promptCalls).toHaveLength(2) - expect(promptCalls[1]?.text).toContain("Verification failed") + expect(hook.getState()).toBeNull() + expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(true) }) test("#given oracle verification fails #when loop restarts #then old oracle session is aborted", async () => { diff --git a/src/plugin/tool-execute-after.ts b/src/plugin/tool-execute-after.ts index 6d62d6130..0cc986b7d 100644 --- a/src/plugin/tool-execute-after.ts +++ b/src/plugin/tool-execute-after.ts @@ -6,6 +6,17 @@ import { readState, writeState } from "../hooks/ralph-loop/storage" const VERIFICATION_ATTEMPT_PATTERN = /(.*?)<\/ulw_verification_attempt_id>/i +function getMetadataString(metadata: Record | undefined, keys: string[]): string | undefined { + for (const key of keys) { + const value = metadata?.[key] + if (typeof value === "string") { + return value + } + } + + return undefined +} + function getPluginDirectory(ctx: PluginContext): string | null { if (typeof ctx === "object" && ctx !== null && "directory" in ctx && typeof ctx.directory === "string") { return ctx.directory @@ -43,9 +54,9 @@ export function createToolExecuteAfterHandler(args: { if (input.tool === "task") { const directory = getPluginDirectory(ctx) - const sessionId = typeof output.metadata?.sessionId === "string" ? output.metadata.sessionId : undefined - const agent = typeof output.metadata?.agent === "string" ? output.metadata.agent : undefined - const prompt = typeof output.metadata?.prompt === "string" ? output.metadata.prompt : undefined + const sessionId = getMetadataString(output.metadata, ["sessionId", "sessionID", "session_id"]) + const agent = getMetadataString(output.metadata, ["agent"]) + const prompt = getMetadataString(output.metadata, ["prompt"]) const verificationAttemptId = prompt?.match(VERIFICATION_ATTEMPT_PATTERN)?.[1]?.trim() const loopState = directory ? readState(directory) : null const isVerificationContext = diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index 866512995..5d4f2c86e 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -20,6 +20,26 @@ export function createToolExecuteBeforeHandler(args: { ) => Promise { const { ctx, hooks } = args + function buildUltraworkOracleVerificationPrompt(prompt: string, originalTask: string, verificationAttemptId: string): string { + const verificationPrompt = [ + "You are verifying the active ULTRAWORK loop result for this session.", + "", + "Original task:", + originalTask, + "", + "Review the work skeptically and critically.", + "Assume it may be incomplete, misleading, or subtly broken until the evidence proves otherwise.", + "Look for missing scope, weak verification, process violations, hidden regressions, and any reason the task should NOT be considered complete.", + "", + `If the work is fully complete, end your response with ${ULTRAWORK_VERIFICATION_PROMISE}.`, + "If the work is not complete, explain the blocking issues clearly and DO NOT emit that promise.", + "", + `${verificationAttemptId}`, + ].join("\n") + + return `${prompt ? `${prompt}\n\n` : ""}${verificationPrompt}` + } + return async (input, output): Promise => { await hooks.writeExistingFileGuard?.["tool.execute.before"]?.(input, output) await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output) @@ -91,7 +111,11 @@ export function createToolExecuteBeforeHandler(args: { verification_session_id: undefined, }) argsObject.run_in_background = false - argsObject.prompt = `${prompt ? `${prompt}\n\n` : ""}You are verifying the active ULTRAWORK loop result for this session. Review whether the original task is truly complete: ${loopState.prompt}\n\nIf the work is fully complete, end your response with ${ULTRAWORK_VERIFICATION_PROMISE}. If the work is not complete, explain the blocking issues clearly and DO NOT emit that promise.\n\n${verificationAttemptId}` + argsObject.prompt = buildUltraworkOracleVerificationPrompt( + prompt, + loopState.prompt, + verificationAttemptId, + ) } } diff --git a/src/plugin/tool-execute-before.ulw-loop.test.ts b/src/plugin/tool-execute-before.ulw-loop.test.ts index 74cd5cf8f..50e29ca05 100644 --- a/src/plugin/tool-execute-before.ulw-loop.test.ts +++ b/src/plugin/tool-execute-before.ulw-loop.test.ts @@ -65,7 +65,9 @@ describe("tool.execute.before ultrawork oracle verification", () => { expect(readState(directory)?.verification_attempt_id).toBeTruthy() expect(output.args.run_in_background).toBe(false) + expect(output.args.prompt).toContain("Original task:") expect(output.args.prompt).toContain("Ship feature") + expect(output.args.prompt).toContain("Review the work skeptically and critically") expect(output.args.prompt).toContain(`${ULTRAWORK_VERIFICATION_PROMISE}`) clearState(directory) @@ -171,6 +173,45 @@ describe("tool.execute.before ultrawork oracle verification", () => { rmSync(directory, { recursive: true, force: true }) }) + test("#given ulw loop is awaiting verification #when oracle metadata uses sessionID #then oracle session id is stored", async () => { + const directory = join(tmpdir(), `tool-after-ulw-sessionid-${Date.now()}`) + mkdirSync(directory, { recursive: true }) + writeState(directory, { + active: true, + iteration: 3, + completion_promise: ULTRAWORK_VERIFICATION_PROMISE, + initial_completion_promise: "DONE", + started_at: new Date().toISOString(), + prompt: "Ship feature", + session_id: "ses-main", + ultrawork: true, + verification_pending: true, + }) + + const handler = createToolExecuteAfterHandler({ + ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + hooks: {} as Parameters[0]["hooks"], + }) + + await handler( + { tool: "task", sessionID: "ses-main", callID: "call-1" }, + { + title: "oracle task", + output: "done", + metadata: { + agent: "oracle", + sessionID: "ses-oracle-alt", + sync: true, + }, + }, + ) + + expect(readState(directory)?.verification_session_id).toBe("ses-oracle-alt") + + clearState(directory) + rmSync(directory, { recursive: true, force: true }) + }) + test("#given newer oracle attempt exists #when older oracle task finishes #then old session does not overwrite active verification", async () => { const directory = join(tmpdir(), `tool-race-ulw-${Date.now()}`) mkdirSync(directory, { recursive: true })