@sureshsankaran/ralph-wiggum 0.1.5 → 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
@@ -1,6 +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
- const STATE_FILE = ".opencode/ralph-loop.local.md";
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
+ }
4
12
  function parseState(content) {
5
13
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
6
14
  if (!frontmatterMatch)
@@ -55,81 +63,48 @@ function extractPromiseText(text) {
55
63
  *
56
64
  * The AI should output <promise>DONE</promise> when the task is complete.
57
65
  */
58
- export const RalphWiggumPlugin = async ({ client, directory }) => {
59
- const stateFilePath = join(directory, STATE_FILE);
60
- // In-memory state to prevent race conditions
61
- // These are scoped to this plugin instance
66
+ export const RalphWiggumPlugin = async ({ client }) => {
67
+ const stateFilePath = getStateFilePath();
68
+ // In-memory state to prevent double-processing
62
69
  let completionDetected = false;
63
- let processingIteration = false;
64
70
  let lastProcessedIteration = -1;
65
- return {
66
- event: async ({ event }) => {
67
- // Check message.part.updated events for completion promise
68
- // This happens BEFORE session goes idle, so we can detect early
69
- if (event.type === "message.part.updated") {
70
- const props = event.properties;
71
- const part = props?.part;
72
- // Only check text parts with completed status (time.end is set)
73
- if (part?.type === "text" && part?.text && part?.time?.end && existsSync(stateFilePath)) {
74
- const content = readFileSync(stateFilePath, "utf-8");
75
- const state = parseState(content);
76
- if (state?.completion_promise && !completionDetected) {
77
- // Skip if this text is part of the user's prompt
78
- if (part.text.includes(state.prompt.slice(0, 30))) {
79
- return;
80
- }
81
- const promiseText = extractPromiseText(part.text);
82
- if (promiseText === state.completion_promise) {
83
- completionDetected = true;
84
- console.log(`\nRalph loop complete! Detected <promise>${state.completion_promise}</promise>`);
85
- try {
86
- unlinkSync(stateFilePath);
87
- }
88
- catch { }
89
- return;
90
- }
91
- }
92
- }
93
- return;
94
- }
95
- // Handle session idle to continue the loop
96
- // Only listen to session.status (session.idle is deprecated)
97
- const isIdle = event.type === "session.status" && event.properties?.status?.type === "idle";
98
- if (!isIdle)
99
- return;
100
- // If completion was already detected, silently ignore
101
- if (completionDetected) {
102
- return;
103
- }
104
- // If we're already processing an iteration, skip
105
- // This prevents race conditions when multiple idle events fire
106
- if (processingIteration) {
107
- return;
108
- }
109
- const sessionID = event.properties?.sessionID;
110
- // 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
111
76
  if (!existsSync(stateFilePath))
112
77
  return;
113
78
  const content = readFileSync(stateFilePath, "utf-8");
114
79
  const state = parseState(content);
115
- if (!state || !state.active) {
80
+ if (!state || !state.active)
116
81
  return;
117
- }
118
- // Validate numeric fields
119
- if (isNaN(state.iteration) || isNaN(state.max_iterations)) {
120
- console.error("\nRalph loop: State file corrupted - invalid numeric fields");
121
- try {
122
- unlinkSync(stateFilePath);
123
- }
124
- catch { }
82
+ // If completion already detected, allow normal exit
83
+ if (completionDetected)
125
84
  return;
126
- }
127
- // Check if we've already processed this iteration
128
- // This handles cases where the state file hasn't been updated yet
129
- 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)
130
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
+ }
131
106
  }
132
- // Check if max iterations reached
107
+ // Max-iteration safety
133
108
  if (state.max_iterations > 0 && state.iteration >= state.max_iterations) {
134
109
  console.log(`\nRalph loop: Max iterations (${state.max_iterations}) reached.`);
135
110
  try {
@@ -138,41 +113,34 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
138
113
  catch { }
139
114
  return;
140
115
  }
141
- // Mark as processing to prevent concurrent handling
142
- processingIteration = true;
143
- try {
144
- const nextIteration = state.iteration + 1;
145
- lastProcessedIteration = nextIteration;
146
- // Update state file with new iteration
147
- const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
148
- writeFileSync(stateFilePath, updated);
149
- // Build system message
150
- const systemMsg = state.completion_promise
151
- ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
152
- : `Ralph iteration ${nextIteration} | No completion promise set`;
153
- console.log(`\n${systemMsg}`);
154
- // Send the same prompt back to continue the session
155
- if (sessionID) {
156
- await client.session.promptAsync({
157
- path: { id: sessionID },
158
- body: {
159
- parts: [
160
- {
161
- type: "text",
162
- text: `[${systemMsg}]\n\n${state.prompt}`,
163
- },
164
- ],
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}`,
165
136
  },
166
- });
167
- }
168
- }
169
- catch (err) {
170
- console.error("\nRalph loop: Failed to process iteration", err);
171
- }
172
- finally {
173
- processingIteration = false;
174
- }
137
+ ],
138
+ },
139
+ });
140
+ // Block loop exit so we can continue iterating
141
+ output.decision = "block";
175
142
  },
176
143
  };
144
+ return hooks;
177
145
  };
178
146
  export default RalphWiggumPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sureshsankaran/ralph-wiggum",
3
- "version": "0.1.5",
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",