// @bun
// src/ralph-loop/config.ts
import { existsSync, readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
function resolveOpenCodeConfigDir() {
const xdg = process.env.XDG_CONFIG_HOME;
if (xdg && xdg.length > 0) {
return join(xdg, "opencode");
}
return join(homedir(), ".config", "opencode");
}
function stripJsonComments(input) {
let out = "";
let i = 0;
let inString = null;
while (i < input.length) {
const ch = input[i];
const next = i + 1 < input.length ? input[i + 1] : "";
if (inString) {
out += ch;
if (ch === "\\") {
if (i + 1 < input.length) {
out += input[i + 1];
i += 2;
continue;
}
}
if (ch === inString) {
inString = null;
}
i++;
continue;
}
if (ch === '"' || ch === "'") {
inString = ch;
out += ch;
i++;
continue;
}
if (ch === "/" && next === "/") {
i += 2;
while (i < input.length && input[i] !== `
`)
i++;
continue;
}
if (ch === "/" && next === "*") {
i += 2;
while (i < input.length) {
if (input[i] === "*" && i + 1 < input.length && input[i + 1] === "/") {
i += 2;
break;
}
i++;
}
continue;
}
out += ch;
i++;
}
return out;
}
function tryReadConfig(path) {
if (!existsSync(path))
return null;
try {
const raw = readFileSync(path, "utf-8");
const parsed = JSON.parse(stripJsonComments(raw));
if (!parsed || typeof parsed !== "object")
return null;
return parsed;
} catch {
return null;
}
}
function loadRalphLoopConfig(projectDir) {
const userDir = resolveOpenCodeConfigDir();
const user = tryReadConfig(join(userDir, "ralph-loop.jsonc")) ?? tryReadConfig(join(userDir, "ralph-loop.json"));
const project = tryReadConfig(join(projectDir, ".opencode", "ralph-loop.jsonc")) ?? tryReadConfig(join(projectDir, ".opencode", "ralph-loop.json"));
return {
...user ?? {},
...project ?? {}
};
}
// src/ralph-loop/commands.ts
var RALPH_LOOP_TEMPLATE = `You are starting a Ralph Loop - a self-referential development loop that runs until task completion.
## How Ralph Loop Works
1. You will work on the task continuously
2. When you believe the task is FULLY complete, output: \`{{COMPLETION_PROMISE}}\`
3. If you don't output the promise, the loop will automatically inject another prompt to continue
4. Maximum iterations: Configurable (default 100)
## Rules
- Focus on completing the task fully, not partially
- Don't output the completion promise until the task is truly done
- Each iteration should make meaningful progress toward the goal
- If stuck, try different approaches
- Use todos to track your progress
## Exit Conditions
1. **Completion**: Output your completion promise tag when fully complete
2. **Max Iterations**: Loop stops automatically at limit
3. **Cancel**: User runs \`/cancel-ralph\` command
## Your Task
Parse the arguments below and begin working on the task. The format is:
\`"task description" [--completion-promise=TEXT] [--max-iterations=N] [--strategy=reset|continue]\`
Default completion promise is "DONE" and default max iterations is 100.`;
var CANCEL_RALPH_TEMPLATE = `Cancel the currently active Ralph Loop.
This will:
1. Stop the loop from continuing
2. Clear the loop state file
3. Allow the session to end normally
Check if a loop is active and cancel it. Inform the user of the result.`;
var RALPH_LOOP_COMMANDS = {
"ralph-loop": {
name: "ralph-loop",
description: "(builtin) Start self-referential development loop until completion",
template: `
${RALPH_LOOP_TEMPLATE}
$ARGUMENTS
`
},
"ulw-loop": {
name: "ulw-loop",
description: "(builtin) Start ultrawork loop - continues until completion with ultrawork mode",
template: `
${RALPH_LOOP_TEMPLATE}
$ARGUMENTS
`
},
"cancel-ralph": {
name: "cancel-ralph",
description: "(builtin) Cancel active Ralph Loop",
template: `
${CANCEL_RALPH_TEMPLATE}
`
}
};
// src/ralph-loop/command-arguments.ts
var DEFAULT_PROMPT = "Complete the task as instructed";
function parseRalphLoopArguments(rawArguments) {
const taskMatch = rawArguments.match(/^("|')(.+?)\1/);
const promptCandidate = taskMatch?.[2] ?? (rawArguments.startsWith("--") ? "" : rawArguments.split(/\s+--/)[0]?.trim() ?? "");
const prompt = promptCandidate || DEFAULT_PROMPT;
const maxIterationMatch = rawArguments.match(/--max-iterations=(\d+)/i);
const maxIterationsRaw = maxIterationMatch?.[1];
const completionPromiseQuoted = rawArguments.match(/--completion-promise=("|')(.+?)\1/i);
const completionPromiseUnquoted = rawArguments.match(/--completion-promise=([^\s\"']+)/i);
const completionPromise = completionPromiseQuoted?.[2] ?? completionPromiseUnquoted?.[1];
const strategyMatch = rawArguments.match(/--strategy=(reset|continue)/i);
const strategyValue = strategyMatch?.[1]?.toLowerCase();
return {
prompt,
maxIterations: maxIterationsRaw ? Number.parseInt(maxIterationsRaw, 10) : undefined,
completionPromise,
strategy: strategyValue === "reset" || strategyValue === "continue" ? strategyValue : undefined
};
}
// src/ralph-loop/transcript.ts
import { join as join3 } from "path";
// src/ralph-loop/claude-config-dir.ts
import { homedir as homedir2 } from "os";
import { join as join2 } from "path";
function getClaudeConfigDir() {
const envConfigDir = process.env.CLAUDE_CONFIG_DIR;
if (envConfigDir)
return envConfigDir;
return join2(homedir2(), ".claude");
}
// src/ralph-loop/transcript.ts
var TRANSCRIPT_DIR = join3(getClaudeConfigDir(), "transcripts");
function getTranscriptPath(sessionId) {
return join3(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
}
// src/ralph-loop/loop-session-recovery.ts
function createLoopSessionRecovery(options) {
const recoveryWindowMs = options?.recoveryWindowMs ?? 5000;
const sessions = new Map;
function getSessionState(sessionID) {
let state = sessions.get(sessionID);
if (!state) {
state = {};
sessions.set(sessionID, state);
}
return state;
}
return {
isRecovering(sessionID) {
return getSessionState(sessionID).isRecovering === true;
},
markRecovering(sessionID) {
const state = getSessionState(sessionID);
state.isRecovering = true;
setTimeout(() => {
state.isRecovering = false;
}, recoveryWindowMs);
},
clear(sessionID) {
sessions.delete(sessionID);
}
};
}
// src/ralph-loop/constants.ts
var HOOK_NAME = "ralph-loop";
var DEFAULT_STATE_FILE = ".sisyphus/ralph-loop.local.md";
var DEFAULT_MAX_ITERATIONS = 100;
var DEFAULT_COMPLETION_PROMISE = "DONE";
// src/ralph-loop/storage.ts
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
import { dirname, join as join4 } from "path";
// src/ralph-loop/simple-frontmatter.ts
function parseFrontmatter(content) {
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n?---\r?\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return { data: {}, body: content };
}
const yaml = match[1] ?? "";
const body = match[2] ?? "";
const data = {};
for (const rawLine of yaml.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line)
continue;
const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
if (!m)
continue;
const key = m[1];
if (!key)
continue;
let value = m[2] ?? "";
if (typeof value === "string") {
const v = value.trim();
if (v === "true")
value = true;
else if (v === "false")
value = false;
else if (/^-?\d+(\.\d+)?$/.test(v))
value = Number(v);
else
value = v;
}
data[key] = value;
}
return { data, body };
}
// src/ralph-loop/storage.ts
function getStateFilePath(directory, customPath) {
return customPath ? join4(directory, customPath) : join4(directory, DEFAULT_STATE_FILE);
}
function readState(directory, customPath) {
const filePath = getStateFilePath(directory, customPath);
if (!existsSync2(filePath))
return null;
try {
const content = readFileSync2(filePath, "utf-8");
const { data, body } = parseFrontmatter(content);
const active = data.active;
const iteration = data.iteration;
if (active === undefined || iteration === undefined)
return null;
const isActive = active === true || active === "true";
const iterationNum = typeof iteration === "number" ? iteration : Number(iteration);
if (Number.isNaN(iterationNum))
return null;
const stripQuotes = (val) => {
const str = String(val ?? "");
return str.replace(/^["']|["']$/g, "");
};
return {
active: isActive,
iteration: iterationNum,
max_iterations: Number(data.max_iterations) || DEFAULT_MAX_ITERATIONS,
completion_promise: stripQuotes(data.completion_promise) || DEFAULT_COMPLETION_PROMISE,
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,
strategy: data.strategy === "reset" || data.strategy === "continue" ? data.strategy : undefined
};
} catch {
return null;
}
}
function writeState(directory, state, customPath) {
const filePath = getStateFilePath(directory, customPath);
try {
const dir = dirname(filePath);
if (!existsSync2(dir)) {
mkdirSync(dir, { recursive: true });
}
const sessionIdLine = state.session_id ? `session_id: "${state.session_id}"
` : "";
const ultraworkLine = state.ultrawork !== undefined ? `ultrawork: ${state.ultrawork}
` : "";
const strategyLine = state.strategy ? `strategy: "${state.strategy}"
` : "";
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}${strategyLine}---
${state.prompt}
`;
writeFileSync(filePath, content, "utf-8");
return true;
} catch {
return false;
}
}
function clearState(directory, customPath) {
const filePath = getStateFilePath(directory, customPath);
try {
if (existsSync2(filePath)) {
unlinkSync(filePath);
}
return true;
} catch {
return false;
}
}
function incrementIteration(directory, customPath) {
const state = readState(directory, customPath);
if (!state)
return null;
state.iteration += 1;
return writeState(directory, state, customPath) ? state : null;
}
// src/ralph-loop/logger.ts
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
var logFile = path.join(os.tmpdir(), "opencode-plugin-ralph-loop.log");
function shouldLogToConsole() {
const raw = process.env.RALPH_LOOP_DEBUG;
if (!raw)
return false;
return raw === "1" || raw.toLowerCase() === "true" || raw.toLowerCase() === "yes";
}
function log(message, meta) {
try {
const timestamp = new Date().toISOString();
const entry = `[${timestamp}] ${message}${meta !== undefined ? ` ${JSON.stringify(meta)}` : ""}
`;
fs.appendFileSync(logFile, entry);
} catch {}
if (shouldLogToConsole()) {
try {
console.log(message, meta);
} catch {
console.log(message);
}
}
}
// src/ralph-loop/loop-state-controller.ts
function createLoopStateController(options) {
const directory = options.directory;
const stateDir = options.stateDir;
const config = options.config;
return {
startLoop(sessionID, prompt, loopOptions) {
const state = {
active: true,
iteration: 1,
max_iterations: loopOptions?.maxIterations ?? config?.default_max_iterations ?? DEFAULT_MAX_ITERATIONS,
completion_promise: loopOptions?.completionPromise ?? DEFAULT_COMPLETION_PROMISE,
ultrawork: loopOptions?.ultrawork,
strategy: loopOptions?.strategy ?? config?.default_strategy ?? "continue",
started_at: new Date().toISOString(),
prompt,
session_id: sessionID
};
const success = writeState(directory, state, stateDir);
if (success) {
log(`[${HOOK_NAME}] Loop started`, {
sessionID,
maxIterations: state.max_iterations,
completionPromise: state.completion_promise
});
}
return success;
},
cancelLoop(sessionID) {
const state = readState(directory, stateDir);
if (!state || state.session_id !== sessionID) {
return false;
}
const success = clearState(directory, stateDir);
if (success) {
log(`[${HOOK_NAME}] Loop cancelled`, { sessionID, iteration: state.iteration });
}
return success;
},
getState() {
return readState(directory, stateDir);
},
clear() {
return clearState(directory, stateDir);
},
incrementIteration() {
return incrementIteration(directory, stateDir);
},
setSessionID(sessionID) {
const state = readState(directory, stateDir);
if (!state)
return null;
state.session_id = sessionID;
if (!writeState(directory, state, stateDir))
return null;
return state;
}
};
}
// src/ralph-loop/completion-promise-detector.ts
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
// src/ralph-loop/with-timeout.ts
async function withTimeout(promise, timeoutMs) {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error("API timeout"));
}, timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
}
}
// src/ralph-loop/completion-promise-detector.ts
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function buildPromisePattern(promise) {
return new RegExp(`\\s*${escapeRegex(promise)}\\s*`, "is");
}
function detectCompletionInTranscript(transcriptPath, promise) {
if (!transcriptPath)
return false;
try {
if (!existsSync3(transcriptPath))
return false;
const content = readFileSync3(transcriptPath, "utf-8");
const pattern = buildPromisePattern(promise);
const lines = content.split(`
`).filter((line) => line.trim());
for (const line of lines) {
try {
const entry = JSON.parse(line);
if (entry.type !== "user" && pattern.test(line))
return true;
} catch {}
}
return false;
} catch {
return false;
}
}
async function detectCompletionInSessionMessages(ctx, options) {
try {
const sessionApi = ctx.client.session;
if (typeof sessionApi.messages !== "function")
return false;
const response = await withTimeout(sessionApi.messages({
path: { id: options.sessionID },
query: { directory: options.directory }
}), options.apiTimeoutMs);
const messageArray = Array.isArray(response) ? response : isRecord(response) && Array.isArray(response.data) ? response.data : isRecord(response) && Array.isArray(response["200"]) ? response["200"] : [];
const assistantMessages = messageArray.filter((msg) => msg.info?.role === "assistant");
if (assistantMessages.length === 0)
return false;
const pattern = buildPromisePattern(options.promise);
const recentAssistants = assistantMessages.slice(-3);
for (const assistant of recentAssistants) {
if (!assistant.parts)
continue;
let responseText = "";
for (const part of assistant.parts) {
if (part.type !== "text")
continue;
responseText += `${responseText ? `
` : ""}${part.text ?? ""}`;
}
if (pattern.test(responseText)) {
return true;
}
}
return false;
} catch (err) {
setTimeout(() => {
log(`[${HOOK_NAME}] Session messages check failed`, {
sessionID: options.sessionID,
error: String(err)
});
}, 0);
return false;
}
}
// src/ralph-loop/system-directive.ts
var SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: OH-MY-OPENCODE";
// src/ralph-loop/continuation-prompt-builder.ts
var CONTINUATION_PROMPT = `${SYSTEM_DIRECTIVE_PREFIX} - RALPH LOOP {{ITERATION}}/{{MAX}}]
Your previous attempt did not output the completion promise. Continue working on the task.
IMPORTANT:
- Review your progress so far
- Continue from where you left off
- When FULLY complete, output: {{PROMISE}}
- Do not stop until the task is truly done
Original task:
{{PROMPT}}`;
function buildContinuationPrompt(state) {
const continuationPrompt = CONTINUATION_PROMPT.replace("{{ITERATION}}", String(state.iteration)).replace("{{MAX}}", String(state.max_iterations)).replace("{{PROMISE}}", state.completion_promise).replace("{{PROMPT}}", state.prompt);
return state.ultrawork ? `ultrawork ${continuationPrompt}` : continuationPrompt;
}
// src/ralph-loop/internal-initiator-marker.ts
var OMO_INTERNAL_INITIATOR_MARKER = "";
function createInternalAgentTextPart(text) {
return {
type: "text",
text: `${text}
${OMO_INTERNAL_INITIATOR_MARKER}`
};
}
// src/ralph-loop/normalize-sdk-response.ts
function normalizeSDKResponse(response, fallback, options) {
const prefer = options?.preferResponseOnMissingData ?? false;
if (response && typeof response === "object") {
const rec = response;
if ("data" in rec) {
const d = rec.data;
if (d !== undefined)
return d;
}
if ("200" in rec) {
const d = rec["200"];
if (d !== undefined)
return d;
}
}
if (prefer && response !== undefined) {
return response;
}
return fallback;
}
// src/ralph-loop/prompt-tools.ts
function normalizePromptTools(tools) {
if (!tools)
return;
const normalized = {};
for (const [toolName, permission] of Object.entries(tools)) {
if (permission === false || permission === "deny") {
normalized[toolName] = false;
continue;
}
if (permission === true || permission === "allow" || permission === "ask") {
normalized[toolName] = true;
}
}
return Object.keys(normalized).length > 0 ? normalized : undefined;
}
// src/ralph-loop/continuation-prompt-injector.ts
function isRecord2(value) {
return typeof value === "object" && value !== null;
}
function getPromptApi(client) {
const clientRecord = client;
if (!isRecord2(clientRecord))
return null;
const sessionValue = clientRecord.session;
if (!isRecord2(sessionValue))
return null;
const promptAsyncValue = sessionValue.promptAsync;
if (typeof promptAsyncValue === "function") {
return (args) => Reflect.apply(promptAsyncValue, sessionValue, [args]);
}
const promptValue = sessionValue.prompt;
if (typeof promptValue === "function") {
return (args) => Reflect.apply(promptValue, sessionValue, [args]);
}
return null;
}
async function injectContinuationPrompt(ctx, options) {
let agent;
let model;
let tools;
const sourceSessionID = options.inheritFromSessionID ?? options.sessionID;
try {
const sessionApi = ctx.client.session;
if (typeof sessionApi.messages !== "function") {
throw new Error("OpenCode client missing session.messages");
}
const messagesResp = await withTimeout(sessionApi.messages({
path: { id: sourceSessionID },
query: { directory: options.directory }
}), options.apiTimeoutMs);
const messages = normalizeSDKResponse(messagesResp, [], {
preferResponseOnMissingData: true
});
for (let i = messages.length - 1;i >= 0; i--) {
const info = messages[i]?.info;
if (info?.agent || info?.model || info?.modelID && info?.providerID) {
agent = info.agent;
model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined);
tools = info.tools;
break;
}
}
} catch {}
const inheritedTools = normalizePromptTools(tools);
const promptApi = getPromptApi(ctx.client);
if (!promptApi) {
throw new Error("OpenCode client missing session.prompt/promptAsync");
}
await promptApi({
path: { id: options.sessionID },
body: {
...agent !== undefined ? { agent } : {},
...model !== undefined ? { model } : {},
...inheritedTools ? { tools: inheritedTools } : {},
parts: [createInternalAgentTextPart(options.prompt)]
},
query: { directory: options.directory }
});
log("[ralph-loop] continuation injected", { sessionID: options.sessionID });
}
// src/ralph-loop/session-reset-strategy.ts
function isRecord3(value) {
return typeof value === "object" && value !== null;
}
async function createIterationSession(ctx, parentSessionID, directory) {
const sessionApi = ctx.client.session;
if (typeof sessionApi.create !== "function")
return null;
const createResult = await sessionApi.create({
body: { parentID: parentSessionID, title: "Ralph Loop Iteration" },
query: { directory }
});
const data = isRecord3(createResult) ? createResult.data : undefined;
const id = isRecord3(data) ? data.id : undefined;
const error = isRecord3(createResult) ? createResult.error : undefined;
if (error !== undefined || typeof id !== "string" || id.length === 0) {
log("[ralph-loop] Failed to create iteration session", {
parentSessionID,
error: String(error ?? "No session ID returned")
});
return null;
}
return id;
}
async function selectSessionInTui(client, sessionID) {
const selectSession = getSelectSessionApi(client);
if (!selectSession)
return false;
try {
await selectSession({ body: { sessionID } });
return true;
} catch (error) {
log("[ralph-loop] Failed to select session in TUI", {
sessionID,
error: String(error)
});
return false;
}
}
function getSelectSessionApi(client) {
if (!isRecord3(client))
return null;
const tuiValue = client.tui;
if (!isRecord3(tuiValue))
return null;
const selectSessionValue = tuiValue.selectSession;
if (typeof selectSessionValue !== "function")
return null;
return (args) => Reflect.apply(selectSessionValue, tuiValue, [args]);
}
// src/ralph-loop/iteration-continuation.ts
async function continueIteration(ctx, state, options) {
const strategy = state.strategy ?? "continue";
const continuationPrompt = buildContinuationPrompt(state);
if (strategy === "reset") {
const newSessionID = await createIterationSession(ctx, options.previousSessionID, options.directory);
if (!newSessionID)
return;
await injectContinuationPrompt(ctx, {
sessionID: newSessionID,
inheritFromSessionID: options.previousSessionID,
prompt: continuationPrompt,
directory: options.directory,
apiTimeoutMs: options.apiTimeoutMs
});
await selectSessionInTui(ctx.client, newSessionID);
const boundState = options.loopState.setSessionID(newSessionID);
if (!boundState) {
log(`[${HOOK_NAME}] Failed to bind loop state to new session`, {
previousSessionID: options.previousSessionID,
newSessionID
});
return;
}
return;
}
await injectContinuationPrompt(ctx, {
sessionID: options.previousSessionID,
prompt: continuationPrompt,
directory: options.directory,
apiTimeoutMs: options.apiTimeoutMs
});
}
// src/ralph-loop/ralph-loop-event-handler.ts
function isRecord4(value) {
return typeof value === "object" && value !== null;
}
function getShowToastApi(client) {
if (!isRecord4(client))
return null;
const tui = client.tui;
if (!isRecord4(tui))
return null;
const showToast = tui.showToast;
if (typeof showToast !== "function")
return null;
return (args) => Reflect.apply(showToast, tui, [args]);
}
function createRalphLoopEventHandler(ctx, options) {
return async ({ event }) => {
const props = event.properties;
const showToast = getShowToastApi(ctx.client);
if (event.type === "session.idle") {
const sessionID = props?.sessionID;
if (!sessionID)
return;
if (options.sessionRecovery.isRecovering(sessionID)) {
log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID });
return;
}
const state = options.loopState.getState();
if (!state || !state.active)
return;
if (state.session_id && state.session_id !== sessionID) {
if (options.checkSessionExists) {
try {
const exists = await options.checkSessionExists(state.session_id);
if (!exists) {
options.loopState.clear();
log(`[${HOOK_NAME}] Cleared orphaned state from deleted session`, {
orphanedSessionId: state.session_id,
currentSessionId: sessionID
});
return;
}
} catch (err) {
log(`[${HOOK_NAME}] Failed to check session existence`, {
sessionId: state.session_id,
error: String(err)
});
}
}
return;
}
const transcriptPath = options.getTranscriptPath(sessionID);
const completionViaTranscript = detectCompletionInTranscript(transcriptPath, state.completion_promise);
const completionViaApi = completionViaTranscript ? false : await detectCompletionInSessionMessages(ctx, {
sessionID,
promise: state.completion_promise,
apiTimeoutMs: options.apiTimeoutMs,
directory: options.directory
});
if (completionViaTranscript || completionViaApi) {
log(`[${HOOK_NAME}] Completion detected!`, {
sessionID,
iteration: state.iteration,
promise: state.completion_promise,
detectedVia: completionViaTranscript ? "transcript_file" : "session_messages_api"
});
options.loopState.clear();
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 showToast?.({ body: { title, message, variant: "success", duration: 5000 } }).catch(() => {});
return;
}
if (state.iteration >= state.max_iterations) {
log(`[${HOOK_NAME}] Max iterations reached`, {
sessionID,
iteration: state.iteration,
max: state.max_iterations
});
options.loopState.clear();
await showToast?.({
body: {
title: "Ralph Loop Stopped",
message: `Max iterations (${state.max_iterations}) reached without completion`,
variant: "warning",
duration: 5000
}
}).catch(() => {});
return;
}
const newState = options.loopState.incrementIteration();
if (!newState) {
log(`[${HOOK_NAME}] Failed to increment iteration`, { sessionID });
return;
}
log(`[${HOOK_NAME}] Continuing loop`, {
sessionID,
iteration: newState.iteration,
max: newState.max_iterations
});
await showToast?.({
body: {
title: "Ralph Loop",
message: `Iteration ${newState.iteration}/${newState.max_iterations}`,
variant: "info",
duration: 2000
}
}).catch(() => {});
try {
await continueIteration(ctx, newState, {
previousSessionID: sessionID,
directory: options.directory,
apiTimeoutMs: options.apiTimeoutMs,
loopState: options.loopState
});
} catch (err) {
log(`[${HOOK_NAME}] Failed to inject continuation`, {
sessionID,
error: String(err)
});
}
return;
}
if (event.type === "session.deleted") {
const sessionInfo = props?.info;
if (!sessionInfo?.id)
return;
const state = options.loopState.getState();
if (state?.session_id === sessionInfo.id) {
options.loopState.clear();
log(`[${HOOK_NAME}] Session deleted, loop cleared`, { sessionID: sessionInfo.id });
}
options.sessionRecovery.clear(sessionInfo.id);
return;
}
if (event.type === "session.error") {
const sessionID = props?.sessionID;
const error = props?.error;
if (error?.name === "MessageAbortedError") {
if (sessionID) {
const state = options.loopState.getState();
if (state?.session_id === sessionID) {
options.loopState.clear();
log(`[${HOOK_NAME}] User aborted, loop cleared`, { sessionID });
}
options.sessionRecovery.clear(sessionID);
}
return;
}
if (sessionID) {
options.sessionRecovery.markRecovering(sessionID);
}
}
};
}
// src/ralph-loop/ralph-loop-hook.ts
var DEFAULT_API_TIMEOUT = 5000;
function createRalphLoopHook(ctx, options) {
const config = options?.config;
const stateDir = config?.state_dir;
const getTranscriptPath2 = options?.getTranscriptPath ?? ((id) => getTranscriptPath(id));
const apiTimeout = options?.apiTimeout ?? DEFAULT_API_TIMEOUT;
const checkSessionExists = options?.checkSessionExists;
const loopState = createLoopStateController({
directory: ctx.directory,
stateDir,
config
});
const sessionRecovery = createLoopSessionRecovery();
const event = createRalphLoopEventHandler(ctx, {
directory: ctx.directory,
apiTimeoutMs: apiTimeout,
getTranscriptPath: getTranscriptPath2,
checkSessionExists,
sessionRecovery,
loopState
});
return {
event,
startLoop: loopState.startLoop,
cancelLoop: loopState.cancelLoop,
getState: loopState.getState
};
}
// src/index.ts
function isRecord5(value) {
return typeof value === "object" && value !== null;
}
var RalphLoopPlugin = async (ctx) => {
const ralphConfig = loadRalphLoopConfig(ctx.directory);
const ralphLoop = createRalphLoopHook(ctx, {
config: ralphConfig,
checkSessionExists: async (sessionID) => {
try {
const sessionApi = ctx.client.session;
if (typeof sessionApi.get !== "function")
return false;
const response = await sessionApi.get({
path: { id: sessionID },
query: { directory: ctx.directory }
});
const data = isRecord5(response) && "data" in response ? response.data : isRecord5(response) && ("200" in response) ? response["200"] : response;
const id = isRecord5(data) ? data.id : undefined;
return typeof id === "string" && id.length > 0;
} catch {
return false;
}
}
});
return {
config: async (config) => {
const existing = config.command ?? {};
config.command = { ...RALPH_LOOP_COMMANDS, ...existing };
},
"tool.execute.before": async (input, output) => {
if (!ralphLoop)
return;
if (input.tool !== "skill")
return;
const rawName = typeof output.args.name === "string" ? output.args.name : undefined;
if (!rawName)
return;
const command = rawName.replace(/^\//, "").toLowerCase();
const sessionID = input.sessionID;
if (!sessionID)
return;
if (command.startsWith("ralph-loop")) {
const rawArgs = rawName.replace(/^\/?(ralph-loop)\s*/i, "");
const parsed = parseRalphLoopArguments(rawArgs);
ralphLoop.startLoop(sessionID, parsed.prompt, {
maxIterations: parsed.maxIterations,
completionPromise: parsed.completionPromise,
strategy: parsed.strategy
});
} else if (command.startsWith("ulw-loop")) {
const rawArgs = rawName.replace(/^\/?(ulw-loop)\s*/i, "");
const parsed = parseRalphLoopArguments(rawArgs);
ralphLoop.startLoop(sessionID, parsed.prompt, {
ultrawork: true,
maxIterations: parsed.maxIterations,
completionPromise: parsed.completionPromise,
strategy: parsed.strategy
});
} else if (command.startsWith("cancel-ralph")) {
ralphLoop.cancelLoop(sessionID);
}
},
"chat.message": async (input, output) => {
if (!ralphLoop)
return;
const parts = output.parts;
const promptText = parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
`).trim() || "";
const isRalphLoopTemplate = promptText.includes("You are starting a Ralph Loop") && promptText.includes("");
const isCancelRalphTemplate = promptText.includes("Cancel the currently active Ralph Loop");
if (isRalphLoopTemplate) {
const taskMatch = promptText.match(/\s*([\s\S]*?)\s*<\/user-task>/i);
const rawTask = taskMatch?.[1]?.trim() || "";
const parsed = parseRalphLoopArguments(rawTask);
ralphLoop.startLoop(input.sessionID, parsed.prompt, {
maxIterations: parsed.maxIterations,
completionPromise: parsed.completionPromise,
strategy: parsed.strategy
});
} else if (isCancelRalphTemplate) {
ralphLoop.cancelLoop(input.sessionID);
}
},
event: async (input) => {
await ralphLoop.event(input);
}
};
};
var src_default = RalphLoopPlugin;
export {
src_default as default
};