@sureshsankaran/ralph-wiggum 0.1.4 → 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.
- package/dist/index.js +32 -58
- 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,35 +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
|
-
// 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
46
|
/**
|
|
77
47
|
* Ralph Wiggum Plugin - Iterative AI Development
|
|
78
48
|
*
|
|
@@ -87,9 +57,11 @@ function tryClaimIteration(stateFilePath, lockFilePath, currentIteration) {
|
|
|
87
57
|
*/
|
|
88
58
|
export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
89
59
|
const stateFilePath = join(directory, STATE_FILE);
|
|
90
|
-
|
|
91
|
-
//
|
|
60
|
+
// In-memory state to prevent race conditions
|
|
61
|
+
// These are scoped to this plugin instance
|
|
92
62
|
let completionDetected = false;
|
|
63
|
+
let processingIteration = false;
|
|
64
|
+
let lastProcessedIteration = -1;
|
|
93
65
|
return {
|
|
94
66
|
event: async ({ event }) => {
|
|
95
67
|
// Check message.part.updated events for completion promise
|
|
@@ -114,10 +86,6 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
114
86
|
unlinkSync(stateFilePath);
|
|
115
87
|
}
|
|
116
88
|
catch { }
|
|
117
|
-
try {
|
|
118
|
-
unlinkSync(lockFilePath);
|
|
119
|
-
}
|
|
120
|
-
catch { }
|
|
121
89
|
return;
|
|
122
90
|
}
|
|
123
91
|
}
|
|
@@ -125,7 +93,7 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
125
93
|
return;
|
|
126
94
|
}
|
|
127
95
|
// Handle session idle to continue the loop
|
|
128
|
-
// Only listen to session.status (session.idle is deprecated
|
|
96
|
+
// Only listen to session.status (session.idle is deprecated)
|
|
129
97
|
const isIdle = event.type === "session.status" && event.properties?.status?.type === "idle";
|
|
130
98
|
if (!isIdle)
|
|
131
99
|
return;
|
|
@@ -133,6 +101,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
133
101
|
if (completionDetected) {
|
|
134
102
|
return;
|
|
135
103
|
}
|
|
104
|
+
// If we're already processing an iteration, skip
|
|
105
|
+
// This prevents race conditions when multiple idle events fire
|
|
106
|
+
if (processingIteration) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
136
109
|
const sessionID = event.properties?.sessionID;
|
|
137
110
|
// Check if ralph-loop is active
|
|
138
111
|
if (!existsSync(stateFilePath))
|
|
@@ -149,10 +122,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
149
122
|
unlinkSync(stateFilePath);
|
|
150
123
|
}
|
|
151
124
|
catch { }
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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) {
|
|
156
130
|
return;
|
|
157
131
|
}
|
|
158
132
|
// Check if max iterations reached
|
|
@@ -162,25 +136,22 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
162
136
|
unlinkSync(stateFilePath);
|
|
163
137
|
}
|
|
164
138
|
catch { }
|
|
165
|
-
try {
|
|
166
|
-
unlinkSync(lockFilePath);
|
|
167
|
-
}
|
|
168
|
-
catch { }
|
|
169
139
|
return;
|
|
170
140
|
}
|
|
171
|
-
//
|
|
172
|
-
|
|
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
|
|
141
|
+
// Mark as processing to prevent concurrent handling
|
|
142
|
+
processingIteration = true;
|
|
183
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
|
|
184
155
|
if (sessionID) {
|
|
185
156
|
await client.session.promptAsync({
|
|
186
157
|
path: { id: sessionID },
|
|
@@ -196,7 +167,10 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
196
167
|
}
|
|
197
168
|
}
|
|
198
169
|
catch (err) {
|
|
199
|
-
console.error("\nRalph loop: Failed to
|
|
170
|
+
console.error("\nRalph loop: Failed to process iteration", err);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
processingIteration = false;
|
|
200
174
|
}
|
|
201
175
|
},
|
|
202
176
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sureshsankaran/ralph-wiggum",
|
|
3
|
-
"version": "0.1.
|
|
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",
|