feat(ralph-loop): add strategy option for fresh context per iteration
Closes #1901 Add 'default_strategy' config option (default: 'continue') to control whether ralph-loop creates a new session per iteration ('reset') or keeps the same session ('continue'). The 'reset' strategy keeps the model in the smart zone by starting with fresh context for each iteration. Supports --strategy flag for per-command override.
This commit is contained in:
@@ -3343,11 +3343,20 @@
|
||||
},
|
||||
"state_dir": {
|
||||
"type": "string"
|
||||
},
|
||||
"default_strategy": {
|
||||
"default": "continue",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"reset",
|
||||
"continue"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"default_max_iterations"
|
||||
"default_max_iterations",
|
||||
"default_strategy"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ export const RalphLoopConfigSchema = z.object({
|
||||
default_max_iterations: z.number().min(1).max(1000).default(100),
|
||||
/** Custom state file directory relative to project root (default: .opencode/) */
|
||||
state_dir: z.string().optional(),
|
||||
default_strategy: z.enum(["reset", "continue"]).default("continue"),
|
||||
})
|
||||
|
||||
export type RalphLoopConfig = z.infer<typeof RalphLoopConfigSchema>
|
||||
|
||||
@@ -28,7 +28,7 @@ ${RALPH_LOOP_TEMPLATE}
|
||||
<user-task>
|
||||
$ARGUMENTS
|
||||
</user-task>`,
|
||||
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N]',
|
||||
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N] [--strategy=reset|continue]',
|
||||
},
|
||||
"ulw-loop": {
|
||||
description: "(builtin) Start ultrawork loop - continues until completion with ultrawork mode",
|
||||
@@ -39,7 +39,7 @@ ${RALPH_LOOP_TEMPLATE}
|
||||
<user-task>
|
||||
$ARGUMENTS
|
||||
</user-task>`,
|
||||
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N]',
|
||||
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N] [--strategy=reset|continue]',
|
||||
},
|
||||
"cancel-ralph": {
|
||||
description: "(builtin) Cancel active Ralph Loop",
|
||||
|
||||
@@ -24,7 +24,7 @@ export const RALPH_LOOP_TEMPLATE = `You are starting a Ralph Loop - a self-refer
|
||||
## Your Task
|
||||
|
||||
Parse the arguments below and begin working on the task. The format is:
|
||||
\`"task description" [--completion-promise=TEXT] [--max-iterations=N]\`
|
||||
\`"task description" [--completion-promise=TEXT] [--max-iterations=N] [--strategy=reset|continue]\`
|
||||
|
||||
Default completion promise is "DONE" and default max iterations is 100.`
|
||||
|
||||
|
||||
27
src/hooks/ralph-loop/command-arguments.ts
Normal file
27
src/hooks/ralph-loop/command-arguments.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export type RalphLoopStrategy = "reset" | "continue"
|
||||
|
||||
export type ParsedRalphLoopArguments = {
|
||||
prompt: string
|
||||
maxIterations?: number
|
||||
completionPromise?: string
|
||||
strategy?: RalphLoopStrategy
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT = "Complete the task as instructed"
|
||||
|
||||
export function parseRalphLoopArguments(rawArguments: string): ParsedRalphLoopArguments {
|
||||
const taskMatch = rawArguments.match(/^["'](.+?)["']/)
|
||||
const prompt = taskMatch?.[1] || rawArguments.split(/\s+--/)[0]?.trim() || DEFAULT_PROMPT
|
||||
|
||||
const maxIterationMatch = rawArguments.match(/--max-iterations=(\d+)/i)
|
||||
const completionPromiseMatch = rawArguments.match(/--completion-promise=["']?([^"'\s]+)["']?/i)
|
||||
const strategyMatch = rawArguments.match(/--strategy=(reset|continue)/i)
|
||||
const strategyValue = strategyMatch?.[1]?.toLowerCase()
|
||||
|
||||
return {
|
||||
prompt,
|
||||
maxIterations: maxIterationMatch ? Number.parseInt(maxIterationMatch[1], 10) : undefined,
|
||||
completionPromise: completionPromiseMatch?.[1],
|
||||
strategy: strategyValue === "reset" || strategyValue === "continue" ? strategyValue : undefined,
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,23 @@ type MessageInfo = {
|
||||
|
||||
export async function injectContinuationPrompt(
|
||||
ctx: PluginInput,
|
||||
options: { sessionID: string; prompt: string; directory: string; apiTimeoutMs: number },
|
||||
options: {
|
||||
sessionID: string
|
||||
prompt: string
|
||||
directory: string
|
||||
apiTimeoutMs: number
|
||||
inheritFromSessionID?: string
|
||||
},
|
||||
): Promise<void> {
|
||||
let agent: string | undefined
|
||||
let model: { providerID: string; modelID: string } | undefined
|
||||
let tools: Record<string, boolean | "allow" | "deny" | "ask"> | undefined
|
||||
|
||||
try {
|
||||
const sourceSessionID = options.inheritFromSessionID ?? options.sessionID
|
||||
const messagesResp = await withTimeout(
|
||||
ctx.client.session.messages({
|
||||
path: { id: options.sessionID },
|
||||
path: { id: sourceSessionID },
|
||||
}),
|
||||
options.apiTimeoutMs,
|
||||
)
|
||||
|
||||
@@ -6,12 +6,14 @@ import { tmpdir } from "node:os"
|
||||
import { createRalphLoopHook } from "./index"
|
||||
import { readState, writeState, clearState } from "./storage"
|
||||
import type { RalphLoopState } from "./types"
|
||||
import { parseRalphLoopArguments } from "./command-arguments"
|
||||
|
||||
describe("ralph-loop", () => {
|
||||
const TEST_DIR = join(tmpdir(), "ralph-loop-test-" + Date.now())
|
||||
let promptCalls: Array<{ sessionID: string; text: string }>
|
||||
let toastCalls: Array<{ title: string; message: string; variant: string }>
|
||||
let messagesCalls: Array<{ sessionID: string }>
|
||||
let createSessionCalls: Array<{ parentID?: string; title?: string; directory?: string }>
|
||||
let mockSessionMessages: Array<{ info?: { role?: string }; parts?: Array<{ type: string; text?: string }> }>
|
||||
let mockMessagesApiResponseShape: "data" | "array"
|
||||
|
||||
@@ -37,6 +39,17 @@ describe("ralph-loop", () => {
|
||||
messagesCalls.push({ sessionID: opts.path.id })
|
||||
return mockMessagesApiResponseShape === "array" ? mockSessionMessages : { data: mockSessionMessages }
|
||||
},
|
||||
create: async (opts: {
|
||||
body: { parentID?: string; title?: string }
|
||||
query?: { directory?: string }
|
||||
}) => {
|
||||
createSessionCalls.push({
|
||||
parentID: opts.body.parentID,
|
||||
title: opts.body.title,
|
||||
directory: opts.query?.directory,
|
||||
})
|
||||
return { data: { id: `new-session-${createSessionCalls.length}` } }
|
||||
},
|
||||
},
|
||||
tui: {
|
||||
showToast: async (opts: { body: { title: string; message: string; variant: string } }) => {
|
||||
@@ -57,6 +70,7 @@ describe("ralph-loop", () => {
|
||||
promptCalls = []
|
||||
toastCalls = []
|
||||
messagesCalls = []
|
||||
createSessionCalls = []
|
||||
mockSessionMessages = []
|
||||
mockMessagesApiResponseShape = "data"
|
||||
|
||||
@@ -123,6 +137,26 @@ describe("ralph-loop", () => {
|
||||
expect(readResult?.ultrawork).toBe(true)
|
||||
})
|
||||
|
||||
test("should store and read strategy field", () => {
|
||||
// given - a state object with strategy
|
||||
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",
|
||||
strategy: "reset",
|
||||
}
|
||||
|
||||
// when - write and read state
|
||||
writeState(TEST_DIR, state)
|
||||
const readResult = readState(TEST_DIR)
|
||||
|
||||
// then - strategy should be preserved
|
||||
expect(readResult?.strategy).toBe("reset")
|
||||
})
|
||||
|
||||
test("should return null for non-existent state", () => {
|
||||
// given - no state file exists
|
||||
// when - read state
|
||||
@@ -173,6 +207,32 @@ describe("ralph-loop", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("command arguments", () => {
|
||||
test("should parse --strategy=reset flag", () => {
|
||||
// given - ralph-loop command arguments with reset strategy
|
||||
const rawArguments = '"Build feature X" --strategy=reset --max-iterations=12'
|
||||
|
||||
// when - parse command arguments
|
||||
const parsedArguments = parseRalphLoopArguments(rawArguments)
|
||||
|
||||
// then - strategy should be parsed as reset
|
||||
expect(parsedArguments.strategy).toBe("reset")
|
||||
expect(parsedArguments.prompt).toBe("Build feature X")
|
||||
expect(parsedArguments.maxIterations).toBe(12)
|
||||
})
|
||||
|
||||
test("should parse --strategy=continue flag", () => {
|
||||
// given - ralph-loop command arguments with continue strategy
|
||||
const rawArguments = '"Build feature X" --strategy=continue'
|
||||
|
||||
// when - parse command arguments
|
||||
const parsedArguments = parseRalphLoopArguments(rawArguments)
|
||||
|
||||
// then - strategy should be parsed as continue
|
||||
expect(parsedArguments.strategy).toBe("continue")
|
||||
})
|
||||
})
|
||||
|
||||
describe("hook", () => {
|
||||
test("should start loop and write state", () => {
|
||||
// given - hook instance
|
||||
@@ -445,6 +505,38 @@ describe("ralph-loop", () => {
|
||||
expect(state?.max_iterations).toBe(200)
|
||||
})
|
||||
|
||||
test("should default strategy to continue when not specified", () => {
|
||||
// given - hook with no strategy option
|
||||
const hook = createRalphLoopHook(createMockPluginInput())
|
||||
|
||||
// when - start loop without strategy
|
||||
hook.startLoop("session-123", "Test task")
|
||||
|
||||
// then - strategy should default to continue
|
||||
const state = hook.getState()
|
||||
expect(state?.strategy).toBe("continue")
|
||||
})
|
||||
|
||||
test("should create new session for reset strategy", async () => {
|
||||
// given - hook with reset strategy
|
||||
const hook = createRalphLoopHook(createMockPluginInput())
|
||||
hook.startLoop("session-123", "Build a feature", { strategy: "reset" })
|
||||
|
||||
// when - session goes idle
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: "session-123" },
|
||||
},
|
||||
})
|
||||
|
||||
// then - new session should be created and continuation injected there
|
||||
expect(createSessionCalls.length).toBe(1)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
expect(promptCalls[0].sessionID).toBe("new-session-1")
|
||||
expect(hook.getState()?.session_id).toBe("new-session-1")
|
||||
})
|
||||
|
||||
test("should not inject when no loop is active", async () => {
|
||||
// given - no active loop
|
||||
const hook = createRalphLoopHook(createMockPluginInput())
|
||||
|
||||
63
src/hooks/ralph-loop/iteration-continuation.ts
Normal file
63
src/hooks/ralph-loop/iteration-continuation.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { RalphLoopState } from "./types"
|
||||
import { log } from "../../shared/logger"
|
||||
import { HOOK_NAME } from "./constants"
|
||||
import { buildContinuationPrompt } from "./continuation-prompt-builder"
|
||||
import { injectContinuationPrompt } from "./continuation-prompt-injector"
|
||||
import { createIterationSession, selectSessionInTui } from "./session-reset-strategy"
|
||||
|
||||
type ContinuationOptions = {
|
||||
directory: string
|
||||
apiTimeoutMs: number
|
||||
previousSessionID: string
|
||||
loopState: {
|
||||
setSessionID: (sessionID: string) => RalphLoopState | null
|
||||
}
|
||||
}
|
||||
|
||||
export async function continueIteration(
|
||||
ctx: PluginInput,
|
||||
state: RalphLoopState,
|
||||
options: ContinuationOptions,
|
||||
): Promise<void> {
|
||||
const strategy = state.strategy ?? "continue"
|
||||
const continuationPrompt = buildContinuationPrompt(state)
|
||||
|
||||
if (strategy === "reset") {
|
||||
const newSessionID = await createIterationSession(
|
||||
ctx,
|
||||
options.previousSessionID,
|
||||
options.directory,
|
||||
)
|
||||
if (!newSessionID) {
|
||||
return
|
||||
}
|
||||
|
||||
const boundState = options.loopState.setSessionID(newSessionID)
|
||||
if (!boundState) {
|
||||
log(`[${HOOK_NAME}] Failed to bind loop state to new session`, {
|
||||
previousSessionID: options.previousSessionID,
|
||||
newSessionID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await injectContinuationPrompt(ctx, {
|
||||
sessionID: newSessionID,
|
||||
inheritFromSessionID: options.previousSessionID,
|
||||
prompt: continuationPrompt,
|
||||
directory: options.directory,
|
||||
apiTimeoutMs: options.apiTimeoutMs,
|
||||
})
|
||||
|
||||
await selectSessionInTui(ctx.client, newSessionID)
|
||||
return
|
||||
}
|
||||
|
||||
await injectContinuationPrompt(ctx, {
|
||||
sessionID: options.previousSessionID,
|
||||
prompt: continuationPrompt,
|
||||
directory: options.directory,
|
||||
apiTimeoutMs: options.apiTimeoutMs,
|
||||
})
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export function createLoopStateController(options: {
|
||||
maxIterations?: number
|
||||
completionPromise?: string
|
||||
ultrawork?: boolean
|
||||
strategy?: "reset" | "continue"
|
||||
},
|
||||
): boolean {
|
||||
const state: RalphLoopState = {
|
||||
@@ -37,6 +38,7 @@ export function createLoopStateController(options: {
|
||||
loopOptions?.completionPromise ??
|
||||
DEFAULT_COMPLETION_PROMISE,
|
||||
ultrawork: loopOptions?.ultrawork,
|
||||
strategy: loopOptions?.strategy ?? config?.default_strategy ?? "continue",
|
||||
started_at: new Date().toISOString(),
|
||||
prompt,
|
||||
session_id: sessionID,
|
||||
@@ -77,5 +79,19 @@ export function createLoopStateController(options: {
|
||||
incrementIteration(): RalphLoopState | null {
|
||||
return incrementIteration(directory, stateDir)
|
||||
},
|
||||
|
||||
setSessionID(sessionID: string): RalphLoopState | null {
|
||||
const state = readState(directory, stateDir)
|
||||
if (!state) {
|
||||
return null
|
||||
}
|
||||
|
||||
state.session_id = sessionID
|
||||
if (!writeState(directory, state, stateDir)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return state
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,19 @@ import {
|
||||
detectCompletionInSessionMessages,
|
||||
detectCompletionInTranscript,
|
||||
} from "./completion-promise-detector"
|
||||
import { buildContinuationPrompt } from "./continuation-prompt-builder"
|
||||
import { injectContinuationPrompt } from "./continuation-prompt-injector"
|
||||
import { continueIteration } from "./iteration-continuation"
|
||||
|
||||
type SessionRecovery = {
|
||||
isRecovering: (sessionID: string) => boolean
|
||||
markRecovering: (sessionID: string) => void
|
||||
clear: (sessionID: string) => void
|
||||
}
|
||||
type LoopStateController = { getState: () => RalphLoopState | null; clear: () => boolean; incrementIteration: () => RalphLoopState | null }
|
||||
type LoopStateController = {
|
||||
getState: () => RalphLoopState | null
|
||||
clear: () => boolean
|
||||
incrementIteration: () => RalphLoopState | null
|
||||
setSessionID: (sessionID: string) => RalphLoopState | null
|
||||
}
|
||||
type RalphLoopEventHandlerOptions = { directory: string; apiTimeoutMs: number; getTranscriptPath: (sessionID: string) => string | undefined; checkSessionExists?: RalphLoopOptions["checkSessionExists"]; sessionRecovery: SessionRecovery; loopState: LoopStateController }
|
||||
|
||||
export function createRalphLoopEventHandler(
|
||||
@@ -128,11 +132,11 @@ export function createRalphLoopEventHandler(
|
||||
.catch(() => {})
|
||||
|
||||
try {
|
||||
await injectContinuationPrompt(ctx, {
|
||||
sessionID,
|
||||
prompt: buildContinuationPrompt(newState),
|
||||
await continueIteration(ctx, newState, {
|
||||
previousSessionID: sessionID,
|
||||
directory: options.directory,
|
||||
apiTimeoutMs: options.apiTimeoutMs,
|
||||
loopState: options.loopState,
|
||||
})
|
||||
} catch (err) {
|
||||
log(`[${HOOK_NAME}] Failed to inject continuation`, {
|
||||
|
||||
@@ -10,7 +10,12 @@ export interface RalphLoopHook {
|
||||
startLoop: (
|
||||
sessionID: string,
|
||||
prompt: string,
|
||||
options?: { maxIterations?: number; completionPromise?: string; ultrawork?: boolean }
|
||||
options?: {
|
||||
maxIterations?: number
|
||||
completionPromise?: string
|
||||
ultrawork?: boolean
|
||||
strategy?: "reset" | "continue"
|
||||
}
|
||||
) => boolean
|
||||
cancelLoop: (sessionID: string) => boolean
|
||||
getState: () => RalphLoopState | null
|
||||
|
||||
65
src/hooks/ralph-loop/session-reset-strategy.ts
Normal file
65
src/hooks/ralph-loop/session-reset-strategy.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { getServerBasicAuthHeader } from "../../shared/opencode-server-auth"
|
||||
import { getServerBaseUrl, log } from "../../shared"
|
||||
|
||||
export async function createIterationSession(
|
||||
ctx: PluginInput,
|
||||
parentSessionID: string,
|
||||
directory: string,
|
||||
): Promise<string | null> {
|
||||
const createResult = await ctx.client.session.create({
|
||||
body: {
|
||||
parentID: parentSessionID,
|
||||
title: "Ralph Loop Iteration",
|
||||
},
|
||||
query: { directory },
|
||||
})
|
||||
|
||||
if (createResult.error || !createResult.data?.id) {
|
||||
log("[ralph-loop] Failed to create iteration session", {
|
||||
parentSessionID,
|
||||
error: String(createResult.error ?? "No session ID returned"),
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
return createResult.data.id
|
||||
}
|
||||
|
||||
export async function selectSessionInTui(
|
||||
client: PluginInput["client"],
|
||||
sessionID: string,
|
||||
): Promise<boolean> {
|
||||
const baseUrl = getServerBaseUrl(client)
|
||||
const authorization = getServerBasicAuthHeader()
|
||||
|
||||
if (!baseUrl || !authorization) {
|
||||
return false
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/tui/select-session`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: authorization,
|
||||
},
|
||||
body: JSON.stringify({ sessionID }),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
}).catch((error: unknown) => {
|
||||
log("[ralph-loop] Failed to select session in TUI", {
|
||||
sessionID,
|
||||
error: String(error),
|
||||
})
|
||||
return null
|
||||
})
|
||||
|
||||
if (!response?.ok) {
|
||||
log("[ralph-loop] TUI session select request failed", {
|
||||
sessionID,
|
||||
status: response?.status,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -49,6 +49,7 @@ export function readState(directory: string, customPath?: string): RalphLoopStat
|
||||
prompt: body.trim(),
|
||||
session_id: data.session_id ? stripQuotes(data.session_id) : undefined,
|
||||
ultrawork: data.ultrawork === true || data.ultrawork === "true" ? true : undefined,
|
||||
strategy: data.strategy === "reset" || data.strategy === "continue" ? data.strategy : undefined,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
@@ -70,13 +71,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 strategyLine = state.strategy ? `strategy: "${state.strategy}"\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}${ultraworkLine}---
|
||||
${sessionIdLine}${ultraworkLine}${strategyLine}---
|
||||
${state.prompt}
|
||||
`
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface RalphLoopState {
|
||||
prompt: string
|
||||
session_id?: string
|
||||
ultrawork?: boolean
|
||||
strategy?: "reset" | "continue"
|
||||
}
|
||||
|
||||
export interface RalphLoopOptions {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { hasConnectedProvidersCache } from "../shared"
|
||||
import { setSessionModel } from "../shared/session-model-state"
|
||||
import { setSessionAgent } from "../features/claude-code-session-state"
|
||||
import { applyUltraworkModelOverrideOnMessage } from "./ultrawork-model-override"
|
||||
import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments"
|
||||
|
||||
import type { CreatedHooks } from "../create-hooks"
|
||||
|
||||
@@ -119,20 +120,12 @@ export function createChatMessageHandler(args: {
|
||||
if (isRalphLoopTemplate) {
|
||||
const taskMatch = promptText.match(/<user-task>\s*([\s\S]*?)\s*<\/user-task>/i)
|
||||
const rawTask = taskMatch?.[1]?.trim() || ""
|
||||
const quotedMatch = rawTask.match(/^["'](.+?)["']/)
|
||||
const prompt =
|
||||
quotedMatch?.[1] ||
|
||||
rawTask.split(/\s+--/)[0]?.trim() ||
|
||||
"Complete the task as instructed"
|
||||
const parsedArguments = parseRalphLoopArguments(rawTask)
|
||||
|
||||
const maxIterMatch = rawTask.match(/--max-iterations=(\d+)/i)
|
||||
const promiseMatch = rawTask.match(
|
||||
/--completion-promise=["']?([^"'\s]+)["']?/i,
|
||||
)
|
||||
|
||||
hooks.ralphLoop.startLoop(input.sessionID, prompt, {
|
||||
maxIterations: maxIterMatch ? parseInt(maxIterMatch[1], 10) : undefined,
|
||||
completionPromise: promiseMatch?.[1],
|
||||
hooks.ralphLoop.startLoop(input.sessionID, parsedArguments.prompt, {
|
||||
maxIterations: parsedArguments.maxIterations,
|
||||
completionPromise: parsedArguments.completionPromise,
|
||||
strategy: parsedArguments.strategy,
|
||||
})
|
||||
} else if (isCancelRalphTemplate) {
|
||||
hooks.ralphLoop.cancelLoop(input.sessionID)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getMainSessionID } from "../features/claude-code-session-state"
|
||||
import { clearBoulderState } from "../features/boulder-state"
|
||||
import { log } from "../shared"
|
||||
import { resolveSessionAgent } from "./session-agent-resolver"
|
||||
import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments"
|
||||
|
||||
import type { CreatedHooks } from "../create-hooks"
|
||||
|
||||
@@ -51,36 +52,24 @@ export function createToolExecuteBeforeHandler(args: {
|
||||
|
||||
if (command === "ralph-loop" && sessionID) {
|
||||
const rawArgs = rawName?.replace(/^\/?(ralph-loop)\s*/i, "") || ""
|
||||
const taskMatch = rawArgs.match(/^["'](.+?)["']/)
|
||||
const prompt =
|
||||
taskMatch?.[1] ||
|
||||
rawArgs.split(/\s+--/)[0]?.trim() ||
|
||||
"Complete the task as instructed"
|
||||
const parsedArguments = parseRalphLoopArguments(rawArgs)
|
||||
|
||||
const maxIterMatch = rawArgs.match(/--max-iterations=(\d+)/i)
|
||||
const promiseMatch = rawArgs.match(/--completion-promise=["']?([^"'\s]+)["']?/i)
|
||||
|
||||
hooks.ralphLoop.startLoop(sessionID, prompt, {
|
||||
maxIterations: maxIterMatch ? parseInt(maxIterMatch[1], 10) : undefined,
|
||||
completionPromise: promiseMatch?.[1],
|
||||
hooks.ralphLoop.startLoop(sessionID, parsedArguments.prompt, {
|
||||
maxIterations: parsedArguments.maxIterations,
|
||||
completionPromise: parsedArguments.completionPromise,
|
||||
strategy: parsedArguments.strategy,
|
||||
})
|
||||
} else if (command === "cancel-ralph" && sessionID) {
|
||||
hooks.ralphLoop.cancelLoop(sessionID)
|
||||
} else if (command === "ulw-loop" && sessionID) {
|
||||
const rawArgs = rawName?.replace(/^\/?(ulw-loop)\s*/i, "") || ""
|
||||
const taskMatch = rawArgs.match(/^["'](.+?)["']/)
|
||||
const prompt =
|
||||
taskMatch?.[1] ||
|
||||
rawArgs.split(/\s+--/)[0]?.trim() ||
|
||||
"Complete the task as instructed"
|
||||
const parsedArguments = parseRalphLoopArguments(rawArgs)
|
||||
|
||||
const maxIterMatch = rawArgs.match(/--max-iterations=(\d+)/i)
|
||||
const promiseMatch = rawArgs.match(/--completion-promise=["']?([^"'\s]+)["']?/i)
|
||||
|
||||
hooks.ralphLoop.startLoop(sessionID, prompt, {
|
||||
hooks.ralphLoop.startLoop(sessionID, parsedArguments.prompt, {
|
||||
ultrawork: true,
|
||||
maxIterations: maxIterMatch ? parseInt(maxIterMatch[1], 10) : undefined,
|
||||
completionPromise: promiseMatch?.[1],
|
||||
maxIterations: parsedArguments.maxIterations,
|
||||
completionPromise: parsedArguments.completionPromise,
|
||||
strategy: parsedArguments.strategy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user