@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.
- package/dist/index.js +43 -61
- 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
|
-
|
|
4
|
-
|
|
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 =
|
|
90
|
-
|
|
91
|
-
//
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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",
|