@sureshsankaran/ralph-wiggum 0.1.6 → 0.1.9
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 +64 -98
- 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,57 @@ 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
|
|
72
|
-
let
|
|
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
|
|
70
|
+
let pendingIteration = -1; // Track iteration we're currently processing/waiting for
|
|
71
|
+
let lastAssistantId = ""; // Track last assistant message to detect new responses
|
|
72
|
+
// Cast to extended hooks type that includes experimental.session.stop
|
|
73
|
+
const hooks = {
|
|
74
|
+
// Stop hook: called when main session loop is about to exit
|
|
75
|
+
"experimental.session.stop": async (input, output) => {
|
|
76
|
+
// Only handle main session loop, not subtasks
|
|
119
77
|
if (!existsSync(stateFilePath))
|
|
120
78
|
return;
|
|
121
79
|
const content = readFileSync(stateFilePath, "utf-8");
|
|
122
80
|
const state = parseState(content);
|
|
123
|
-
if (!state || !state.active)
|
|
81
|
+
if (!state || !state.active)
|
|
124
82
|
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 { }
|
|
83
|
+
// If completion already detected, allow normal exit
|
|
84
|
+
if (completionDetected)
|
|
133
85
|
return;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
86
|
+
// Fetch last assistant message text for this session
|
|
87
|
+
const messages = await client.session.messages({ path: { id: input.sessionID } }).then((res) => res.data ?? []);
|
|
88
|
+
const lastAssistant = [...messages]
|
|
89
|
+
.reverse()
|
|
90
|
+
.find((m) => m.info.role === "assistant" && m.parts.some((p) => p.type === "text"));
|
|
91
|
+
if (!lastAssistant)
|
|
92
|
+
return;
|
|
93
|
+
// Prevent double-triggering: if we already sent a prompt for this iteration
|
|
94
|
+
// and haven't seen a new assistant message, skip
|
|
95
|
+
if (pendingIteration === state.iteration && lastAssistant.info.id === lastAssistantId) {
|
|
96
|
+
output.decision = "block"; // Still block, we're waiting for AI to respond
|
|
138
97
|
return;
|
|
139
98
|
}
|
|
140
|
-
//
|
|
99
|
+
// Update tracking
|
|
100
|
+
lastAssistantId = lastAssistant.info.id;
|
|
101
|
+
const textParts = lastAssistant.parts.filter((p) => p.type === "text");
|
|
102
|
+
const fullText = textParts.map((p) => p.text).join("\n");
|
|
103
|
+
// Check completion promise
|
|
104
|
+
if (state.completion_promise) {
|
|
105
|
+
const promiseText = extractPromiseText(fullText);
|
|
106
|
+
if (promiseText === state.completion_promise) {
|
|
107
|
+
completionDetected = true;
|
|
108
|
+
console.log(`\nRalph loop complete! Detected <promise>${state.completion_promise}</promise>`);
|
|
109
|
+
try {
|
|
110
|
+
unlinkSync(stateFilePath);
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Max-iteration safety
|
|
141
117
|
if (state.max_iterations > 0 && state.iteration >= state.max_iterations) {
|
|
142
118
|
console.log(`\nRalph loop: Max iterations (${state.max_iterations}) reached.`);
|
|
143
119
|
try {
|
|
@@ -146,41 +122,31 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
|
|
|
146
122
|
catch { }
|
|
147
123
|
return;
|
|
148
124
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
body: {
|
|
167
|
-
parts: [
|
|
168
|
-
{
|
|
169
|
-
type: "text",
|
|
170
|
-
text: `[${systemMsg}]\n\n${state.prompt}`,
|
|
171
|
-
},
|
|
172
|
-
],
|
|
125
|
+
const nextIteration = state.iteration + 1;
|
|
126
|
+
pendingIteration = nextIteration;
|
|
127
|
+
// Update state file with new iteration
|
|
128
|
+
const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
|
|
129
|
+
writeFileSync(stateFilePath, updated);
|
|
130
|
+
const systemMsg = state.completion_promise
|
|
131
|
+
? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
|
|
132
|
+
: `Ralph iteration ${nextIteration} | No completion promise set`;
|
|
133
|
+
console.log(`\n${systemMsg}`);
|
|
134
|
+
// Enqueue next iteration by sending same prompt back into main session
|
|
135
|
+
await client.session.promptAsync({
|
|
136
|
+
path: { id: input.sessionID },
|
|
137
|
+
body: {
|
|
138
|
+
parts: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `[${systemMsg}]\n\n${state.prompt}`,
|
|
173
142
|
},
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
finally {
|
|
181
|
-
processingIteration = false;
|
|
182
|
-
}
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
// Block loop exit so we can continue iterating
|
|
147
|
+
output.decision = "block";
|
|
183
148
|
},
|
|
184
149
|
};
|
|
150
|
+
return hooks;
|
|
185
151
|
};
|
|
186
152
|
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.9",
|
|
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",
|