@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.
@@ -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 `.opencode/ralph-loop.local.md` exists using Bash: `test -f .opencode/ralph-loop.local.md && echo "EXISTS" || echo "NOT_FOUND"`
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 `.opencode/ralph-loop.local.md` to get the current iteration number from the `iteration:` field
15
- - Remove the file using Bash: `rm .opencode/ralph-loop.local.md`
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
@@ -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 dir = '.opencode';
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, directory }) => {
66
+ export const RalphWiggumPlugin = async ({ client }) => {
67
67
  const stateFilePath = getStateFilePath();
68
- // In-memory state to prevent race conditions
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
- let lastProcessedIteration = -1;
73
- return {
74
- event: async ({ event }) => {
75
- // Check message.part.updated events for completion promise
76
- // This happens BEFORE session goes idle, so we can detect early
77
- if (event.type === "message.part.updated") {
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
- // Validate numeric fields
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
- // 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) {
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
- // Check if max iterations reached
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
- // Mark as processing to prevent concurrent handling
150
- processingIteration = true;
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
163
- if (sessionID) {
164
- await client.session.promptAsync({
165
- path: { id: sessionID },
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
- catch (err) {
178
- console.error("\nRalph loop: Failed to process iteration", err);
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.6",
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",