@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/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
|