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();