diff --git a/.gitignore b/.gitignore
index ba34fb6..2752eb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-dist/
node_modules/
.DS_Store
diff --git a/dist/index.d.ts b/dist/index.d.ts
new file mode 100644
index 0000000..61ee373
--- /dev/null
+++ b/dist/index.d.ts
@@ -0,0 +1,3 @@
+import type { Plugin } from "@opencode-ai/plugin";
+declare const RalphLoopPlugin: Plugin;
+export default RalphLoopPlugin;
diff --git a/dist/index.js b/dist/index.js
new file mode 100644
index 0000000..9203a53
--- /dev/null
+++ b/dist/index.js
@@ -0,0 +1,1036 @@
+// @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
+};
diff --git a/dist/ralph-loop/claude-config-dir.d.ts b/dist/ralph-loop/claude-config-dir.d.ts
new file mode 100644
index 0000000..d44e148
--- /dev/null
+++ b/dist/ralph-loop/claude-config-dir.d.ts
@@ -0,0 +1 @@
+export declare function getClaudeConfigDir(): string;
diff --git a/dist/ralph-loop/command-arguments.d.ts b/dist/ralph-loop/command-arguments.d.ts
new file mode 100644
index 0000000..8c0b542
--- /dev/null
+++ b/dist/ralph-loop/command-arguments.d.ts
@@ -0,0 +1,8 @@
+export type RalphLoopStrategy = "reset" | "continue";
+export type ParsedRalphLoopArguments = {
+ prompt: string;
+ maxIterations?: number;
+ completionPromise?: string;
+ strategy?: RalphLoopStrategy;
+};
+export declare function parseRalphLoopArguments(rawArguments: string): ParsedRalphLoopArguments;
diff --git a/dist/ralph-loop/commands.d.ts b/dist/ralph-loop/commands.d.ts
new file mode 100644
index 0000000..137128a
--- /dev/null
+++ b/dist/ralph-loop/commands.d.ts
@@ -0,0 +1,3 @@
+export declare const RALPH_LOOP_TEMPLATE = "You are starting a Ralph Loop - a self-referential development loop that runs until task completion.\n\n## How Ralph Loop Works\n\n1. You will work on the task continuously\n2. When you believe the task is FULLY complete, output: `{{COMPLETION_PROMISE}}`\n3. If you don't output the promise, the loop will automatically inject another prompt to continue\n4. Maximum iterations: Configurable (default 100)\n\n## Rules\n\n- Focus on completing the task fully, not partially\n- Don't output the completion promise until the task is truly done\n- Each iteration should make meaningful progress toward the goal\n- If stuck, try different approaches\n- Use todos to track your progress\n\n## Exit Conditions\n\n1. **Completion**: Output your completion promise tag when fully complete\n2. **Max Iterations**: Loop stops automatically at limit\n3. **Cancel**: User runs `/cancel-ralph` command\n\n## Your Task\n\nParse the arguments below and begin working on the task. The format is:\n`\"task description\" [--completion-promise=TEXT] [--max-iterations=N] [--strategy=reset|continue]`\n\nDefault completion promise is \"DONE\" and default max iterations is 100.";
+export declare const CANCEL_RALPH_TEMPLATE = "Cancel the currently active Ralph Loop.\n\nThis will:\n1. Stop the loop from continuing\n2. Clear the loop state file\n3. Allow the session to end normally\n\nCheck if a loop is active and cancel it. Inform the user of the result.";
+export declare const RALPH_LOOP_COMMANDS: Record;
diff --git a/dist/ralph-loop/completion-promise-detector.d.ts b/dist/ralph-loop/completion-promise-detector.d.ts
new file mode 100644
index 0000000..e61f927
--- /dev/null
+++ b/dist/ralph-loop/completion-promise-detector.d.ts
@@ -0,0 +1,8 @@
+import type { PluginInput } from "@opencode-ai/plugin";
+export declare function detectCompletionInTranscript(transcriptPath: string | undefined, promise: string): boolean;
+export declare function detectCompletionInSessionMessages(ctx: PluginInput, options: {
+ sessionID: string;
+ promise: string;
+ apiTimeoutMs: number;
+ directory: string;
+}): Promise;
diff --git a/dist/ralph-loop/config.d.ts b/dist/ralph-loop/config.d.ts
new file mode 100644
index 0000000..212461a
--- /dev/null
+++ b/dist/ralph-loop/config.d.ts
@@ -0,0 +1,8 @@
+export type RalphLoopStrategy = "reset" | "continue";
+export type RalphLoopConfig = {
+ enabled?: boolean;
+ default_max_iterations?: number;
+ state_dir?: string;
+ default_strategy?: RalphLoopStrategy;
+};
+export declare function loadRalphLoopConfig(projectDir: string): RalphLoopConfig;
diff --git a/dist/ralph-loop/constants.d.ts b/dist/ralph-loop/constants.d.ts
new file mode 100644
index 0000000..e980c6b
--- /dev/null
+++ b/dist/ralph-loop/constants.d.ts
@@ -0,0 +1,4 @@
+export declare const HOOK_NAME = "ralph-loop";
+export declare const DEFAULT_STATE_FILE = ".sisyphus/ralph-loop.local.md";
+export declare const DEFAULT_MAX_ITERATIONS = 100;
+export declare const DEFAULT_COMPLETION_PROMISE = "DONE";
diff --git a/dist/ralph-loop/continuation-prompt-builder.d.ts b/dist/ralph-loop/continuation-prompt-builder.d.ts
new file mode 100644
index 0000000..4f3d2a1
--- /dev/null
+++ b/dist/ralph-loop/continuation-prompt-builder.d.ts
@@ -0,0 +1,2 @@
+import type { RalphLoopState } from "./types";
+export declare function buildContinuationPrompt(state: RalphLoopState): string;
diff --git a/dist/ralph-loop/continuation-prompt-injector.d.ts b/dist/ralph-loop/continuation-prompt-injector.d.ts
new file mode 100644
index 0000000..1aa451e
--- /dev/null
+++ b/dist/ralph-loop/continuation-prompt-injector.d.ts
@@ -0,0 +1,8 @@
+import type { PluginInput } from "@opencode-ai/plugin";
+export declare function injectContinuationPrompt(ctx: PluginInput, options: {
+ sessionID: string;
+ prompt: string;
+ directory: string;
+ apiTimeoutMs: number;
+ inheritFromSessionID?: string;
+}): Promise;
diff --git a/dist/ralph-loop/internal-initiator-marker.d.ts b/dist/ralph-loop/internal-initiator-marker.d.ts
new file mode 100644
index 0000000..2610934
--- /dev/null
+++ b/dist/ralph-loop/internal-initiator-marker.d.ts
@@ -0,0 +1,5 @@
+export declare const OMO_INTERNAL_INITIATOR_MARKER = "";
+export declare function createInternalAgentTextPart(text: string): {
+ type: "text";
+ text: string;
+};
diff --git a/dist/ralph-loop/iteration-continuation.d.ts b/dist/ralph-loop/iteration-continuation.d.ts
new file mode 100644
index 0000000..43fb9fa
--- /dev/null
+++ b/dist/ralph-loop/iteration-continuation.d.ts
@@ -0,0 +1,12 @@
+import type { PluginInput } from "@opencode-ai/plugin";
+import type { RalphLoopState } from "./types";
+type ContinuationOptions = {
+ directory: string;
+ apiTimeoutMs: number;
+ previousSessionID: string;
+ loopState: {
+ setSessionID: (sessionID: string) => RalphLoopState | null;
+ };
+};
+export declare function continueIteration(ctx: PluginInput, state: RalphLoopState, options: ContinuationOptions): Promise;
+export {};
diff --git a/dist/ralph-loop/logger.d.ts b/dist/ralph-loop/logger.d.ts
new file mode 100644
index 0000000..1aff0a6
--- /dev/null
+++ b/dist/ralph-loop/logger.d.ts
@@ -0,0 +1,2 @@
+export declare function log(message: string, meta?: unknown): void;
+export declare function getLogFilePath(): string;
diff --git a/dist/ralph-loop/loop-session-recovery.d.ts b/dist/ralph-loop/loop-session-recovery.d.ts
new file mode 100644
index 0000000..6fd01aa
--- /dev/null
+++ b/dist/ralph-loop/loop-session-recovery.d.ts
@@ -0,0 +1,7 @@
+export declare function createLoopSessionRecovery(options?: {
+ recoveryWindowMs?: number;
+}): {
+ isRecovering(sessionID: string): boolean;
+ markRecovering(sessionID: string): void;
+ clear(sessionID: string): void;
+};
diff --git a/dist/ralph-loop/loop-state-controller.d.ts b/dist/ralph-loop/loop-state-controller.d.ts
new file mode 100644
index 0000000..c88bc08
--- /dev/null
+++ b/dist/ralph-loop/loop-state-controller.d.ts
@@ -0,0 +1,18 @@
+import type { RalphLoopOptions, RalphLoopState } from "./types";
+export declare function createLoopStateController(options: {
+ directory: string;
+ stateDir: string | undefined;
+ config: RalphLoopOptions["config"] | undefined;
+}): {
+ startLoop(sessionID: string, prompt: string, loopOptions?: {
+ maxIterations?: number;
+ completionPromise?: string;
+ ultrawork?: boolean;
+ strategy?: "reset" | "continue";
+ }): boolean;
+ cancelLoop(sessionID: string): boolean;
+ getState(): RalphLoopState | null;
+ clear(): boolean;
+ incrementIteration(): RalphLoopState | null;
+ setSessionID(sessionID: string): RalphLoopState | null;
+};
diff --git a/dist/ralph-loop/normalize-sdk-response.d.ts b/dist/ralph-loop/normalize-sdk-response.d.ts
new file mode 100644
index 0000000..c9b3634
--- /dev/null
+++ b/dist/ralph-loop/normalize-sdk-response.d.ts
@@ -0,0 +1,3 @@
+export declare function normalizeSDKResponse(response: unknown, fallback: T, options?: {
+ preferResponseOnMissingData?: boolean;
+}): T;
diff --git a/dist/ralph-loop/prompt-tools.d.ts b/dist/ralph-loop/prompt-tools.d.ts
new file mode 100644
index 0000000..0e4affe
--- /dev/null
+++ b/dist/ralph-loop/prompt-tools.d.ts
@@ -0,0 +1,2 @@
+export type PromptToolPermission = boolean | "allow" | "deny" | "ask";
+export declare function normalizePromptTools(tools: Record | undefined): Record | undefined;
diff --git a/dist/ralph-loop/ralph-loop-event-handler.d.ts b/dist/ralph-loop/ralph-loop-event-handler.d.ts
new file mode 100644
index 0000000..67a9534
--- /dev/null
+++ b/dist/ralph-loop/ralph-loop-event-handler.d.ts
@@ -0,0 +1,28 @@
+import type { PluginInput } from "@opencode-ai/plugin";
+import type { RalphLoopOptions, RalphLoopState } from "./types";
+type SessionRecovery = {
+ isRecovering: (sessionID: string) => boolean;
+ markRecovering: (sessionID: string) => void;
+ clear: (sessionID: string) => void;
+};
+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 declare function createRalphLoopEventHandler(ctx: PluginInput, options: RalphLoopEventHandlerOptions): ({ event }: {
+ event: {
+ type: string;
+ properties?: unknown;
+ };
+}) => Promise;
+export {};
diff --git a/dist/ralph-loop/ralph-loop-hook.d.ts b/dist/ralph-loop/ralph-loop-hook.d.ts
new file mode 100644
index 0000000..80c1391
--- /dev/null
+++ b/dist/ralph-loop/ralph-loop-hook.d.ts
@@ -0,0 +1,19 @@
+import type { PluginInput } from "@opencode-ai/plugin";
+import type { RalphLoopOptions, RalphLoopState } from "./types";
+export interface RalphLoopHook {
+ event: (input: {
+ event: {
+ type: string;
+ properties?: unknown;
+ };
+ }) => Promise;
+ startLoop: (sessionID: string, prompt: string, options?: {
+ maxIterations?: number;
+ completionPromise?: string;
+ ultrawork?: boolean;
+ strategy?: "reset" | "continue";
+ }) => boolean;
+ cancelLoop: (sessionID: string) => boolean;
+ getState: () => RalphLoopState | null;
+}
+export declare function createRalphLoopHook(ctx: PluginInput, options?: RalphLoopOptions): RalphLoopHook;
diff --git a/dist/ralph-loop/session-reset-strategy.d.ts b/dist/ralph-loop/session-reset-strategy.d.ts
new file mode 100644
index 0000000..0619991
--- /dev/null
+++ b/dist/ralph-loop/session-reset-strategy.d.ts
@@ -0,0 +1,3 @@
+import type { PluginInput } from "@opencode-ai/plugin";
+export declare function createIterationSession(ctx: PluginInput, parentSessionID: string, directory: string): Promise;
+export declare function selectSessionInTui(client: PluginInput["client"], sessionID: string): Promise;
diff --git a/dist/ralph-loop/simple-frontmatter.d.ts b/dist/ralph-loop/simple-frontmatter.d.ts
new file mode 100644
index 0000000..7aaab1d
--- /dev/null
+++ b/dist/ralph-loop/simple-frontmatter.d.ts
@@ -0,0 +1,4 @@
+export declare function parseFrontmatter(content: string): {
+ data: Record;
+ body: string;
+};
diff --git a/dist/ralph-loop/storage.d.ts b/dist/ralph-loop/storage.d.ts
new file mode 100644
index 0000000..72948fc
--- /dev/null
+++ b/dist/ralph-loop/storage.d.ts
@@ -0,0 +1,6 @@
+import type { RalphLoopState } from "./types";
+export declare function getStateFilePath(directory: string, customPath?: string): string;
+export declare function readState(directory: string, customPath?: string): RalphLoopState | null;
+export declare function writeState(directory: string, state: RalphLoopState, customPath?: string): boolean;
+export declare function clearState(directory: string, customPath?: string): boolean;
+export declare function incrementIteration(directory: string, customPath?: string): RalphLoopState | null;
diff --git a/dist/ralph-loop/system-directive.d.ts b/dist/ralph-loop/system-directive.d.ts
new file mode 100644
index 0000000..c556495
--- /dev/null
+++ b/dist/ralph-loop/system-directive.d.ts
@@ -0,0 +1 @@
+export declare const SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: OH-MY-OPENCODE";
diff --git a/dist/ralph-loop/transcript.d.ts b/dist/ralph-loop/transcript.d.ts
new file mode 100644
index 0000000..3ba9503
--- /dev/null
+++ b/dist/ralph-loop/transcript.d.ts
@@ -0,0 +1 @@
+export declare function getTranscriptPath(sessionId: string): string;
diff --git a/dist/ralph-loop/types.d.ts b/dist/ralph-loop/types.d.ts
new file mode 100644
index 0000000..61dbd5d
--- /dev/null
+++ b/dist/ralph-loop/types.d.ts
@@ -0,0 +1,18 @@
+import type { RalphLoopConfig } from "./config";
+export interface RalphLoopState {
+ active: boolean;
+ iteration: number;
+ max_iterations: number;
+ completion_promise: string;
+ started_at: string;
+ prompt: string;
+ session_id?: string;
+ ultrawork?: boolean;
+ strategy?: "reset" | "continue";
+}
+export interface RalphLoopOptions {
+ config?: RalphLoopConfig;
+ getTranscriptPath?: (sessionId: string) => string | undefined;
+ apiTimeout?: number;
+ checkSessionExists?: (sessionId: string) => Promise;
+}
diff --git a/dist/ralph-loop/with-timeout.d.ts b/dist/ralph-loop/with-timeout.d.ts
new file mode 100644
index 0000000..87fb4a5
--- /dev/null
+++ b/dist/ralph-loop/with-timeout.d.ts
@@ -0,0 +1 @@
+export declare function withTimeout(promise: Promise, timeoutMs: number): Promise;