@sureshsankaran/ralph-wiggum 0.1.2 → 0.1.4

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 +87 -75
  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,34 @@ 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
+ // Returns the next iteration number if we won the lock, null if another handler is processing
49
+ function tryClaimIteration(stateFilePath, lockFilePath, currentIteration) {
50
+ const nextIteration = currentIteration + 1;
51
+ // Check if lock exists for THIS specific iteration transition
52
+ if (existsSync(lockFilePath)) {
53
+ const lockContent = readFileSync(lockFilePath, "utf-8").trim();
54
+ // Lock format: "from:to" e.g., "1:2" means transitioning from iteration 1 to 2
55
+ const [fromStr, toStr] = lockContent.split(":");
56
+ const from = parseInt(fromStr, 10);
57
+ const to = parseInt(toStr, 10);
58
+ // If lock exists for this exact transition, someone else is handling it
59
+ if (from === currentIteration && to === nextIteration) {
60
+ return null;
61
+ }
62
+ }
63
+ // Write our lock with format "from:to"
64
+ writeFileSync(lockFilePath, `${currentIteration}:${nextIteration}`);
65
+ // Double-check we got the lock
66
+ const lockCheck = readFileSync(lockFilePath, "utf-8").trim();
67
+ if (lockCheck !== `${currentIteration}:${nextIteration}`) {
68
+ return null; // Someone else won the race
69
+ }
70
+ // Update state file
71
+ const content = readFileSync(stateFilePath, "utf-8");
72
+ const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
73
+ writeFileSync(stateFilePath, updated);
74
+ return nextIteration;
50
75
  }
51
76
  /**
52
77
  * Ralph Wiggum Plugin - Iterative AI Development
@@ -62,13 +87,9 @@ function updateIteration(filePath, newIteration) {
62
87
  */
63
88
  export const RalphWiggumPlugin = async ({ client, directory }) => {
64
89
  const stateFilePath = join(directory, STATE_FILE);
65
- const lockFilePath = join(directory, ".opencode/ralph-loop.lock");
90
+ const lockFilePath = join(directory, LOCK_FILE);
66
91
  // Track if completion was detected to prevent race conditions
67
92
  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
93
  return {
73
94
  event: async ({ event }) => {
74
95
  // Check message.part.updated events for completion promise
@@ -87,8 +108,8 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
87
108
  }
88
109
  const promiseText = extractPromiseText(part.text);
89
110
  if (promiseText === state.completion_promise) {
90
- console.log(`Ralph loop: Detected <promise>${state.completion_promise}</promise> - task complete!`);
91
111
  completionDetected = true;
112
+ console.log(`\nRalph loop complete! Detected <promise>${state.completion_promise}</promise>`);
92
113
  try {
93
114
  unlinkSync(stateFilePath);
94
115
  }
@@ -108,83 +129,74 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
108
129
  const isIdle = event.type === "session.status" && event.properties?.status?.type === "idle";
109
130
  if (!isIdle)
110
131
  return;
111
- // If completion was already detected, don't continue
132
+ // If completion was already detected, silently ignore
112
133
  if (completionDetected) {
113
134
  return;
114
135
  }
115
- // Prevent concurrent processing
116
- if (isProcessing) {
136
+ const sessionID = event.properties?.sessionID;
137
+ // Check if ralph-loop is active
138
+ if (!existsSync(stateFilePath))
139
+ return;
140
+ const content = readFileSync(stateFilePath, "utf-8");
141
+ const state = parseState(content);
142
+ if (!state || !state.active) {
117
143
  return;
118
144
  }
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;
145
+ // Validate numeric fields
146
+ if (isNaN(state.iteration) || isNaN(state.max_iterations)) {
147
+ console.error("\nRalph loop: State file corrupted - invalid numeric fields");
148
+ try {
149
+ unlinkSync(stateFilePath);
133
150
  }
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;
151
+ catch { }
152
+ try {
153
+ unlinkSync(lockFilePath);
143
154
  }
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;
155
+ catch { }
156
+ return;
157
+ }
158
+ // Check if max iterations reached
159
+ if (state.max_iterations > 0 && state.iteration >= state.max_iterations) {
160
+ console.log(`\nRalph loop: Max iterations (${state.max_iterations}) reached.`);
161
+ try {
162
+ unlinkSync(stateFilePath);
156
163
  }
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
164
+ catch { }
167
165
  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
- }
166
+ unlinkSync(lockFilePath);
181
167
  }
182
- catch (err) {
183
- console.error("Ralph loop: Failed to send prompt", err);
168
+ catch { }
169
+ return;
170
+ }
171
+ // Try to atomically claim this iteration - this prevents race conditions
172
+ const nextIteration = tryClaimIteration(stateFilePath, lockFilePath, state.iteration);
173
+ if (nextIteration === null) {
174
+ // Another event handler won the race, skip this one silently
175
+ return;
176
+ }
177
+ // Build system message
178
+ const systemMsg = state.completion_promise
179
+ ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
180
+ : `Ralph iteration ${nextIteration} | No completion promise set`;
181
+ console.log(`\n${systemMsg}`);
182
+ // Send the same prompt back to continue the session
183
+ try {
184
+ if (sessionID) {
185
+ await client.session.promptAsync({
186
+ path: { id: sessionID },
187
+ body: {
188
+ parts: [
189
+ {
190
+ type: "text",
191
+ text: `[${systemMsg}]\n\n${state.prompt}`,
192
+ },
193
+ ],
194
+ },
195
+ });
184
196
  }
185
197
  }
186
- finally {
187
- isProcessing = false;
198
+ catch (err) {
199
+ console.error("\nRalph loop: Failed to send prompt", err);
188
200
  }
189
201
  },
190
202
  };
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.4",
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",