@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.
Files changed (39) hide show
  1. package/.claude-plugin/plugin.json +11 -0
  2. package/LICENSE +21 -0
  3. package/README.ko.md +358 -0
  4. package/README.md +282 -0
  5. package/bin/install.mjs +10 -0
  6. package/bin/loophaus.mjs +192 -0
  7. package/bin/uninstall.mjs +233 -0
  8. package/codex/commands/cancel-ralph.md +30 -0
  9. package/codex/commands/ralph-loop.md +73 -0
  10. package/commands/cancel-ralph.md +23 -0
  11. package/commands/help.md +96 -0
  12. package/commands/loop-plan.md +55 -0
  13. package/commands/loop-pulse.md +38 -0
  14. package/commands/loop-stop.md +29 -0
  15. package/commands/loop.md +17 -0
  16. package/commands/ralph-loop.md +18 -0
  17. package/core/engine.mjs +84 -0
  18. package/core/event-logger.mjs +37 -0
  19. package/core/loop.schema.json +29 -0
  20. package/hooks/hooks.json +15 -0
  21. package/hooks/stop-hook.mjs +79 -0
  22. package/lib/paths.mjs +91 -0
  23. package/lib/state.mjs +46 -0
  24. package/lib/stop-hook-core.mjs +162 -0
  25. package/package.json +57 -0
  26. package/platforms/claude-code/adapter.mjs +20 -0
  27. package/platforms/claude-code/installer.mjs +165 -0
  28. package/platforms/codex-cli/adapter.mjs +20 -0
  29. package/platforms/codex-cli/installer.mjs +131 -0
  30. package/platforms/kiro-cli/adapter.mjs +21 -0
  31. package/platforms/kiro-cli/installer.mjs +115 -0
  32. package/scripts/setup-ralph-loop.sh +145 -0
  33. package/skills/ralph-claude-cancel/SKILL.md +23 -0
  34. package/skills/ralph-claude-interview/SKILL.md +178 -0
  35. package/skills/ralph-claude-loop/SKILL.md +101 -0
  36. package/skills/ralph-claude-orchestrator/SKILL.md +129 -0
  37. package/skills/ralph-interview/SKILL.md +275 -0
  38. package/skills/ralph-orchestrator/SKILL.md +254 -0
  39. package/store/state-store.mjs +80 -0
@@ -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
@@ -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