@graypark/loophaus 2.1.0 → 2.1.2

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
 
@@ -202,6 +203,21 @@ export async function uninstall({
202
203
  }
203
204
  }
204
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
+
205
221
  // 4. Remove state file
206
222
  const statePath = getStatePath();
207
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.1.0",
3
+ "version": "2.1.2",
4
4
  "type": "module",
5
5
  "description": "loophaus — Control plane for coding agents. Iterative dev loops with multi-agent orchestration.",
6
6
  "license": "MIT",
@@ -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,7 +19,7 @@ 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
 
23
25
  // Legacy ralph-* skill names to clean up
@@ -143,12 +145,15 @@ export async function install({ dryRun = false, force = false, local = false } =
143
145
  }
144
146
  }
145
147
 
146
- // Step 1: Clean up legacy ralph-* skills
147
- console.log("[1/4] Cleaning up legacy skills...");
148
- for (const name of LEGACY_SKILLS) {
148
+ const totalSteps = 4;
149
+
150
+ // Step 1: Clean up legacy skills from ~/.codex/skills/
151
+ console.log(`[1/${totalSteps}] Cleaning up legacy skills...`);
152
+ const CLEANUP_FROM_CODEX = [...LEGACY_SKILLS, "loop", "loop-stop", "loop-plan", "loop-pulse"];
153
+ for (const name of CLEANUP_FROM_CODEX) {
149
154
  const legacyDir = join(skillsDir, name);
150
155
  if (await fileExists(legacyDir)) {
151
- console.log(` > Remove legacy skill: ${name}`);
156
+ console.log(` > Remove from ~/.codex/skills/: ${name}`);
152
157
  if (!dryRun) {
153
158
  await rm(legacyDir, { recursive: true, force: true });
154
159
  }
@@ -156,7 +161,7 @@ export async function install({ dryRun = false, force = false, local = false } =
156
161
  }
157
162
 
158
163
  // Step 2: Copy files
159
- console.log("[2/4] Copying plugin files...");
164
+ console.log(`[2/${totalSteps}] Copying plugin files...`);
160
165
  for (const dir of ["hooks", "codex/commands", "lib", "core", "store"]) {
161
166
  const src = join(PROJECT_ROOT, dir);
162
167
  if (!(await fileExists(src))) continue;
@@ -171,7 +176,7 @@ export async function install({ dryRun = false, force = false, local = false } =
171
176
  if (!dryRun) await cp(join(PROJECT_ROOT, "package.json"), join(pluginDir, "package.json"));
172
177
 
173
178
  // Step 3: Hooks
174
- console.log("[3/4] Configuring Stop hook...");
179
+ console.log(`[3/${totalSteps}] Configuring Stop hook...`);
175
180
  const stopCmd = `node "${join(pluginDir, "hooks", "stop-hook.mjs")}"`;
176
181
  let existing = { hooks: {} };
177
182
  if (await fileExists(hooksJsonPath)) {
@@ -188,10 +193,13 @@ export async function install({ dryRun = false, force = false, local = false } =
188
193
  await writeFile(hooksJsonPath, JSON.stringify(existing, null, 2), "utf-8");
189
194
  }
190
195
 
191
- // Step 4: Install new skills (loop, loop-stop, loop-plan, loop-pulse)
192
- console.log("[4/4] Installing skills...");
196
+ // Step 4: Install skills to standard path
197
+ // Global: ~/.agents/skills/ (new Codex CLI standard — avoids duplicates with ~/.codex/skills/)
198
+ // Local: .codex/skills/ (project-scoped)
199
+ const targetSkillsDir = local ? skillsDir : getAgentsSkillsDir();
200
+ console.log(`[4/${totalSteps}] Installing skills to ${local ? ".codex/skills/" : "~/.agents/skills/"}...`);
193
201
  for (const [name, skill] of Object.entries(CODEX_SKILLS)) {
194
- const skillDir = join(skillsDir, name);
202
+ const skillDir = join(targetSkillsDir, name);
195
203
  console.log(` > Install skill: ${name}`);
196
204
  if (!dryRun) {
197
205
  await mkdir(skillDir, { recursive: true });
@@ -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,27 +15,97 @@ async function fileExists(p) {
15
15
  try { await access(p); return true; } catch { return false; }
16
16
  }
17
17
 
18
- /**
19
- * Convert Claude Code frontmatter to Kiro steering manual mode format.
20
- * Strips Claude-specific fields (allowed-tools, argument-hint, hide-from-slash-command-tool)
21
- * and ensures `inclusion: manual` is set for Kiro CLI slash command support.
22
- */
23
- function convertToKiroFrontmatter(content) {
24
- // Match the YAML frontmatter block
25
- const fmRegex = /^---\n([\s\S]*?)\n---\n/;
26
- const match = content.match(fmRegex);
27
-
28
- if (!match) {
29
- // No frontmatter found — add Kiro frontmatter
30
- return `---\ninclusion: manual\n---\n\n${content}`;
31
- }
32
-
33
- // Extract the body after frontmatter
34
- const body = content.slice(match[0].length);
35
-
36
- // Build new Kiro frontmatter with inclusion: manual
37
- return `---\ninclusion: manual\n---\n\n${body}`;
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": ""
38
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
+ ];
39
109
 
40
110
  export async function detect() {
41
111
  return fileExists(getKiroHome());
@@ -44,7 +114,7 @@ export async function detect() {
44
114
  export async function install({ dryRun = false, force = false } = {}) {
45
115
  const kiroHome = getKiroHome();
46
116
  const agentsDir = join(kiroHome, "agents");
47
- const steeringDir = join(kiroHome, "steering");
117
+ const skillsDir = join(kiroHome, "skills");
48
118
 
49
119
  console.log("");
50
120
  console.log(`loophaus installer — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
@@ -52,7 +122,7 @@ export async function install({ dryRun = false, force = false } = {}) {
52
122
  console.log("");
53
123
 
54
124
  // Step 1: Create agent config with stop hook
55
- console.log("[1/3] Configuring agent with stop hook...");
125
+ console.log("[1/4] Configuring agent with stop hook...");
56
126
  const agentConfig = {
57
127
  name: "loophaus",
58
128
  description: "loophaus — iterative dev loop agent",
@@ -77,39 +147,35 @@ export async function install({ dryRun = false, force = false } = {}) {
77
147
  await writeFile(agentPath, JSON.stringify(agentConfig, null, 2), "utf-8");
78
148
  }
79
149
 
80
- // Step 2: Clean up legacy ralph-* steering files
81
- console.log("[2/3] Cleaning up legacy steering files...");
82
- const legacySteering = [
83
- "ralph-loop.md",
84
- "cancel-ralph.md",
85
- ];
86
- for (const name of legacySteering) {
87
- const legacyPath = join(steeringDir, name);
88
- if (await fileExists(legacyPath)) {
89
- console.log(` > Remove legacy steering: ${name}`);
90
- if (!dryRun) await rm(legacyPath);
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 });
91
168
  }
92
169
  }
93
170
 
94
- // Step 3: Copy steering files with Kiro frontmatter conversion
95
- console.log("[3/3] Installing steering files...");
96
- const commands = [
97
- { src: "commands/loop.md", dest: "loop.md" },
98
- { src: "commands/loop-plan.md", dest: "loop-plan.md" },
99
- { src: "commands/loop-stop.md", dest: "loop-stop.md" },
100
- { src: "commands/loop-pulse.md", dest: "loop-pulse.md" },
101
- ];
102
- for (const { src, dest } of commands) {
103
- const srcPath = join(PROJECT_ROOT, src);
104
- if (await fileExists(srcPath)) {
105
- const destPath = join(steeringDir, dest);
106
- console.log(` > Copy ${src} -> ${destPath} (Kiro frontmatter)`);
107
- if (!dryRun) {
108
- await mkdir(steeringDir, { recursive: true });
109
- const content = await readFile(srcPath, "utf-8");
110
- const kiroContent = convertToKiroFrontmatter(content);
111
- await writeFile(destPath, kiroContent, "utf-8");
112
- }
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");
113
179
  }
114
180
  }
115
181
 
@@ -118,7 +184,7 @@ export async function install({ dryRun = false, force = false } = {}) {
118
184
  console.log(" \u2714 Dry run complete. No files were modified.");
119
185
  } else {
120
186
  console.log(" \u2714 loophaus installed for Kiro CLI!");
121
- console.log(" Commands: /loop, /loop-plan, /loop-stop, /loop-pulse");
187
+ console.log(" Skills auto-match via description keywords.");
122
188
  console.log(" To uninstall: npx @graypark/loophaus uninstall --kiro");
123
189
  }
124
190
  console.log("");
@@ -128,29 +194,51 @@ export async function install({ dryRun = false, force = false } = {}) {
128
194
  export async function uninstall({ dryRun = false } = {}) {
129
195
  const kiroHome = getKiroHome();
130
196
 
131
- const targets = [
132
- join(kiroHome, "agents", "loophaus.json"),
133
- join(kiroHome, "steering", "loop.md"),
134
- join(kiroHome, "steering", "loop-plan.md"),
135
- join(kiroHome, "steering", "loop-stop.md"),
136
- join(kiroHome, "steering", "loop-pulse.md"),
137
- // Legacy steering files
138
- join(kiroHome, "steering", "ralph-loop.md"),
139
- join(kiroHome, "steering", "cancel-ralph.md"),
140
- ];
141
-
142
197
  console.log("");
143
198
  console.log(`loophaus uninstaller — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
144
199
  console.log("");
145
200
 
146
- for (const p of targets) {
147
- if (await fileExists(p)) {
148
- console.log(` > Remove ${p}`);
149
- 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 });
150
234
  }
151
235
  }
152
236
 
153
237
  console.log("");
154
- 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
+ }
155
243
  console.log("");
156
244
  }