@sureshsankaran/ralph-wiggum 0.1.3 → 0.1.5

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 +34 -57
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
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";
5
4
  function parseState(content) {
6
5
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
7
6
  if (!frontmatterMatch)
@@ -44,31 +43,6 @@ function extractPromiseText(text) {
44
43
  const match = text.match(/<promise>(.*?)<\/promise>/s);
45
44
  return match ? match[1].trim().replace(/\s+/g, " ") : null;
46
45
  }
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;
71
- }
72
46
  /**
73
47
  * Ralph Wiggum Plugin - Iterative AI Development
74
48
  *
@@ -83,9 +57,11 @@ function tryIncrementIteration(stateFilePath, lockFilePath, expectedIteration) {
83
57
  */
84
58
  export const RalphWiggumPlugin = async ({ client, directory }) => {
85
59
  const stateFilePath = join(directory, STATE_FILE);
86
- const lockFilePath = join(directory, LOCK_FILE);
87
- // Track if completion was detected to prevent race conditions
60
+ // In-memory state to prevent race conditions
61
+ // These are scoped to this plugin instance
88
62
  let completionDetected = false;
63
+ let processingIteration = false;
64
+ let lastProcessedIteration = -1;
89
65
  return {
90
66
  event: async ({ event }) => {
91
67
  // Check message.part.updated events for completion promise
@@ -105,15 +81,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
105
81
  const promiseText = extractPromiseText(part.text);
106
82
  if (promiseText === state.completion_promise) {
107
83
  completionDetected = true;
108
- console.log(`\nRalph loop: Detected <promise>${state.completion_promise}</promise> - task complete!`);
84
+ console.log(`\nRalph loop complete! Detected <promise>${state.completion_promise}</promise>`);
109
85
  try {
110
86
  unlinkSync(stateFilePath);
111
87
  }
112
88
  catch { }
113
- try {
114
- unlinkSync(lockFilePath);
115
- }
116
- catch { }
117
89
  return;
118
90
  }
119
91
  }
@@ -121,13 +93,17 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
121
93
  return;
122
94
  }
123
95
  // Handle session idle to continue the loop
124
- // Only listen to session.status (session.idle is deprecated and fires at the same time)
96
+ // Only listen to session.status (session.idle is deprecated)
125
97
  const isIdle = event.type === "session.status" && event.properties?.status?.type === "idle";
126
98
  if (!isIdle)
127
99
  return;
128
- // If completion was already detected, don't continue
100
+ // If completion was already detected, silently ignore
129
101
  if (completionDetected) {
130
- console.log("\nRalph loop: Completion already detected, not continuing");
102
+ return;
103
+ }
104
+ // If we're already processing an iteration, skip
105
+ // This prevents race conditions when multiple idle events fire
106
+ if (processingIteration) {
131
107
  return;
132
108
  }
133
109
  const sessionID = event.properties?.sessionID;
@@ -146,10 +122,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
146
122
  unlinkSync(stateFilePath);
147
123
  }
148
124
  catch { }
149
- try {
150
- unlinkSync(lockFilePath);
151
- }
152
- catch { }
125
+ return;
126
+ }
127
+ // Check if we've already processed this iteration
128
+ // This handles cases where the state file hasn't been updated yet
129
+ if (state.iteration <= lastProcessedIteration) {
153
130
  return;
154
131
  }
155
132
  // Check if max iterations reached
@@ -159,25 +136,22 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
159
136
  unlinkSync(stateFilePath);
160
137
  }
161
138
  catch { }
162
- try {
163
- unlinkSync(lockFilePath);
164
- }
165
- catch { }
166
139
  return;
167
140
  }
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
141
+ // Mark as processing to prevent concurrent handling
142
+ processingIteration = true;
180
143
  try {
144
+ const nextIteration = state.iteration + 1;
145
+ lastProcessedIteration = nextIteration;
146
+ // Update state file with new iteration
147
+ const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
148
+ writeFileSync(stateFilePath, updated);
149
+ // Build system message
150
+ const systemMsg = state.completion_promise
151
+ ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
152
+ : `Ralph iteration ${nextIteration} | No completion promise set`;
153
+ console.log(`\n${systemMsg}`);
154
+ // Send the same prompt back to continue the session
181
155
  if (sessionID) {
182
156
  await client.session.promptAsync({
183
157
  path: { id: sessionID },
@@ -193,7 +167,10 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
193
167
  }
194
168
  }
195
169
  catch (err) {
196
- console.error("\nRalph loop: Failed to send prompt", err);
170
+ console.error("\nRalph loop: Failed to process iteration", err);
171
+ }
172
+ finally {
173
+ processingIteration = false;
197
174
  }
198
175
  },
199
176
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sureshsankaran/ralph-wiggum",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",