@sureshsankaran/ralph-wiggum 0.1.4 → 0.1.6

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 +43 -61
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,14 @@
1
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- const STATE_FILE = ".opencode/ralph-loop.local.md";
4
- const LOCK_FILE = ".opencode/ralph-loop.lock";
3
+ import { homedir } from "node:os";
4
+ // Use a global state directory that won't be affected by project snapshot/revert
5
+ function getStateFilePath() {
6
+ const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
7
+ const stateDir = join(configDir, "opencode", "state");
8
+ // Ensure directory exists
9
+ mkdirSync(stateDir, { recursive: true });
10
+ return join(stateDir, "ralph-loop.local.md");
11
+ }
5
12
  function parseState(content) {
6
13
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
7
14
  if (!frontmatterMatch)
@@ -44,35 +51,6 @@ function extractPromiseText(text) {
44
51
  const match = text.match(/<promise>(.*?)<\/promise>/s);
45
52
  return match ? match[1].trim().replace(/\s+/g, " ") : null;
46
53
  }
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;
75
- }
76
54
  /**
77
55
  * Ralph Wiggum Plugin - Iterative AI Development
78
56
  *
@@ -86,10 +64,12 @@ function tryClaimIteration(stateFilePath, lockFilePath, currentIteration) {
86
64
  * The AI should output <promise>DONE</promise> when the task is complete.
87
65
  */
88
66
  export const RalphWiggumPlugin = async ({ client, directory }) => {
89
- const stateFilePath = join(directory, STATE_FILE);
90
- const lockFilePath = join(directory, LOCK_FILE);
91
- // Track if completion was detected to prevent race conditions
67
+ const stateFilePath = getStateFilePath();
68
+ // In-memory state to prevent race conditions
69
+ // These are scoped to this plugin instance
92
70
  let completionDetected = false;
71
+ let processingIteration = false;
72
+ let lastProcessedIteration = -1;
93
73
  return {
94
74
  event: async ({ event }) => {
95
75
  // Check message.part.updated events for completion promise
@@ -114,10 +94,6 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
114
94
  unlinkSync(stateFilePath);
115
95
  }
116
96
  catch { }
117
- try {
118
- unlinkSync(lockFilePath);
119
- }
120
- catch { }
121
97
  return;
122
98
  }
123
99
  }
@@ -125,7 +101,7 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
125
101
  return;
126
102
  }
127
103
  // Handle session idle to continue the loop
128
- // Only listen to session.status (session.idle is deprecated and fires at the same time)
104
+ // Only listen to session.status (session.idle is deprecated)
129
105
  const isIdle = event.type === "session.status" && event.properties?.status?.type === "idle";
130
106
  if (!isIdle)
131
107
  return;
@@ -133,6 +109,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
133
109
  if (completionDetected) {
134
110
  return;
135
111
  }
112
+ // If we're already processing an iteration, skip
113
+ // This prevents race conditions when multiple idle events fire
114
+ if (processingIteration) {
115
+ return;
116
+ }
136
117
  const sessionID = event.properties?.sessionID;
137
118
  // Check if ralph-loop is active
138
119
  if (!existsSync(stateFilePath))
@@ -149,10 +130,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
149
130
  unlinkSync(stateFilePath);
150
131
  }
151
132
  catch { }
152
- try {
153
- unlinkSync(lockFilePath);
154
- }
155
- catch { }
133
+ return;
134
+ }
135
+ // Check if we've already processed this iteration
136
+ // This handles cases where the state file hasn't been updated yet
137
+ if (state.iteration <= lastProcessedIteration) {
156
138
  return;
157
139
  }
158
140
  // Check if max iterations reached
@@ -162,25 +144,22 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
162
144
  unlinkSync(stateFilePath);
163
145
  }
164
146
  catch { }
165
- try {
166
- unlinkSync(lockFilePath);
167
- }
168
- catch { }
169
147
  return;
170
148
  }
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
149
+ // Mark as processing to prevent concurrent handling
150
+ processingIteration = true;
183
151
  try {
152
+ const nextIteration = state.iteration + 1;
153
+ lastProcessedIteration = nextIteration;
154
+ // Update state file with new iteration
155
+ const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
156
+ writeFileSync(stateFilePath, updated);
157
+ // Build system message
158
+ const systemMsg = state.completion_promise
159
+ ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
160
+ : `Ralph iteration ${nextIteration} | No completion promise set`;
161
+ console.log(`\n${systemMsg}`);
162
+ // Send the same prompt back to continue the session
184
163
  if (sessionID) {
185
164
  await client.session.promptAsync({
186
165
  path: { id: sessionID },
@@ -196,7 +175,10 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
196
175
  }
197
176
  }
198
177
  catch (err) {
199
- console.error("\nRalph loop: Failed to send prompt", err);
178
+ console.error("\nRalph loop: Failed to process iteration", err);
179
+ }
180
+ finally {
181
+ processingIteration = false;
200
182
  }
201
183
  },
202
184
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sureshsankaran/ralph-wiggum",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",