@graypark/loophaus 2.0.2 → 2.1.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/bin/uninstall.mjs CHANGED
@@ -175,8 +175,14 @@ export async function uninstall({
175
175
  log("-", "Plugin directory not found");
176
176
  }
177
177
 
178
- // 3. Remove skill directories
178
+ // 3. Remove skill directories (both legacy ralph-* and new loop-* names)
179
179
  const skillNames = [
180
+ // New skill names
181
+ "loop",
182
+ "loop-stop",
183
+ "loop-plan",
184
+ "loop-pulse",
185
+ // Legacy skill names
180
186
  "ralph-loop",
181
187
  "cancel-ralph",
182
188
  "ralph-interview",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graypark/loophaus",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
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 {
@@ -20,6 +20,103 @@ export async function detect() {
20
20
  return fileExists(getCodexHome());
21
21
  }
22
22
 
23
+ // Legacy ralph-* skill names to clean up
24
+ const LEGACY_SKILLS = [
25
+ "ralph-loop",
26
+ "cancel-ralph",
27
+ "ralph-interview",
28
+ "ralph-orchestrator",
29
+ "ralph-claude-interview",
30
+ "ralph-claude-loop",
31
+ "ralph-claude-cancel",
32
+ "ralph-claude-orchestrator",
33
+ ];
34
+
35
+ // New skill definitions for Codex CLI
36
+ const CODEX_SKILLS = {
37
+ loop: {
38
+ content: `---
39
+ name: loop
40
+ description: "Start iterative dev loop"
41
+ argument-hint: "PROMPT [--max-iterations N] [--completion-promise TEXT]"
42
+ ---
43
+
44
+ # /loop — Start Iterative Dev Loop
45
+
46
+ Parse the user's arguments:
47
+ 1. Extract \`--max-iterations N\` (default: 20)
48
+ 2. Extract \`--completion-promise TEXT\` (default: "TADA")
49
+ 3. Everything else is the prompt
50
+
51
+ Create \`.loophaus/state.json\`:
52
+ \`\`\`json
53
+ {
54
+ "active": true,
55
+ "prompt": "<user's prompt>",
56
+ "completionPromise": "<promise text>",
57
+ "maxIterations": 20,
58
+ "currentIteration": 0,
59
+ "sessionId": ""
60
+ }
61
+ \`\`\`
62
+
63
+ Then begin working on the task. The stop hook intercepts exit and feeds the SAME PROMPT back.
64
+
65
+ CRITICAL: If a completion promise is set, ONLY output \`<promise>TEXT</promise>\` when genuinely complete.
66
+ `,
67
+ },
68
+ "loop-stop": {
69
+ content: `---
70
+ name: loop-stop
71
+ description: "Stop active loop"
72
+ ---
73
+
74
+ # /loop-stop
75
+
76
+ 1. Check \`.loophaus/state.json\` exists
77
+ - Also check legacy \`.codex/ralph-loop.state.json\`
78
+
79
+ 2. If not found: "No active loop."
80
+
81
+ 3. If found: read \`currentIteration\`, set \`active: false\`, report "Stopped loop at iteration N."
82
+ `,
83
+ },
84
+ "loop-plan": {
85
+ content: `---
86
+ name: loop-plan
87
+ description: "Plan and start loop via interactive interview"
88
+ argument-hint: "TASK_DESCRIPTION"
89
+ ---
90
+
91
+ # /loop-plan — Interactive Planning & Loop
92
+
93
+ ## Phase 1: Discovery Interview
94
+ Ask 3-5 focused questions about the task to understand scope, acceptance criteria, constraints.
95
+
96
+ ## Phase 2: PRD Generation
97
+ Generate \`prd.json\` with right-sized user stories.
98
+
99
+ ## Phase 3: Loop Activation
100
+ Create \`.loophaus/state.json\` and start working on US-001 immediately.
101
+
102
+ Use \`<promise>TASK COMPLETE</promise>\` ONLY when ALL stories pass.
103
+ `,
104
+ },
105
+ "loop-pulse": {
106
+ content: `---
107
+ name: loop-pulse
108
+ description: "Check loop status"
109
+ ---
110
+
111
+ # /loop-pulse
112
+
113
+ 1. Read \`.loophaus/state.json\` (or legacy paths)
114
+ 2. If active, show iteration, promise, progress
115
+ 3. If \`prd.json\` exists, show story progress
116
+ `,
117
+ },
118
+ };
119
+
23
120
  export async function install({ dryRun = false, force = false, local = false } = {}) {
24
121
  const pluginDir = local
25
122
  ? join(process.cwd(), ".codex", "plugins", "loophaus")
@@ -46,8 +143,20 @@ export async function install({ dryRun = false, force = false, local = false } =
46
143
  }
47
144
  }
48
145
 
49
- // Step 1: Copy files
50
- console.log("[1/3] Copying plugin files...");
146
+ // Step 1: Clean up legacy ralph-* skills
147
+ console.log("[1/4] Cleaning up legacy skills...");
148
+ for (const name of LEGACY_SKILLS) {
149
+ const legacyDir = join(skillsDir, name);
150
+ if (await fileExists(legacyDir)) {
151
+ console.log(` > Remove legacy skill: ${name}`);
152
+ if (!dryRun) {
153
+ await rm(legacyDir, { recursive: true, force: true });
154
+ }
155
+ }
156
+ }
157
+
158
+ // Step 2: Copy files
159
+ console.log("[2/4] Copying plugin files...");
51
160
  for (const dir of ["hooks", "codex/commands", "lib", "core", "store"]) {
52
161
  const src = join(PROJECT_ROOT, dir);
53
162
  if (!(await fileExists(src))) continue;
@@ -61,8 +170,8 @@ export async function install({ dryRun = false, force = false, local = false } =
61
170
  }
62
171
  if (!dryRun) await cp(join(PROJECT_ROOT, "package.json"), join(pluginDir, "package.json"));
63
172
 
64
- // Step 2: Hooks
65
- console.log("[2/3] Configuring Stop hook...");
173
+ // Step 3: Hooks
174
+ console.log("[3/4] Configuring Stop hook...");
66
175
  const stopCmd = `node "${join(pluginDir, "hooks", "stop-hook.mjs")}"`;
67
176
  let existing = { hooks: {} };
68
177
  if (await fileExists(hooksJsonPath)) {
@@ -79,42 +188,18 @@ export async function install({ dryRun = false, force = false, local = false } =
79
188
  await writeFile(hooksJsonPath, JSON.stringify(existing, null, 2), "utf-8");
80
189
  }
81
190
 
82
- // Step 3: Skills
83
- console.log("[3/3] Installing skills...");
84
- for (const name of ["ralph-loop", "cancel-ralph"]) {
191
+ // Step 4: Install new skills (loop, loop-stop, loop-plan, loop-pulse)
192
+ console.log("[4/4] Installing skills...");
193
+ for (const [name, skill] of Object.entries(CODEX_SKILLS)) {
85
194
  const skillDir = join(skillsDir, name);
86
- const src = name === "ralph-loop" ? "codex/commands/ralph-loop.md" : "codex/commands/cancel-ralph.md";
87
195
  console.log(` > Install skill: ${name}`);
88
196
  if (!dryRun) {
89
197
  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
- }
198
+ await writeFile(
199
+ join(skillDir, "SKILL.md"),
200
+ skill.content.replaceAll("${RALPH_CODEX_ROOT}", pluginDir).replaceAll("${LOOPHAUS_ROOT}", pluginDir),
201
+ "utf-8",
202
+ );
118
203
  }
119
204
  }
120
205
 
@@ -15,6 +15,28 @@ 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}`;
38
+ }
39
+
18
40
  export async function detect() {
19
41
  return fileExists(getKiroHome());
20
42
  }
@@ -30,7 +52,7 @@ export async function install({ dryRun = false, force = false } = {}) {
30
52
  console.log("");
31
53
 
32
54
  // Step 1: Create agent config with stop hook
33
- console.log("[1/2] Configuring agent with stop hook...");
55
+ console.log("[1/3] Configuring agent with stop hook...");
34
56
  const agentConfig = {
35
57
  name: "loophaus",
36
58
  description: "loophaus — iterative dev loop agent",
@@ -55,8 +77,22 @@ export async function install({ dryRun = false, force = false } = {}) {
55
77
  await writeFile(agentPath, JSON.stringify(agentConfig, null, 2), "utf-8");
56
78
  }
57
79
 
58
- // Step 2: Copy steering files
59
- console.log("[2/2] Installing steering files...");
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);
91
+ }
92
+ }
93
+
94
+ // Step 3: Copy steering files with Kiro frontmatter conversion
95
+ console.log("[3/3] Installing steering files...");
60
96
  const commands = [
61
97
  { src: "commands/loop.md", dest: "loop.md" },
62
98
  { src: "commands/loop-plan.md", dest: "loop-plan.md" },
@@ -67,10 +103,12 @@ export async function install({ dryRun = false, force = false } = {}) {
67
103
  const srcPath = join(PROJECT_ROOT, src);
68
104
  if (await fileExists(srcPath)) {
69
105
  const destPath = join(steeringDir, dest);
70
- console.log(` > Copy ${src} -> ${destPath}`);
106
+ console.log(` > Copy ${src} -> ${destPath} (Kiro frontmatter)`);
71
107
  if (!dryRun) {
72
108
  await mkdir(steeringDir, { recursive: true });
73
- await cp(srcPath, destPath);
109
+ const content = await readFile(srcPath, "utf-8");
110
+ const kiroContent = convertToKiroFrontmatter(content);
111
+ await writeFile(destPath, kiroContent, "utf-8");
74
112
  }
75
113
  }
76
114
  }
@@ -96,6 +134,9 @@ export async function uninstall({ dryRun = false } = {}) {
96
134
  join(kiroHome, "steering", "loop-plan.md"),
97
135
  join(kiroHome, "steering", "loop-stop.md"),
98
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"),
99
140
  ];
100
141
 
101
142
  console.log("");