@graypark/loophaus 2.0.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/.claude-plugin/plugin.json +11 -0
- package/LICENSE +21 -0
- package/README.ko.md +358 -0
- package/README.md +282 -0
- package/bin/install.mjs +10 -0
- package/bin/loophaus.mjs +192 -0
- package/bin/uninstall.mjs +233 -0
- package/codex/commands/cancel-ralph.md +30 -0
- package/codex/commands/ralph-loop.md +73 -0
- package/commands/cancel-ralph.md +23 -0
- package/commands/help.md +96 -0
- package/commands/loop-plan.md +55 -0
- package/commands/loop-pulse.md +38 -0
- package/commands/loop-stop.md +29 -0
- package/commands/loop.md +17 -0
- package/commands/ralph-loop.md +18 -0
- package/core/engine.mjs +84 -0
- package/core/event-logger.mjs +37 -0
- package/core/loop.schema.json +29 -0
- package/hooks/hooks.json +15 -0
- package/hooks/stop-hook.mjs +79 -0
- package/lib/paths.mjs +91 -0
- package/lib/state.mjs +46 -0
- package/lib/stop-hook-core.mjs +162 -0
- package/package.json +57 -0
- package/platforms/claude-code/adapter.mjs +20 -0
- package/platforms/claude-code/installer.mjs +165 -0
- package/platforms/codex-cli/adapter.mjs +20 -0
- package/platforms/codex-cli/installer.mjs +131 -0
- package/platforms/kiro-cli/adapter.mjs +21 -0
- package/platforms/kiro-cli/installer.mjs +115 -0
- package/scripts/setup-ralph-loop.sh +145 -0
- package/skills/ralph-claude-cancel/SKILL.md +23 -0
- package/skills/ralph-claude-interview/SKILL.md +178 -0
- package/skills/ralph-claude-loop/SKILL.md +101 -0
- package/skills/ralph-claude-orchestrator/SKILL.md +129 -0
- package/skills/ralph-interview/SKILL.md +275 -0
- package/skills/ralph-orchestrator/SKILL.md +254 -0
- package/store/state-store.mjs +80 -0
package/bin/loophaus.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// loophaus CLI — install, status, stats, uninstall
|
|
3
|
+
|
|
4
|
+
import { resolve, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { access } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const command = args[0] || "install";
|
|
14
|
+
const dryRun = args.includes("--dry-run");
|
|
15
|
+
const force = args.includes("--force");
|
|
16
|
+
const local = args.includes("--local");
|
|
17
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
18
|
+
|
|
19
|
+
function getHost() {
|
|
20
|
+
if (args.includes("--claude")) return "claude-code";
|
|
21
|
+
if (args.includes("--kiro")) return "kiro-cli";
|
|
22
|
+
const idx = args.indexOf("--host");
|
|
23
|
+
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const host = getHost();
|
|
28
|
+
|
|
29
|
+
if (showHelp || command === "help") {
|
|
30
|
+
console.log(`loophaus — Control plane for coding agents
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
npx @graypark/loophaus install [--host <name>] [--force] [--dry-run]
|
|
34
|
+
npx @graypark/loophaus uninstall [--host <name>]
|
|
35
|
+
npx @graypark/loophaus status
|
|
36
|
+
npx @graypark/loophaus stats
|
|
37
|
+
npx @graypark/loophaus --version
|
|
38
|
+
|
|
39
|
+
Hosts:
|
|
40
|
+
claude-code Claude Code (auto-detected via ~/.claude/)
|
|
41
|
+
codex-cli Codex CLI (auto-detected via ~/.codex/)
|
|
42
|
+
kiro-cli Kiro CLI (auto-detected via ~/.kiro/)
|
|
43
|
+
|
|
44
|
+
Install auto-detects available hosts if --host is not specified.
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--host <name> Target a specific host
|
|
48
|
+
--claude Shorthand for --host claude-code
|
|
49
|
+
--kiro Shorthand for --host kiro-cli
|
|
50
|
+
--local Install to project-local .codex/ (Codex only)
|
|
51
|
+
--force Overwrite existing installation
|
|
52
|
+
--dry-run Preview changes without modifying files
|
|
53
|
+
`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (args.includes("--version")) {
|
|
58
|
+
const { getPackageVersion } = await import("../lib/paths.mjs");
|
|
59
|
+
console.log(getPackageVersion());
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function detectHosts() {
|
|
64
|
+
const hosts = [];
|
|
65
|
+
const { detect: detectClaude } = await import("../platforms/claude-code/installer.mjs");
|
|
66
|
+
const { detect: detectCodex } = await import("../platforms/codex-cli/installer.mjs");
|
|
67
|
+
const { detect: detectKiro } = await import("../platforms/kiro-cli/installer.mjs");
|
|
68
|
+
|
|
69
|
+
if (await detectClaude()) hosts.push("claude-code");
|
|
70
|
+
if (await detectCodex()) hosts.push("codex-cli");
|
|
71
|
+
if (await detectKiro()) hosts.push("kiro-cli");
|
|
72
|
+
return hosts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runInstall() {
|
|
76
|
+
let targets = [];
|
|
77
|
+
|
|
78
|
+
if (host) {
|
|
79
|
+
targets = [host];
|
|
80
|
+
} else {
|
|
81
|
+
targets = await detectHosts();
|
|
82
|
+
if (targets.length === 0) {
|
|
83
|
+
console.log("No supported hosts detected. Install Claude Code, Codex CLI, or Kiro CLI first.");
|
|
84
|
+
console.log("Or specify a host: npx @graypark/loophaus install --host claude-code");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(`Detected hosts: ${targets.join(", ")}\n`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const t of targets) {
|
|
91
|
+
if (t === "claude-code") {
|
|
92
|
+
const { install } = await import("../platforms/claude-code/installer.mjs");
|
|
93
|
+
await install({ dryRun, force });
|
|
94
|
+
} else if (t === "codex-cli") {
|
|
95
|
+
const { install } = await import("../platforms/codex-cli/installer.mjs");
|
|
96
|
+
await install({ dryRun, force, local });
|
|
97
|
+
} else if (t === "kiro-cli") {
|
|
98
|
+
const { install } = await import("../platforms/kiro-cli/installer.mjs");
|
|
99
|
+
await install({ dryRun, force });
|
|
100
|
+
} else {
|
|
101
|
+
console.log(`Unknown host: ${t}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runUninstall() {
|
|
107
|
+
if (host === "claude-code" || args.includes("--claude")) {
|
|
108
|
+
const { uninstall } = await import("./uninstall.mjs");
|
|
109
|
+
await uninstall({ dryRun, claude: true });
|
|
110
|
+
} else if (host === "kiro-cli" || args.includes("--kiro")) {
|
|
111
|
+
const { uninstall } = await import("../platforms/kiro-cli/installer.mjs");
|
|
112
|
+
await uninstall({ dryRun });
|
|
113
|
+
} else {
|
|
114
|
+
const { uninstall } = await import("./uninstall.mjs");
|
|
115
|
+
await uninstall({ dryRun, local });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runStatus() {
|
|
120
|
+
const { read } = await import("../store/state-store.mjs");
|
|
121
|
+
const state = await read();
|
|
122
|
+
if (!state.active) {
|
|
123
|
+
console.log("No active loop.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const iterInfo = state.maxIterations > 0
|
|
127
|
+
? `${state.currentIteration}/${state.maxIterations}`
|
|
128
|
+
: `${state.currentIteration}`;
|
|
129
|
+
console.log(`Loop Status`);
|
|
130
|
+
console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
131
|
+
console.log(`Active: yes`);
|
|
132
|
+
console.log(`Iteration: ${iterInfo}`);
|
|
133
|
+
console.log(`Promise: ${state.completionPromise || "(none)"}`);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const { readFile } = await import("node:fs/promises");
|
|
137
|
+
const prd = JSON.parse(await readFile("prd.json", "utf-8"));
|
|
138
|
+
if (Array.isArray(prd.userStories)) {
|
|
139
|
+
const done = prd.userStories.filter(s => s.passes === true).length;
|
|
140
|
+
const total = prd.userStories.length;
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log("Stories");
|
|
143
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
144
|
+
for (const s of prd.userStories) {
|
|
145
|
+
const icon = s.passes ? "\u2713" : " ";
|
|
146
|
+
console.log(` ${icon} ${s.id} ${s.title}`);
|
|
147
|
+
}
|
|
148
|
+
console.log(`\n Progress: ${done}/${total} done`);
|
|
149
|
+
}
|
|
150
|
+
} catch { /* no prd.json */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runStats() {
|
|
154
|
+
const { readTrace } = await import("../core/event-logger.mjs");
|
|
155
|
+
const events = await readTrace();
|
|
156
|
+
if (events.length === 0) {
|
|
157
|
+
console.log("No trace data found. Run a loop first.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const iterations = events.filter(e => e.event === "iteration").length;
|
|
161
|
+
const stops = events.filter(e => e.event === "stop");
|
|
162
|
+
const lastStop = stops[stops.length - 1];
|
|
163
|
+
console.log(`Loop Stats`);
|
|
164
|
+
console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
165
|
+
console.log(`Total iterations: ${iterations}`);
|
|
166
|
+
console.log(`Total stops: ${stops.length}`);
|
|
167
|
+
if (lastStop) {
|
|
168
|
+
console.log(`Last stop reason: ${lastStop.reason || "unknown"}`);
|
|
169
|
+
console.log(`Last stop at: ${lastStop.ts || "unknown"}`);
|
|
170
|
+
}
|
|
171
|
+
console.log(`Trace file: .loophaus/trace.jsonl (${events.length} events)`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
switch (command) {
|
|
176
|
+
case "install": await runInstall(); break;
|
|
177
|
+
case "uninstall": await runUninstall(); break;
|
|
178
|
+
case "status": await runStatus(); break;
|
|
179
|
+
case "stats": await runStats(); break;
|
|
180
|
+
default:
|
|
181
|
+
if (command.startsWith("-")) {
|
|
182
|
+
await runInstall();
|
|
183
|
+
} else {
|
|
184
|
+
console.log(`Unknown command: ${command}`);
|
|
185
|
+
console.log("Run: npx @graypark/loophaus --help");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error(`\u2718 ${err.message}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, rm, access } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
getHooksJsonPath,
|
|
7
|
+
getPluginInstallDir,
|
|
8
|
+
getSkillsDir,
|
|
9
|
+
getLocalHooksJsonPath,
|
|
10
|
+
getLocalPluginDir,
|
|
11
|
+
getLocalSkillsDir,
|
|
12
|
+
getClaudePluginCacheDir,
|
|
13
|
+
getClaudeSettingsPath,
|
|
14
|
+
getClaudeInstalledPluginsPath,
|
|
15
|
+
} from "../lib/paths.mjs";
|
|
16
|
+
import { getStatePath } from "../lib/state.mjs";
|
|
17
|
+
|
|
18
|
+
const RALPH_HOOK_MARKER = "loophaus";
|
|
19
|
+
|
|
20
|
+
function log(icon, msg) {
|
|
21
|
+
console.log(` ${icon} ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function fileExists(p) {
|
|
25
|
+
try {
|
|
26
|
+
await access(p);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function uninstallClaude({ dryRun = false } = {}) {
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(
|
|
36
|
+
`loophaus uninstaller — Claude Code${dryRun ? " (DRY RUN)" : ""}`,
|
|
37
|
+
);
|
|
38
|
+
console.log("");
|
|
39
|
+
|
|
40
|
+
// 1. Remove plugin cache directory
|
|
41
|
+
const cacheDir = getClaudePluginCacheDir();
|
|
42
|
+
if (await fileExists(cacheDir)) {
|
|
43
|
+
log(">", `Remove plugin cache: ${cacheDir}`);
|
|
44
|
+
if (!dryRun) {
|
|
45
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
log("-", "Plugin cache not found");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Remove from installed_plugins.json
|
|
52
|
+
const installedPluginsPath = getClaudeInstalledPluginsPath();
|
|
53
|
+
if (await fileExists(installedPluginsPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const raw = await readFile(installedPluginsPath, "utf-8");
|
|
56
|
+
const data = JSON.parse(raw);
|
|
57
|
+
const pluginKey = "loophaus@loophaus-marketplace";
|
|
58
|
+
if (data.plugins && data.plugins[pluginKey]) {
|
|
59
|
+
delete data.plugins[pluginKey];
|
|
60
|
+
log(">", `Remove ${pluginKey} from installed_plugins.json`);
|
|
61
|
+
if (!dryRun) {
|
|
62
|
+
await writeFile(
|
|
63
|
+
installedPluginsPath,
|
|
64
|
+
JSON.stringify(data, null, 2),
|
|
65
|
+
"utf-8",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
log("-", "Plugin not found in installed_plugins.json");
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
log("!", "Warning: could not parse installed_plugins.json");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Remove from settings.json
|
|
77
|
+
const settingsPath = getClaudeSettingsPath();
|
|
78
|
+
if (await fileExists(settingsPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
81
|
+
const settings = JSON.parse(raw);
|
|
82
|
+
const pluginKey = "loophaus@loophaus-marketplace";
|
|
83
|
+
if (
|
|
84
|
+
settings.enabledPlugins &&
|
|
85
|
+
settings.enabledPlugins[pluginKey] !== undefined
|
|
86
|
+
) {
|
|
87
|
+
delete settings.enabledPlugins[pluginKey];
|
|
88
|
+
log(">", `Disable ${pluginKey} in settings.json`);
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
await writeFile(
|
|
91
|
+
settingsPath,
|
|
92
|
+
JSON.stringify(settings, null, 2),
|
|
93
|
+
"utf-8",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
log("!", "Warning: could not parse settings.json");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("");
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
log("\u2714", "Dry run complete. No files were modified.");
|
|
105
|
+
} else {
|
|
106
|
+
log("\u2714", "loophaus uninstalled from Claude Code.");
|
|
107
|
+
console.log("");
|
|
108
|
+
console.log(" Run /reload-plugins in Claude Code to apply.");
|
|
109
|
+
}
|
|
110
|
+
console.log("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function uninstall({
|
|
114
|
+
dryRun = false,
|
|
115
|
+
local = false,
|
|
116
|
+
claude = false,
|
|
117
|
+
} = {}) {
|
|
118
|
+
if (claude) {
|
|
119
|
+
return uninstallClaude({ dryRun });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const pluginDir = local ? getLocalPluginDir() : getPluginInstallDir();
|
|
123
|
+
const hooksJsonPath = local ? getLocalHooksJsonPath() : getHooksJsonPath();
|
|
124
|
+
const skillsDir = local ? getLocalSkillsDir() : getSkillsDir();
|
|
125
|
+
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(
|
|
128
|
+
`loophaus uninstaller — Codex CLI${dryRun ? " (DRY RUN)" : ""}`,
|
|
129
|
+
);
|
|
130
|
+
console.log("");
|
|
131
|
+
|
|
132
|
+
// 1. Remove ralph-codex entries from hooks.json
|
|
133
|
+
if (await fileExists(hooksJsonPath)) {
|
|
134
|
+
try {
|
|
135
|
+
const raw = await readFile(hooksJsonPath, "utf-8");
|
|
136
|
+
const config = JSON.parse(raw);
|
|
137
|
+
|
|
138
|
+
if (config.hooks && Array.isArray(config.hooks.Stop)) {
|
|
139
|
+
const before = config.hooks.Stop.length;
|
|
140
|
+
config.hooks.Stop = config.hooks.Stop.filter((entry) => {
|
|
141
|
+
const cmds = entry.hooks || [];
|
|
142
|
+
return !cmds.some(
|
|
143
|
+
(h) => h.command && h.command.includes(RALPH_HOOK_MARKER),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
const removed = before - config.hooks.Stop.length;
|
|
147
|
+
|
|
148
|
+
if (removed > 0) {
|
|
149
|
+
log(">", `Remove ${removed} Stop hook entry from ${hooksJsonPath}`);
|
|
150
|
+
if (!dryRun) {
|
|
151
|
+
await writeFile(
|
|
152
|
+
hooksJsonPath,
|
|
153
|
+
JSON.stringify(config, null, 2),
|
|
154
|
+
"utf-8",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
log("-", "No loophaus hooks found in hooks.json");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
log("!", `Warning: could not parse ${hooksJsonPath}`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
log("-", "No hooks.json found");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Remove plugin directory
|
|
169
|
+
if (await fileExists(pluginDir)) {
|
|
170
|
+
log(">", `Remove plugin directory: ${pluginDir}`);
|
|
171
|
+
if (!dryRun) {
|
|
172
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
log("-", "Plugin directory not found");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. Remove skill directories
|
|
179
|
+
const skillNames = [
|
|
180
|
+
"ralph-loop",
|
|
181
|
+
"cancel-ralph",
|
|
182
|
+
"ralph-interview",
|
|
183
|
+
"ralph-orchestrator",
|
|
184
|
+
"ralph-claude-interview",
|
|
185
|
+
"ralph-claude-loop",
|
|
186
|
+
"ralph-claude-cancel",
|
|
187
|
+
"ralph-claude-orchestrator",
|
|
188
|
+
];
|
|
189
|
+
for (const name of skillNames) {
|
|
190
|
+
const skillDir = join(skillsDir, name);
|
|
191
|
+
if (await fileExists(skillDir)) {
|
|
192
|
+
log(">", `Remove skill: ${skillDir}`);
|
|
193
|
+
if (!dryRun) {
|
|
194
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 4. Remove state file
|
|
200
|
+
const statePath = getStatePath();
|
|
201
|
+
if (await fileExists(statePath)) {
|
|
202
|
+
log(">", `Remove state file: ${statePath}`);
|
|
203
|
+
if (!dryRun) {
|
|
204
|
+
await rm(statePath, { force: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log("");
|
|
209
|
+
if (dryRun) {
|
|
210
|
+
log("\u2714", "Dry run complete. No files were modified.");
|
|
211
|
+
} else {
|
|
212
|
+
log("\u2714", "loophaus uninstalled successfully.");
|
|
213
|
+
}
|
|
214
|
+
console.log("");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Run directly if not imported
|
|
218
|
+
const isDirectRun =
|
|
219
|
+
process.argv[1] &&
|
|
220
|
+
(process.argv[1].endsWith("uninstall.mjs") ||
|
|
221
|
+
process.argv[1].endsWith("uninstall"));
|
|
222
|
+
|
|
223
|
+
if (isDirectRun) {
|
|
224
|
+
const args = process.argv.slice(2);
|
|
225
|
+
const dryRun = args.includes("--dry-run");
|
|
226
|
+
const local = args.includes("--local");
|
|
227
|
+
const claude = args.includes("--claude");
|
|
228
|
+
|
|
229
|
+
uninstall({ dryRun, local, claude }).catch((err) => {
|
|
230
|
+
console.error(`\u2718 Uninstall failed: ${err.message}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cancel-ralph
|
|
3
|
+
description: "Cancel the active Ralph Loop"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cancel Ralph
|
|
7
|
+
|
|
8
|
+
To cancel the Ralph loop:
|
|
9
|
+
|
|
10
|
+
1. Check if the state file exists at `.codex/ralph-loop.state.json`.
|
|
11
|
+
|
|
12
|
+
2. **If NOT found**: Say "No active Ralph loop found."
|
|
13
|
+
|
|
14
|
+
3. **If found**:
|
|
15
|
+
- Read the state file to get `currentIteration`.
|
|
16
|
+
- Set `active` to `false` by running:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node -e "
|
|
20
|
+
import { readState, writeState } from '${RALPH_CODEX_ROOT}/lib/state.mjs';
|
|
21
|
+
const state = await readState();
|
|
22
|
+
const iter = state.currentIteration;
|
|
23
|
+
state.active = false;
|
|
24
|
+
await writeState(state);
|
|
25
|
+
console.log('Cancelled Ralph loop at iteration ' + iter);
|
|
26
|
+
"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Alternatively, edit `.codex/ralph-loop.state.json` directly and set `"active": false`.
|
|
30
|
+
- Report: "Cancelled Ralph loop (was at iteration N)."
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ralph-loop
|
|
3
|
+
description: "Start a Ralph Loop — self-referential iterative development loop"
|
|
4
|
+
argument-hint: "PROMPT [--max-iterations N] [--completion-promise TEXT]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Ralph Loop Command
|
|
8
|
+
|
|
9
|
+
You are about to start a Ralph Loop. Parse the user's arguments as follows:
|
|
10
|
+
|
|
11
|
+
## Argument Parsing
|
|
12
|
+
|
|
13
|
+
1. Extract `--max-iterations N` (default: 20). Must be a positive integer or 0 (unlimited).
|
|
14
|
+
2. Extract `--completion-promise TEXT` (default: "TADA"). Multi-word values must be quoted.
|
|
15
|
+
3. Everything else is the **prompt** — the task description.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Run this command to initialize the Ralph loop state file:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node -e "
|
|
23
|
+
import { writeState } from '${RALPH_CODEX_ROOT}/lib/state.mjs';
|
|
24
|
+
await writeState({
|
|
25
|
+
active: true,
|
|
26
|
+
prompt: PROMPT_HERE,
|
|
27
|
+
completionPromise: PROMISE_HERE,
|
|
28
|
+
maxIterations: MAX_HERE,
|
|
29
|
+
currentIteration: 0,
|
|
30
|
+
sessionId: ''
|
|
31
|
+
});
|
|
32
|
+
"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Replace `PROMPT_HERE`, `PROMISE_HERE`, and `MAX_HERE` with the parsed values. Use proper JSON string escaping for the prompt.
|
|
36
|
+
|
|
37
|
+
Alternatively, create the state file directly at `.codex/ralph-loop.state.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"active": true,
|
|
42
|
+
"prompt": "<user's prompt>",
|
|
43
|
+
"completionPromise": "<promise text>",
|
|
44
|
+
"maxIterations": 20,
|
|
45
|
+
"currentIteration": 0,
|
|
46
|
+
"sessionId": ""
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## After Setup
|
|
51
|
+
|
|
52
|
+
Display this message to the user:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Ralph loop activated!
|
|
56
|
+
|
|
57
|
+
Iteration: 1
|
|
58
|
+
Max iterations: <N or "unlimited">
|
|
59
|
+
Completion promise: <promise text>
|
|
60
|
+
|
|
61
|
+
The stop hook will now block session exit and feed the SAME PROMPT back.
|
|
62
|
+
Your previous work persists in files and git history.
|
|
63
|
+
To cancel: /cancel-ralph
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then immediately begin working on the user's task prompt.
|
|
67
|
+
|
|
68
|
+
## Rules
|
|
69
|
+
|
|
70
|
+
- When a completion promise is set, you may ONLY output `<promise>TEXT</promise>` when the statement is genuinely and completely TRUE.
|
|
71
|
+
- Do NOT output false promises to exit the loop.
|
|
72
|
+
- Each iteration sees your previous work in files. Build on it incrementally.
|
|
73
|
+
- If stuck, document what's blocking and try a different approach.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Cancel active Ralph Loop"
|
|
3
|
+
allowed-tools:
|
|
4
|
+
[
|
|
5
|
+
"Bash(test -f .claude/ralph-loop.local.md:*)",
|
|
6
|
+
"Bash(rm .claude/ralph-loop.local.md)",
|
|
7
|
+
"Read(.claude/ralph-loop.local.md)",
|
|
8
|
+
]
|
|
9
|
+
hide-from-slash-command-tool: "true"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Cancel Ralph
|
|
13
|
+
|
|
14
|
+
To cancel the Ralph loop:
|
|
15
|
+
|
|
16
|
+
1. Check if `.claude/ralph-loop.local.md` exists using Bash: `test -f .claude/ralph-loop.local.md && echo "EXISTS" || echo "NOT_FOUND"`
|
|
17
|
+
|
|
18
|
+
2. **If NOT_FOUND**: Say "No active Ralph loop found."
|
|
19
|
+
|
|
20
|
+
3. **If EXISTS**:
|
|
21
|
+
- Read `.claude/ralph-loop.local.md` to get the current iteration number from the `iteration:` field
|
|
22
|
+
- Remove the file using Bash: `rm .claude/ralph-loop.local.md`
|
|
23
|
+
- Report: "Cancelled Ralph loop (was at iteration N)" where N is the iteration value
|
package/commands/help.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Explain loophaus plugin and available commands"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Loophaus Plugin Help
|
|
6
|
+
|
|
7
|
+
Please explain the following to the user:
|
|
8
|
+
|
|
9
|
+
## What is Loop?
|
|
10
|
+
|
|
11
|
+
Loop implements the Ralph Wiggum technique — an iterative development methodology based on continuous AI loops, pioneered by Geoffrey Huntley.
|
|
12
|
+
|
|
13
|
+
**Core concept:**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
while :; do
|
|
17
|
+
cat PROMPT.md | claude-code --continue
|
|
18
|
+
done
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The same prompt is fed to Claude repeatedly. The "self-referential" aspect comes from Claude seeing its own previous work in the files and git history, not from feeding output back as input.
|
|
22
|
+
|
|
23
|
+
**Each iteration:**
|
|
24
|
+
|
|
25
|
+
1. Claude receives the SAME prompt
|
|
26
|
+
2. Works on the task, modifying files
|
|
27
|
+
3. Tries to exit
|
|
28
|
+
4. Stop hook intercepts and feeds the same prompt again
|
|
29
|
+
5. Claude sees its previous work in the files
|
|
30
|
+
6. Iteratively improves until completion
|
|
31
|
+
|
|
32
|
+
## Available Commands
|
|
33
|
+
|
|
34
|
+
### /loop <PROMPT> [OPTIONS]
|
|
35
|
+
|
|
36
|
+
Start a loop in your current session.
|
|
37
|
+
|
|
38
|
+
**Usage:**
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
/loop "Refactor the cache layer" --max-iterations 20
|
|
42
|
+
/loop "Add tests" --completion-promise "TESTS COMPLETE"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Options:**
|
|
46
|
+
|
|
47
|
+
- `--max-iterations <n>` — Max iterations before auto-stop
|
|
48
|
+
- `--completion-promise <text>` — Promise phrase to signal completion
|
|
49
|
+
|
|
50
|
+
### /loop-stop
|
|
51
|
+
|
|
52
|
+
Stop an active loop (removes the loop state file).
|
|
53
|
+
|
|
54
|
+
### /loop-plan
|
|
55
|
+
|
|
56
|
+
Interactive interview that generates a PRD with right-sized stories, activates the loop, and starts implementing story by story.
|
|
57
|
+
|
|
58
|
+
**Usage:**
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
/loop-plan Add user authentication with JWT and login UI
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### /loop-pulse
|
|
65
|
+
|
|
66
|
+
Check the status of an active loop, including iteration count and story progress.
|
|
67
|
+
|
|
68
|
+
### /loop-orchestrator
|
|
69
|
+
|
|
70
|
+
Multi-agent orchestration patterns for parallel work streams.
|
|
71
|
+
|
|
72
|
+
## Available Skills (via Skill tool)
|
|
73
|
+
|
|
74
|
+
- `loophaus:ralph-interview` — PRD generation + loop start
|
|
75
|
+
- `loophaus:ralph-orchestrator` — Multi-agent patterns
|
|
76
|
+
- `loophaus:ralph-loop` — Direct loop execution
|
|
77
|
+
- `loophaus:cancel-ralph` — Cancel active loop
|
|
78
|
+
|
|
79
|
+
## Key Concepts
|
|
80
|
+
|
|
81
|
+
### Completion Promises
|
|
82
|
+
|
|
83
|
+
To signal completion, Claude must output a `<promise>` tag:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
<promise>TASK COMPLETE</promise>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### PRD-driven Loops
|
|
90
|
+
|
|
91
|
+
loophaus extends basic Loop with PRD (prd.json) and progress tracking (progress.txt), enabling story-by-story implementation with learnings carried across iterations.
|
|
92
|
+
|
|
93
|
+
## Learn More
|
|
94
|
+
|
|
95
|
+
- Original technique: https://ghuntley.com/ralph/
|
|
96
|
+
- loophaus: https://github.com/vcz-Gray/ralph-codex
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Plan and start loop via interactive interview"
|
|
3
|
+
argument-hint: "TASK_DESCRIPTION"
|
|
4
|
+
allowed-tools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "Agent", "Skill"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /loop-plan — Interactive Planning & Loop
|
|
8
|
+
|
|
9
|
+
## Phase 1: Discovery Interview
|
|
10
|
+
|
|
11
|
+
Ask the user 3-5 focused questions about $ARGUMENTS to understand:
|
|
12
|
+
- What exactly needs to be built
|
|
13
|
+
- Acceptance criteria
|
|
14
|
+
- Technical constraints
|
|
15
|
+
- Dependencies
|
|
16
|
+
|
|
17
|
+
## Phase 2: PRD Generation
|
|
18
|
+
|
|
19
|
+
Generate `prd.json` with user stories:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"title": "<project title>",
|
|
24
|
+
"userStories": [
|
|
25
|
+
{ "id": "US-001", "title": "<story>", "acceptance": "<criteria>", "passes": false },
|
|
26
|
+
...
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Right-size stories: each should be completable in 1-2 loop iterations.
|
|
32
|
+
|
|
33
|
+
## Phase 3: Loop Activation
|
|
34
|
+
|
|
35
|
+
After PRD approval, initialize the loop:
|
|
36
|
+
|
|
37
|
+
1. Create `.loophaus/state.json`:
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"active": true,
|
|
41
|
+
"prompt": "<implementation prompt based on PRD>",
|
|
42
|
+
"completionPromise": "TASK COMPLETE",
|
|
43
|
+
"maxIterations": <stories * 2 + 3>,
|
|
44
|
+
"currentIteration": 0,
|
|
45
|
+
"sessionId": ""
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. Start working on US-001 immediately.
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
- Each iteration: pick next pending story, implement, verify, mark passes=true if done
|
|
54
|
+
- Update `progress.txt` with status after each story
|
|
55
|
+
- Use `<promise>TASK COMPLETE</promise>` ONLY when ALL stories pass
|