feat: add Ralph Loop self-referential development loop (#337)
* feat(config): add RalphLoopConfigSchema and hook name - Add ralph-loop to HookNameSchema enum - Add RalphLoopConfigSchema with enabled, default_max_iterations, state_dir - Add ralph_loop field to OhMyOpenCodeConfigSchema - Export RalphLoopConfig type * feat(ralph-loop): add hook directory structure with constants and types - Add constants.ts with HOOK_NAME, DEFAULT_STATE_FILE, COMPLETION_TAG_PATTERN - Add types.ts with RalphLoopState and RalphLoopOptions interfaces - Export RalphLoopConfig from config/index.ts * feat(ralph-loop): add storage module for markdown state file management - Implement readState/writeState/clearState/incrementIteration - Use YAML frontmatter format for state persistence - Support custom state file paths via config * feat(ralph-loop): implement main hook with session.idle handler - Add createRalphLoopHook factory with event handler - Implement startLoop, cancelLoop, getState API - Detect completion promise in transcript - Auto-continue with iteration tracking - Handle max iterations limit - Show toast notifications for status updates - Support session recovery and cleanup * test(ralph-loop): add comprehensive BDD-style tests - Add 17 test cases covering storage, hook lifecycle, iteration - Test completion detection, cancellation, recovery, session cleanup - Fix storage.ts to handle YAML value parsing correctly - Use BDD #given/#when/#then comments per project convention * feat(builtin-commands): add ralph-loop and cancel-ralph commands * feat(ralph-loop): register hook in main plugin * docs: add Ralph Loop feature to all README files * chore: regenerate JSON schema with ralph-loop config * feat(ralph-loop): change state file path from .opencode to .sisyphus 🤖 Generated with assistance of https://github.com/code-yeongyu/oh-my-opencode * feat(ralph-loop): integrate ralph-loop and cancel-ralph command handlers into plugin hooks - Add chat.message hook to detect and start ralph-loop or cancel-ralph templates - Add slashcommand hook to handle /ralph-loop and /cancel-ralph commands - Support custom --max-iterations and --completion-promise options 🤖 Generated with assistance of https://github.com/code-yeongyu/oh-my-opencode --------- Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
61
src/index.ts
61
src/index.ts
@@ -24,6 +24,7 @@ import {
|
||||
createInteractiveBashSessionHook,
|
||||
createEmptyMessageSanitizerHook,
|
||||
createThinkingBlockValidatorHook,
|
||||
createRalphLoopHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
@@ -310,6 +311,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
? createThinkingBlockValidatorHook()
|
||||
: null;
|
||||
|
||||
const ralphLoop = isHookEnabled("ralph-loop")
|
||||
? createRalphLoopHook(ctx, { config: pluginConfig.ralph_loop })
|
||||
: null;
|
||||
|
||||
const backgroundManager = new BackgroundManager(ctx);
|
||||
|
||||
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
|
||||
@@ -361,6 +366,39 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
"chat.message": async (input, output) => {
|
||||
await claudeCodeHooks["chat.message"]?.(input, output);
|
||||
await keywordDetector?.["chat.message"]?.(input, output);
|
||||
|
||||
if (ralphLoop) {
|
||||
const parts = (output as { parts?: Array<{ type: string; text?: string }> }).parts;
|
||||
const promptText = parts
|
||||
?.filter((p) => p.type === "text" && p.text)
|
||||
.map((p) => p.text)
|
||||
.join("\n")
|
||||
.trim() || "";
|
||||
|
||||
const isRalphLoopTemplate = promptText.includes("You are starting a Ralph Loop") &&
|
||||
promptText.includes("<user-task>");
|
||||
const isCancelRalphTemplate = promptText.includes("Cancel the currently active Ralph Loop");
|
||||
|
||||
if (isRalphLoopTemplate) {
|
||||
const taskMatch = promptText.match(/<user-task>\s*([\s\S]*?)\s*<\/user-task>/i);
|
||||
const rawTask = taskMatch?.[1]?.trim() || "";
|
||||
|
||||
const quotedMatch = rawTask.match(/^["'](.+?)["']/);
|
||||
const prompt = quotedMatch?.[1] || rawTask.split(/\s+--/)[0]?.trim() || "Complete the task as instructed";
|
||||
|
||||
const maxIterMatch = rawTask.match(/--max-iterations=(\d+)/i);
|
||||
const promiseMatch = rawTask.match(/--completion-promise=["']?([^"'\s]+)["']?/i);
|
||||
|
||||
log("[ralph-loop] Starting loop from chat.message", { sessionID: input.sessionID, prompt });
|
||||
ralphLoop.startLoop(input.sessionID, prompt, {
|
||||
maxIterations: maxIterMatch ? parseInt(maxIterMatch[1], 10) : undefined,
|
||||
completionPromise: promiseMatch?.[1],
|
||||
});
|
||||
} else if (isCancelRalphTemplate) {
|
||||
log("[ralph-loop] Cancelling loop from chat.message", { sessionID: input.sessionID });
|
||||
ralphLoop.cancelLoop(input.sessionID);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"experimental.chat.messages.transform": async (
|
||||
@@ -584,6 +622,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await preemptiveCompaction?.event(input);
|
||||
await agentUsageReminder?.event(input);
|
||||
await interactiveBashSession?.event(input);
|
||||
await ralphLoop?.event(input);
|
||||
|
||||
const { event } = input;
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
@@ -650,6 +689,28 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
...(isExploreOrLibrarian ? { call_omo_agent: false } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
if (ralphLoop && input.tool === "slashcommand") {
|
||||
const args = output.args as { command?: string } | undefined;
|
||||
const command = args?.command?.replace(/^\//, "").toLowerCase();
|
||||
const sessionID = input.sessionID || getMainSessionID();
|
||||
|
||||
if (command === "ralph-loop" && sessionID) {
|
||||
const rawArgs = args?.command?.replace(/^\/?(ralph-loop)\s*/i, "") || "";
|
||||
const taskMatch = rawArgs.match(/^["'](.+?)["']/);
|
||||
const prompt = taskMatch?.[1] || rawArgs.split(/\s+--/)[0]?.trim() || "Complete the task as instructed";
|
||||
|
||||
const maxIterMatch = rawArgs.match(/--max-iterations=(\d+)/i);
|
||||
const promiseMatch = rawArgs.match(/--completion-promise=["']?([^"'\s]+)["']?/i);
|
||||
|
||||
ralphLoop.startLoop(sessionID, prompt, {
|
||||
maxIterations: maxIterMatch ? parseInt(maxIterMatch[1], 10) : undefined,
|
||||
completionPromise: promiseMatch?.[1],
|
||||
});
|
||||
} else if (command === "cancel-ralph" && sessionID) {
|
||||
ralphLoop.cancelLoop(sessionID);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tool.execute.after": async (input, output) => {
|
||||
|
||||
Reference in New Issue
Block a user