@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
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@graypark/loophaus",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "description": "loophaus — Control plane for coding agents. Iterative dev loops with multi-agent orchestration.",
6
+ "license": "MIT",
7
+ "author": "graypark",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/vcz-Gray/loophaus"
11
+ },
12
+ "homepage": "https://github.com/vcz-Gray/loophaus#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/vcz-Gray/loophaus/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "loophaus": "./bin/loophaus.mjs"
21
+ },
22
+ "files": [
23
+ "bin/",
24
+ "hooks/",
25
+ "commands/",
26
+ "codex/",
27
+ "core/",
28
+ "store/",
29
+ "platforms/",
30
+ "scripts/",
31
+ "lib/",
32
+ "skills/",
33
+ ".claude-plugin/",
34
+ "README.md",
35
+ "README.ko.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "test": "vitest run",
40
+ "postinstall": "echo 'Run: npx loophaus --help'"
41
+ },
42
+ "engines": {
43
+ "node": ">=20.0.0"
44
+ },
45
+ "keywords": [
46
+ "loophaus",
47
+ "loop",
48
+ "ai-agent",
49
+ "iterative-development",
50
+ "stop-hook",
51
+ "control-plane",
52
+ "coding-agents"
53
+ ],
54
+ "devDependencies": {
55
+ "vitest": "^4.1.0"
56
+ }
57
+ }
@@ -0,0 +1,20 @@
1
+ // platforms/claude-code/adapter.mjs
2
+
3
+ export const name = "claude-code";
4
+ export const platform = "claude-code";
5
+
6
+ export function parseInput(raw) {
7
+ const input = typeof raw === "string" ? JSON.parse(raw) : raw;
8
+ return {
9
+ session_id: input.session_id || "",
10
+ transcript_path: input.transcript_path || null,
11
+ cwd: input.cwd || process.cwd(),
12
+ stop_hook_active: input.stop_hook_active || false,
13
+ last_assistant_message: input.last_assistant_message || "",
14
+ };
15
+ }
16
+
17
+ export function renderOutput(result) {
18
+ if (result.output) return JSON.stringify(result.output);
19
+ return "";
20
+ }
@@ -0,0 +1,165 @@
1
+ // platforms/claude-code/installer.mjs
2
+ import { readFile, writeFile, mkdir, cp, access, chmod } from "node:fs/promises";
3
+ import { join, resolve, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ getClaudeHome,
7
+ getClaudePluginsDir,
8
+ getClaudePluginCacheDir,
9
+ getClaudeSettingsPath,
10
+ getClaudeInstalledPluginsPath,
11
+ getPackageVersion,
12
+ } from "../../lib/paths.mjs";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const PROJECT_ROOT = resolve(dirname(__filename), "../..");
16
+
17
+ async function fileExists(p) {
18
+ try { await access(p); return true; } catch { return false; }
19
+ }
20
+
21
+ export async function detect() {
22
+ return fileExists(join(getClaudeHome(), "settings.json")) || fileExists(getClaudeHome());
23
+ }
24
+
25
+ export async function install({ dryRun = false, force = false } = {}) {
26
+ const MARKETPLACE_NAME = "loophaus-marketplace";
27
+ const PLUGIN_KEY = `loophaus@${MARKETPLACE_NAME}`;
28
+ const GITHUB_REPO = "vcz-Gray/loophaus";
29
+ const version = getPackageVersion();
30
+ const cacheDir = getClaudePluginCacheDir();
31
+ const pluginsDir = getClaudePluginsDir();
32
+ const marketplaceDir = join(pluginsDir, "marketplaces", MARKETPLACE_NAME);
33
+
34
+ console.log("");
35
+ console.log(`loophaus installer — Claude Code${dryRun ? " (DRY RUN)" : ""}`);
36
+ console.log(`Version: ${version}`);
37
+ console.log(`Target: ${cacheDir}`);
38
+ console.log("");
39
+
40
+ if (!force && await fileExists(cacheDir)) {
41
+ if (dryRun) {
42
+ console.log(" ! Existing installation found (would prompt for --force)");
43
+ } else {
44
+ console.log(" ! Existing installation found. Use --force to overwrite.");
45
+ return false;
46
+ }
47
+ }
48
+
49
+ // Step 1: Copy plugin files
50
+ console.log("[1/4] Copying plugin files...");
51
+ const dirs = [".claude-plugin", "commands", "hooks", "scripts", "skills", "lib", "core", "store"];
52
+ for (const dir of dirs) {
53
+ const src = join(PROJECT_ROOT, dir);
54
+ if (!(await fileExists(src))) continue;
55
+ const dest = join(cacheDir, dir);
56
+ console.log(` > Copy ${dir}/ -> ${dest}`);
57
+ if (!dryRun) {
58
+ await mkdir(dest, { recursive: true });
59
+ await cp(src, dest, { recursive: true });
60
+ }
61
+ }
62
+
63
+ for (const file of ["package.json", "LICENSE", "README.md"]) {
64
+ const src = join(PROJECT_ROOT, file);
65
+ if (await fileExists(src)) {
66
+ console.log(` > Copy ${file}`);
67
+ if (!dryRun) await cp(src, join(cacheDir, file));
68
+ }
69
+ }
70
+
71
+ if (!dryRun) {
72
+ const sh = join(cacheDir, "scripts", "setup-ralph-loop.sh");
73
+ if (await fileExists(sh)) await chmod(sh, 0o755);
74
+ }
75
+
76
+ // Step 2: Register marketplace
77
+ console.log("[2/4] Registering marketplace...");
78
+ const knownPath = join(pluginsDir, "known_marketplaces.json");
79
+ let known = {};
80
+ if (await fileExists(knownPath)) {
81
+ try { known = JSON.parse(await readFile(knownPath, "utf-8")); } catch {}
82
+ }
83
+ known[MARKETPLACE_NAME] = {
84
+ source: { source: "github", repo: GITHUB_REPO },
85
+ installLocation: marketplaceDir,
86
+ lastUpdated: new Date().toISOString(),
87
+ };
88
+ if (!dryRun) await writeFile(knownPath, JSON.stringify(known, null, 2), "utf-8");
89
+
90
+ const mpDir = join(marketplaceDir, ".claude-plugin");
91
+ if (!dryRun) {
92
+ await mkdir(mpDir, { recursive: true });
93
+ await writeFile(join(mpDir, "marketplace.json"), JSON.stringify({
94
+ name: MARKETPLACE_NAME,
95
+ owner: { name: "graypark" },
96
+ metadata: { description: "loophaus — Control plane for coding agents", version },
97
+ plugins: [{
98
+ name: "loophaus",
99
+ source: "./",
100
+ description: "Iterative dev loops with stop hooks",
101
+ version,
102
+ keywords: ["loophaus", "loop", "control-plane"],
103
+ category: "productivity",
104
+ skills: "./skills/",
105
+ }],
106
+ }, null, 2), "utf-8");
107
+ await cp(
108
+ join(PROJECT_ROOT, ".claude-plugin", "plugin.json"),
109
+ join(mpDir, "plugin.json"),
110
+ );
111
+ }
112
+
113
+ // Step 3: installed_plugins.json
114
+ console.log("[3/4] Registering plugin...");
115
+ const ipPath = getClaudeInstalledPluginsPath();
116
+ let ip = { version: 2, plugins: {} };
117
+ if (await fileExists(ipPath)) {
118
+ try { ip = JSON.parse(await readFile(ipPath, "utf-8")); } catch {}
119
+ }
120
+ delete ip.plugins["ralph-codex@ralph-codex"];
121
+ delete ip.plugins["ralph-codex@ralph-codex-marketplace"];
122
+ ip.plugins[PLUGIN_KEY] = [{
123
+ scope: "user",
124
+ installPath: cacheDir,
125
+ version,
126
+ installedAt: new Date().toISOString(),
127
+ lastUpdated: new Date().toISOString(),
128
+ }];
129
+ if (!dryRun) await writeFile(ipPath, JSON.stringify(ip, null, 2), "utf-8");
130
+
131
+ // Step 4: settings.json
132
+ console.log("[4/4] Enabling plugin...");
133
+ const settingsPath = getClaudeSettingsPath();
134
+ let settings = {};
135
+ if (await fileExists(settingsPath)) {
136
+ try { settings = JSON.parse(await readFile(settingsPath, "utf-8")); } catch {}
137
+ }
138
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
139
+ delete settings.enabledPlugins["ralph-codex@ralph-codex"];
140
+ delete settings.enabledPlugins["ralph-codex@ralph-codex-marketplace"];
141
+ settings.enabledPlugins[PLUGIN_KEY] = true;
142
+ if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
143
+ settings.extraKnownMarketplaces[MARKETPLACE_NAME] = {
144
+ source: { source: "github", repo: GITHUB_REPO },
145
+ };
146
+ if (!dryRun) await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
147
+
148
+ console.log("");
149
+ if (dryRun) {
150
+ console.log(" \u2714 Dry run complete.");
151
+ } else {
152
+ console.log(" \u2714 loophaus installed for Claude Code!");
153
+ console.log("");
154
+ console.log(" Run /reload-plugins in Claude Code to activate.");
155
+ console.log(" Commands: /loop, /loop-plan, /loop-stop, /loop-pulse");
156
+ console.log(" To uninstall: npx @graypark/loophaus uninstall --claude");
157
+ }
158
+ console.log("");
159
+ return true;
160
+ }
161
+
162
+ export async function uninstall({ dryRun = false } = {}) {
163
+ const { uninstall: doUninstall } = await import("../../bin/uninstall.mjs");
164
+ return doUninstall({ dryRun, claude: true });
165
+ }
@@ -0,0 +1,20 @@
1
+ // platforms/codex-cli/adapter.mjs
2
+
3
+ export const name = "codex-cli";
4
+ export const platform = "codex-cli";
5
+
6
+ export function parseInput(raw) {
7
+ const input = typeof raw === "string" ? JSON.parse(raw) : raw;
8
+ return {
9
+ session_id: input.session_id || "",
10
+ transcript_path: input.transcript_path || null,
11
+ cwd: input.cwd || process.cwd(),
12
+ stop_hook_active: input.stop_hook_active || false,
13
+ last_assistant_message: input.last_assistant_message || "",
14
+ };
15
+ }
16
+
17
+ export function renderOutput(result) {
18
+ if (result.output) return JSON.stringify(result.output);
19
+ return "";
20
+ }
@@ -0,0 +1,131 @@
1
+ // platforms/codex-cli/installer.mjs
2
+ import { readFile, writeFile, mkdir, cp, access } from "node:fs/promises";
3
+ import { join, resolve, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ getCodexHome,
7
+ getHooksJsonPath,
8
+ getPluginInstallDir,
9
+ getSkillsDir,
10
+ } from "../../lib/paths.mjs";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const PROJECT_ROOT = resolve(dirname(__filename), "../..");
14
+
15
+ async function fileExists(p) {
16
+ try { await access(p); return true; } catch { return false; }
17
+ }
18
+
19
+ export async function detect() {
20
+ return fileExists(getCodexHome());
21
+ }
22
+
23
+ export async function install({ dryRun = false, force = false, local = false } = {}) {
24
+ const pluginDir = local
25
+ ? join(process.cwd(), ".codex", "plugins", "loophaus")
26
+ : getPluginInstallDir();
27
+ const hooksJsonPath = local
28
+ ? join(process.cwd(), ".codex", "hooks.json")
29
+ : getHooksJsonPath();
30
+ const skillsDir = local
31
+ ? join(process.cwd(), ".codex", "skills")
32
+ : getSkillsDir();
33
+
34
+ console.log("");
35
+ console.log(`loophaus installer — Codex CLI${dryRun ? " (DRY RUN)" : ""}`);
36
+ console.log(`Mode: ${local ? "local (.codex/)" : "global (~/.codex/)"}`);
37
+ console.log(`Target: ${pluginDir}`);
38
+ console.log("");
39
+
40
+ if (!force && await fileExists(pluginDir)) {
41
+ if (dryRun) {
42
+ console.log(" ! Existing installation found (would prompt for --force)");
43
+ } else {
44
+ console.log(" ! Existing installation found. Use --force to overwrite.");
45
+ return false;
46
+ }
47
+ }
48
+
49
+ // Step 1: Copy files
50
+ console.log("[1/3] Copying plugin files...");
51
+ for (const dir of ["hooks", "codex/commands", "lib", "core", "store"]) {
52
+ const src = join(PROJECT_ROOT, dir);
53
+ if (!(await fileExists(src))) continue;
54
+ const destDir = dir === "codex/commands" ? "commands" : dir;
55
+ const dest = join(pluginDir, destDir);
56
+ console.log(` > Copy ${dir}/ -> ${dest}`);
57
+ if (!dryRun) {
58
+ await mkdir(dest, { recursive: true });
59
+ await cp(src, dest, { recursive: true });
60
+ }
61
+ }
62
+ if (!dryRun) await cp(join(PROJECT_ROOT, "package.json"), join(pluginDir, "package.json"));
63
+
64
+ // Step 2: Hooks
65
+ console.log("[2/3] Configuring Stop hook...");
66
+ const stopCmd = `node "${join(pluginDir, "hooks", "stop-hook.mjs")}"`;
67
+ let existing = { hooks: {} };
68
+ if (await fileExists(hooksJsonPath)) {
69
+ try { existing = JSON.parse(await readFile(hooksJsonPath, "utf-8")); } catch { existing = { hooks: {} }; }
70
+ }
71
+ if (!existing.hooks) existing.hooks = {};
72
+ if (!Array.isArray(existing.hooks.Stop)) existing.hooks.Stop = [];
73
+ existing.hooks.Stop = existing.hooks.Stop.filter(e =>
74
+ !(e.hooks || []).some(h => h.command && h.command.includes("loophaus"))
75
+ );
76
+ existing.hooks.Stop.push({ hooks: [{ type: "command", command: stopCmd, timeout: 30 }] });
77
+ if (!dryRun) {
78
+ await mkdir(dirname(hooksJsonPath), { recursive: true });
79
+ await writeFile(hooksJsonPath, JSON.stringify(existing, null, 2), "utf-8");
80
+ }
81
+
82
+ // Step 3: Skills
83
+ console.log("[3/3] Installing skills...");
84
+ for (const name of ["ralph-loop", "cancel-ralph"]) {
85
+ const skillDir = join(skillsDir, name);
86
+ const src = name === "ralph-loop" ? "codex/commands/ralph-loop.md" : "codex/commands/cancel-ralph.md";
87
+ console.log(` > Install skill: ${name}`);
88
+ if (!dryRun) {
89
+ await mkdir(skillDir, { recursive: true });
90
+ const srcPath = join(PROJECT_ROOT, src);
91
+ if (await fileExists(srcPath)) {
92
+ const content = await readFile(srcPath, "utf-8");
93
+ await writeFile(
94
+ join(skillDir, "SKILL.md"),
95
+ content.replaceAll("${RALPH_CODEX_ROOT}", pluginDir).replaceAll("${LOOPHAUS_ROOT}", pluginDir),
96
+ "utf-8",
97
+ );
98
+ }
99
+ }
100
+ }
101
+
102
+ const standaloneSkills = [
103
+ "ralph-interview",
104
+ "ralph-orchestrator",
105
+ "ralph-claude-interview",
106
+ "ralph-claude-loop",
107
+ "ralph-claude-cancel",
108
+ "ralph-claude-orchestrator",
109
+ ];
110
+ for (const sk of standaloneSkills) {
111
+ const srcDir = join(PROJECT_ROOT, "skills", sk);
112
+ if (await fileExists(srcDir)) {
113
+ console.log(` > Install skill: ${sk}`);
114
+ if (!dryRun) {
115
+ await mkdir(join(skillsDir, sk), { recursive: true });
116
+ await cp(srcDir, join(skillsDir, sk), { recursive: true });
117
+ }
118
+ }
119
+ }
120
+
121
+ console.log("");
122
+ if (dryRun) {
123
+ console.log(" \u2714 Dry run complete. No files were modified.");
124
+ } else {
125
+ console.log(" \u2714 loophaus installed for Codex CLI!");
126
+ console.log(" Commands: /loop, /loop-plan, /loop-stop, /loop-pulse");
127
+ console.log(` To uninstall: npx @graypark/loophaus uninstall${local ? " --local" : ""}`);
128
+ }
129
+ console.log("");
130
+ return true;
131
+ }
@@ -0,0 +1,21 @@
1
+ // platforms/kiro-cli/adapter.mjs
2
+
3
+ export const name = "kiro-cli";
4
+ export const platform = "kiro-cli";
5
+
6
+ export function parseInput(raw) {
7
+ const input = typeof raw === "string" ? JSON.parse(raw) : raw;
8
+ return {
9
+ session_id: input.session_id || "",
10
+ transcript_path: input.transcript_path || null,
11
+ cwd: input.cwd || process.cwd(),
12
+ stop_hook_active: input.stop_hook_active || false,
13
+ last_assistant_message: input.last_assistant_message || "",
14
+ hook_event_name: input.hook_event_name || "stop",
15
+ };
16
+ }
17
+
18
+ export function renderOutput(result) {
19
+ if (result.output) return JSON.stringify(result.output);
20
+ return "";
21
+ }
@@ -0,0 +1,115 @@
1
+ // platforms/kiro-cli/installer.mjs
2
+ import { readFile, writeFile, mkdir, cp, access, rm } from "node:fs/promises";
3
+ import { join, resolve, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { homedir } from "node:os";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const PROJECT_ROOT = resolve(dirname(__filename), "../..");
9
+
10
+ function getKiroHome() {
11
+ return join(homedir(), ".kiro");
12
+ }
13
+
14
+ async function fileExists(p) {
15
+ try { await access(p); return true; } catch { return false; }
16
+ }
17
+
18
+ export async function detect() {
19
+ return fileExists(getKiroHome());
20
+ }
21
+
22
+ export async function install({ dryRun = false, force = false } = {}) {
23
+ const kiroHome = getKiroHome();
24
+ const agentsDir = join(kiroHome, "agents");
25
+ const steeringDir = join(kiroHome, "steering");
26
+
27
+ console.log("");
28
+ console.log(`loophaus installer — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
29
+ console.log(`Target: ${kiroHome}`);
30
+ console.log("");
31
+
32
+ // Step 1: Create agent config with stop hook
33
+ console.log("[1/2] Configuring agent with stop hook...");
34
+ const agentConfig = {
35
+ name: "loophaus",
36
+ description: "loophaus — iterative dev loop agent",
37
+ hooks: {
38
+ stop: [{ command: `node "${join(PROJECT_ROOT, "hooks", "stop-hook.mjs")}"` }],
39
+ },
40
+ };
41
+
42
+ const agentPath = join(agentsDir, "loophaus.json");
43
+ if (!force && await fileExists(agentPath)) {
44
+ if (dryRun) {
45
+ console.log(" ! Existing agent config found (would prompt for --force)");
46
+ } else {
47
+ console.log(" ! Existing agent config found. Use --force to overwrite.");
48
+ return false;
49
+ }
50
+ }
51
+
52
+ console.log(` > Write ${agentPath}`);
53
+ if (!dryRun) {
54
+ await mkdir(agentsDir, { recursive: true });
55
+ await writeFile(agentPath, JSON.stringify(agentConfig, null, 2), "utf-8");
56
+ }
57
+
58
+ // Step 2: Copy steering files
59
+ console.log("[2/2] Installing steering files...");
60
+ const commands = [
61
+ { src: "commands/loop.md", dest: "loop.md" },
62
+ { src: "commands/loop-plan.md", dest: "loop-plan.md" },
63
+ { src: "commands/loop-stop.md", dest: "loop-stop.md" },
64
+ { src: "commands/loop-pulse.md", dest: "loop-pulse.md" },
65
+ ];
66
+ for (const { src, dest } of commands) {
67
+ const srcPath = join(PROJECT_ROOT, src);
68
+ if (await fileExists(srcPath)) {
69
+ const destPath = join(steeringDir, dest);
70
+ console.log(` > Copy ${src} -> ${destPath}`);
71
+ if (!dryRun) {
72
+ await mkdir(steeringDir, { recursive: true });
73
+ await cp(srcPath, destPath);
74
+ }
75
+ }
76
+ }
77
+
78
+ console.log("");
79
+ if (dryRun) {
80
+ console.log(" \u2714 Dry run complete. No files were modified.");
81
+ } else {
82
+ console.log(" \u2714 loophaus installed for Kiro CLI!");
83
+ console.log(" Commands: /loop, /loop-plan, /loop-stop, /loop-pulse");
84
+ console.log(" To uninstall: npx @graypark/loophaus uninstall --kiro");
85
+ }
86
+ console.log("");
87
+ return true;
88
+ }
89
+
90
+ export async function uninstall({ dryRun = false } = {}) {
91
+ const kiroHome = getKiroHome();
92
+
93
+ const targets = [
94
+ join(kiroHome, "agents", "loophaus.json"),
95
+ join(kiroHome, "steering", "loop.md"),
96
+ join(kiroHome, "steering", "loop-plan.md"),
97
+ join(kiroHome, "steering", "loop-stop.md"),
98
+ join(kiroHome, "steering", "loop-pulse.md"),
99
+ ];
100
+
101
+ console.log("");
102
+ console.log(`loophaus uninstaller — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
103
+ console.log("");
104
+
105
+ for (const p of targets) {
106
+ if (await fileExists(p)) {
107
+ console.log(` > Remove ${p}`);
108
+ if (!dryRun) await rm(p);
109
+ }
110
+ }
111
+
112
+ console.log("");
113
+ console.log(" \u2714 loophaus removed from Kiro CLI.");
114
+ console.log("");
115
+ }
@@ -0,0 +1,145 @@
1
+ #!/bin/bash
2
+
3
+ # loophaus Setup Script
4
+ # Creates state file for in-session loop
5
+
6
+ set -euo pipefail
7
+
8
+ # Parse arguments
9
+ PROMPT_PARTS=()
10
+ MAX_ITERATIONS=0
11
+ COMPLETION_PROMISE="null"
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case $1 in
15
+ -h|--help)
16
+ cat << 'HELP_EOF'
17
+ loophaus — Iterative coding loop
18
+
19
+ USAGE:
20
+ /loop [PROMPT...] [OPTIONS]
21
+
22
+ ARGUMENTS:
23
+ PROMPT... Initial prompt to start the loop (can be multiple words without quotes)
24
+
25
+ OPTIONS:
26
+ --max-iterations <n> Maximum iterations before auto-stop (default: unlimited)
27
+ --completion-promise '<text>' Promise phrase (USE QUOTES for multi-word)
28
+ -h, --help Show this help message
29
+
30
+ DESCRIPTION:
31
+ Starts a loop in your CURRENT session. The stop hook prevents
32
+ exit and feeds your output back as input until completion or iteration limit.
33
+
34
+ To signal completion, you must output: <promise>YOUR_PHRASE</promise>
35
+
36
+ EXAMPLES:
37
+ /loop Build a todo API --completion-promise 'DONE' --max-iterations 20
38
+ /loop --max-iterations 10 Fix the auth bug
39
+ /loop Refactor cache layer (runs forever)
40
+
41
+ STOPPING:
42
+ Only by reaching --max-iterations or detecting --completion-promise
43
+ To cancel manually: /loop-stop
44
+
45
+ MONITORING:
46
+ cat .loophaus/state.json
47
+ HELP_EOF
48
+ exit 0
49
+ ;;
50
+ --max-iterations)
51
+ if [[ -z "${2:-}" ]]; then
52
+ echo "❌ Error: --max-iterations requires a number argument" >&2
53
+ exit 1
54
+ fi
55
+ if ! [[ "$2" =~ ^[0-9]+$ ]]; then
56
+ echo "❌ Error: --max-iterations must be a positive integer or 0, got: $2" >&2
57
+ exit 1
58
+ fi
59
+ MAX_ITERATIONS="$2"
60
+ shift 2
61
+ ;;
62
+ --completion-promise)
63
+ if [[ -z "${2:-}" ]]; then
64
+ echo "❌ Error: --completion-promise requires a text argument" >&2
65
+ exit 1
66
+ fi
67
+ COMPLETION_PROMISE="$2"
68
+ shift 2
69
+ ;;
70
+ *)
71
+ PROMPT_PARTS+=("$1")
72
+ shift
73
+ ;;
74
+ esac
75
+ done
76
+
77
+ PROMPT="${PROMPT_PARTS[*]:-}"
78
+
79
+ if [[ -z "$PROMPT" ]]; then
80
+ echo "❌ Error: No prompt provided" >&2
81
+ echo "" >&2
82
+ echo " Examples:" >&2
83
+ echo " /loop Build a REST API for todos" >&2
84
+ echo " /loop Fix the auth bug --max-iterations 20" >&2
85
+ echo " /loop --completion-promise 'DONE' Refactor code" >&2
86
+ exit 1
87
+ fi
88
+
89
+ # Create state file
90
+ mkdir -p .loophaus
91
+
92
+ if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
93
+ COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""
94
+ else
95
+ COMPLETION_PROMISE_YAML="null"
96
+ fi
97
+
98
+ cat > .loophaus/loop.local.md <<EOF
99
+ ---
100
+ active: true
101
+ iteration: 1
102
+ session_id: ${CLAUDE_CODE_SESSION_ID:-}
103
+ max_iterations: $MAX_ITERATIONS
104
+ completion_promise: $COMPLETION_PROMISE_YAML
105
+ started_at: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
106
+ ---
107
+
108
+ $PROMPT
109
+ EOF
110
+
111
+ # Output setup message
112
+ cat <<EOF
113
+ 🔄 Loop activated!
114
+
115
+ Iteration: 1
116
+ Max iterations: $(if [[ $MAX_ITERATIONS -gt 0 ]]; then echo $MAX_ITERATIONS; else echo "unlimited"; fi)
117
+ Completion promise: $(if [[ "$COMPLETION_PROMISE" != "null" ]]; then echo "${COMPLETION_PROMISE//\"/} (ONLY output when TRUE)"; else echo "none (runs forever)"; fi)
118
+
119
+ The stop hook is now active. When you try to exit, the SAME PROMPT will be
120
+ fed back to you. Your previous work persists in files and git history.
121
+
122
+ To cancel: /loop-stop
123
+ To monitor: cat .loophaus/loop.local.md
124
+ EOF
125
+
126
+ if [[ -n "$PROMPT" ]]; then
127
+ echo ""
128
+ echo "$PROMPT"
129
+ fi
130
+
131
+ if [[ "$COMPLETION_PROMISE" != "null" ]]; then
132
+ echo ""
133
+ echo "═══════════════════════════════════════════════════════════"
134
+ echo "CRITICAL — Loop Completion Promise"
135
+ echo "═══════════════════════════════════════════════════════════"
136
+ echo ""
137
+ echo "To complete this loop, output this EXACT text:"
138
+ echo " <promise>$COMPLETION_PROMISE</promise>"
139
+ echo ""
140
+ echo "STRICT REQUIREMENTS:"
141
+ echo " ✓ Use <promise> XML tags EXACTLY as shown"
142
+ echo " ✓ The statement MUST be completely TRUE"
143
+ echo " ✓ Do NOT output false promises to exit the loop"
144
+ echo "═══════════════════════════════════════════════════════════"
145
+ fi