@sureshsankaran/ralph-wiggum 0.1.12 → 0.2.0
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/package.json +14 -30
- package/src/index.ts +361 -0
- package/tsconfig.json +14 -0
- package/README.md +0 -243
- package/commands/cancel-ralph.md +0 -16
- package/commands/ralph-loop.md +0 -15
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -188
- package/scripts/postinstall.js +0 -63
- package/scripts/setup-loop.js +0 -71
package/package.json
CHANGED
|
@@ -1,47 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sureshsankaran/ralph-wiggum",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Ralph Wiggum
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Ralph Wiggum plugin for OpenCode - iterative, self-referential AI development loops",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"import": "./dist/index.js",
|
|
11
|
-
"types": "./dist/index.d.ts"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"dist",
|
|
16
|
-
"commands",
|
|
17
|
-
"scripts"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc",
|
|
21
|
-
"prepublishOnly": "npm run build",
|
|
22
|
-
"postinstall": "node scripts/postinstall.js"
|
|
23
|
-
},
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
24
8
|
"keywords": [
|
|
25
9
|
"opencode",
|
|
26
10
|
"plugin",
|
|
27
11
|
"ralph-wiggum",
|
|
28
|
-
"ai",
|
|
29
12
|
"iterative",
|
|
30
|
-
"
|
|
13
|
+
"ai",
|
|
14
|
+
"development"
|
|
31
15
|
],
|
|
32
|
-
"author": "",
|
|
16
|
+
"author": "sureshsankaran",
|
|
33
17
|
"license": "MIT",
|
|
34
18
|
"repository": {
|
|
35
19
|
"type": "git",
|
|
36
|
-
"url": "https://github.com/
|
|
37
|
-
"directory": "
|
|
20
|
+
"url": "https://github.com/sureshsankaran/opencode.git",
|
|
21
|
+
"directory": "packages/ralph-wiggum"
|
|
38
22
|
},
|
|
39
|
-
"
|
|
40
|
-
"
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
41
25
|
},
|
|
26
|
+
"dependencies": {},
|
|
42
27
|
"devDependencies": {
|
|
43
|
-
"
|
|
44
|
-
"@types/node": "^
|
|
45
|
-
"typescript": "^5.0.0"
|
|
28
|
+
"typescript": "^5.8.3",
|
|
29
|
+
"@types/node": "^20.0.0"
|
|
46
30
|
}
|
|
47
31
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
package/README.md
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
# Ralph Wiggum Plugin
|
|
2
|
-
|
|
3
|
-
Implementation of the Ralph Wiggum technique for iterative, self-referential AI development loops in OpenCode.
|
|
4
|
-
|
|
5
|
-
## What is Ralph?
|
|
6
|
-
|
|
7
|
-
Ralph is a development methodology based on continuous AI agent loops. As Geoffrey Huntley describes it: **"Ralph is a Bash loop"** - a simple `while true` that repeatedly feeds an AI agent a prompt file, allowing it to iteratively improve its work until completion.
|
|
8
|
-
|
|
9
|
-
The technique is named after Ralph Wiggum from The Simpsons, embodying the philosophy of persistent iteration despite setbacks.
|
|
10
|
-
|
|
11
|
-
### Core Concept
|
|
12
|
-
|
|
13
|
-
This plugin implements Ralph using a **session.idle hook** that intercepts session completion:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
# You run ONCE:
|
|
17
|
-
/ralph-loop "Your task description" --completion-promise "DONE"
|
|
18
|
-
|
|
19
|
-
# Then OpenCode automatically:
|
|
20
|
-
# 1. Works on the task
|
|
21
|
-
# 2. Session becomes idle
|
|
22
|
-
# 3. session.idle hook intercepts
|
|
23
|
-
# 4. Hook feeds the SAME prompt back
|
|
24
|
-
# 5. Repeat until completion
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The loop happens **inside your current session** - you don't need external bash loops. The plugin creates a self-referential feedback loop by intercepting session idle events.
|
|
28
|
-
|
|
29
|
-
This creates a **self-referential feedback loop** where:
|
|
30
|
-
|
|
31
|
-
- The prompt never changes between iterations
|
|
32
|
-
- Previous work persists in files
|
|
33
|
-
- Each iteration sees modified files and git history
|
|
34
|
-
- OpenCode autonomously improves by reading its own past work in files
|
|
35
|
-
|
|
36
|
-
## Installation
|
|
37
|
-
|
|
38
|
-
### Option 1: NPM Package (Recommended)
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install opencode-ralph-wiggum
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The postinstall script automatically copies the command files to `~/.config/opencode/command/`.
|
|
45
|
-
|
|
46
|
-
Then add the plugin to your `opencode.json`:
|
|
47
|
-
|
|
48
|
-
```json
|
|
49
|
-
{
|
|
50
|
-
"plugin": ["opencode-ralph-wiggum"]
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Option 2: Manual Installation
|
|
55
|
-
|
|
56
|
-
Copy the files from this plugin to your opencode config:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# Copy plugin
|
|
60
|
-
cp plugins/ralph-wiggum/src/index.ts ~/.config/opencode/plugin/ralph-wiggum.ts
|
|
61
|
-
|
|
62
|
-
# Copy commands
|
|
63
|
-
cp plugins/ralph-wiggum/commands/*.md ~/.config/opencode/command/
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Then configure in your `opencode.json`:
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"plugin": ["~/.config/opencode/plugin/ralph-wiggum.ts"]
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Quick Start
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
/ralph-loop "Build a REST API for todos. Requirements: CRUD operations, input validation, tests. Output <promise>COMPLETE</promise> when done." --completion-promise "COMPLETE" --max-iterations 50
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
OpenCode will:
|
|
81
|
-
|
|
82
|
-
- Implement the API iteratively
|
|
83
|
-
- Run tests and see failures
|
|
84
|
-
- Fix bugs based on test output
|
|
85
|
-
- Iterate until all requirements met
|
|
86
|
-
- Output the completion promise when done
|
|
87
|
-
|
|
88
|
-
## Commands
|
|
89
|
-
|
|
90
|
-
### /ralph-loop
|
|
91
|
-
|
|
92
|
-
Start a Ralph loop in your current session.
|
|
93
|
-
|
|
94
|
-
**Usage:**
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
/ralph-loop "<prompt>" --max-iterations <n> --completion-promise "<text>"
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**Options:**
|
|
101
|
-
|
|
102
|
-
- `--max-iterations <n>` - Stop after N iterations (default: unlimited)
|
|
103
|
-
- `--completion-promise <text>` - Phrase that signals completion
|
|
104
|
-
|
|
105
|
-
### /cancel-ralph
|
|
106
|
-
|
|
107
|
-
Cancel the active Ralph loop.
|
|
108
|
-
|
|
109
|
-
**Usage:**
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
/cancel-ralph
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### /help
|
|
116
|
-
|
|
117
|
-
Get detailed help on the Ralph Wiggum technique and commands.
|
|
118
|
-
|
|
119
|
-
**Usage:**
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
/help
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
## Prompt Writing Best Practices
|
|
126
|
-
|
|
127
|
-
### 1. Clear Completion Criteria
|
|
128
|
-
|
|
129
|
-
Bad: "Build a todo API and make it good."
|
|
130
|
-
|
|
131
|
-
Good:
|
|
132
|
-
|
|
133
|
-
```markdown
|
|
134
|
-
Build a REST API for todos.
|
|
135
|
-
|
|
136
|
-
When complete:
|
|
137
|
-
|
|
138
|
-
- All CRUD endpoints working
|
|
139
|
-
- Input validation in place
|
|
140
|
-
- Tests passing (coverage > 80%)
|
|
141
|
-
- README with API docs
|
|
142
|
-
- Output: <promise>COMPLETE</promise>
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### 2. Incremental Goals
|
|
146
|
-
|
|
147
|
-
Bad: "Create a complete e-commerce platform."
|
|
148
|
-
|
|
149
|
-
Good:
|
|
150
|
-
|
|
151
|
-
```markdown
|
|
152
|
-
Phase 1: User authentication (JWT, tests)
|
|
153
|
-
Phase 2: Product catalog (list/search, tests)
|
|
154
|
-
Phase 3: Shopping cart (add/remove, tests)
|
|
155
|
-
|
|
156
|
-
Output <promise>COMPLETE</promise> when all phases done.
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### 3. Self-Correction
|
|
160
|
-
|
|
161
|
-
Bad: "Write code for feature X."
|
|
162
|
-
|
|
163
|
-
Good:
|
|
164
|
-
|
|
165
|
-
```markdown
|
|
166
|
-
Implement feature X following TDD:
|
|
167
|
-
|
|
168
|
-
1. Write failing tests
|
|
169
|
-
2. Implement feature
|
|
170
|
-
3. Run tests
|
|
171
|
-
4. If any fail, debug and fix
|
|
172
|
-
5. Refactor if needed
|
|
173
|
-
6. Repeat until all green
|
|
174
|
-
7. Output: <promise>COMPLETE</promise>
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### 4. Escape Hatches
|
|
178
|
-
|
|
179
|
-
Always use `--max-iterations` as a safety net to prevent infinite loops on impossible tasks:
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
# Recommended: Always set a reasonable iteration limit
|
|
183
|
-
/ralph-loop "Try to implement feature X" --max-iterations 20
|
|
184
|
-
|
|
185
|
-
# In your prompt, include what to do if stuck:
|
|
186
|
-
# "After 15 iterations, if not complete:
|
|
187
|
-
# - Document what's blocking progress
|
|
188
|
-
# - List what was attempted
|
|
189
|
-
# - Suggest alternative approaches"
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
**Note**: The `--completion-promise` uses exact string matching, so you cannot use it for multiple completion conditions (like "SUCCESS" vs "BLOCKED"). Always rely on `--max-iterations` as your primary safety mechanism.
|
|
193
|
-
|
|
194
|
-
## Philosophy
|
|
195
|
-
|
|
196
|
-
Ralph embodies several key principles:
|
|
197
|
-
|
|
198
|
-
### 1. Iteration > Perfection
|
|
199
|
-
|
|
200
|
-
Don't aim for perfect on first try. Let the loop refine the work.
|
|
201
|
-
|
|
202
|
-
### 2. Failures Are Data
|
|
203
|
-
|
|
204
|
-
"Deterministically bad" means failures are predictable and informative. Use them to tune prompts.
|
|
205
|
-
|
|
206
|
-
### 3. Operator Skill Matters
|
|
207
|
-
|
|
208
|
-
Success depends on writing good prompts, not just having a good model.
|
|
209
|
-
|
|
210
|
-
### 4. Persistence Wins
|
|
211
|
-
|
|
212
|
-
Keep trying until success. The loop handles retry logic automatically.
|
|
213
|
-
|
|
214
|
-
## When to Use Ralph
|
|
215
|
-
|
|
216
|
-
**Good for:**
|
|
217
|
-
|
|
218
|
-
- Well-defined tasks with clear success criteria
|
|
219
|
-
- Tasks requiring iteration and refinement (e.g., getting tests to pass)
|
|
220
|
-
- Greenfield projects where you can walk away
|
|
221
|
-
- Tasks with automatic verification (tests, linters)
|
|
222
|
-
|
|
223
|
-
**Not good for:**
|
|
224
|
-
|
|
225
|
-
- Tasks requiring human judgment or design decisions
|
|
226
|
-
- One-shot operations
|
|
227
|
-
- Tasks with unclear success criteria
|
|
228
|
-
- Production debugging (use targeted debugging instead)
|
|
229
|
-
|
|
230
|
-
## Real-World Results
|
|
231
|
-
|
|
232
|
-
- Successfully generated 6 repositories overnight in Y Combinator hackathon testing
|
|
233
|
-
- One $50k contract completed for $297 in API costs
|
|
234
|
-
- Created entire programming language ("cursed") over 3 months using this approach
|
|
235
|
-
|
|
236
|
-
## Learn More
|
|
237
|
-
|
|
238
|
-
- Original technique: https://ghuntley.com/ralph/
|
|
239
|
-
- Ralph Orchestrator: https://github.com/mikeyobrien/ralph-orchestrator
|
|
240
|
-
|
|
241
|
-
## For Help
|
|
242
|
-
|
|
243
|
-
Run `/help` in OpenCode for detailed command reference and examples.
|
package/commands/cancel-ralph.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Cancel active Ralph Wiggum loop"
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Cancel Ralph
|
|
6
|
-
|
|
7
|
-
To cancel the Ralph loop:
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
2. **If NOT_FOUND**: Say "No active Ralph loop found."
|
|
12
|
-
|
|
13
|
-
3. **If EXISTS**:
|
|
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
|
-
- Report: "Cancelled Ralph loop (was at iteration N)" where N is the iteration value
|
package/commands/ralph-loop.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Start Ralph Wiggum loop in current session"
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Ralph Loop Command
|
|
6
|
-
|
|
7
|
-
!`node ~/.config/opencode/scripts/ralph-wiggum/setup-loop.js $ARGUMENTS`
|
|
8
|
-
|
|
9
|
-
Please work on the task described above. The Ralph loop is now active.
|
|
10
|
-
|
|
11
|
-
When the session becomes idle, the session.stop hook will feed the SAME PROMPT back to you for the next iteration. You'll see your previous work in files and git history, allowing you to iterate and improve.
|
|
12
|
-
|
|
13
|
-
CRITICAL RULE: If a completion promise is set, you may ONLY output it when the statement is completely and unequivocally TRUE. Do not output false promises to escape the loop, even if you think you're stuck or should exit for other reasons. The loop is designed to continue until genuine completion.
|
|
14
|
-
|
|
15
|
-
To complete the loop, output: `<promise>YOUR_COMPLETION_PROMISE</promise>`
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, appendFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
function getStateFilePath() {
|
|
5
|
-
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
6
|
-
const stateDir = join(configDir, "opencode", "state");
|
|
7
|
-
mkdirSync(stateDir, { recursive: true });
|
|
8
|
-
return join(stateDir, "ralph-loop.local.md");
|
|
9
|
-
}
|
|
10
|
-
function getLogPath() {
|
|
11
|
-
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
12
|
-
return join(configDir, "opencode", "state", "ralph-debug.log");
|
|
13
|
-
}
|
|
14
|
-
function log(msg) {
|
|
15
|
-
const ts = new Date().toISOString();
|
|
16
|
-
appendFileSync(getLogPath(), `[${ts}] ${msg}\n`);
|
|
17
|
-
}
|
|
18
|
-
function parseState(content) {
|
|
19
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
20
|
-
if (!frontmatterMatch)
|
|
21
|
-
return null;
|
|
22
|
-
const [, frontmatter, prompt] = frontmatterMatch;
|
|
23
|
-
const lines = frontmatter.split("\n");
|
|
24
|
-
const state = { prompt: prompt.trim(), last_processed_id: null };
|
|
25
|
-
for (const line of lines) {
|
|
26
|
-
const [key, ...valueParts] = line.split(":");
|
|
27
|
-
if (!key)
|
|
28
|
-
continue;
|
|
29
|
-
const value = valueParts.join(":").trim();
|
|
30
|
-
switch (key.trim()) {
|
|
31
|
-
case "active":
|
|
32
|
-
state.active = value === "true";
|
|
33
|
-
break;
|
|
34
|
-
case "iteration":
|
|
35
|
-
state.iteration = parseInt(value, 10);
|
|
36
|
-
break;
|
|
37
|
-
case "max_iterations":
|
|
38
|
-
state.max_iterations = parseInt(value, 10);
|
|
39
|
-
break;
|
|
40
|
-
case "completion_promise":
|
|
41
|
-
state.completion_promise = value === "null" ? null : value.replace(/^"|"$/g, "");
|
|
42
|
-
break;
|
|
43
|
-
case "started_at":
|
|
44
|
-
state.started_at = value.replace(/^"|"$/g, "");
|
|
45
|
-
break;
|
|
46
|
-
case "last_processed_id":
|
|
47
|
-
state.last_processed_id = value === "null" ? null : value.replace(/^"|"$/g, "");
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (state.active === undefined ||
|
|
52
|
-
state.iteration === undefined ||
|
|
53
|
-
state.max_iterations === undefined ||
|
|
54
|
-
!state.prompt) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
return state;
|
|
58
|
-
}
|
|
59
|
-
function extractPromiseText(text) {
|
|
60
|
-
const match = text.match(/<promise>(.*?)<\/promise>/s);
|
|
61
|
-
return match ? match[1].trim().replace(/\s+/g, " ") : null;
|
|
62
|
-
}
|
|
63
|
-
export const RalphWiggumPlugin = async ({ client }) => {
|
|
64
|
-
const version = "0.1.12";
|
|
65
|
-
log(`Plugin loaded, version=${version}`);
|
|
66
|
-
const stateFilePath = getStateFilePath();
|
|
67
|
-
// Clear log file on plugin load
|
|
68
|
-
try {
|
|
69
|
-
writeFileSync(getLogPath(), "");
|
|
70
|
-
}
|
|
71
|
-
catch { }
|
|
72
|
-
let completionDetected = false;
|
|
73
|
-
let lastProcessedId = null;
|
|
74
|
-
let iterationInProgress = false;
|
|
75
|
-
let callCount = 0;
|
|
76
|
-
log(`Plugin initialized`);
|
|
77
|
-
const hooks = {
|
|
78
|
-
"experimental.session.stop": async (input, output) => {
|
|
79
|
-
const myCallId = ++callCount;
|
|
80
|
-
log(`[${myCallId}] Hook entry - iterationInProgress=${iterationInProgress}, lastProcessedId=${lastProcessedId}`);
|
|
81
|
-
// FIRST: Check lock synchronously before any async operations
|
|
82
|
-
if (iterationInProgress) {
|
|
83
|
-
log(`[${myCallId}] Blocked by lock`);
|
|
84
|
-
output.decision = "block";
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (!existsSync(stateFilePath)) {
|
|
88
|
-
log(`[${myCallId}] No state file`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const content = readFileSync(stateFilePath, "utf-8");
|
|
92
|
-
const state = parseState(content);
|
|
93
|
-
if (!state || !state.active) {
|
|
94
|
-
log(`[${myCallId}] No active state`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (completionDetected) {
|
|
98
|
-
log(`[${myCallId}] Completion already detected`);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// Acquire lock BEFORE async operation
|
|
102
|
-
iterationInProgress = true;
|
|
103
|
-
log(`[${myCallId}] Lock acquired, state.iteration=${state.iteration}`);
|
|
104
|
-
try {
|
|
105
|
-
const messages = await client.session.messages({ path: { id: input.sessionID } }).then((res) => res.data ?? []);
|
|
106
|
-
const lastAssistant = [...messages]
|
|
107
|
-
.reverse()
|
|
108
|
-
.find((m) => m.info.role === "assistant" && m.parts.some((p) => p.type === "text"));
|
|
109
|
-
if (!lastAssistant) {
|
|
110
|
-
log(`[${myCallId}] No assistant message, releasing lock`);
|
|
111
|
-
iterationInProgress = false;
|
|
112
|
-
output.decision = "block";
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const assistantId = lastAssistant.info.id;
|
|
116
|
-
log(`[${myCallId}] Found assistant id=${assistantId}, lastProcessedId=${lastProcessedId}`);
|
|
117
|
-
// Check if we already processed this message
|
|
118
|
-
if (lastProcessedId === assistantId) {
|
|
119
|
-
log(`[${myCallId}] Already processed, keeping lock, blocking`);
|
|
120
|
-
output.decision = "block";
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
// Mark this message as processed IMMEDIATELY
|
|
124
|
-
lastProcessedId = assistantId;
|
|
125
|
-
log(`[${myCallId}] Processing new message, set lastProcessedId=${assistantId}`);
|
|
126
|
-
const textParts = lastAssistant.parts.filter((p) => p.type === "text");
|
|
127
|
-
const fullText = textParts.map((p) => p.text).join("\n");
|
|
128
|
-
// Check completion promise
|
|
129
|
-
if (state.completion_promise) {
|
|
130
|
-
const promiseText = extractPromiseText(fullText);
|
|
131
|
-
if (promiseText === state.completion_promise) {
|
|
132
|
-
completionDetected = true;
|
|
133
|
-
console.log(`\nRalph loop complete! Detected <promise>${state.completion_promise}</promise>`);
|
|
134
|
-
try {
|
|
135
|
-
unlinkSync(stateFilePath);
|
|
136
|
-
}
|
|
137
|
-
catch { }
|
|
138
|
-
iterationInProgress = false;
|
|
139
|
-
log(`[${myCallId}] Completion detected, allowing exit`);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Max-iteration safety
|
|
144
|
-
if (state.max_iterations > 0 && state.iteration >= state.max_iterations) {
|
|
145
|
-
console.log(`\nRalph loop: Max iterations (${state.max_iterations}) reached.`);
|
|
146
|
-
try {
|
|
147
|
-
unlinkSync(stateFilePath);
|
|
148
|
-
}
|
|
149
|
-
catch { }
|
|
150
|
-
iterationInProgress = false;
|
|
151
|
-
log(`[${myCallId}] Max iterations reached, allowing exit`);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const nextIteration = state.iteration + 1;
|
|
155
|
-
log(`[${myCallId}] Advancing to iteration ${nextIteration}`);
|
|
156
|
-
// Update state file
|
|
157
|
-
let updatedContent = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
|
|
158
|
-
if (updatedContent.includes("last_processed_id:")) {
|
|
159
|
-
updatedContent = updatedContent.replace(/^last_processed_id: .*$/m, `last_processed_id: "${assistantId}"`);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
updatedContent = updatedContent.replace(/^(started_at: .*)$/m, `$1\nlast_processed_id: "${assistantId}"`);
|
|
163
|
-
}
|
|
164
|
-
writeFileSync(stateFilePath, updatedContent);
|
|
165
|
-
const systemMsg = state.completion_promise
|
|
166
|
-
? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
|
|
167
|
-
: `Ralph iteration ${nextIteration} | No completion promise set`;
|
|
168
|
-
console.log(`\n${systemMsg}`);
|
|
169
|
-
await client.session.promptAsync({
|
|
170
|
-
path: { id: input.sessionID },
|
|
171
|
-
body: {
|
|
172
|
-
parts: [{ type: "text", text: `[${systemMsg}]\n\n${state.prompt}` }],
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
iterationInProgress = false;
|
|
176
|
-
log(`[${myCallId}] Prompt sent, lock released, blocking exit`);
|
|
177
|
-
output.decision = "block";
|
|
178
|
-
}
|
|
179
|
-
catch (err) {
|
|
180
|
-
log(`[${myCallId}] Error: ${err}`);
|
|
181
|
-
iterationInProgress = false;
|
|
182
|
-
throw err;
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
return hooks;
|
|
187
|
-
};
|
|
188
|
-
export default RalphWiggumPlugin;
|
package/scripts/postinstall.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { copyFileSync, mkdirSync, existsSync } from "node:fs"
|
|
4
|
-
import { join, dirname } from "node:path"
|
|
5
|
-
import { homedir } from "node:os"
|
|
6
|
-
import { fileURLToPath } from "node:url"
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
const packageRoot = join(__dirname, "..")
|
|
10
|
-
|
|
11
|
-
const configBase = join(homedir(), ".config", "opencode")
|
|
12
|
-
const commandDir = join(configBase, "command")
|
|
13
|
-
const scriptsDir = join(configBase, "scripts", "ralph-wiggum")
|
|
14
|
-
|
|
15
|
-
// Create directories if they don't exist
|
|
16
|
-
for (const dir of [commandDir, scriptsDir]) {
|
|
17
|
-
if (!existsSync(dir)) {
|
|
18
|
-
mkdirSync(dir, { recursive: true })
|
|
19
|
-
console.log(`Created directory: ${dir}`)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Copy command files
|
|
24
|
-
const commands = ["ralph-loop.md", "cancel-ralph.md"]
|
|
25
|
-
const commandsDir = join(packageRoot, "commands")
|
|
26
|
-
|
|
27
|
-
for (const cmd of commands) {
|
|
28
|
-
const src = join(commandsDir, cmd)
|
|
29
|
-
const dest = join(commandDir, cmd)
|
|
30
|
-
|
|
31
|
-
if (existsSync(src)) {
|
|
32
|
-
copyFileSync(src, dest)
|
|
33
|
-
console.log(`Installed command: ${cmd}`)
|
|
34
|
-
} else {
|
|
35
|
-
console.warn(`Warning: Command file not found: ${src}`)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Copy setup script
|
|
40
|
-
const setupSrc = join(packageRoot, "scripts", "setup-loop.js")
|
|
41
|
-
const setupDest = join(scriptsDir, "setup-loop.js")
|
|
42
|
-
|
|
43
|
-
if (existsSync(setupSrc)) {
|
|
44
|
-
copyFileSync(setupSrc, setupDest)
|
|
45
|
-
console.log(`Installed script: setup-loop.js`)
|
|
46
|
-
} else {
|
|
47
|
-
console.warn(`Warning: Script file not found: ${setupSrc}`)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.log("")
|
|
51
|
-
console.log("Ralph Wiggum commands installed!")
|
|
52
|
-
console.log("")
|
|
53
|
-
console.log("Next step: Add the plugin to your opencode config:")
|
|
54
|
-
console.log("")
|
|
55
|
-
console.log(" In opencode.json:")
|
|
56
|
-
console.log(" {")
|
|
57
|
-
console.log(' "plugin": ["@sureshsankaran/ralph-wiggum"]')
|
|
58
|
-
console.log(" }")
|
|
59
|
-
console.log("")
|
|
60
|
-
console.log("Usage:")
|
|
61
|
-
console.log(' /ralph-loop "Your task" --max-iterations 10 --completion-promise "DONE"')
|
|
62
|
-
console.log(" /cancel-ralph")
|
|
63
|
-
console.log("")
|
package/scripts/setup-loop.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Ralph Loop Setup Script
|
|
4
|
-
*
|
|
5
|
-
* Called by the ralph-loop command template to initialize the loop state.
|
|
6
|
-
* Arguments are passed directly to this script (not via shell substitution).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const fs = require("fs")
|
|
10
|
-
const path = require("path")
|
|
11
|
-
const os = require("os")
|
|
12
|
-
|
|
13
|
-
// Get arguments from command line (skip node and script name)
|
|
14
|
-
const rawArgs = process.argv.slice(2)
|
|
15
|
-
|
|
16
|
-
// Parse arguments
|
|
17
|
-
let maxIterations = 0
|
|
18
|
-
let completionPromise = null
|
|
19
|
-
let promptParts = []
|
|
20
|
-
|
|
21
|
-
for (let i = 0; i < rawArgs.length; i++) {
|
|
22
|
-
const arg = rawArgs[i]
|
|
23
|
-
|
|
24
|
-
if (arg === "--max-iterations" && i + 1 < rawArgs.length) {
|
|
25
|
-
maxIterations = parseInt(rawArgs[i + 1], 10) || 0
|
|
26
|
-
i++ // skip next arg
|
|
27
|
-
} else if (arg === "--completion-promise" && i + 1 < rawArgs.length) {
|
|
28
|
-
completionPromise = rawArgs[i + 1]
|
|
29
|
-
i++ // skip next arg
|
|
30
|
-
} else if (!arg.startsWith("--")) {
|
|
31
|
-
promptParts.push(arg)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const prompt = promptParts.join(" ").trim()
|
|
36
|
-
|
|
37
|
-
if (!prompt) {
|
|
38
|
-
console.log("Error: No prompt provided.")
|
|
39
|
-
console.log('Usage: --command ralph-loop "<prompt>" --max-iterations N --completion-promise TEXT')
|
|
40
|
-
process.exit(1)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Determine state directory
|
|
44
|
-
const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
|
|
45
|
-
const stateDir = path.join(configDir, "opencode", "state")
|
|
46
|
-
fs.mkdirSync(stateDir, { recursive: true })
|
|
47
|
-
|
|
48
|
-
// Build state file content
|
|
49
|
-
const cpYaml = completionPromise ? `"${completionPromise}"` : "null"
|
|
50
|
-
const lines = [
|
|
51
|
-
"---",
|
|
52
|
-
"active: true",
|
|
53
|
-
"iteration: 1",
|
|
54
|
-
`max_iterations: ${maxIterations}`,
|
|
55
|
-
`completion_promise: ${cpYaml}`,
|
|
56
|
-
`started_at: "${new Date().toISOString()}"`,
|
|
57
|
-
"last_processed_id: null",
|
|
58
|
-
"---",
|
|
59
|
-
"",
|
|
60
|
-
prompt,
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
const stateFilePath = path.join(stateDir, "ralph-loop.local.md")
|
|
64
|
-
fs.writeFileSync(stateFilePath, lines.join("\n") + "\n")
|
|
65
|
-
|
|
66
|
-
console.log("Ralph loop activated!")
|
|
67
|
-
console.log(`Iteration: 1`)
|
|
68
|
-
console.log(`Max iterations: ${maxIterations > 0 ? maxIterations : "unlimited"}`)
|
|
69
|
-
console.log(`Completion promise: ${completionPromise || "none"}`)
|
|
70
|
-
console.log("")
|
|
71
|
-
console.log(prompt)
|