@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 CHANGED
@@ -1,47 +1,31 @@
1
1
  {
2
2
  "name": "@sureshsankaran/ralph-wiggum",
3
- "version": "0.1.12",
4
- "description": "Ralph Wiggum iterative AI development plugin for OpenCode - continuously loops the same prompt until task completion",
3
+ "version": "0.2.0",
4
+ "description": "Ralph Wiggum plugin for OpenCode - iterative, self-referential AI development loops",
5
5
  "type": "module",
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
- "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
- "automation"
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/anomalyco/opencode.git",
37
- "directory": "plugins/ralph-wiggum"
20
+ "url": "https://github.com/sureshsankaran/opencode.git",
21
+ "directory": "packages/ralph-wiggum"
38
22
  },
39
- "peerDependencies": {
40
- "@opencode-ai/plugin": ">=1.0.0"
23
+ "scripts": {
24
+ "typecheck": "tsc --noEmit"
41
25
  },
26
+ "dependencies": {},
42
27
  "devDependencies": {
43
- "@opencode-ai/plugin": "^1.0.224",
44
- "@types/node": "^22.0.0",
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.
@@ -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
@@ -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
@@ -1,3 +0,0 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
- export declare const RalphWiggumPlugin: Plugin;
3
- export default RalphWiggumPlugin;
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;
@@ -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("")
@@ -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)