diff --git a/src/hooks/ralph-loop/completion-promise-detector.ts b/src/hooks/ralph-loop/completion-promise-detector.ts
index 35b781a07..ccfba8888 100644
--- a/src/hooks/ralph-loop/completion-promise-detector.ts
+++ b/src/hooks/ralph-loop/completion-promise-detector.ts
@@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { existsSync, readFileSync } from "node:fs"
import { log } from "../../shared/logger"
import { HOOK_NAME } from "./constants"
+import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants"
import { withTimeout } from "./with-timeout"
interface OpenCodeSessionMessage {
@@ -16,6 +17,8 @@ interface TranscriptEntry {
tool_output?: { output?: string } | string
}
+const ORACLE_AGENT_PATTERN = /Agent:\s*oracle/i
+
function extractTranscriptEntryText(entry: TranscriptEntry): string {
if (typeof entry.content === "string") return entry.content
if (typeof entry.tool_output === "string") return entry.tool_output
@@ -31,6 +34,22 @@ function buildPromisePattern(promise: string): RegExp {
return new RegExp(`\\s*${escapeRegex(promise)}\\s*`, "is")
}
+function shouldInspectTranscriptEntry(
+ entry: TranscriptEntry,
+ promise: string,
+ entryText: string,
+): boolean {
+ if (entry.type === "assistant" || entry.type === "text") {
+ return true
+ }
+
+ if (entry.type !== "tool_result") {
+ return false
+ }
+
+ return promise === ULTRAWORK_VERIFICATION_PROMISE && ORACLE_AGENT_PATTERN.test(entryText)
+}
+
export function detectCompletionInTranscript(
transcriptPath: string | undefined,
promise: string,
@@ -49,10 +68,10 @@ export function detectCompletionInTranscript(
try {
const entry = JSON.parse(line) as TranscriptEntry
if (entry.type === "user") continue
- if (entry.type !== "assistant" && entry.type !== "text") continue
if (startedAt && entry.timestamp && entry.timestamp < startedAt) continue
const entryText = extractTranscriptEntryText(entry)
if (!entryText) continue
+ if (!shouldInspectTranscriptEntry(entry, promise, entryText)) continue
if (pattern.test(entryText)) return true
} catch {
continue
diff --git a/src/hooks/ralph-loop/completion-promise-transcript-detector.test.ts b/src/hooks/ralph-loop/completion-promise-transcript-detector.test.ts
index 0d2509b57..975ca29b6 100644
--- a/src/hooks/ralph-loop/completion-promise-transcript-detector.test.ts
+++ b/src/hooks/ralph-loop/completion-promise-transcript-detector.test.ts
@@ -78,5 +78,22 @@ describe("detectCompletionInTranscript", () => {
// #then
expect(detected).toBe(false)
})
+
+ test("#when oracle tool output contains VERIFIED promise #then should detect verification completion", () => {
+ // #given
+ const transcriptPath = createTranscriptFile([
+ JSON.stringify({
+ type: "tool_result",
+ timestamp: "2026-03-28T10:01:00.000Z",
+ tool_output: "Task completed.\n\nAgent: oracle\n\nVERIFIED\n\n\nsession_id: ses_oracle_123\n",
+ }),
+ ])
+
+ // #when
+ const detected = detectCompletionInTranscript(transcriptPath, "VERIFIED")
+
+ // #then
+ expect(detected).toBe(true)
+ })
})
})
diff --git a/src/hooks/ralph-loop/ulw-loop-verification.test.ts b/src/hooks/ralph-loop/ulw-loop-verification.test.ts
index 3fbeabdb2..1f2edfa85 100644
--- a/src/hooks/ralph-loop/ulw-loop-verification.test.ts
+++ b/src/hooks/ralph-loop/ulw-loop-verification.test.ts
@@ -135,6 +135,36 @@ describe("ulw-loop verification", () => {
expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(true)
})
+ test("#given ulw loop is awaiting verification #when oracle transcript stores VERIFIED inside tool_result #then loop completes", async () => {
+ const hook = createRalphLoopHook(createMockPluginInput(), {
+ getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath,
+ })
+ hook.startLoop("session-123", "Build API", { ultrawork: true })
+ writeFileSync(
+ parentTranscriptPath,
+ `${JSON.stringify({ type: "assistant", timestamp: new Date().toISOString(), content: "done DONE" })}\n`,
+ )
+
+ await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
+ writeState(testDir, {
+ ...hook.getState()!,
+ verification_session_id: "ses-oracle",
+ })
+ writeFileSync(
+ oracleTranscriptPath,
+ `${JSON.stringify({
+ type: "tool_result",
+ timestamp: new Date().toISOString(),
+ tool_output: `Task completed.\n\nAgent: oracle\n\n${ULTRAWORK_VERIFICATION_PROMISE}\n\n\nsession_id: ses-oracle\n`,
+ })}\n`,
+ )
+
+ await hook.event({ event: { type: "session.idle", properties: { sessionID: "ses-oracle" } } })
+
+ expect(hook.getState()).toBeNull()
+ expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(true)
+ })
+
test("#given ulw loop is awaiting verification without oracle session #when parent idles again #then loop continues until oracle verifies", async () => {
const hook = createRalphLoopHook(createMockPluginInput(), {
getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath,