@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.
@@ -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,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, 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
70
  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
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
- // 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 { }
82
+ // If completion already detected, allow normal exit
83
+ if (completionDetected)
133
84
  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) {
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
- // Check if max iterations reached
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
- // 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
- ],
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
- catch (err) {
178
- console.error("\nRalph loop: Failed to process iteration", err);
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.6",
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",