@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.
- package/dist/index.js +34 -57
- 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
|
-
|
|
87
|
-
//
|
|
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
|
|
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
|
|
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,
|
|
100
|
+
// If completion was already detected, silently ignore
|
|
129
101
|
if (completionDetected) {
|
|
130
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
//
|
|
169
|
-
|
|
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
|
|
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
|
+
"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",
|