@sureshsankaran/ralph-wiggum 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +83 -74
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  const STATE_FILE = ".opencode/ralph-loop.local.md";
4
+ const LOCK_FILE = ".opencode/ralph-loop.lock";
4
5
  function parseState(content) {
5
6
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
6
7
  if (!frontmatterMatch)
@@ -43,10 +44,30 @@ function extractPromiseText(text) {
43
44
  const match = text.match(/<promise>(.*?)<\/promise>/s);
44
45
  return match ? match[1].trim().replace(/\s+/g, " ") : null;
45
46
  }
46
- function updateIteration(filePath, newIteration) {
47
- const content = readFileSync(filePath, "utf-8");
48
- const updated = content.replace(/^iteration: \d+$/m, `iteration: ${newIteration}`);
49
- writeFileSync(filePath, updated);
47
+ // Atomically increment iteration using file-based locking
48
+ function tryIncrementIteration(stateFilePath, lockFilePath, expectedIteration) {
49
+ // Try to acquire lock by checking if lock file exists with different iteration
50
+ if (existsSync(lockFilePath)) {
51
+ const lockContent = readFileSync(lockFilePath, "utf-8").trim();
52
+ const lockedIteration = parseInt(lockContent, 10);
53
+ // If lock exists for same or higher iteration, someone else is handling it
54
+ if (!isNaN(lockedIteration) && lockedIteration >= expectedIteration) {
55
+ return null;
56
+ }
57
+ }
58
+ // Write our lock
59
+ const nextIteration = expectedIteration + 1;
60
+ writeFileSync(lockFilePath, String(nextIteration));
61
+ // Double-check we got the lock (simple mutex)
62
+ const lockCheck = readFileSync(lockFilePath, "utf-8").trim();
63
+ if (parseInt(lockCheck, 10) !== nextIteration) {
64
+ return null; // Someone else won the race
65
+ }
66
+ // Update state file
67
+ const content = readFileSync(stateFilePath, "utf-8");
68
+ const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
69
+ writeFileSync(stateFilePath, updated);
70
+ return nextIteration;
50
71
  }
51
72
  /**
52
73
  * Ralph Wiggum Plugin - Iterative AI Development
@@ -62,13 +83,9 @@ function updateIteration(filePath, newIteration) {
62
83
  */
63
84
  export const RalphWiggumPlugin = async ({ client, directory }) => {
64
85
  const stateFilePath = join(directory, STATE_FILE);
65
- const lockFilePath = join(directory, ".opencode/ralph-loop.lock");
86
+ const lockFilePath = join(directory, LOCK_FILE);
66
87
  // Track if completion was detected to prevent race conditions
67
88
  let completionDetected = false;
68
- // Track the last iteration we processed to prevent duplicate handling
69
- let lastProcessedIteration = 0;
70
- // Lock to prevent concurrent event handling
71
- let isProcessing = false;
72
89
  return {
73
90
  event: async ({ event }) => {
74
91
  // Check message.part.updated events for completion promise
@@ -87,8 +104,8 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
87
104
  }
88
105
  const promiseText = extractPromiseText(part.text);
89
106
  if (promiseText === state.completion_promise) {
90
- console.log(`Ralph loop: Detected <promise>${state.completion_promise}</promise> - task complete!`);
91
107
  completionDetected = true;
108
+ console.log(`\nRalph loop: Detected <promise>${state.completion_promise}</promise> - task complete!`);
92
109
  try {
93
110
  unlinkSync(stateFilePath);
94
111
  }
@@ -110,81 +127,73 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
110
127
  return;
111
128
  // If completion was already detected, don't continue
112
129
  if (completionDetected) {
130
+ console.log("\nRalph loop: Completion already detected, not continuing");
113
131
  return;
114
132
  }
115
- // Prevent concurrent processing
116
- if (isProcessing) {
133
+ const sessionID = event.properties?.sessionID;
134
+ // Check if ralph-loop is active
135
+ if (!existsSync(stateFilePath))
136
+ return;
137
+ const content = readFileSync(stateFilePath, "utf-8");
138
+ const state = parseState(content);
139
+ if (!state || !state.active) {
117
140
  return;
118
141
  }
119
- isProcessing = true;
120
- try {
121
- const sessionID = event.properties?.sessionID;
122
- // Check if ralph-loop is active
123
- if (!existsSync(stateFilePath))
124
- return;
125
- const content = readFileSync(stateFilePath, "utf-8");
126
- const state = parseState(content);
127
- if (!state || !state.active) {
128
- return;
129
- }
130
- // Prevent processing the same iteration multiple times
131
- if (state.iteration <= lastProcessedIteration) {
132
- return;
142
+ // Validate numeric fields
143
+ if (isNaN(state.iteration) || isNaN(state.max_iterations)) {
144
+ console.error("\nRalph loop: State file corrupted - invalid numeric fields");
145
+ try {
146
+ unlinkSync(stateFilePath);
133
147
  }
134
- console.log("Ralph loop: session idle, iteration " + state.iteration);
135
- // Validate numeric fields
136
- if (isNaN(state.iteration) || isNaN(state.max_iterations)) {
137
- console.error("Ralph loop: State file corrupted - invalid numeric fields");
138
- try {
139
- unlinkSync(stateFilePath);
140
- }
141
- catch { }
142
- return;
148
+ catch { }
149
+ try {
150
+ unlinkSync(lockFilePath);
143
151
  }
144
- // Check if max iterations reached
145
- if (state.max_iterations > 0 && state.iteration >= state.max_iterations) {
146
- console.log(`Ralph loop: Max iterations (${state.max_iterations}) reached.`);
147
- try {
148
- unlinkSync(stateFilePath);
149
- }
150
- catch { }
151
- try {
152
- unlinkSync(lockFilePath);
153
- }
154
- catch { }
155
- return;
152
+ catch { }
153
+ return;
154
+ }
155
+ // Check if max iterations reached
156
+ if (state.max_iterations > 0 && state.iteration >= state.max_iterations) {
157
+ console.log(`\nRalph loop: Max iterations (${state.max_iterations}) reached.`);
158
+ try {
159
+ unlinkSync(stateFilePath);
156
160
  }
157
- // Not complete - continue loop with SAME PROMPT
158
- const nextIteration = state.iteration + 1;
159
- lastProcessedIteration = nextIteration;
160
- updateIteration(stateFilePath, nextIteration);
161
- // Build system message
162
- const systemMsg = state.completion_promise
163
- ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when statement is TRUE - do not lie to exit!)`
164
- : `Ralph iteration ${nextIteration} | No completion promise set - loop runs infinitely`;
165
- console.log(systemMsg);
166
- // Send the same prompt back to continue the session
161
+ catch { }
167
162
  try {
168
- if (sessionID) {
169
- await client.session.promptAsync({
170
- path: { id: sessionID },
171
- body: {
172
- parts: [
173
- {
174
- type: "text",
175
- text: `[${systemMsg}]\n\n${state.prompt}`,
176
- },
177
- ],
178
- },
179
- });
180
- }
163
+ unlinkSync(lockFilePath);
181
164
  }
182
- catch (err) {
183
- console.error("Ralph loop: Failed to send prompt", err);
165
+ catch { }
166
+ return;
167
+ }
168
+ // Try to atomically increment - this prevents race conditions
169
+ const nextIteration = tryIncrementIteration(stateFilePath, lockFilePath, state.iteration);
170
+ if (nextIteration === null) {
171
+ // Another event handler won the race, skip this one
172
+ return;
173
+ }
174
+ console.log(`\nRalph loop: Starting iteration ${nextIteration}`);
175
+ // Build system message
176
+ const systemMsg = state.completion_promise
177
+ ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when statement is TRUE - do not lie to exit!)`
178
+ : `Ralph iteration ${nextIteration} | No completion promise set - loop runs infinitely`;
179
+ // Send the same prompt back to continue the session
180
+ try {
181
+ if (sessionID) {
182
+ await client.session.promptAsync({
183
+ path: { id: sessionID },
184
+ body: {
185
+ parts: [
186
+ {
187
+ type: "text",
188
+ text: `[${systemMsg}]\n\n${state.prompt}`,
189
+ },
190
+ ],
191
+ },
192
+ });
184
193
  }
185
194
  }
186
- finally {
187
- isProcessing = false;
195
+ catch (err) {
196
+ console.error("\nRalph loop: Failed to send prompt", err);
188
197
  }
189
198
  },
190
199
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sureshsankaran/ralph-wiggum",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Ralph Wiggum iterative AI development plugin for OpenCode - continuously loops the same prompt until task completion",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",