From c96a5204ebafda579d03f9b98d31ff2d002c7b62 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Tue, 6 Jan 2026 21:13:50 -0800 Subject: [PATCH] feat: add Ralph Wiggum mode (#484) Co-authored-by: Letta --- src/cli/App.tsx | 365 ++++++++++++++++++++++++++++++- src/cli/commands/registry.ts | 18 ++ src/cli/components/InputRich.tsx | 57 ++++- src/ralph/mode.ts | 161 ++++++++++++++ 4 files changed, 597 insertions(+), 4 deletions(-) create mode 100644 src/ralph/mode.ts diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 7c14fad..32cdcc2 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -34,6 +34,11 @@ import { getModelDisplayName, getModelInfo } from "../agent/model"; import { SessionStats } from "../agent/stats"; import type { ApprovalContext } from "../permissions/analyzer"; import { type PermissionMode, permissionMode } from "../permissions/mode"; +import { + DEFAULT_COMPLETION_PROMISE, + type RalphState, + ralphMode, +} from "../ralph/mode"; import { updateProjectSettings } from "../settings"; import { settingsManager } from "../settings-manager"; import { telemetry } from "../telemetry"; @@ -361,6 +366,100 @@ function getSkillUnloadReminder(): string { return ""; } +// Parse /ralph or /yolo-ralph command arguments +function parseRalphArgs(input: string): { + prompt: string | null; + completionPromise: string | null | undefined; // undefined = use default, null = no promise + maxIterations: number; +} { + let rest = input.replace(/^\/(yolo-)?ralph\s*/, ""); + + // Extract --completion-promise "value" or --completion-promise 'value' + // Also handles --completion-promise "" or none for opt-out + let completionPromise: string | null | undefined; + const promiseMatch = rest.match(/--completion-promise\s+["']([^"']*)["']/); + if (promiseMatch) { + const val = promiseMatch[1] ?? ""; + completionPromise = val === "" || val.toLowerCase() === "none" ? null : val; + rest = rest.replace(/--completion-promise\s+["'][^"']*["']\s*/, ""); + } + + // Extract --max-iterations N + const maxMatch = rest.match(/--max-iterations\s+(\d+)/); + const maxIterations = maxMatch?.[1] ? parseInt(maxMatch[1], 10) : 0; + rest = rest.replace(/--max-iterations\s+\d+\s*/, ""); + + // Remaining text is the inline prompt (may be quoted) + const prompt = rest.trim().replace(/^["']|["']$/g, "") || null; + return { prompt, completionPromise, maxIterations }; +} + +// Build Ralph first-turn reminder (when activating) +// Uses exact wording from claude-code/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +function buildRalphFirstTurnReminder(state: RalphState): string { + const iterInfo = + state.maxIterations > 0 + ? `${state.currentIteration}/${state.maxIterations}` + : `${state.currentIteration}`; + + let reminder = ` +πŸ”„ Ralph Wiggum mode activated (iteration ${iterInfo}) +`; + + if (state.completionPromise) { + reminder += ` +═══════════════════════════════════════════════════════════ +RALPH LOOP COMPLETION PROMISE +═══════════════════════════════════════════════════════════ + +To complete this loop, output this EXACT text: + ${state.completionPromise} + +STRICT REQUIREMENTS (DO NOT VIOLATE): + βœ“ Use XML tags EXACTLY as shown above + βœ“ The statement MUST be completely and unequivocally TRUE + βœ“ Do NOT output false statements to exit the loop + βœ“ Do NOT lie even if you think you should exit + +IMPORTANT - Do not circumvent the loop: + Even if you believe you're stuck, the task is impossible, + or you've been running too long - you MUST NOT output a + false promise statement. The loop is designed to continue + until the promise is GENUINELY TRUE. Trust the process. + + If the loop should stop, the promise statement will become + true naturally. Do not force it by lying. +═══════════════════════════════════════════════════════════ +`; + } else { + reminder += ` +No completion promise set - loop runs until --max-iterations or ESC/Shift+Tab to exit. +`; + } + + reminder += ``; + return reminder; +} + +// Build Ralph continuation reminder (on subsequent iterations) +// Exact format from claude-code/plugins/ralph-wiggum/hooks/stop-hook.sh line 160 +function buildRalphContinuationReminder(state: RalphState): string { + const iterInfo = + state.maxIterations > 0 + ? `${state.currentIteration}/${state.maxIterations}` + : `${state.currentIteration}`; + + if (state.completionPromise) { + return ` +πŸ”„ Ralph iteration ${iterInfo} | To stop: output ${state.completionPromise} (ONLY when statement is TRUE - do not lie to exit!) +`; + } else { + return ` +πŸ”„ Ralph iteration ${iterInfo} | No completion promise set - loop runs infinitely +`; + } +} + // Items that have finished rendering and no longer change type StaticItem = | { @@ -544,6 +643,18 @@ export default function App({ [], ); + // Ralph Wiggum mode: config waiting for next message to capture as prompt + const [pendingRalphConfig, setPendingRalphConfig] = useState<{ + completionPromise: string | null | undefined; + maxIterations: number; + isYolo: boolean; + } | null>(null); + + // Track ralph mode for UI updates (singleton state doesn't trigger re-renders) + const [uiRalphActive, setUiRalphActive] = useState( + ralphMode.getState().isActive, + ); + // Derive current approval from pending approvals and results // This is the approval currently being shown to the user const currentApproval = pendingApprovals[approvalResults.length]; @@ -1216,6 +1327,91 @@ export default function App({ initialInput: Array, options?: { allowReentry?: boolean; submissionGeneration?: number }, ): Promise => { + // Helper function for Ralph Wiggum mode continuation + // Defined here to have access to buffersRef, processConversation via closure + const handleRalphContinuation = () => { + const ralphState = ralphMode.getState(); + + // Extract LAST assistant message from buffers to check for promise + // (We only want to check the most recent response, not the entire transcript) + const lines = toLines(buffersRef.current); + const assistantLines = lines.filter( + (l): l is Line & { kind: "assistant" } => l.kind === "assistant", + ); + const lastAssistantText = + assistantLines.length > 0 + ? (assistantLines[assistantLines.length - 1]?.text ?? "") + : ""; + + // Check for completion promise + if (ralphMode.checkForPromise(lastAssistantText)) { + // Promise matched - exit ralph mode + const wasYolo = ralphState.isYolo; + ralphMode.deactivate(); + setUiRalphActive(false); + if (wasYolo) { + permissionMode.setMode("default"); + } + + // Add completion status to transcript + const statusId = uid("status"); + buffersRef.current.byId.set(statusId, { + kind: "status", + id: statusId, + lines: [ + `βœ… Ralph loop complete: promise detected after ${ralphState.currentIteration} iteration(s)`, + ], + }); + buffersRef.current.order.push(statusId); + refreshDerived(); + return; + } + + // Check iteration limit + if (!ralphMode.shouldContinue()) { + // Max iterations reached - exit ralph mode + const wasYolo = ralphState.isYolo; + ralphMode.deactivate(); + setUiRalphActive(false); + if (wasYolo) { + permissionMode.setMode("default"); + } + + // Add status to transcript + const statusId = uid("status"); + buffersRef.current.byId.set(statusId, { + kind: "status", + id: statusId, + lines: [ + `πŸ›‘ Ralph loop: Max iterations (${ralphState.maxIterations}) reached`, + ], + }); + buffersRef.current.order.push(statusId); + refreshDerived(); + return; + } + + // Continue loop - increment iteration and re-send prompt + ralphMode.incrementIteration(); + const newState = ralphMode.getState(); + const systemMsg = buildRalphContinuationReminder(newState); + + // Re-inject original prompt with ralph reminder prepended + // Use setTimeout to avoid blocking the current render cycle + setTimeout(() => { + processConversation( + [ + { + type: "message", + role: "user", + content: `${systemMsg}\n\n${newState.originalPrompt}`, + }, + ], + { allowReentry: true }, + ); + }, 0); + }; + // Copy so we can safely mutate for retry recovery flows const currentInput = [...initialInput]; const allowReentry = options?.allowReentry ?? false; @@ -1444,6 +1640,14 @@ export default function App({ queueSnapshotRef.current = []; } + // === RALPH WIGGUM CONTINUATION CHECK === + // Check if ralph mode is active and should auto-continue + // This happens at the very end, right before we'd release input + if (ralphMode.getState().isActive) { + handleRalphContinuation(); + return; + } + return; } @@ -1478,6 +1682,23 @@ export default function App({ if (!EAGER_CANCEL) { appendError(INTERRUPT_MESSAGE, true); } + + // In ralph mode, ESC interrupts but does NOT exit ralph + // User can type additional instructions, which will get ralph prefix prepended + // (Similar to how plan mode works) + if (ralphMode.getState().isActive) { + // Add status to transcript showing ralph is paused + const statusId = uid("status"); + buffersRef.current.byId.set(statusId, { + kind: "status", + id: statusId, + lines: [ + `⏸️ Ralph loop paused - type to continue or shift+tab to exit`, + ], + }); + buffersRef.current.order.push(statusId); + refreshDerived(); + } } return; @@ -2902,6 +3123,44 @@ export default function App({ // Note: userCancelledRef.current was already reset above before the queue check // to ensure the dequeue effect isn't blocked by a stale cancellation flag. + // Handle pending Ralph config - activate ralph mode but let message flow through normal path + // This ensures session context and other reminders are included + // Track if we just activated so we can use first turn reminder vs continuation + let justActivatedRalph = false; + if (pendingRalphConfig && !msg.startsWith("/")) { + const { completionPromise, maxIterations, isYolo } = pendingRalphConfig; + ralphMode.activate(msg, completionPromise, maxIterations, isYolo); + setUiRalphActive(true); + setPendingRalphConfig(null); + justActivatedRalph = true; + if (isYolo) { + permissionMode.setMode("bypassPermissions"); + } + + const ralphState = ralphMode.getState(); + + // Add status to transcript + const statusId = uid("status"); + const promiseDisplay = ralphState.completionPromise + ? `"${ralphState.completionPromise.slice(0, 50)}${ralphState.completionPromise.length > 50 ? "..." : ""}"` + : "(none)"; + buffersRef.current.byId.set(statusId, { + kind: "status", + id: statusId, + lines: [ + `πŸ”„ ${isYolo ? "yolo-ralph" : "ralph"} mode started (iter 1/${maxIterations || "∞"})`, + `Promise: ${promiseDisplay}`, + ], + }); + buffersRef.current.order.push(statusId); + refreshDerived(); + + // Don't return - let message flow through normal path which will: + // 1. Add session context reminder (if first message) + // 2. Add ralph mode reminder (since ralph is now active) + // 3. Add other reminders (skill unload, memory, etc.) + } + let aliasedMsg = msg; if (msg === "exit" || msg === "quit") { aliasedMsg = "/exit"; @@ -3227,6 +3486,75 @@ export default function App({ return { submitted: true }; } + // Special handling for /ralph and /yolo-ralph commands - Ralph Wiggum mode + if (trimmed.startsWith("/yolo-ralph") || trimmed.startsWith("/ralph")) { + const isYolo = trimmed.startsWith("/yolo-ralph"); + const { prompt, completionPromise, maxIterations } = + parseRalphArgs(trimmed); + + const cmdId = uid("cmd"); + + if (prompt) { + // Inline prompt - activate immediately and send + ralphMode.activate( + prompt, + completionPromise, + maxIterations, + isYolo, + ); + setUiRalphActive(true); + if (isYolo) { + permissionMode.setMode("bypassPermissions"); + } + + const ralphState = ralphMode.getState(); + const promiseDisplay = ralphState.completionPromise + ? `"${ralphState.completionPromise.slice(0, 50)}${ralphState.completionPromise.length > 50 ? "..." : ""}"` + : "(none)"; + + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: trimmed, + output: `πŸ”„ ${isYolo ? "yolo-ralph" : "ralph"} mode activated (iter 1/${maxIterations || "∞"})\nPromise: ${promiseDisplay}`, + phase: "finished", + success: true, + }); + buffersRef.current.order.push(cmdId); + refreshDerived(); + + // Send the prompt with ralph reminder prepended + const systemMsg = buildRalphFirstTurnReminder(ralphState); + processConversation([ + { + type: "message", + role: "user", + content: `${systemMsg}\n\n${prompt}`, + }, + ]); + } else { + // No inline prompt - wait for next message + setPendingRalphConfig({ completionPromise, maxIterations, isYolo }); + + const defaultPromisePreview = DEFAULT_COMPLETION_PROMISE.slice( + 0, + 40, + ); + + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: trimmed, + output: `πŸ”„ ${isYolo ? "yolo-ralph" : "ralph"} mode ready (waiting for task)\nMax iterations: ${maxIterations || "unlimited"}\nPromise: ${completionPromise === null ? "(none)" : (completionPromise ?? `"${defaultPromisePreview}..." (default)`)}\n\nType your task to begin the loop.`, + phase: "finished", + success: true, + }); + buffersRef.current.order.push(cmdId); + refreshDerived(); + } + return { submitted: true }; + } + // Special handling for /stream command - toggle and save if (msg.trim() === "/stream") { const newValue = !tokenStreamingEnabled; @@ -4248,6 +4576,21 @@ ${gitContext} // Prepend plan mode reminder if in plan mode const planModeReminder = getPlanModeReminder(); + // Prepend ralph mode reminder if in ralph mode + let ralphModeReminder = ""; + if (ralphMode.getState().isActive) { + if (justActivatedRalph) { + // First turn - use full first turn reminder, don't increment (already at 1) + const ralphState = ralphMode.getState(); + ralphModeReminder = `${buildRalphFirstTurnReminder(ralphState)}\n\n`; + } else { + // Continuation after ESC - increment iteration and use shorter reminder + ralphMode.incrementIteration(); + const ralphState = ralphMode.getState(); + ralphModeReminder = `${buildRalphContinuationReminder(ralphState)}\n\n`; + } + } + // Prepend skill unload reminder if skills are loaded (using cached flag) const skillUnloadReminder = getSkillUnloadReminder(); @@ -4294,10 +4637,11 @@ DO NOT respond to these messages or otherwise consider them in your response unl // Increment turn count for next iteration turnCountRef.current += 1; - // Combine reminders with content (session context first, then plan mode, then skill unload, then bash commands, then memory reminder) + // Combine reminders with content (session context first, then plan mode, then ralph mode, then skill unload, then bash commands, then memory reminder) const allReminders = sessionContextReminder + planModeReminder + + ralphModeReminder + skillUnloadReminder + bashCommandPrefix + memoryReminderContent; @@ -4747,6 +5091,7 @@ DO NOT respond to these messages or otherwise consider them in your response unl isAgentBusy, setStreaming, setCommandRunning, + pendingRalphConfig, ], ); @@ -5636,6 +5981,20 @@ DO NOT respond to these messages or otherwise consider them in your response unl permissionMode.getMode(), ); + // Handle ralph mode exit from Input component (shift+tab) + const handleRalphExit = useCallback(() => { + const ralph = ralphMode.getState(); + if (ralph.isActive) { + const wasYolo = ralph.isYolo; + ralphMode.deactivate(); + setUiRalphActive(false); + if (wasYolo) { + permissionMode.setMode("default"); + setUiPermissionMode("default"); + } + } + }, []); + // Handle permission mode changes from the Input component (e.g., shift+tab cycling) const handlePermissionModeChange = useCallback((mode: PermissionMode) => { // When entering plan mode via tab cycling, generate and set the plan file path @@ -6446,6 +6805,10 @@ Plan file path: ${planFilePath}`; onEscapeCancel={ profileConfirmPending ? handleProfileEscapeCancel : undefined } + ralphActive={uiRalphActive} + ralphPending={pendingRalphConfig !== null} + ralphPendingYolo={pendingRalphConfig?.isYolo ?? false} + onRalphExit={handleRalphExit} /> diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index 3bbc742..3b3973e 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -287,6 +287,24 @@ export const commands: Record = { }, }, + // === Ralph Wiggum mode (order 45-46) === + "/ralph": { + desc: 'Start Ralph Wiggum loop (/ralph [prompt] [--completion-promise "X"] [--max-iterations N])', + order: 45, + handler: () => { + // Handled specially in App.tsx + return "Activating ralph mode..."; + }, + }, + "/yolo-ralph": { + desc: "Start Ralph loop with bypass permissions (yolo + ralph)", + order: 46, + handler: () => { + // Handled specially in App.tsx + return "Activating yolo-ralph mode..."; + }, + }, + // === Hidden commands (not shown in autocomplete) === "/stream": { desc: "Toggle token streaming on/off", diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index bcd1e29..2d114c1 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -14,6 +14,7 @@ import { import type { PermissionMode } from "../../permissions/mode"; import { permissionMode } from "../../permissions/mode"; import { ANTHROPIC_PROVIDER_NAME } from "../../providers/anthropic-provider"; +import { ralphMode } from "../../ralph/mode"; import { settingsManager } from "../../settings-manager"; import { charsToTokens, formatCompact } from "../helpers/format"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; @@ -56,6 +57,10 @@ export function Input({ messageQueue, onEnterQueueEditMode, onEscapeCancel, + ralphActive = false, + ralphPending = false, + ralphPendingYolo = false, + onRalphExit, }: { visible?: boolean; streaming: boolean; @@ -75,6 +80,10 @@ export function Input({ messageQueue?: string[]; onEnterQueueEditMode?: () => void; onEscapeCancel?: () => void; + ralphActive?: boolean; + ralphPending?: boolean; + ralphPendingYolo?: boolean; + onRalphExit?: () => void; }) { const [value, setValue] = useState(""); const [escapePressed, setEscapePressed] = useState(false); @@ -236,7 +245,7 @@ export function Input({ // Note: bash mode entry/exit is implemented inside PasteAwareTextInput so we can // consume the keystroke before it renders (no flicker). - // Handle Shift+Tab for permission mode cycling + // Handle Shift+Tab for permission mode cycling (or ralph mode exit) useInput((_input, key) => { if (!visible) return; // Debug logging for shift+tab detection @@ -247,6 +256,12 @@ export function Input({ ); } if (key.shift && key.tab) { + // If ralph mode is active, exit it first (goes to default mode) + if (ralphActive && onRalphExit) { + onRalphExit(); + return; + } + // Cycle through permission modes const modes: PermissionMode[] = [ "default", @@ -570,8 +585,43 @@ export function Input({ setCursorPos(selectedCommand.length); }; - // Get display name and color for permission mode + // Get display name and color for permission mode (ralph modes take precedence) const getModeInfo = () => { + // Check ralph pending first (waiting for task input) + if (ralphPending) { + if (ralphPendingYolo) { + return { + name: "yolo-ralph (waiting)", + color: "#FF8C00", // dark orange + }; + } + return { + name: "ralph (waiting)", + color: "#FEE19C", // yellow (brandColors.statusWarning) + }; + } + + // Check ralph mode active (using prop for reactivity) + if (ralphActive) { + const ralph = ralphMode.getState(); + const iterDisplay = + ralph.maxIterations > 0 + ? `${ralph.currentIteration}/${ralph.maxIterations}` + : `${ralph.currentIteration}`; + + if (ralph.isYolo) { + return { + name: `yolo-ralph (iter ${iterDisplay})`, + color: "#FF8C00", // dark orange + }; + } + return { + name: `ralph (iter ${iterDisplay})`, + color: "#FEE19C", // yellow (brandColors.statusWarning) + }; + } + + // Fall through to permission modes switch (currentMode) { case "acceptEdits": return { name: "accept edits", color: colors.status.processing }; @@ -597,6 +647,7 @@ export function Input({ const elapsedMinutes = Math.floor(elapsedMs / 60000); // Build the status hint text (esc to interrupt Β· 2m Β· 1.2k ↑) + // In ralph mode, also show "shift+tab to exit" const statusHintText = (() => { const hintColor = chalk.hex(colors.subagent.hint); const hintBold = hintColor.bold; @@ -716,7 +767,7 @@ export function Input({ ⏡⏡ {modeInfo.name} {" "} - (shift+tab to cycle) + (shift+tab to {ralphActive || ralphPending ? "exit" : "cycle"}) ) : ( diff --git a/src/ralph/mode.ts b/src/ralph/mode.ts new file mode 100644 index 0000000..3530615 --- /dev/null +++ b/src/ralph/mode.ts @@ -0,0 +1,161 @@ +// src/ralph/mode.ts +// Ralph Wiggum mode state management +// Singleton pattern matching src/permissions/mode.ts + +// Default completion promise inspired by The_Whole_Daisy's recommendations +// Source: X post 2008625420741341355 +export const DEFAULT_COMPLETION_PROMISE = + "The task is complete. All requirements have been implemented and verified working. " + + "Any tests that were relevant have been run and are passing. The implementation is " + + "clean and production-ready. I have not taken any shortcuts or faked anything to " + + "meet these requirements."; + +export type RalphState = { + isActive: boolean; + isYolo: boolean; + originalPrompt: string; + completionPromise: string | null; // null = no promise check (Claude Code style) + maxIterations: number; // 0 = unlimited + currentIteration: number; +}; + +// Use globalThis to ensure singleton across bundle +const RALPH_KEY = Symbol.for("@letta/ralphMode"); + +type GlobalWithRalph = typeof globalThis & { + [RALPH_KEY]: RalphState; +}; + +function getDefaultState(): RalphState { + return { + isActive: false, + isYolo: false, + originalPrompt: "", + completionPromise: null, + maxIterations: 0, + currentIteration: 0, + }; +} + +function getGlobalState(): RalphState { + const global = globalThis as GlobalWithRalph; + if (!global[RALPH_KEY]) { + global[RALPH_KEY] = getDefaultState(); + } + return global[RALPH_KEY]; +} + +function setGlobalState(state: RalphState): void { + const global = globalThis as GlobalWithRalph; + global[RALPH_KEY] = state; +} + +/** + * Ralph Wiggum mode state manager. + * Implements iterative development loops where the agent keeps working + * until it outputs a completion promise. + */ +class RalphModeManager { + /** + * Activate Ralph mode with the given configuration. + * @param prompt - The task prompt + * @param completionPromise - Promise text to check for (null = no check, uses default if undefined) + * @param maxIterations - Max iterations before auto-stop (0 = unlimited) + * @param isYolo - Whether to bypass permissions + */ + activate( + prompt: string, + completionPromise: string | null | undefined, + maxIterations: number, + isYolo: boolean, + ): void { + // If completionPromise is undefined, use default + // If it's null or empty string, that means "no promise check" (Claude Code style) + let resolvedPromise: string | null; + if (completionPromise === undefined) { + resolvedPromise = DEFAULT_COMPLETION_PROMISE; + } else if ( + completionPromise === null || + completionPromise === "" || + completionPromise.toLowerCase() === "none" + ) { + resolvedPromise = null; + } else { + resolvedPromise = completionPromise; + } + + setGlobalState({ + isActive: true, + isYolo, + originalPrompt: prompt, + completionPromise: resolvedPromise, + maxIterations, + currentIteration: 1, + }); + } + + /** + * Deactivate Ralph mode and reset state. + */ + deactivate(): void { + setGlobalState(getDefaultState()); + } + + /** + * Get current Ralph mode state. + */ + getState(): RalphState { + return getGlobalState(); + } + + /** + * Increment the iteration counter. + */ + incrementIteration(): void { + const state = getGlobalState(); + setGlobalState({ + ...state, + currentIteration: state.currentIteration + 1, + }); + } + + /** + * Check if the assistant's output contains the completion promise. + * Uses regex to find ... tags. + * @param text - The assistant's output text + * @returns true if promise was found and matches + */ + checkForPromise(text: string): boolean { + const state = getGlobalState(); + if (!state.completionPromise) return false; + + // Match ... tags (case insensitive, handles multiline) + const match = text.match(/([\s\S]*?)<\/promise>/i); + if (!match || match[1] === undefined) return false; + + // Normalize whitespace and compare + const promiseText = match[1].trim().replace(/\s+/g, " "); + const expected = state.completionPromise.trim().replace(/\s+/g, " "); + + return promiseText === expected; + } + + /** + * Check if the loop should continue. + * @returns true if active and under iteration limit + */ + shouldContinue(): boolean { + const state = getGlobalState(); + if (!state.isActive) return false; + if ( + state.maxIterations > 0 && + state.currentIteration >= state.maxIterations + ) { + return false; + } + return true; + } +} + +// Singleton instance +export const ralphMode = new RalphModeManager();