@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.
- package/dist/index.js +83 -74
- 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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
|
-
|
|
116
|
-
if
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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.
|
|
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",
|