@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.
- package/dist/index.js +87 -75
- 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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,
|
|
132
|
+
// If completion was already detected, silently ignore
|
|
112
133
|
if (completionDetected) {
|
|
113
134
|
return;
|
|
114
135
|
}
|
|
115
|
-
|
|
116
|
-
if
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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.
|
|
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",
|