Add /ulw-loop command for ultrawork mode loop (#867)

* feat(ralph-loop): add ultrawork field to RalphLoopState

* feat(ralph-loop): persist ultrawork field in storage

* feat(ralph-loop): accept ultrawork option in startLoop

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(ralph-loop): prepend ultrawork keyword when mode active

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(ralph-loop): custom toast for ultrawork mode

* feat(ralph-loop): add /ulw-loop command for ultrawork mode

* fix(ralph-loop): add non-null assertion for type safety

* fix(ralph-loop): mirror argument parsing in ulw-loop handler

- Parse quoted prompts and strip flags from task text
- Support --max-iterations and --completion-promise options
- Add default prompt for empty input
- Fixes behavior inconsistency with /ralph-loop

---------

Co-authored-by: justsisyphus <sisyphus-dev-ai@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim 2026-01-17 19:56:50 +09:00 committed by GitHub
parent 36b665ed89
commit d13e8411f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 176 additions and 16 deletions

View File

@ -17,17 +17,28 @@ $ARGUMENTS
</user-request>`,
argumentHint: "[--create-new] [--max-depth=N]",
},
"ralph-loop": {
description: "(builtin) Start self-referential development loop until completion",
template: `<command-instruction>
"ralph-loop": {
description: "(builtin) Start self-referential development loop until completion",
template: `<command-instruction>
${RALPH_LOOP_TEMPLATE}
</command-instruction>
<user-task>
$ARGUMENTS
</user-task>`,
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N]',
},
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N]',
},
"ulw-loop": {
description: "(builtin) Start ultrawork loop - continues until completion with ultrawork mode",
template: `<command-instruction>
${RALPH_LOOP_TEMPLATE}
</command-instruction>
<user-task>
$ARGUMENTS
</user-task>`,
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N]',
},
"cancel-ralph": {
description: "(builtin) Cancel active Ralph Loop",
template: `<command-instruction>

View File

@ -1,6 +1,6 @@
import type { CommandDefinition } from "../claude-code-command-loader"
export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "refactor" | "start-work"
export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work"
export interface BuiltinCommandConfig {
disabled_commands?: BuiltinCommandName[]

View File

@ -8,4 +8,5 @@ export const SLASH_COMMAND_PATTERN = /^\/([a-zA-Z][\w-]*)\s*(.*)/
export const EXCLUDED_COMMANDS = new Set([
"ralph-loop",
"cancel-ralph",
"ulw-loop",
])

View File

@ -92,6 +92,27 @@ describe("ralph-loop", () => {
expect(readResult?.session_id).toBe("test-session-123")
})
test("should handle ultrawork field", () => {
// #given - a state object with ultrawork enabled
const state: RalphLoopState = {
active: true,
iteration: 1,
max_iterations: 50,
completion_promise: "DONE",
started_at: "2025-12-30T01:00:00Z",
prompt: "Build a REST API",
session_id: "test-session-123",
ultrawork: true,
}
// #when - write and read state
writeState(TEST_DIR, state)
const readResult = readState(TEST_DIR)
// #then - ultrawork field should be preserved
expect(readResult?.ultrawork).toBe(true)
})
test("should return null for non-existent state", () => {
// #given - no state file exists
// #when - read state
@ -164,6 +185,30 @@ describe("ralph-loop", () => {
expect(state?.session_id).toBe("session-123")
})
test("should accept ultrawork option in startLoop", () => {
// #given - hook instance
const hook = createRalphLoopHook(createMockPluginInput())
// #when - start loop with ultrawork
hook.startLoop("session-123", "Build something", { ultrawork: true })
// #then - state should have ultrawork=true
const state = hook.getState()
expect(state?.ultrawork).toBe(true)
})
test("should handle missing ultrawork option in startLoop", () => {
// #given - hook instance
const hook = createRalphLoopHook(createMockPluginInput())
// #when - start loop without ultrawork
hook.startLoop("session-123", "Build something")
// #then - state should have ultrawork=undefined
const state = hook.getState()
expect(state?.ultrawork).toBeUndefined()
})
test("should inject continuation when loop active and no completion detected", async () => {
// #given - active loop state
const hook = createRalphLoopHook(createMockPluginInput())
@ -672,7 +717,10 @@ describe("ralph-loop", () => {
// #when - session goes idle
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
event: {
type: "session.idle",
properties: { sessionID: "session-123" },
},
})
// #then - should complete via transcript (API not called when transcript succeeds)
@ -681,6 +729,70 @@ describe("ralph-loop", () => {
// API should NOT be called since transcript found completion
expect(messagesCalls.length).toBe(0)
})
test("should show ultrawork completion toast", async () => {
// #given - hook with ultrawork mode and completion in transcript
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
const hook = createRalphLoopHook(createMockPluginInput(), {
getTranscriptPath: () => transcriptPath,
})
writeFileSync(transcriptPath, JSON.stringify({ content: "<promise>DONE</promise>" }))
hook.startLoop("test-id", "Build API", { ultrawork: true })
// #when - idle event triggered
await hook.event({ event: { type: "session.idle", properties: { sessionID: "test-id" } } })
// #then - ultrawork toast shown
const completionToast = toastCalls.find(t => t.title === "ULTRAWORK LOOP COMPLETE!")
expect(completionToast).toBeDefined()
expect(completionToast!.message).toMatch(/JUST ULW ULW!/)
})
test("should show regular completion toast when ultrawork disabled", async () => {
// #given - hook without ultrawork
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
const hook = createRalphLoopHook(createMockPluginInput(), {
getTranscriptPath: () => transcriptPath,
})
writeFileSync(transcriptPath, JSON.stringify({ content: "<promise>DONE</promise>" }))
hook.startLoop("test-id", "Build API")
// #when - idle event triggered
await hook.event({ event: { type: "session.idle", properties: { sessionID: "test-id" } } })
// #then - regular toast shown
expect(toastCalls.some(t => t.title === "Ralph Loop Complete!")).toBe(true)
})
test("should prepend ultrawork to continuation prompt when ultrawork=true", async () => {
// #given - hook with ultrawork mode enabled
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build API", { ultrawork: true })
// #when - session goes idle (continuation triggered)
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - prompt should start with "ultrawork "
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].text).toMatch(/^ultrawork /)
})
test("should NOT prepend ultrawork to continuation prompt when ultrawork=false", async () => {
// #given - hook without ultrawork mode
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build API")
// #when - session goes idle (continuation triggered)
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - prompt should NOT start with "ultrawork "
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].text).not.toMatch(/^ultrawork /)
})
})
describe("API timeout protection", () => {

View File

@ -61,7 +61,7 @@ export interface RalphLoopHook {
startLoop: (
sessionID: string,
prompt: string,
options?: { maxIterations?: number; completionPromise?: string }
options?: { maxIterations?: number; completionPromise?: string; ultrawork?: boolean }
) => boolean
cancelLoop: (sessionID: string) => boolean
getState: () => RalphLoopState | null
@ -150,7 +150,7 @@ export function createRalphLoopHook(
const startLoop = (
sessionID: string,
prompt: string,
loopOptions?: { maxIterations?: number; completionPromise?: string }
loopOptions?: { maxIterations?: number; completionPromise?: string; ultrawork?: boolean }
): boolean => {
const state: RalphLoopState = {
active: true,
@ -158,6 +158,7 @@ export function createRalphLoopHook(
max_iterations:
loopOptions?.maxIterations ?? config?.default_max_iterations ?? DEFAULT_MAX_ITERATIONS,
completion_promise: loopOptions?.completionPromise ?? DEFAULT_COMPLETION_PROMISE,
ultrawork: loopOptions?.ultrawork,
started_at: new Date().toISOString(),
prompt,
session_id: sessionID,
@ -251,11 +252,18 @@ export function createRalphLoopHook(
})
clearState(ctx.directory, stateDir)
const title = state.ultrawork
? "ULTRAWORK LOOP COMPLETE!"
: "Ralph Loop Complete!"
const message = state.ultrawork
? `JUST ULW ULW! Task completed after ${state.iteration} iteration(s)`
: `Task completed after ${state.iteration} iteration(s)`
await ctx.client.tui
.showToast({
body: {
title: "Ralph Loop Complete!",
message: `Task completed after ${state.iteration} iteration(s)`,
title,
message,
variant: "success",
duration: 5000,
},
@ -304,6 +312,10 @@ export function createRalphLoopHook(
.replace("{{PROMISE}}", newState.completion_promise)
.replace("{{PROMPT}}", newState.prompt)
const finalPrompt = newState.ultrawork
? `ultrawork ${continuationPrompt}`
: continuationPrompt
await ctx.client.tui
.showToast({
body: {
@ -346,7 +358,7 @@ export function createRalphLoopHook(
body: {
...(agent !== undefined ? { agent } : {}),
...(model !== undefined ? { model } : {}),
parts: [{ type: "text", text: continuationPrompt }],
parts: [{ type: "text", text: finalPrompt }],
},
query: { directory: ctx.directory },
})

View File

@ -48,6 +48,7 @@ export function readState(directory: string, customPath?: string): RalphLoopStat
started_at: stripQuotes(data.started_at) || new Date().toISOString(),
prompt: body.trim(),
session_id: data.session_id ? stripQuotes(data.session_id) : undefined,
ultrawork: data.ultrawork === true || data.ultrawork === "true" ? true : undefined,
}
} catch {
return null
@ -68,13 +69,14 @@ export function writeState(
}
const sessionIdLine = state.session_id ? `session_id: "${state.session_id}"\n` : ""
const ultraworkLine = state.ultrawork !== undefined ? `ultrawork: ${state.ultrawork}\n` : ""
const content = `---
active: ${state.active}
iteration: ${state.iteration}
max_iterations: ${state.max_iterations}
completion_promise: "${state.completion_promise}"
started_at: "${state.started_at}"
${sessionIdLine}---
${sessionIdLine}${ultraworkLine}---
${state.prompt}
`

View File

@ -8,6 +8,7 @@ export interface RalphLoopState {
started_at: string
prompt: string
session_id?: string
ultrawork?: boolean
}
export interface RalphLoopOptions {

View File

@ -525,9 +525,30 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
: undefined,
completionPromise: promiseMatch?.[1],
});
} else if (command === "cancel-ralph" && sessionID) {
ralphLoop.cancelLoop(sessionID);
}
} else if (command === "cancel-ralph" && sessionID) {
ralphLoop.cancelLoop(sessionID);
} else if (command === "ulw-loop" && sessionID) {
const rawArgs =
args?.command?.replace(/^\/?(ulw-loop)\s*/i, "") || "";
const taskMatch = rawArgs.match(/^["'](.+?)["']/);
const prompt =
taskMatch?.[1] ||
rawArgs.split(/\s+--/)[0]?.trim() ||
"Complete the task as instructed";
const maxIterMatch = rawArgs.match(/--max-iterations=(\d+)/i);
const promiseMatch = rawArgs.match(
/--completion-promise=["']?([^"'\s]+)["']?/i
);
ralphLoop.startLoop(sessionID, prompt, {
ultrawork: true,
maxIterations: maxIterMatch
? parseInt(maxIterMatch[1], 10)
: undefined,
completionPromise: promiseMatch?.[1],
});
}
}
},