@sureshsankaran/ralph-wiggum 0.1.6 → 0.1.8
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/commands/cancel-ralph.md +3 -3
- package/commands/ralph-loop.md +2 -1
- package/dist/index.js +57 -97
- package/package.json +1 -1
package/commands/cancel-ralph.md
CHANGED
|
@@ -6,11 +6,11 @@ description: "Cancel active Ralph Wiggum loop"
|
|
|
6
6
|
|
|
7
7
|
To cancel the Ralph loop:
|
|
8
8
|
|
|
9
|
-
1. Check if
|
|
9
|
+
1. Check if `~/.config/opencode/state/ralph-loop.local.md` exists using Bash: `test -f ~/.config/opencode/state/ralph-loop.local.md && echo "EXISTS" || echo "NOT_FOUND"`
|
|
10
10
|
|
|
11
11
|
2. **If NOT_FOUND**: Say "No active Ralph loop found."
|
|
12
12
|
|
|
13
13
|
3. **If EXISTS**:
|
|
14
|
-
- Read
|
|
15
|
-
- Remove the file using Bash: `rm
|
|
14
|
+
- Read `~/.config/opencode/state/ralph-loop.local.md` to get the current iteration number from the `iteration:` field
|
|
15
|
+
- Remove the file using Bash: `rm ~/.config/opencode/state/ralph-loop.local.md`
|
|
16
16
|
- Report: "Cancelled Ralph loop (was at iteration N)" where N is the iteration value
|
package/commands/ralph-loop.md
CHANGED
|
@@ -46,7 +46,8 @@ console.log('Usage: /ralph-loop \"<prompt>\" [--max-iterations N] [--completion-
|
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
var
|
|
49
|
+
var configDir = process.env.XDG_CONFIG_HOME || path.join(require('os').homedir(), '.config');
|
|
50
|
+
var dir = path.join(configDir, 'opencode', 'state');
|
|
50
51
|
fs.mkdirSync(dir, { recursive: true });
|
|
51
52
|
|
|
52
53
|
var cpYaml = completionPromise === 'null' ? 'null' : '\"' + completionPromise + '\"';
|
package/dist/index.js
CHANGED
|
@@ -63,81 +63,48 @@ function extractPromiseText(text) {
|
|
|
63
63
|
*
|
|
64
64
|
* The AI should output <promise>DONE</promise> when the task is complete.
|
|
65
65
|
*/
|
|
66
|
-
export const RalphWiggumPlugin = async ({ client
|
|
66
|
+
export const RalphWiggumPlugin = async ({ client }) => {
|
|
67
67
|
const stateFilePath = getStateFilePath();
|
|
68
|
-
// In-memory state to prevent
|
|
69
|
-
// These are scoped to this plugin instance
|
|
68
|
+
// In-memory state to prevent double-processing
|
|
70
69
|
let completionDetected = false;
|
|
71
|
-
let processingIteration = false;
|
|
72
70
|
let lastProcessedIteration = -1;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const props = event.properties;
|
|
79
|
-
const part = props?.part;
|
|
80
|
-
// Only check text parts with completed status (time.end is set)
|
|
81
|
-
if (part?.type === "text" && part?.text && part?.time?.end && existsSync(stateFilePath)) {
|
|
82
|
-
const content = readFileSync(stateFilePath, "utf-8");
|
|
83
|
-
const state = parseState(content);
|
|
84
|
-
if (state?.completion_promise && !completionDetected) {
|
|
85
|
-
// Skip if this text is part of the user's prompt
|
|
86
|
-
if (part.text.includes(state.prompt.slice(0, 30))) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const promiseText = extractPromiseText(part.text);
|
|
90
|
-
if (promiseText === state.completion_promise) {
|
|
91
|
-
completionDetected = true;
|
|
92
|
-
console.log(`\nRalph loop complete! Detected <promise>${state.completion_promise}</promise>`);
|
|
93
|
-
try {
|
|
94
|
-
unlinkSync(stateFilePath);
|
|
95
|
-
}
|
|
96
|
-
catch { }
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
// Handle session idle to continue the loop
|
|
104
|
-
// Only listen to session.status (session.idle is deprecated)
|
|
105
|
-
const isIdle = event.type === "session.status" && event.properties?.status?.type === "idle";
|
|
106
|
-
if (!isIdle)
|
|
107
|
-
return;
|
|
108
|
-
// If completion was already detected, silently ignore
|
|
109
|
-
if (completionDetected) {
|
|
110
|
-
return;
|
|
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
|
-
}
|
|
117
|
-
const sessionID = event.properties?.sessionID;
|
|
118
|
-
// Check if ralph-loop is active
|
|
71
|
+
// Cast to extended hooks type that includes experimental.session.stop
|
|
72
|
+
const hooks = {
|
|
73
|
+
// Stop hook: called when main session loop is about to exit
|
|
74
|
+
"experimental.session.stop": async (input, output) => {
|
|
75
|
+
// Only handle main session loop, not subtasks
|
|
119
76
|
if (!existsSync(stateFilePath))
|
|
120
77
|
return;
|
|
121
78
|
const content = readFileSync(stateFilePath, "utf-8");
|
|
122
79
|
const state = parseState(content);
|
|
123
|
-
if (!state || !state.active)
|
|
80
|
+
if (!state || !state.active)
|
|
124
81
|
return;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (isNaN(state.iteration) || isNaN(state.max_iterations)) {
|
|
128
|
-
console.error("\nRalph loop: State file corrupted - invalid numeric fields");
|
|
129
|
-
try {
|
|
130
|
-
unlinkSync(stateFilePath);
|
|
131
|
-
}
|
|
132
|
-
catch { }
|
|
82
|
+
// If completion already detected, allow normal exit
|
|
83
|
+
if (completionDetected)
|
|
133
84
|
return;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
85
|
+
// Fetch last assistant message text for this session
|
|
86
|
+
const messages = await client.session.messages({ path: { id: input.sessionID } }).then((res) => res.data ?? []);
|
|
87
|
+
const lastAssistant = [...messages]
|
|
88
|
+
.reverse()
|
|
89
|
+
.find((m) => m.info.role === "assistant" && m.parts.some((p) => p.type === "text"));
|
|
90
|
+
if (!lastAssistant)
|
|
138
91
|
return;
|
|
92
|
+
const textParts = lastAssistant.parts.filter((p) => p.type === "text");
|
|
93
|
+
const fullText = textParts.map((p) => p.text).join("\n");
|
|
94
|
+
// Check completion promise
|
|
95
|
+
if (state.completion_promise) {
|
|
96
|
+
const promiseText = extractPromiseText(fullText);
|
|
97
|
+
if (promiseText === state.completion_promise) {
|
|
98
|
+
completionDetected = true;
|
|
99
|
+
console.log(`\nRalph loop complete! Detected <promise>${state.completion_promise}</promise>`);
|
|
100
|
+
try {
|
|
101
|
+
unlinkSync(stateFilePath);
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
139
106
|
}
|
|
140
|
-
//
|
|
107
|
+
// Max-iteration safety
|
|
141
108
|
if (state.max_iterations > 0 && state.iteration >= state.max_iterations) {
|
|
142
109
|
console.log(`\nRalph loop: Max iterations (${state.max_iterations}) reached.`);
|
|
143
110
|
try {
|
|
@@ -146,41 +113,34 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
146
113
|
catch { }
|
|
147
114
|
return;
|
|
148
115
|
}
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
type: "text",
|
|
170
|
-
text: `[${systemMsg}]\n\n${state.prompt}`,
|
|
171
|
-
},
|
|
172
|
-
],
|
|
116
|
+
// Check if we've already advanced this iteration
|
|
117
|
+
if (state.iteration <= lastProcessedIteration)
|
|
118
|
+
return;
|
|
119
|
+
const nextIteration = state.iteration + 1;
|
|
120
|
+
lastProcessedIteration = nextIteration;
|
|
121
|
+
// Update state file with new iteration
|
|
122
|
+
const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
|
|
123
|
+
writeFileSync(stateFilePath, updated);
|
|
124
|
+
const systemMsg = state.completion_promise
|
|
125
|
+
? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
|
|
126
|
+
: `Ralph iteration ${nextIteration} | No completion promise set`;
|
|
127
|
+
console.log(`\n${systemMsg}`);
|
|
128
|
+
// Enqueue next iteration by sending same prompt back into main session
|
|
129
|
+
await client.session.promptAsync({
|
|
130
|
+
path: { id: input.sessionID },
|
|
131
|
+
body: {
|
|
132
|
+
parts: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `[${systemMsg}]\n\n${state.prompt}`,
|
|
173
136
|
},
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
finally {
|
|
181
|
-
processingIteration = false;
|
|
182
|
-
}
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
// Block loop exit so we can continue iterating
|
|
141
|
+
output.decision = "block";
|
|
183
142
|
},
|
|
184
143
|
};
|
|
144
|
+
return hooks;
|
|
185
145
|
};
|
|
186
146
|
export default RalphWiggumPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sureshsankaran/ralph-wiggum",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|