@sureshsankaran/ralph-wiggum 0.2.0 → 0.2.1
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/dist/index.d.ts +68 -0
- package/dist/index.js +620 -0
- package/package.json +19 -5
- package/src/index.ts +0 -361
- package/tsconfig.json +0 -14
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ralph Wiggum Plugin for OpenCode
|
|
3
|
+
* Implements the Ralph Wiggum technique for iterative, self-referential AI development loops.
|
|
4
|
+
*
|
|
5
|
+
* Based on: https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ralph-loop "Your task here" --max 8 --promise "DONE"
|
|
9
|
+
* ralph-loop "Your task here" --max 8 --promise "DONE" --state-file /custom/path.json
|
|
10
|
+
* ralph-loop "Your task here" --no-state # Disable state file
|
|
11
|
+
*
|
|
12
|
+
* The loop will:
|
|
13
|
+
* 1. Execute the prompt
|
|
14
|
+
* 2. Continue iterating until max iterations OR completion promise is found
|
|
15
|
+
* 3. Feed the SAME original prompt back each iteration
|
|
16
|
+
* 4. Show iteration count in system message
|
|
17
|
+
* 5. Write state to ~/.config/opencode/state/ralph-wiggum.json (or custom path) for verification
|
|
18
|
+
*/
|
|
19
|
+
export default function ralphWiggum(input: {
|
|
20
|
+
client: any;
|
|
21
|
+
project: string;
|
|
22
|
+
worktree: string;
|
|
23
|
+
directory: string;
|
|
24
|
+
serverUrl: string;
|
|
25
|
+
$: any;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
command: {
|
|
28
|
+
"ralph-loop": {
|
|
29
|
+
description: string;
|
|
30
|
+
template: string;
|
|
31
|
+
};
|
|
32
|
+
"cancel-ralph": {
|
|
33
|
+
description: string;
|
|
34
|
+
template: string;
|
|
35
|
+
};
|
|
36
|
+
"ralph-status": {
|
|
37
|
+
description: string;
|
|
38
|
+
template: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
tool: {
|
|
42
|
+
"cancel-ralph": {
|
|
43
|
+
description: string;
|
|
44
|
+
args: {};
|
|
45
|
+
execute(_args: {}, ctx: any): Promise<"Ralph loop cancelled" | "No active Ralph loop to cancel">;
|
|
46
|
+
};
|
|
47
|
+
"ralph-status": {
|
|
48
|
+
description: string;
|
|
49
|
+
args: {};
|
|
50
|
+
execute(_args: {}, ctx: any): Promise<string>;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
event(input: {
|
|
54
|
+
event: any;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
"session.stop"(hookInput: {
|
|
57
|
+
sessionID: string;
|
|
58
|
+
step: number;
|
|
59
|
+
lastAssistantText?: string;
|
|
60
|
+
}, output: {
|
|
61
|
+
stop: boolean;
|
|
62
|
+
prompt?: string;
|
|
63
|
+
systemMessage?: string;
|
|
64
|
+
}): Promise<void>;
|
|
65
|
+
"experimental.chat.system.transform"(_input: {}, output: {
|
|
66
|
+
system: string[];
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ralph Wiggum Plugin for OpenCode
|
|
3
|
+
* Implements the Ralph Wiggum technique for iterative, self-referential AI development loops.
|
|
4
|
+
*
|
|
5
|
+
* Based on: https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ralph-loop "Your task here" --max 8 --promise "DONE"
|
|
9
|
+
* ralph-loop "Your task here" --max 8 --promise "DONE" --state-file /custom/path.json
|
|
10
|
+
* ralph-loop "Your task here" --no-state # Disable state file
|
|
11
|
+
*
|
|
12
|
+
* The loop will:
|
|
13
|
+
* 1. Execute the prompt
|
|
14
|
+
* 2. Continue iterating until max iterations OR completion promise is found
|
|
15
|
+
* 3. Feed the SAME original prompt back each iteration
|
|
16
|
+
* 4. Show iteration count in system message
|
|
17
|
+
* 5. Write state to ~/.config/opencode/state/ralph-wiggum.json (or custom path) for verification
|
|
18
|
+
*/
|
|
19
|
+
import { writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { join, dirname } from "path";
|
|
22
|
+
// Default state file path
|
|
23
|
+
const DEFAULT_STATE_DIR = join(homedir(), ".config", "opencode", "state");
|
|
24
|
+
const DEFAULT_STATE_FILE = join(DEFAULT_STATE_DIR, "ralph-wiggum.json");
|
|
25
|
+
const state = {};
|
|
26
|
+
// Track active review session for system prompt injection
|
|
27
|
+
let activeReviewSession = null;
|
|
28
|
+
/**
|
|
29
|
+
* Ensure directory exists for state file
|
|
30
|
+
*/
|
|
31
|
+
function ensureDir(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Ignore errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clean up existing state file on start
|
|
41
|
+
*/
|
|
42
|
+
function cleanupExistingStateFile(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(filePath)) {
|
|
45
|
+
unlinkSync(filePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Write state to file for external verification
|
|
54
|
+
*/
|
|
55
|
+
function writeStateFile(sessionID, s) {
|
|
56
|
+
if (!s.stateFile)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
ensureDir(s.stateFile);
|
|
60
|
+
const stateData = {
|
|
61
|
+
sessionID,
|
|
62
|
+
active: s.active,
|
|
63
|
+
prompt: s.prompt,
|
|
64
|
+
promise: s.promise || "DONE",
|
|
65
|
+
iterations: s.iterations,
|
|
66
|
+
max: s.max ?? null,
|
|
67
|
+
remaining: s.max != null ? s.max - s.iterations : null,
|
|
68
|
+
startedAt: s.startedAt,
|
|
69
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
70
|
+
status: s.status,
|
|
71
|
+
// Review system fields
|
|
72
|
+
phase: s.phase,
|
|
73
|
+
reviewCount: s.reviewCount,
|
|
74
|
+
maxReviews: s.maxReviews,
|
|
75
|
+
lastReviewFeedback: s.lastReviewFeedback || null,
|
|
76
|
+
};
|
|
77
|
+
writeFileSync(s.stateFile, JSON.stringify(stateData, null, 2));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Silently ignore write errors
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Write final state when loop ends
|
|
85
|
+
*/
|
|
86
|
+
function writeFinalState(sessionID, s) {
|
|
87
|
+
if (!s.stateFile)
|
|
88
|
+
return;
|
|
89
|
+
s.lastUpdatedAt = new Date().toISOString();
|
|
90
|
+
writeStateFile(sessionID, s);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Tokenize a string respecting quoted strings.
|
|
94
|
+
* Handles both single and double quotes, preserving content within quotes as single tokens.
|
|
95
|
+
* Also handles escape sequences like \" and \\
|
|
96
|
+
*/
|
|
97
|
+
function tokenize(input) {
|
|
98
|
+
const tokens = [];
|
|
99
|
+
let current = "";
|
|
100
|
+
let inQuote = null;
|
|
101
|
+
for (let i = 0; i < input.length; i++) {
|
|
102
|
+
const char = input[i];
|
|
103
|
+
// Handle escape sequences
|
|
104
|
+
if (char === "\\" && i + 1 < input.length) {
|
|
105
|
+
const nextChar = input[i + 1];
|
|
106
|
+
if (nextChar === '"' || nextChar === "'" || nextChar === "\\") {
|
|
107
|
+
// Escaped quote or backslash - add the escaped character
|
|
108
|
+
current += nextChar;
|
|
109
|
+
i++; // Skip the next character
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (inQuote) {
|
|
114
|
+
if (char === inQuote) {
|
|
115
|
+
// End of quoted section
|
|
116
|
+
inQuote = null;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
current += char;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (char === '"' || char === "'") {
|
|
123
|
+
// Start of quoted section
|
|
124
|
+
inQuote = char;
|
|
125
|
+
}
|
|
126
|
+
else if (char === " " || char === "\t") {
|
|
127
|
+
// Whitespace outside quotes - token boundary
|
|
128
|
+
if (current) {
|
|
129
|
+
tokens.push(current);
|
|
130
|
+
current = "";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
current += char;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Don't forget the last token
|
|
138
|
+
if (current) {
|
|
139
|
+
tokens.push(current);
|
|
140
|
+
}
|
|
141
|
+
return tokens;
|
|
142
|
+
}
|
|
143
|
+
// Parse arguments from command invocation
|
|
144
|
+
// Supports: ralph-loop "prompt text with spaces" --max 5 --promise "DONE" --state-file /tmp/ralph.json --no-state
|
|
145
|
+
function parseArgs(args) {
|
|
146
|
+
let input = args.trim();
|
|
147
|
+
// Handle CLI double-quoting: if the entire input is wrapped in quotes with escaped inner quotes,
|
|
148
|
+
// strip the outer quotes and unescape the inner ones
|
|
149
|
+
// e.g., "\"say hello\" --max 2" -> "say hello" --max 2
|
|
150
|
+
if (input.startsWith('"') && input.endsWith('"') && input.length > 2) {
|
|
151
|
+
// Check if this looks like double-quoted input (contains escaped quotes inside)
|
|
152
|
+
const inner = input.slice(1, -1);
|
|
153
|
+
if (inner.includes('\\"')) {
|
|
154
|
+
// Unescape the inner quotes and use that as input
|
|
155
|
+
input = inner.replace(/\\"/g, '"');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const tokens = tokenize(input);
|
|
159
|
+
const promptParts = [];
|
|
160
|
+
let maxIterations = 8;
|
|
161
|
+
let completionPromise;
|
|
162
|
+
let stateFile = DEFAULT_STATE_FILE;
|
|
163
|
+
let noState = false;
|
|
164
|
+
let i = 0;
|
|
165
|
+
while (i < tokens.length) {
|
|
166
|
+
const token = tokens[i];
|
|
167
|
+
if (token === "--max" || token === "--max-iterations") {
|
|
168
|
+
maxIterations = parseInt(tokens[++i] || "8", 10);
|
|
169
|
+
}
|
|
170
|
+
else if (token === "--promise" || token === "--completion-promise") {
|
|
171
|
+
completionPromise = tokens[++i];
|
|
172
|
+
}
|
|
173
|
+
else if (token === "--state-file" || token === "--state") {
|
|
174
|
+
stateFile = tokens[++i] || DEFAULT_STATE_FILE;
|
|
175
|
+
}
|
|
176
|
+
else if (token === "--no-state") {
|
|
177
|
+
noState = true;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Accumulate as prompt
|
|
181
|
+
promptParts.push(token);
|
|
182
|
+
}
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
prompt: promptParts.join(" ") || "Continue working on the task",
|
|
187
|
+
maxIterations,
|
|
188
|
+
completionPromise,
|
|
189
|
+
stateFile: noState ? null : stateFile,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if the assistant's response contains the completion promise.
|
|
194
|
+
* Looks for <promise>TEXT</promise> pattern where TEXT matches the expected promise.
|
|
195
|
+
*/
|
|
196
|
+
function checkCompletionPromise(text, expectedPromise) {
|
|
197
|
+
if (!text || !expectedPromise)
|
|
198
|
+
return false;
|
|
199
|
+
// Look for <promise>TEXT</promise> pattern
|
|
200
|
+
const promiseRegex = /<promise>([\s\S]*?)<\/promise>/gi;
|
|
201
|
+
const matches = text.matchAll(promiseRegex);
|
|
202
|
+
for (const match of matches) {
|
|
203
|
+
const promiseText = match[1].trim();
|
|
204
|
+
if (promiseText === expectedPromise) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
// System prompt for REVIEW phase
|
|
211
|
+
const REVIEW_SYSTEM_PROMPT = `
|
|
212
|
+
<ralph-review-mode>
|
|
213
|
+
## Code Review Mode Active
|
|
214
|
+
|
|
215
|
+
You are in CODE REVIEW mode. Be EXTREMELY THOROUGH and CRITICAL. Do not rubber-stamp changes.
|
|
216
|
+
|
|
217
|
+
### Review Philosophy
|
|
218
|
+
- Assume there ARE issues until proven otherwise
|
|
219
|
+
- Question every design decision
|
|
220
|
+
- Look for what's missing, not just what's wrong
|
|
221
|
+
- Consider maintainability, scalability, and future implications
|
|
222
|
+
- A good review finds problems; an excellent review prevents future ones
|
|
223
|
+
|
|
224
|
+
### 1. Retrieve and Inspect Changes
|
|
225
|
+
- Run \`git diff HEAD --stat\` to see which files changed
|
|
226
|
+
- Run \`git diff HEAD\` or \`git diff HEAD -- <file>\` to inspect specific changes
|
|
227
|
+
- Run \`git status\` to see untracked files
|
|
228
|
+
- Read full files if needed for context
|
|
229
|
+
- Do NOT skip any files - review everything
|
|
230
|
+
|
|
231
|
+
### 2. Design Review (Be Critical)
|
|
232
|
+
- Is this the RIGHT solution, not just A solution?
|
|
233
|
+
- Are there simpler approaches that were overlooked?
|
|
234
|
+
- Does the architecture make sense for the problem?
|
|
235
|
+
- Will this scale? Is it maintainable?
|
|
236
|
+
- Are there unnecessary abstractions or missing abstractions?
|
|
237
|
+
|
|
238
|
+
### 3. Spec Compliance (Be Thorough)
|
|
239
|
+
- Verify EVERY requirement from the original spec is addressed
|
|
240
|
+
- Check for missing functionality or incomplete implementations
|
|
241
|
+
- Look for requirements that were misunderstood
|
|
242
|
+
- Verify edge cases mentioned in the spec are handled
|
|
243
|
+
|
|
244
|
+
### 4. Code Quality Review (Be Rigorous)
|
|
245
|
+
- Look for bugs, logic errors, race conditions, edge cases
|
|
246
|
+
- Check error handling - what happens when things fail?
|
|
247
|
+
- Review security implications (injection, auth, data exposure)
|
|
248
|
+
- Check for performance issues (N+1 queries, memory leaks, blocking calls)
|
|
249
|
+
- Verify proper input validation and sanitization
|
|
250
|
+
- Look for code smells and anti-patterns
|
|
251
|
+
|
|
252
|
+
### 5. Test Recommendations
|
|
253
|
+
Provide SPECIFIC recommendations for:
|
|
254
|
+
- **Unit Tests**: Test individual functions/components in isolation
|
|
255
|
+
- **E2E Tests**: Tests that verify the UX exactly like a user would interact with it
|
|
256
|
+
- **Edge Cases**: Specific scenarios that MUST be tested
|
|
257
|
+
|
|
258
|
+
### Required Response
|
|
259
|
+
After your review, you MUST respond with ONE of:
|
|
260
|
+
- <promise>APPROVED</promise> - Code meets ALL requirements, no critical/high priority issues, design is sound
|
|
261
|
+
- <promise>NEEDFIX</promise> - Issues found that must be addressed
|
|
262
|
+
|
|
263
|
+
If responding with NEEDFIX, list EVERY issue with priority:
|
|
264
|
+
- **CRITICAL**: Bugs, security issues, data loss risks, spec violations
|
|
265
|
+
- **HIGH**: Design flaws, missing error handling, performance issues
|
|
266
|
+
</ralph-review-mode>
|
|
267
|
+
`;
|
|
268
|
+
// System prompt for FIX phase
|
|
269
|
+
const FIX_SYSTEM_PROMPT = `
|
|
270
|
+
<ralph-fix-mode>
|
|
271
|
+
## Fix Mode Active
|
|
272
|
+
|
|
273
|
+
You are addressing review comments. Your responsibilities:
|
|
274
|
+
|
|
275
|
+
### 1. Address Review Issues
|
|
276
|
+
- Fix all CRITICAL and HIGH priority issues identified
|
|
277
|
+
- Refer to the review feedback for specific issues to address
|
|
278
|
+
|
|
279
|
+
### 2. Implement Tests
|
|
280
|
+
- Add recommended unit tests
|
|
281
|
+
- Add recommended E2E tests that verify UX like a user would
|
|
282
|
+
|
|
283
|
+
### 3. Verify Fixes
|
|
284
|
+
- Run the test suite to verify fixes work correctly
|
|
285
|
+
- Ensure all tests pass before marking complete
|
|
286
|
+
|
|
287
|
+
### Required Response
|
|
288
|
+
When all fixes are complete AND tests pass, respond with:
|
|
289
|
+
<promise>DONE</promise>
|
|
290
|
+
|
|
291
|
+
This will trigger a re-review of the updated changes.
|
|
292
|
+
</ralph-fix-mode>
|
|
293
|
+
`;
|
|
294
|
+
function buildReviewUserPrompt(s) {
|
|
295
|
+
return `## Code Review Request
|
|
296
|
+
|
|
297
|
+
The code implementation is complete. Please review the uncommitted changes.
|
|
298
|
+
|
|
299
|
+
### Original Task/Specification:
|
|
300
|
+
\`\`\`
|
|
301
|
+
${s.prompt}
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
### Instructions:
|
|
305
|
+
1. Run \`git diff HEAD --stat\` to see which files were modified
|
|
306
|
+
2. Run \`git diff HEAD\` or \`git diff HEAD -- <file>\` to inspect the actual changes
|
|
307
|
+
3. Read full files if you need more context
|
|
308
|
+
|
|
309
|
+
### Review Checklist:
|
|
310
|
+
1. **Spec Compliance**: Does this implementation fulfill the original requirements?
|
|
311
|
+
2. **Code Quality**: Are there bugs, logic errors, edge cases, or security issues?
|
|
312
|
+
3. **Test Coverage**: What unit tests and E2E tests should be added?
|
|
313
|
+
|
|
314
|
+
### Required Response:
|
|
315
|
+
- If there are CRITICAL or HIGH priority issues that must be fixed: <promise>NEEDFIX</promise>
|
|
316
|
+
- If the code is acceptable and meets requirements: <promise>APPROVED</promise>`;
|
|
317
|
+
}
|
|
318
|
+
function buildFixUserPrompt(s) {
|
|
319
|
+
return `## Fix Review Comments
|
|
320
|
+
|
|
321
|
+
The code review identified issues that need to be addressed.
|
|
322
|
+
|
|
323
|
+
### Review Feedback:
|
|
324
|
+
${s.lastReviewFeedback}
|
|
325
|
+
|
|
326
|
+
### Original Task/Specification:
|
|
327
|
+
\`\`\`
|
|
328
|
+
${s.prompt}
|
|
329
|
+
\`\`\`
|
|
330
|
+
|
|
331
|
+
### Instructions:
|
|
332
|
+
1. Address all CRITICAL and HIGH priority issues from the review above
|
|
333
|
+
2. Add the recommended unit tests
|
|
334
|
+
3. Add the recommended E2E tests (tests that verify UX exactly like a user would)
|
|
335
|
+
4. **Run the tests** to ensure they pass - do not skip this step
|
|
336
|
+
5. Verify all fixes are working correctly
|
|
337
|
+
|
|
338
|
+
When all fixes are complete AND tests pass, respond with:
|
|
339
|
+
<promise>DONE</promise>`;
|
|
340
|
+
}
|
|
341
|
+
function buildReReviewUserPrompt(s) {
|
|
342
|
+
return `## Re-Review Request (Review #${s.reviewCount + 1})
|
|
343
|
+
|
|
344
|
+
The previous review comments have been addressed. Please verify the fixes.
|
|
345
|
+
|
|
346
|
+
### Original Task/Specification:
|
|
347
|
+
\`\`\`
|
|
348
|
+
${s.prompt}
|
|
349
|
+
\`\`\`
|
|
350
|
+
|
|
351
|
+
### Instructions:
|
|
352
|
+
1. Run \`git diff HEAD --stat\` to see current changes
|
|
353
|
+
2. Run \`git diff HEAD\` to inspect the updated code
|
|
354
|
+
3. Verify all previous issues are resolved
|
|
355
|
+
4. Check that new changes don't introduce new issues
|
|
356
|
+
5. Verify tests are adequate
|
|
357
|
+
|
|
358
|
+
### Required Response:
|
|
359
|
+
- If more issues remain: <promise>NEEDFIX</promise>
|
|
360
|
+
- If everything is acceptable: <promise>APPROVED</promise>`;
|
|
361
|
+
}
|
|
362
|
+
export default async function ralphWiggum(input) {
|
|
363
|
+
return {
|
|
364
|
+
command: {
|
|
365
|
+
"ralph-loop": {
|
|
366
|
+
description: "Start a self-referential Ralph loop. Usage: ralph-loop <prompt> --max <iterations> --promise <text> --state-file <path>",
|
|
367
|
+
template: `You are now in a Ralph Wiggum iterative development loop.
|
|
368
|
+
|
|
369
|
+
The user wants you to work on the following task iteratively:
|
|
370
|
+
$ARGUMENTS
|
|
371
|
+
|
|
372
|
+
Instructions:
|
|
373
|
+
1. Work on the task step by step
|
|
374
|
+
2. After each iteration, the loop will automatically continue
|
|
375
|
+
3. The loop will stop when max iterations is reached OR you output <promise>TEXT</promise> where TEXT matches the completion promise
|
|
376
|
+
4. Focus on making progress with each iteration
|
|
377
|
+
5. When you believe the task is complete, output <promise>COMPLETION_PROMISE_TEXT</promise>
|
|
378
|
+
|
|
379
|
+
Begin working on the task now.`,
|
|
380
|
+
},
|
|
381
|
+
"cancel-ralph": {
|
|
382
|
+
description: "Cancel the active Ralph loop",
|
|
383
|
+
template: "The Ralph loop has been cancelled. Stop the current iteration.",
|
|
384
|
+
},
|
|
385
|
+
"ralph-status": {
|
|
386
|
+
description: "Show the current Ralph loop status",
|
|
387
|
+
template: "Show the current Ralph loop status for this session.",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
tool: {
|
|
391
|
+
"cancel-ralph": {
|
|
392
|
+
description: "Cancel the active Ralph loop for the current session",
|
|
393
|
+
args: {},
|
|
394
|
+
async execute(_args, ctx) {
|
|
395
|
+
const sessionID = ctx.sessionID;
|
|
396
|
+
const s = state[sessionID];
|
|
397
|
+
if (s) {
|
|
398
|
+
s.status = "cancelled";
|
|
399
|
+
s.active = false;
|
|
400
|
+
writeFinalState(sessionID, s);
|
|
401
|
+
delete state[sessionID];
|
|
402
|
+
// Clear active review session if this was it
|
|
403
|
+
if (activeReviewSession === sessionID) {
|
|
404
|
+
activeReviewSession = null;
|
|
405
|
+
}
|
|
406
|
+
return "Ralph loop cancelled";
|
|
407
|
+
}
|
|
408
|
+
return "No active Ralph loop to cancel";
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
"ralph-status": {
|
|
412
|
+
description: "Get the current Ralph loop status for the session",
|
|
413
|
+
args: {},
|
|
414
|
+
async execute(_args, ctx) {
|
|
415
|
+
const sessionID = ctx.sessionID;
|
|
416
|
+
const s = state[sessionID];
|
|
417
|
+
if (!s?.active) {
|
|
418
|
+
return "No active Ralph loop";
|
|
419
|
+
}
|
|
420
|
+
const remaining = s.max != null ? s.max - s.iterations : "unlimited";
|
|
421
|
+
return JSON.stringify({
|
|
422
|
+
active: s.active,
|
|
423
|
+
prompt: s.prompt,
|
|
424
|
+
promise: s.promise || "DONE",
|
|
425
|
+
iterations: s.iterations,
|
|
426
|
+
max: s.max ?? "unlimited",
|
|
427
|
+
remaining,
|
|
428
|
+
phase: s.phase,
|
|
429
|
+
reviewCount: s.reviewCount,
|
|
430
|
+
maxReviews: s.maxReviews,
|
|
431
|
+
reviewPromises: s.reviewPromises,
|
|
432
|
+
stateFile: s.stateFile || "none",
|
|
433
|
+
startedAt: s.startedAt,
|
|
434
|
+
status: s.status,
|
|
435
|
+
}, null, 2);
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
// Hook: Listen for command execution to set up the loop state
|
|
440
|
+
async ["event"](input) {
|
|
441
|
+
const event = input.event;
|
|
442
|
+
if (event?.type === "command.executed" && event?.properties?.name === "ralph-loop") {
|
|
443
|
+
const sessionID = event.properties.sessionID;
|
|
444
|
+
const args = parseArgs(event.properties.arguments || "");
|
|
445
|
+
const now = new Date().toISOString();
|
|
446
|
+
// Clean up existing state file on start
|
|
447
|
+
if (args.stateFile) {
|
|
448
|
+
cleanupExistingStateFile(args.stateFile);
|
|
449
|
+
}
|
|
450
|
+
state[sessionID] = {
|
|
451
|
+
active: true,
|
|
452
|
+
prompt: args.prompt,
|
|
453
|
+
promise: args.completionPromise || "DONE",
|
|
454
|
+
max: args.maxIterations,
|
|
455
|
+
iterations: 0,
|
|
456
|
+
stateFile: args.stateFile,
|
|
457
|
+
startedAt: now,
|
|
458
|
+
lastUpdatedAt: now,
|
|
459
|
+
status: "running",
|
|
460
|
+
// Review system fields
|
|
461
|
+
phase: "working",
|
|
462
|
+
reviewPromises: {
|
|
463
|
+
needFix: "NEEDFIX",
|
|
464
|
+
approved: "APPROVED",
|
|
465
|
+
},
|
|
466
|
+
reviewCount: 0,
|
|
467
|
+
maxReviews: 5,
|
|
468
|
+
};
|
|
469
|
+
// Write initial state
|
|
470
|
+
writeStateFile(sessionID, state[sessionID]);
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
// Hook: session.stop - called just before the session loop exits
|
|
474
|
+
// Modifies output.stop to control whether the loop should continue
|
|
475
|
+
async ["session.stop"](hookInput, output) {
|
|
476
|
+
const s = state[hookInput.sessionID];
|
|
477
|
+
if (!s?.active) {
|
|
478
|
+
return; // No active loop, let it stop
|
|
479
|
+
}
|
|
480
|
+
s.iterations++;
|
|
481
|
+
s.lastUpdatedAt = new Date().toISOString();
|
|
482
|
+
// WORKING PHASE
|
|
483
|
+
if (s.phase === "working") {
|
|
484
|
+
// Check for completion promise (DONE)
|
|
485
|
+
if (checkCompletionPromise(hookInput.lastAssistantText, s.promise)) {
|
|
486
|
+
// Check if this is a git repo, auto-init if not
|
|
487
|
+
const isGitRepo = await input.$ `git rev-parse --is-inside-work-tree`.text().catch(() => "false");
|
|
488
|
+
if (isGitRepo.trim() !== "true") {
|
|
489
|
+
// Auto-initialize git repo
|
|
490
|
+
await input.$ `git init`.quiet().catch(() => { });
|
|
491
|
+
}
|
|
492
|
+
// Check for changes that should trigger review
|
|
493
|
+
// Use git status --porcelain to detect staged, unstaged, and untracked files
|
|
494
|
+
const gitStatus = await input.$ `git status --porcelain`.text().catch(() => "");
|
|
495
|
+
const hasChanges = gitStatus.trim().length > 0;
|
|
496
|
+
if (!hasChanges) {
|
|
497
|
+
// No changes - exit normally
|
|
498
|
+
s.status = "completed";
|
|
499
|
+
s.active = false;
|
|
500
|
+
writeFinalState(hookInput.sessionID, s);
|
|
501
|
+
delete state[hookInput.sessionID];
|
|
502
|
+
output.stop = true;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// Transition to REVIEW phase
|
|
506
|
+
s.phase = "review";
|
|
507
|
+
activeReviewSession = hookInput.sessionID;
|
|
508
|
+
output.stop = false;
|
|
509
|
+
output.systemMessage = `[Ralph - CODE REVIEW MODE]`;
|
|
510
|
+
output.prompt = buildReviewUserPrompt(s);
|
|
511
|
+
writeStateFile(hookInput.sessionID, s);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Check max iterations
|
|
515
|
+
if (s.max != null && s.iterations >= s.max) {
|
|
516
|
+
s.status = "max_reached";
|
|
517
|
+
s.active = false;
|
|
518
|
+
activeReviewSession = null;
|
|
519
|
+
writeFinalState(hookInput.sessionID, s);
|
|
520
|
+
delete state[hookInput.sessionID];
|
|
521
|
+
output.stop = true;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// Continue working - existing logic
|
|
525
|
+
output.stop = false;
|
|
526
|
+
output.prompt = s.prompt;
|
|
527
|
+
const promiseHint = s.promise ? ` | When complete: <promise>${s.promise}</promise>` : "";
|
|
528
|
+
output.systemMessage = `[Ralph iteration ${s.iterations + 1}/${s.max ?? "∞"}${promiseHint}]`;
|
|
529
|
+
writeStateFile(hookInput.sessionID, s);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// REVIEW PHASE
|
|
533
|
+
if (s.phase === "review") {
|
|
534
|
+
// Check for APPROVED
|
|
535
|
+
if (checkCompletionPromise(hookInput.lastAssistantText, s.reviewPromises.approved)) {
|
|
536
|
+
s.status = "approved";
|
|
537
|
+
s.active = false;
|
|
538
|
+
activeReviewSession = null;
|
|
539
|
+
writeFinalState(hookInput.sessionID, s);
|
|
540
|
+
delete state[hookInput.sessionID];
|
|
541
|
+
output.stop = true;
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Check for NEEDFIX
|
|
545
|
+
if (checkCompletionPromise(hookInput.lastAssistantText, s.reviewPromises.needFix)) {
|
|
546
|
+
// Check max reviews limit
|
|
547
|
+
if (s.reviewCount >= s.maxReviews) {
|
|
548
|
+
s.status = "max_reviews_reached";
|
|
549
|
+
s.active = false;
|
|
550
|
+
activeReviewSession = null;
|
|
551
|
+
writeFinalState(hookInput.sessionID, s);
|
|
552
|
+
delete state[hookInput.sessionID];
|
|
553
|
+
output.stop = true;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// Transition to FIX phase
|
|
557
|
+
s.phase = "fix";
|
|
558
|
+
s.lastReviewFeedback = hookInput.lastAssistantText;
|
|
559
|
+
s.reviewCount++;
|
|
560
|
+
output.stop = false;
|
|
561
|
+
output.systemMessage = `[Ralph - FIX MODE (Review #${s.reviewCount})]`;
|
|
562
|
+
output.prompt = buildFixUserPrompt(s);
|
|
563
|
+
writeStateFile(hookInput.sessionID, s);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// Neither promise found - nudge for response
|
|
567
|
+
output.stop = false;
|
|
568
|
+
output.systemMessage = `[Ralph - REVIEW MODE - Awaiting verdict]`;
|
|
569
|
+
output.prompt = `Please complete your review by inspecting the changes with \`git diff HEAD\` and respond with:
|
|
570
|
+
- <promise>APPROVED</promise> if the code meets requirements
|
|
571
|
+
- <promise>NEEDFIX</promise> if there are critical/high priority issues`;
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// FIX PHASE
|
|
575
|
+
if (s.phase === "fix") {
|
|
576
|
+
// Check for DONE (fixes complete)
|
|
577
|
+
if (checkCompletionPromise(hookInput.lastAssistantText, s.promise)) {
|
|
578
|
+
// Transition back to REVIEW for re-review
|
|
579
|
+
s.phase = "review";
|
|
580
|
+
output.stop = false;
|
|
581
|
+
output.systemMessage = `[Ralph - RE-REVIEW MODE (Review #${s.reviewCount + 1})]`;
|
|
582
|
+
output.prompt = buildReReviewUserPrompt(s);
|
|
583
|
+
writeStateFile(hookInput.sessionID, s);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// Check max iterations (safety valve)
|
|
587
|
+
if (s.max != null && s.iterations >= s.max) {
|
|
588
|
+
s.status = "max_reached";
|
|
589
|
+
s.active = false;
|
|
590
|
+
activeReviewSession = null;
|
|
591
|
+
writeFinalState(hookInput.sessionID, s);
|
|
592
|
+
delete state[hookInput.sessionID];
|
|
593
|
+
output.stop = true;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// Continue fixing
|
|
597
|
+
output.stop = false;
|
|
598
|
+
output.systemMessage = `[Ralph - FIX MODE - Addressing review comments]`;
|
|
599
|
+
output.prompt = s.prompt;
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
// Hook: experimental.chat.system.transform - inject system prompts for review/fix modes
|
|
604
|
+
async ["experimental.chat.system.transform"](_input, output) {
|
|
605
|
+
if (!activeReviewSession)
|
|
606
|
+
return;
|
|
607
|
+
const s = state[activeReviewSession];
|
|
608
|
+
if (!s?.active) {
|
|
609
|
+
activeReviewSession = null;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (s.phase === "review") {
|
|
613
|
+
output.system.push(REVIEW_SYSTEM_PROMPT);
|
|
614
|
+
}
|
|
615
|
+
else if (s.phase === "fix") {
|
|
616
|
+
output.system.push(FIX_SYSTEM_PROMPT);
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sureshsankaran/ralph-wiggum",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Ralph Wiggum plugin for OpenCode - iterative, self-referential AI development loops",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
8
17
|
"keywords": [
|
|
9
18
|
"opencode",
|
|
10
19
|
"plugin",
|
|
@@ -21,11 +30,16 @@
|
|
|
21
30
|
"directory": "packages/ralph-wiggum"
|
|
22
31
|
},
|
|
23
32
|
"scripts": {
|
|
24
|
-
"typecheck": "tsc --noEmit"
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"build": "tsc"
|
|
25
35
|
},
|
|
26
36
|
"dependencies": {},
|
|
27
37
|
"devDependencies": {
|
|
28
38
|
"typescript": "^5.8.3",
|
|
29
|
-
"@types/node": "^20.0.0"
|
|
39
|
+
"@types/node": "^20.0.0",
|
|
40
|
+
"@types/bun": "^1.1.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"bun": ">=1.0.0"
|
|
30
44
|
}
|
|
31
45
|
}
|
package/src/index.ts
DELETED
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ralph Wiggum Plugin for OpenCode
|
|
3
|
-
* Implements the Ralph Wiggum technique for iterative, self-referential AI development loops.
|
|
4
|
-
*
|
|
5
|
-
* Based on: https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* ralph-loop "Your task here" --max 10 --promise "DONE"
|
|
9
|
-
* ralph-loop "Your task here" --max 10 --promise "DONE" --state-file /custom/path.json
|
|
10
|
-
* ralph-loop "Your task here" --no-state # Disable state file
|
|
11
|
-
*
|
|
12
|
-
* The loop will:
|
|
13
|
-
* 1. Execute the prompt
|
|
14
|
-
* 2. Continue iterating until max iterations OR completion promise is found
|
|
15
|
-
* 3. Feed the SAME original prompt back each iteration
|
|
16
|
-
* 4. Show iteration count in system message
|
|
17
|
-
* 5. Write state to ~/.config/opencode/state/ralph-wiggum.json (or custom path) for verification
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs"
|
|
21
|
-
import { homedir } from "os"
|
|
22
|
-
import { join, dirname } from "path"
|
|
23
|
-
|
|
24
|
-
// Default state file path
|
|
25
|
-
const DEFAULT_STATE_DIR = join(homedir(), ".config", "opencode", "state")
|
|
26
|
-
const DEFAULT_STATE_FILE = join(DEFAULT_STATE_DIR, "ralph-wiggum.json")
|
|
27
|
-
|
|
28
|
-
type RalphState = {
|
|
29
|
-
active: boolean
|
|
30
|
-
prompt: string
|
|
31
|
-
promise?: string
|
|
32
|
-
max?: number
|
|
33
|
-
iterations: number
|
|
34
|
-
stateFile: string | null
|
|
35
|
-
startedAt: string
|
|
36
|
-
lastUpdatedAt: string
|
|
37
|
-
status: "running" | "completed" | "cancelled" | "max_reached"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const state: Record<string, RalphState> = {}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Ensure directory exists for state file
|
|
44
|
-
*/
|
|
45
|
-
function ensureDir(filePath: string): void {
|
|
46
|
-
try {
|
|
47
|
-
mkdirSync(dirname(filePath), { recursive: true })
|
|
48
|
-
} catch {
|
|
49
|
-
// Ignore errors
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Clean up existing state file on start
|
|
55
|
-
*/
|
|
56
|
-
function cleanupExistingStateFile(filePath: string): void {
|
|
57
|
-
try {
|
|
58
|
-
if (existsSync(filePath)) {
|
|
59
|
-
unlinkSync(filePath)
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// Ignore errors
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Write state to file for external verification
|
|
68
|
-
*/
|
|
69
|
-
function writeStateFile(sessionID: string, s: RalphState): void {
|
|
70
|
-
if (!s.stateFile) return
|
|
71
|
-
try {
|
|
72
|
-
ensureDir(s.stateFile)
|
|
73
|
-
const stateData = {
|
|
74
|
-
sessionID,
|
|
75
|
-
active: s.active,
|
|
76
|
-
prompt: s.prompt,
|
|
77
|
-
promise: s.promise || null,
|
|
78
|
-
iterations: s.iterations,
|
|
79
|
-
max: s.max ?? null,
|
|
80
|
-
remaining: s.max != null ? s.max - s.iterations : null,
|
|
81
|
-
startedAt: s.startedAt,
|
|
82
|
-
lastUpdatedAt: new Date().toISOString(),
|
|
83
|
-
status: s.status,
|
|
84
|
-
}
|
|
85
|
-
writeFileSync(s.stateFile, JSON.stringify(stateData, null, 2))
|
|
86
|
-
} catch {
|
|
87
|
-
// Silently ignore write errors
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Write final state when loop ends
|
|
93
|
-
*/
|
|
94
|
-
function writeFinalState(sessionID: string, s: RalphState): void {
|
|
95
|
-
if (!s.stateFile) return
|
|
96
|
-
s.lastUpdatedAt = new Date().toISOString()
|
|
97
|
-
writeStateFile(sessionID, s)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Tokenize a string respecting quoted strings.
|
|
102
|
-
* Handles both single and double quotes, preserving content within quotes as single tokens.
|
|
103
|
-
*/
|
|
104
|
-
function tokenize(input: string): string[] {
|
|
105
|
-
const tokens: string[] = []
|
|
106
|
-
let current = ""
|
|
107
|
-
let inQuote: '"' | "'" | null = null
|
|
108
|
-
|
|
109
|
-
for (let i = 0; i < input.length; i++) {
|
|
110
|
-
const char = input[i]
|
|
111
|
-
|
|
112
|
-
if (inQuote) {
|
|
113
|
-
if (char === inQuote) {
|
|
114
|
-
// End of quoted section
|
|
115
|
-
inQuote = null
|
|
116
|
-
} else {
|
|
117
|
-
current += char
|
|
118
|
-
}
|
|
119
|
-
} else if (char === '"' || char === "'") {
|
|
120
|
-
// Start of quoted section
|
|
121
|
-
inQuote = char
|
|
122
|
-
} else if (char === " " || char === "\t") {
|
|
123
|
-
// Whitespace outside quotes - token boundary
|
|
124
|
-
if (current) {
|
|
125
|
-
tokens.push(current)
|
|
126
|
-
current = ""
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
current += char
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Don't forget the last token
|
|
134
|
-
if (current) {
|
|
135
|
-
tokens.push(current)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return tokens
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Parse arguments from command invocation
|
|
142
|
-
// Supports: ralph-loop "prompt text with spaces" --max 5 --promise "DONE" --state-file /tmp/ralph.json --no-state
|
|
143
|
-
function parseArgs(args: string): {
|
|
144
|
-
prompt: string
|
|
145
|
-
maxIterations: number
|
|
146
|
-
completionPromise?: string
|
|
147
|
-
stateFile: string | null
|
|
148
|
-
} {
|
|
149
|
-
const tokens = tokenize(args.trim())
|
|
150
|
-
const promptParts: string[] = []
|
|
151
|
-
let maxIterations = 10
|
|
152
|
-
let completionPromise: string | undefined
|
|
153
|
-
let stateFile: string | null = DEFAULT_STATE_FILE
|
|
154
|
-
let noState = false
|
|
155
|
-
|
|
156
|
-
let i = 0
|
|
157
|
-
while (i < tokens.length) {
|
|
158
|
-
const token = tokens[i]
|
|
159
|
-
if (token === "--max" || token === "--max-iterations") {
|
|
160
|
-
maxIterations = parseInt(tokens[++i] || "10", 10)
|
|
161
|
-
} else if (token === "--promise" || token === "--completion-promise") {
|
|
162
|
-
completionPromise = tokens[++i]
|
|
163
|
-
} else if (token === "--state-file" || token === "--state") {
|
|
164
|
-
stateFile = tokens[++i] || DEFAULT_STATE_FILE
|
|
165
|
-
} else if (token === "--no-state") {
|
|
166
|
-
noState = true
|
|
167
|
-
} else {
|
|
168
|
-
// Accumulate as prompt
|
|
169
|
-
promptParts.push(token)
|
|
170
|
-
}
|
|
171
|
-
i++
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
prompt: promptParts.join(" ") || "Continue working on the task",
|
|
176
|
-
maxIterations,
|
|
177
|
-
completionPromise,
|
|
178
|
-
stateFile: noState ? null : stateFile,
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Check if the assistant's response contains the completion promise.
|
|
184
|
-
* Looks for <promise>TEXT</promise> pattern where TEXT matches the expected promise.
|
|
185
|
-
*/
|
|
186
|
-
function checkCompletionPromise(text: string | undefined, expectedPromise: string | undefined): boolean {
|
|
187
|
-
if (!text || !expectedPromise) return false
|
|
188
|
-
|
|
189
|
-
// Look for <promise>TEXT</promise> pattern
|
|
190
|
-
const promiseRegex = /<promise>([\s\S]*?)<\/promise>/gi
|
|
191
|
-
const matches = text.matchAll(promiseRegex)
|
|
192
|
-
|
|
193
|
-
for (const match of matches) {
|
|
194
|
-
const promiseText = match[1].trim()
|
|
195
|
-
if (promiseText === expectedPromise) {
|
|
196
|
-
return true
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return false
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export default async function ralphWiggum(input: {
|
|
204
|
-
client: any
|
|
205
|
-
project: string
|
|
206
|
-
worktree: string
|
|
207
|
-
directory: string
|
|
208
|
-
serverUrl: string
|
|
209
|
-
$: any
|
|
210
|
-
}) {
|
|
211
|
-
return {
|
|
212
|
-
command: {
|
|
213
|
-
"ralph-loop": {
|
|
214
|
-
description:
|
|
215
|
-
"Start a self-referential Ralph loop. Usage: ralph-loop <prompt> --max <iterations> --promise <text> --state-file <path>",
|
|
216
|
-
template: `You are now in a Ralph Wiggum iterative development loop.
|
|
217
|
-
|
|
218
|
-
The user wants you to work on the following task iteratively:
|
|
219
|
-
$ARGUMENTS
|
|
220
|
-
|
|
221
|
-
Instructions:
|
|
222
|
-
1. Work on the task step by step
|
|
223
|
-
2. After each iteration, the loop will automatically continue
|
|
224
|
-
3. The loop will stop when max iterations is reached OR you output <promise>TEXT</promise> where TEXT matches the completion promise
|
|
225
|
-
4. Focus on making progress with each iteration
|
|
226
|
-
5. When you believe the task is complete, output <promise>COMPLETION_PROMISE_TEXT</promise>
|
|
227
|
-
|
|
228
|
-
Begin working on the task now.`,
|
|
229
|
-
},
|
|
230
|
-
"cancel-ralph": {
|
|
231
|
-
description: "Cancel the active Ralph loop",
|
|
232
|
-
template: "The Ralph loop has been cancelled. Stop the current iteration.",
|
|
233
|
-
},
|
|
234
|
-
"ralph-status": {
|
|
235
|
-
description: "Show the current Ralph loop status",
|
|
236
|
-
template: "Show the current Ralph loop status for this session.",
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
tool: {
|
|
241
|
-
"cancel-ralph": {
|
|
242
|
-
description: "Cancel the active Ralph loop for the current session",
|
|
243
|
-
args: {},
|
|
244
|
-
async execute(_args: {}, ctx: any) {
|
|
245
|
-
const sessionID = ctx.sessionID
|
|
246
|
-
const s = state[sessionID]
|
|
247
|
-
if (s) {
|
|
248
|
-
s.status = "cancelled"
|
|
249
|
-
s.active = false
|
|
250
|
-
writeFinalState(sessionID, s)
|
|
251
|
-
delete state[sessionID]
|
|
252
|
-
return "Ralph loop cancelled"
|
|
253
|
-
}
|
|
254
|
-
return "No active Ralph loop to cancel"
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
"ralph-status": {
|
|
258
|
-
description: "Get the current Ralph loop status for the session",
|
|
259
|
-
args: {},
|
|
260
|
-
async execute(_args: {}, ctx: any) {
|
|
261
|
-
const sessionID = ctx.sessionID
|
|
262
|
-
const s = state[sessionID]
|
|
263
|
-
if (!s?.active) {
|
|
264
|
-
return "No active Ralph loop"
|
|
265
|
-
}
|
|
266
|
-
const remaining = s.max != null ? s.max - s.iterations : "unlimited"
|
|
267
|
-
return JSON.stringify(
|
|
268
|
-
{
|
|
269
|
-
active: s.active,
|
|
270
|
-
prompt: s.prompt,
|
|
271
|
-
promise: s.promise || "none",
|
|
272
|
-
iterations: s.iterations,
|
|
273
|
-
max: s.max ?? "unlimited",
|
|
274
|
-
remaining,
|
|
275
|
-
stateFile: s.stateFile || "none",
|
|
276
|
-
startedAt: s.startedAt,
|
|
277
|
-
status: s.status,
|
|
278
|
-
},
|
|
279
|
-
null,
|
|
280
|
-
2,
|
|
281
|
-
)
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
// Hook: Listen for command execution to set up the loop state
|
|
287
|
-
async ["event"](input: { event: any }): Promise<void> {
|
|
288
|
-
const event = input.event
|
|
289
|
-
if (event?.type === "command.executed" && event?.properties?.name === "ralph-loop") {
|
|
290
|
-
const sessionID = event.properties.sessionID
|
|
291
|
-
const args = parseArgs(event.properties.arguments || "")
|
|
292
|
-
const now = new Date().toISOString()
|
|
293
|
-
|
|
294
|
-
// Clean up existing state file on start
|
|
295
|
-
if (args.stateFile) {
|
|
296
|
-
cleanupExistingStateFile(args.stateFile)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
state[sessionID] = {
|
|
300
|
-
active: true,
|
|
301
|
-
prompt: args.prompt,
|
|
302
|
-
promise: args.completionPromise,
|
|
303
|
-
max: args.maxIterations,
|
|
304
|
-
iterations: 0,
|
|
305
|
-
stateFile: args.stateFile,
|
|
306
|
-
startedAt: now,
|
|
307
|
-
lastUpdatedAt: now,
|
|
308
|
-
status: "running",
|
|
309
|
-
}
|
|
310
|
-
// Write initial state
|
|
311
|
-
writeStateFile(sessionID, state[sessionID])
|
|
312
|
-
}
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
// Hook: session.stop - called just before the session loop exits (SYNCHRONOUS)
|
|
316
|
-
// Modifies output.stop to control whether the loop should continue
|
|
317
|
-
["session.stop"](
|
|
318
|
-
hookInput: { sessionID: string; step: number; lastAssistantText?: string },
|
|
319
|
-
output: { stop: boolean; prompt?: string; systemMessage?: string },
|
|
320
|
-
): void {
|
|
321
|
-
const s = state[hookInput.sessionID]
|
|
322
|
-
if (!s?.active) {
|
|
323
|
-
return // No active loop, let it stop
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
s.iterations++
|
|
327
|
-
s.lastUpdatedAt = new Date().toISOString()
|
|
328
|
-
|
|
329
|
-
// Check for completion promise in assistant's response
|
|
330
|
-
if (checkCompletionPromise(hookInput.lastAssistantText, s.promise)) {
|
|
331
|
-
s.status = "completed"
|
|
332
|
-
s.active = false
|
|
333
|
-
writeFinalState(hookInput.sessionID, s)
|
|
334
|
-
delete state[hookInput.sessionID]
|
|
335
|
-
output.stop = true
|
|
336
|
-
return
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Check max iterations
|
|
340
|
-
if (s.max != null && s.iterations >= s.max) {
|
|
341
|
-
s.status = "max_reached"
|
|
342
|
-
s.active = false
|
|
343
|
-
writeFinalState(hookInput.sessionID, s)
|
|
344
|
-
delete state[hookInput.sessionID]
|
|
345
|
-
output.stop = true
|
|
346
|
-
return
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Continue the loop - feed back the SAME original prompt
|
|
350
|
-
output.stop = false
|
|
351
|
-
output.prompt = s.prompt
|
|
352
|
-
|
|
353
|
-
// Write state update
|
|
354
|
-
writeStateFile(hookInput.sessionID, s)
|
|
355
|
-
|
|
356
|
-
// Add system message with iteration info
|
|
357
|
-
const promiseHint = s.promise ? ` | To complete: output <promise>${s.promise}</promise>` : ""
|
|
358
|
-
output.systemMessage = `[Ralph iteration ${s.iterations + 1}/${s.max ?? "∞"}${promiseHint}]`
|
|
359
|
-
},
|
|
360
|
-
}
|
|
361
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"outDir": "dist",
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"types": ["bun"]
|
|
12
|
-
},
|
|
13
|
-
"include": ["src"]
|
|
14
|
-
}
|