@sureshsankaran/ralph-wiggum 0.1.8 → 0.1.11
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/commands/ralph-loop.md +2 -69
- package/dist/index.js +11 -5
- package/package.json +1 -1
- package/scripts/postinstall.js +23 -8
- package/scripts/setup-loop.js +70 -0
package/commands/ralph-loop.md
CHANGED
|
@@ -4,78 +4,11 @@ description: "Start Ralph Wiggum loop in current session"
|
|
|
4
4
|
|
|
5
5
|
# Ralph Loop Command
|
|
6
6
|
|
|
7
|
-
!`
|
|
8
|
-
var args = process.env.RALPH_ARGS || '';
|
|
9
|
-
var fs = require('fs');
|
|
10
|
-
var path = require('path');
|
|
11
|
-
|
|
12
|
-
// Unescape the CLI escaping - remove outer quotes and unescape inner quotes
|
|
13
|
-
args = args.trim();
|
|
14
|
-
if ((args.startsWith('\"') && args.endsWith('\"')) || (args.startsWith(\"'\") && args.endsWith(\"'\"))) {
|
|
15
|
-
args = args.slice(1, -1);
|
|
16
|
-
}
|
|
17
|
-
args = args.replace(/\\\\\"/g, '\"');
|
|
18
|
-
|
|
19
|
-
var maxIterations = 0;
|
|
20
|
-
var completionPromise = 'null';
|
|
21
|
-
var prompt = args;
|
|
22
|
-
|
|
23
|
-
var maxMatch = args.match(/--max-iterations\s+(\d+)/);
|
|
24
|
-
if (maxMatch) {
|
|
25
|
-
maxIterations = parseInt(maxMatch[1], 10);
|
|
26
|
-
prompt = prompt.replace(/--max-iterations\s+\d+/, '');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
var cpMatch = args.match(/--completion-promise\s+\"([^\"]+)\"/);
|
|
30
|
-
if (cpMatch) {
|
|
31
|
-
completionPromise = cpMatch[1];
|
|
32
|
-
prompt = prompt.replace(/--completion-promise\s+\"[^\"]+\"/, '');
|
|
33
|
-
} else {
|
|
34
|
-
var cpMatch2 = args.match(/--completion-promise\s+(\S+)/);
|
|
35
|
-
if (cpMatch2) {
|
|
36
|
-
completionPromise = cpMatch2[1];
|
|
37
|
-
prompt = prompt.replace(/--completion-promise\s+\S+/, '');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
prompt = prompt.trim().replace(/^\"|\"$/g, '');
|
|
42
|
-
|
|
43
|
-
if (!prompt) {
|
|
44
|
-
console.log('Error: No prompt provided.');
|
|
45
|
-
console.log('Usage: /ralph-loop \"<prompt>\" [--max-iterations N] [--completion-promise TEXT]');
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
var configDir = process.env.XDG_CONFIG_HOME || path.join(require('os').homedir(), '.config');
|
|
50
|
-
var dir = path.join(configDir, 'opencode', 'state');
|
|
51
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
52
|
-
|
|
53
|
-
var cpYaml = completionPromise === 'null' ? 'null' : '\"' + completionPromise + '\"';
|
|
54
|
-
var lines = [
|
|
55
|
-
'---',
|
|
56
|
-
'active: true',
|
|
57
|
-
'iteration: 1',
|
|
58
|
-
'max_iterations: ' + maxIterations,
|
|
59
|
-
'completion_promise: ' + cpYaml,
|
|
60
|
-
'started_at: \"' + new Date().toISOString() + '\"',
|
|
61
|
-
'---',
|
|
62
|
-
'',
|
|
63
|
-
prompt
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
fs.writeFileSync(path.join(dir, 'ralph-loop.local.md'), lines.join('\n') + '\n');
|
|
67
|
-
|
|
68
|
-
console.log('Ralph loop activated!');
|
|
69
|
-
console.log('Iteration: 1');
|
|
70
|
-
console.log('Max iterations: ' + (maxIterations > 0 ? maxIterations : 'unlimited'));
|
|
71
|
-
console.log('Completion promise: ' + (completionPromise !== 'null' ? completionPromise : 'none'));
|
|
72
|
-
console.log('');
|
|
73
|
-
console.log(prompt);
|
|
74
|
-
"`
|
|
7
|
+
!`node ~/.config/opencode/scripts/ralph-wiggum/setup-loop.js $ARGUMENTS`
|
|
75
8
|
|
|
76
9
|
Please work on the task described above. The Ralph loop is now active.
|
|
77
10
|
|
|
78
|
-
When the session becomes idle, the session.
|
|
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.
|
|
79
12
|
|
|
80
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.
|
|
81
14
|
|
package/dist/index.js
CHANGED
|
@@ -67,7 +67,8 @@ export const RalphWiggumPlugin = async ({ client }) => {
|
|
|
67
67
|
const stateFilePath = getStateFilePath();
|
|
68
68
|
// In-memory state to prevent double-processing
|
|
69
69
|
let completionDetected = false;
|
|
70
|
-
let
|
|
70
|
+
let pendingIteration = -1; // Track iteration we're currently processing/waiting for
|
|
71
|
+
let lastAssistantId = ""; // Track last assistant message to detect new responses
|
|
71
72
|
// Cast to extended hooks type that includes experimental.session.stop
|
|
72
73
|
const hooks = {
|
|
73
74
|
// Stop hook: called when main session loop is about to exit
|
|
@@ -89,6 +90,14 @@ export const RalphWiggumPlugin = async ({ client }) => {
|
|
|
89
90
|
.find((m) => m.info.role === "assistant" && m.parts.some((p) => p.type === "text"));
|
|
90
91
|
if (!lastAssistant)
|
|
91
92
|
return;
|
|
93
|
+
// Prevent double-triggering: if we already sent a prompt for this iteration
|
|
94
|
+
// and haven't seen a new assistant message, skip
|
|
95
|
+
if (pendingIteration === state.iteration && lastAssistant.info.id === lastAssistantId) {
|
|
96
|
+
output.decision = "block"; // Still block, we're waiting for AI to respond
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Update tracking
|
|
100
|
+
lastAssistantId = lastAssistant.info.id;
|
|
92
101
|
const textParts = lastAssistant.parts.filter((p) => p.type === "text");
|
|
93
102
|
const fullText = textParts.map((p) => p.text).join("\n");
|
|
94
103
|
// Check completion promise
|
|
@@ -113,11 +122,8 @@ export const RalphWiggumPlugin = async ({ client }) => {
|
|
|
113
122
|
catch { }
|
|
114
123
|
return;
|
|
115
124
|
}
|
|
116
|
-
// Check if we've already advanced this iteration
|
|
117
|
-
if (state.iteration <= lastProcessedIteration)
|
|
118
|
-
return;
|
|
119
125
|
const nextIteration = state.iteration + 1;
|
|
120
|
-
|
|
126
|
+
pendingIteration = nextIteration;
|
|
121
127
|
// Update state file with new iteration
|
|
122
128
|
const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
|
|
123
129
|
writeFileSync(stateFilePath, updated);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sureshsankaran/ralph-wiggum",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Ralph Wiggum iterative AI development plugin for OpenCode - continuously loops the same prompt until task completion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -8,12 +8,16 @@ import { fileURLToPath } from "node:url"
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
const packageRoot = join(__dirname, "..")
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
// Copy command files
|
|
@@ -22,7 +26,7 @@ const commandsDir = join(packageRoot, "commands")
|
|
|
22
26
|
|
|
23
27
|
for (const cmd of commands) {
|
|
24
28
|
const src = join(commandsDir, cmd)
|
|
25
|
-
const dest = join(
|
|
29
|
+
const dest = join(commandDir, cmd)
|
|
26
30
|
|
|
27
31
|
if (existsSync(src)) {
|
|
28
32
|
copyFileSync(src, dest)
|
|
@@ -32,6 +36,17 @@ for (const cmd of commands) {
|
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
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
|
+
|
|
35
50
|
console.log("")
|
|
36
51
|
console.log("Ralph Wiggum commands installed!")
|
|
37
52
|
console.log("")
|
|
@@ -39,7 +54,7 @@ console.log("Next step: Add the plugin to your opencode config:")
|
|
|
39
54
|
console.log("")
|
|
40
55
|
console.log(" In opencode.json:")
|
|
41
56
|
console.log(" {")
|
|
42
|
-
console.log(' "plugin": ["
|
|
57
|
+
console.log(' "plugin": ["@sureshsankaran/ralph-wiggum"]')
|
|
43
58
|
console.log(" }")
|
|
44
59
|
console.log("")
|
|
45
60
|
console.log("Usage:")
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
"---",
|
|
58
|
+
"",
|
|
59
|
+
prompt,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const stateFilePath = path.join(stateDir, "ralph-loop.local.md")
|
|
63
|
+
fs.writeFileSync(stateFilePath, lines.join("\n") + "\n")
|
|
64
|
+
|
|
65
|
+
console.log("Ralph loop activated!")
|
|
66
|
+
console.log(`Iteration: 1`)
|
|
67
|
+
console.log(`Max iterations: ${maxIterations > 0 ? maxIterations : "unlimited"}`)
|
|
68
|
+
console.log(`Completion promise: ${completionPromise || "none"}`)
|
|
69
|
+
console.log("")
|
|
70
|
+
console.log(prompt)
|