@graypark/loophaus 2.0.2 → 2.1.1

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/bin/uninstall.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  getClaudePluginCacheDir,
13
13
  getClaudeSettingsPath,
14
14
  getClaudeInstalledPluginsPath,
15
+ getAgentsSkillsDir,
15
16
  } from "../lib/paths.mjs";
16
17
  import { getStatePath } from "../lib/state.mjs";
17
18
 
@@ -175,8 +176,14 @@ export async function uninstall({
175
176
  log("-", "Plugin directory not found");
176
177
  }
177
178
 
178
- // 3. Remove skill directories
179
+ // 3. Remove skill directories (both legacy ralph-* and new loop-* names)
179
180
  const skillNames = [
181
+ // New skill names
182
+ "loop",
183
+ "loop-stop",
184
+ "loop-plan",
185
+ "loop-pulse",
186
+ // Legacy skill names
180
187
  "ralph-loop",
181
188
  "cancel-ralph",
182
189
  "ralph-interview",
@@ -196,6 +203,21 @@ export async function uninstall({
196
203
  }
197
204
  }
198
205
 
206
+ // 3b. Remove skills from ~/.agents/skills/ (new Codex CLI standard path)
207
+ if (!local) {
208
+ const agentsSkillsDir = getAgentsSkillsDir();
209
+ const agentsSkillNames = ["loop", "loop-stop", "loop-plan", "loop-pulse"];
210
+ for (const name of agentsSkillNames) {
211
+ const skillDir = join(agentsSkillsDir, name);
212
+ if (await fileExists(skillDir)) {
213
+ log(">", `Remove agents skill: ${skillDir}`);
214
+ if (!dryRun) {
215
+ await rm(skillDir, { recursive: true, force: true });
216
+ }
217
+ }
218
+ }
219
+ }
220
+
199
221
  // 4. Remove state file
200
222
  const statePath = getStatePath();
201
223
  if (await fileExists(statePath)) {
package/lib/paths.mjs CHANGED
@@ -24,7 +24,7 @@ export function isWindows() {
24
24
  return process.platform === "win32";
25
25
  }
26
26
 
27
- // --- Codex CLI paths ---
27
+ // --- Codex CLI paths (legacy ~/.codex + new ~/.agents) ---
28
28
 
29
29
  export function getCodexHome() {
30
30
  if (process.env.CODEX_HOME) {
@@ -33,6 +33,14 @@ export function getCodexHome() {
33
33
  return join(homedir(), ".codex");
34
34
  }
35
35
 
36
+ export function getAgentsHome() {
37
+ return join(homedir(), ".agents");
38
+ }
39
+
40
+ export function getAgentsSkillsDir() {
41
+ return join(getAgentsHome(), "skills");
42
+ }
43
+
36
44
  export function getHooksJsonPath() {
37
45
  return join(getCodexHome(), "hooks.json");
38
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graypark/loophaus",
3
- "version": "2.0.2",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "loophaus — Control plane for coding agents. Iterative dev loops with multi-agent orchestration.",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  // platforms/codex-cli/installer.mjs
2
- import { readFile, writeFile, mkdir, cp, access } from "node:fs/promises";
2
+ import { readFile, writeFile, mkdir, cp, access, rm } from "node:fs/promises";
3
3
  import { join, resolve, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import {
@@ -7,6 +7,8 @@ import {
7
7
  getHooksJsonPath,
8
8
  getPluginInstallDir,
9
9
  getSkillsDir,
10
+ getAgentsHome,
11
+ getAgentsSkillsDir,
10
12
  } from "../../lib/paths.mjs";
11
13
 
12
14
  const __filename = fileURLToPath(import.meta.url);
@@ -17,9 +19,106 @@ async function fileExists(p) {
17
19
  }
18
20
 
19
21
  export async function detect() {
20
- return fileExists(getCodexHome());
22
+ return (await fileExists(getCodexHome())) || (await fileExists(getAgentsHome()));
21
23
  }
22
24
 
25
+ // Legacy ralph-* skill names to clean up
26
+ const LEGACY_SKILLS = [
27
+ "ralph-loop",
28
+ "cancel-ralph",
29
+ "ralph-interview",
30
+ "ralph-orchestrator",
31
+ "ralph-claude-interview",
32
+ "ralph-claude-loop",
33
+ "ralph-claude-cancel",
34
+ "ralph-claude-orchestrator",
35
+ ];
36
+
37
+ // New skill definitions for Codex CLI
38
+ const CODEX_SKILLS = {
39
+ loop: {
40
+ content: `---
41
+ name: loop
42
+ description: "Start iterative dev loop"
43
+ argument-hint: "PROMPT [--max-iterations N] [--completion-promise TEXT]"
44
+ ---
45
+
46
+ # /loop — Start Iterative Dev Loop
47
+
48
+ Parse the user's arguments:
49
+ 1. Extract \`--max-iterations N\` (default: 20)
50
+ 2. Extract \`--completion-promise TEXT\` (default: "TADA")
51
+ 3. Everything else is the prompt
52
+
53
+ Create \`.loophaus/state.json\`:
54
+ \`\`\`json
55
+ {
56
+ "active": true,
57
+ "prompt": "<user's prompt>",
58
+ "completionPromise": "<promise text>",
59
+ "maxIterations": 20,
60
+ "currentIteration": 0,
61
+ "sessionId": ""
62
+ }
63
+ \`\`\`
64
+
65
+ Then begin working on the task. The stop hook intercepts exit and feeds the SAME PROMPT back.
66
+
67
+ CRITICAL: If a completion promise is set, ONLY output \`<promise>TEXT</promise>\` when genuinely complete.
68
+ `,
69
+ },
70
+ "loop-stop": {
71
+ content: `---
72
+ name: loop-stop
73
+ description: "Stop active loop"
74
+ ---
75
+
76
+ # /loop-stop
77
+
78
+ 1. Check \`.loophaus/state.json\` exists
79
+ - Also check legacy \`.codex/ralph-loop.state.json\`
80
+
81
+ 2. If not found: "No active loop."
82
+
83
+ 3. If found: read \`currentIteration\`, set \`active: false\`, report "Stopped loop at iteration N."
84
+ `,
85
+ },
86
+ "loop-plan": {
87
+ content: `---
88
+ name: loop-plan
89
+ description: "Plan and start loop via interactive interview"
90
+ argument-hint: "TASK_DESCRIPTION"
91
+ ---
92
+
93
+ # /loop-plan — Interactive Planning & Loop
94
+
95
+ ## Phase 1: Discovery Interview
96
+ Ask 3-5 focused questions about the task to understand scope, acceptance criteria, constraints.
97
+
98
+ ## Phase 2: PRD Generation
99
+ Generate \`prd.json\` with right-sized user stories.
100
+
101
+ ## Phase 3: Loop Activation
102
+ Create \`.loophaus/state.json\` and start working on US-001 immediately.
103
+
104
+ Use \`<promise>TASK COMPLETE</promise>\` ONLY when ALL stories pass.
105
+ `,
106
+ },
107
+ "loop-pulse": {
108
+ content: `---
109
+ name: loop-pulse
110
+ description: "Check loop status"
111
+ ---
112
+
113
+ # /loop-pulse
114
+
115
+ 1. Read \`.loophaus/state.json\` (or legacy paths)
116
+ 2. If active, show iteration, promise, progress
117
+ 3. If \`prd.json\` exists, show story progress
118
+ `,
119
+ },
120
+ };
121
+
23
122
  export async function install({ dryRun = false, force = false, local = false } = {}) {
24
123
  const pluginDir = local
25
124
  ? join(process.cwd(), ".codex", "plugins", "loophaus")
@@ -46,8 +145,22 @@ export async function install({ dryRun = false, force = false, local = false } =
46
145
  }
47
146
  }
48
147
 
49
- // Step 1: Copy files
50
- console.log("[1/3] Copying plugin files...");
148
+ const totalSteps = local ? 4 : 5;
149
+
150
+ // Step 1: Clean up legacy ralph-* skills
151
+ console.log(`[1/${totalSteps}] Cleaning up legacy skills...`);
152
+ for (const name of LEGACY_SKILLS) {
153
+ const legacyDir = join(skillsDir, name);
154
+ if (await fileExists(legacyDir)) {
155
+ console.log(` > Remove legacy skill: ${name}`);
156
+ if (!dryRun) {
157
+ await rm(legacyDir, { recursive: true, force: true });
158
+ }
159
+ }
160
+ }
161
+
162
+ // Step 2: Copy files
163
+ console.log(`[2/${totalSteps}] Copying plugin files...`);
51
164
  for (const dir of ["hooks", "codex/commands", "lib", "core", "store"]) {
52
165
  const src = join(PROJECT_ROOT, dir);
53
166
  if (!(await fileExists(src))) continue;
@@ -61,8 +174,8 @@ export async function install({ dryRun = false, force = false, local = false } =
61
174
  }
62
175
  if (!dryRun) await cp(join(PROJECT_ROOT, "package.json"), join(pluginDir, "package.json"));
63
176
 
64
- // Step 2: Hooks
65
- console.log("[2/3] Configuring Stop hook...");
177
+ // Step 3: Hooks
178
+ console.log(`[3/${totalSteps}] Configuring Stop hook...`);
66
179
  const stopCmd = `node "${join(pluginDir, "hooks", "stop-hook.mjs")}"`;
67
180
  let existing = { hooks: {} };
68
181
  if (await fileExists(hooksJsonPath)) {
@@ -79,41 +192,35 @@ export async function install({ dryRun = false, force = false, local = false } =
79
192
  await writeFile(hooksJsonPath, JSON.stringify(existing, null, 2), "utf-8");
80
193
  }
81
194
 
82
- // Step 3: Skills
83
- console.log("[3/3] Installing skills...");
84
- for (const name of ["ralph-loop", "cancel-ralph"]) {
195
+ // Step 4: Install skills to ~/.codex/skills/
196
+ console.log(`[4/${totalSteps}] Installing skills to ~/.codex/skills/...`);
197
+ for (const [name, skill] of Object.entries(CODEX_SKILLS)) {
85
198
  const skillDir = join(skillsDir, name);
86
- const src = name === "ralph-loop" ? "codex/commands/ralph-loop.md" : "codex/commands/cancel-ralph.md";
87
199
  console.log(` > Install skill: ${name}`);
88
200
  if (!dryRun) {
89
201
  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
- }
202
+ await writeFile(
203
+ join(skillDir, "SKILL.md"),
204
+ skill.content.replaceAll("${RALPH_CODEX_ROOT}", pluginDir).replaceAll("${LOOPHAUS_ROOT}", pluginDir),
205
+ "utf-8",
206
+ );
99
207
  }
100
208
  }
101
209
 
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}`);
210
+ // Step 5: Mirror skills to ~/.agents/skills/ (new Codex CLI standard path)
211
+ if (!local) {
212
+ const agentsSkillsDir = getAgentsSkillsDir();
213
+ console.log(`[5/${totalSteps}] Installing skills to ~/.agents/skills/...`);
214
+ for (const [name, skill] of Object.entries(CODEX_SKILLS)) {
215
+ const skillDir = join(agentsSkillsDir, name);
216
+ console.log(` > Install skill: ${name}`);
114
217
  if (!dryRun) {
115
- await mkdir(join(skillsDir, sk), { recursive: true });
116
- await cp(srcDir, join(skillsDir, sk), { recursive: true });
218
+ await mkdir(skillDir, { recursive: true });
219
+ await writeFile(
220
+ join(skillDir, "SKILL.md"),
221
+ skill.content.replaceAll("${RALPH_CODEX_ROOT}", pluginDir).replaceAll("${LOOPHAUS_ROOT}", pluginDir),
222
+ "utf-8",
223
+ );
117
224
  }
118
225
  }
119
226
  }
@@ -1,5 +1,5 @@
1
1
  // platforms/kiro-cli/installer.mjs
2
- import { readFile, writeFile, mkdir, cp, access, rm } from "node:fs/promises";
2
+ import { readFile, writeFile, mkdir, access, rm } from "node:fs/promises";
3
3
  import { join, resolve, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { homedir } from "node:os";
@@ -15,6 +15,98 @@ async function fileExists(p) {
15
15
  try { await access(p); return true; } catch { return false; }
16
16
  }
17
17
 
18
+ // Kiro CLI skill definitions (description-based auto-matching)
19
+ const KIRO_SKILLS = {
20
+ loop: {
21
+ content: `---
22
+ name: loop
23
+ description: "Start iterative dev loop — use when user says 'start loop', 'loop this', 'iterate on task', 'run loop'"
24
+ argument-hint: "PROMPT [--max-iterations N] [--completion-promise TEXT]"
25
+ ---
26
+
27
+ # Start Iterative Dev Loop
28
+
29
+ Parse the user's arguments:
30
+ 1. Extract \`--max-iterations N\` (default: 20)
31
+ 2. Extract \`--completion-promise TEXT\` (default: "TADA")
32
+ 3. Everything else is the prompt
33
+
34
+ Create \`.loophaus/state.json\`:
35
+ \`\`\`json
36
+ {
37
+ "active": true,
38
+ "prompt": "<user's prompt>",
39
+ "completionPromise": "<promise text>",
40
+ "maxIterations": 20,
41
+ "currentIteration": 0,
42
+ "sessionId": ""
43
+ }
44
+ \`\`\`
45
+
46
+ Then begin working on the task. The stop hook intercepts exit and feeds the SAME PROMPT back.
47
+
48
+ CRITICAL: If a completion promise is set, ONLY output \`<promise>TEXT</promise>\` when genuinely complete.
49
+ `,
50
+ },
51
+ "loop-stop": {
52
+ content: `---
53
+ name: loop-stop
54
+ description: "Stop active loop — use when user says 'stop loop', 'cancel loop', 'halt', 'stop iterating'"
55
+ ---
56
+
57
+ # Stop Active Loop
58
+
59
+ 1. Check if \`.loophaus/state.json\` exists
60
+ 2. If not found: "No active loop."
61
+ 3. If found: read currentIteration, set active: false, report "Stopped loop at iteration N."
62
+ `,
63
+ },
64
+ "loop-plan": {
65
+ content: `---
66
+ name: loop-plan
67
+ description: "Plan and start loop via interactive interview — use when user says 'plan loop', 'interview', 'create PRD', 'plan task'"
68
+ argument-hint: "TASK_DESCRIPTION"
69
+ ---
70
+
71
+ # Interactive Planning & Loop
72
+
73
+ ## Phase 1: Discovery Interview
74
+ Ask 3-5 focused questions about the task.
75
+
76
+ ## Phase 2: PRD Generation
77
+ Generate prd.json with right-sized user stories.
78
+
79
+ ## Phase 3: Loop Activation
80
+ Create .loophaus/state.json and start working on US-001 immediately.
81
+
82
+ Use \`<promise>TASK COMPLETE</promise>\` ONLY when ALL stories pass.
83
+ `,
84
+ },
85
+ "loop-pulse": {
86
+ content: `---
87
+ name: loop-pulse
88
+ description: "Check loop status — use when user says 'loop status', 'check progress', 'how is the loop', 'pulse'"
89
+ ---
90
+
91
+ # Check Loop Status
92
+
93
+ 1. Read .loophaus/state.json
94
+ 2. If active: show iteration, promise, progress
95
+ 3. If prd.json exists: show story progress (done/total)
96
+ `,
97
+ },
98
+ };
99
+
100
+ // Legacy files to clean up
101
+ const LEGACY_STEERING = [
102
+ "steering/ralph-loop.md",
103
+ "steering/cancel-ralph.md",
104
+ "steering/loop.md",
105
+ "steering/loop-plan.md",
106
+ "steering/loop-stop.md",
107
+ "steering/loop-pulse.md",
108
+ ];
109
+
18
110
  export async function detect() {
19
111
  return fileExists(getKiroHome());
20
112
  }
@@ -22,7 +114,7 @@ export async function detect() {
22
114
  export async function install({ dryRun = false, force = false } = {}) {
23
115
  const kiroHome = getKiroHome();
24
116
  const agentsDir = join(kiroHome, "agents");
25
- const steeringDir = join(kiroHome, "steering");
117
+ const skillsDir = join(kiroHome, "skills");
26
118
 
27
119
  console.log("");
28
120
  console.log(`loophaus installer — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
@@ -30,7 +122,7 @@ export async function install({ dryRun = false, force = false } = {}) {
30
122
  console.log("");
31
123
 
32
124
  // Step 1: Create agent config with stop hook
33
- console.log("[1/2] Configuring agent with stop hook...");
125
+ console.log("[1/4] Configuring agent with stop hook...");
34
126
  const agentConfig = {
35
127
  name: "loophaus",
36
128
  description: "loophaus — iterative dev loop agent",
@@ -55,23 +147,35 @@ export async function install({ dryRun = false, force = false } = {}) {
55
147
  await writeFile(agentPath, JSON.stringify(agentConfig, null, 2), "utf-8");
56
148
  }
57
149
 
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
- }
150
+ // Step 2: Clean up legacy steering files
151
+ console.log("[2/4] Cleaning up legacy steering files...");
152
+ for (const relPath of LEGACY_STEERING) {
153
+ const fullPath = join(kiroHome, relPath);
154
+ if (await fileExists(fullPath)) {
155
+ console.log(` > Remove legacy: ${relPath}`);
156
+ if (!dryRun) await rm(fullPath);
157
+ }
158
+ }
159
+
160
+ // Step 3: Clean up legacy skill directories (in case of partial migration)
161
+ console.log("[3/4] Cleaning up legacy skill directories...");
162
+ const legacySkillNames = ["ralph-loop", "cancel-ralph"];
163
+ for (const name of legacySkillNames) {
164
+ const legacySkillDir = join(skillsDir, name);
165
+ if (await fileExists(legacySkillDir)) {
166
+ console.log(` > Remove legacy skill: ${name}`);
167
+ if (!dryRun) await rm(legacySkillDir, { recursive: true, force: true });
168
+ }
169
+ }
170
+
171
+ // Step 4: Install skills (description-based auto-matching)
172
+ console.log("[4/4] Installing skills...");
173
+ for (const [name, skill] of Object.entries(KIRO_SKILLS)) {
174
+ const skillDir = join(skillsDir, name);
175
+ console.log(` > Install skill: ${name}`);
176
+ if (!dryRun) {
177
+ await mkdir(skillDir, { recursive: true });
178
+ await writeFile(join(skillDir, "SKILL.md"), skill.content, "utf-8");
75
179
  }
76
180
  }
77
181
 
@@ -80,7 +184,7 @@ export async function install({ dryRun = false, force = false } = {}) {
80
184
  console.log(" \u2714 Dry run complete. No files were modified.");
81
185
  } else {
82
186
  console.log(" \u2714 loophaus installed for Kiro CLI!");
83
- console.log(" Commands: /loop, /loop-plan, /loop-stop, /loop-pulse");
187
+ console.log(" Skills auto-match via description keywords.");
84
188
  console.log(" To uninstall: npx @graypark/loophaus uninstall --kiro");
85
189
  }
86
190
  console.log("");
@@ -90,26 +194,51 @@ export async function install({ dryRun = false, force = false } = {}) {
90
194
  export async function uninstall({ dryRun = false } = {}) {
91
195
  const kiroHome = getKiroHome();
92
196
 
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
197
  console.log("");
102
198
  console.log(`loophaus uninstaller — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
103
199
  console.log("");
104
200
 
105
- for (const p of targets) {
106
- if (await fileExists(p)) {
107
- console.log(` > Remove ${p}`);
108
- if (!dryRun) await rm(p);
201
+ // Remove agent config
202
+ const agentPath = join(kiroHome, "agents", "loophaus.json");
203
+ if (await fileExists(agentPath)) {
204
+ console.log(` > Remove ${agentPath}`);
205
+ if (!dryRun) await rm(agentPath);
206
+ }
207
+
208
+ // Remove skill directories
209
+ const skillNames = ["loop", "loop-stop", "loop-plan", "loop-pulse"];
210
+ for (const name of skillNames) {
211
+ const skillDir = join(kiroHome, "skills", name);
212
+ if (await fileExists(skillDir)) {
213
+ console.log(` > Remove skill: ${skillDir}`);
214
+ if (!dryRun) await rm(skillDir, { recursive: true, force: true });
215
+ }
216
+ }
217
+
218
+ // Remove legacy steering files
219
+ for (const relPath of LEGACY_STEERING) {
220
+ const fullPath = join(kiroHome, relPath);
221
+ if (await fileExists(fullPath)) {
222
+ console.log(` > Remove legacy: ${fullPath}`);
223
+ if (!dryRun) await rm(fullPath);
224
+ }
225
+ }
226
+
227
+ // Remove legacy skill directories
228
+ const legacySkillNames = ["ralph-loop", "cancel-ralph"];
229
+ for (const name of legacySkillNames) {
230
+ const legacySkillDir = join(kiroHome, "skills", name);
231
+ if (await fileExists(legacySkillDir)) {
232
+ console.log(` > Remove legacy skill: ${legacySkillDir}`);
233
+ if (!dryRun) await rm(legacySkillDir, { recursive: true, force: true });
109
234
  }
110
235
  }
111
236
 
112
237
  console.log("");
113
- console.log(" \u2714 loophaus removed from Kiro CLI.");
238
+ if (dryRun) {
239
+ console.log(" \u2714 Dry run complete. No files were modified.");
240
+ } else {
241
+ console.log(" \u2714 loophaus removed from Kiro CLI.");
242
+ }
114
243
  console.log("");
115
244
  }