@graypark/loophaus 3.4.1 → 3.5.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/README.ko.md +81 -17
- package/README.md +69 -15
- package/dist/.claude-plugin/plugin.json +11 -0
- package/dist/LICENSE +21 -0
- package/dist/README.ko.md +422 -0
- package/dist/README.md +336 -0
- package/dist/bin/install.d.ts +3 -0
- package/dist/bin/install.d.ts.map +1 -0
- package/{bin/install.mjs → dist/bin/install.js} +3 -5
- package/dist/bin/install.js.map +1 -0
- package/dist/bin/loophaus.d.ts +3 -0
- package/dist/bin/loophaus.d.ts.map +1 -0
- package/dist/bin/loophaus.js +654 -0
- package/dist/bin/loophaus.js.map +1 -0
- package/dist/bin/uninstall.d.ts +8 -0
- package/dist/bin/uninstall.d.ts.map +1 -0
- package/dist/bin/uninstall.js +209 -0
- package/dist/bin/uninstall.js.map +1 -0
- package/dist/codex/commands/cancel-ralph.md +30 -0
- package/dist/codex/commands/ralph-loop.md +73 -0
- package/dist/commands/cancel-ralph.md +23 -0
- package/dist/commands/help.md +96 -0
- package/dist/commands/loop-plan.md +257 -0
- package/dist/commands/loop-pulse.md +38 -0
- package/dist/commands/loop-stop.md +29 -0
- package/dist/commands/loop.md +17 -0
- package/dist/commands/ralph-loop.md +18 -0
- package/dist/core/cost-tracker.d.ts +33 -0
- package/dist/core/cost-tracker.d.ts.map +1 -0
- package/dist/core/cost-tracker.js +41 -0
- package/dist/core/cost-tracker.js.map +1 -0
- package/dist/core/engine.d.ts +4 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +109 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/event-logger.d.ts +5 -0
- package/dist/core/event-logger.d.ts.map +1 -0
- package/dist/core/event-logger.js +48 -0
- package/dist/core/event-logger.js.map +1 -0
- package/dist/core/events.d.ts +34 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +44 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/io-helpers.d.ts +3 -0
- package/dist/core/io-helpers.d.ts.map +1 -0
- package/dist/core/io-helpers.js +65 -0
- package/dist/core/io-helpers.js.map +1 -0
- package/dist/core/loop-registry.d.ts +10 -0
- package/dist/core/loop-registry.d.ts.map +1 -0
- package/dist/core/loop-registry.js +37 -0
- package/dist/core/loop-registry.js.map +1 -0
- package/dist/core/merge-strategy.d.ts +7 -0
- package/dist/core/merge-strategy.d.ts.map +1 -0
- package/dist/core/merge-strategy.js +82 -0
- package/dist/core/merge-strategy.js.map +1 -0
- package/dist/core/parallel-runner.d.ts +32 -0
- package/dist/core/parallel-runner.d.ts.map +1 -0
- package/dist/core/parallel-runner.js +88 -0
- package/dist/core/parallel-runner.js.map +1 -0
- package/dist/core/policy.d.ts +22 -0
- package/dist/core/policy.d.ts.map +1 -0
- package/dist/core/policy.js +54 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/quality-scorer.d.ts +40 -0
- package/dist/core/quality-scorer.d.ts.map +1 -0
- package/dist/core/quality-scorer.js +128 -0
- package/dist/core/quality-scorer.js.map +1 -0
- package/dist/core/refine-loop.d.ts +16 -0
- package/dist/core/refine-loop.d.ts.map +1 -0
- package/dist/core/refine-loop.js +26 -0
- package/dist/core/refine-loop.js.map +1 -0
- package/dist/core/session.d.ts +27 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +67 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/trace-analyzer.d.ts +28 -0
- package/dist/core/trace-analyzer.d.ts.map +1 -0
- package/dist/core/trace-analyzer.js +46 -0
- package/dist/core/trace-analyzer.js.map +1 -0
- package/dist/core/types.d.ts +99 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validate.d.ts +7 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +55 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/core/worktree.d.ts +13 -0
- package/dist/core/worktree.d.ts.map +1 -0
- package/dist/core/worktree.js +108 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/hooks/hooks.json +15 -0
- package/dist/hooks/stop-hook.mjs +111 -0
- package/dist/lib/paths.d.ts +18 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +74 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/stop-hook-core.d.ts +19 -0
- package/dist/lib/stop-hook-core.d.ts.map +1 -0
- package/dist/lib/stop-hook-core.js +36 -0
- package/dist/lib/stop-hook-core.js.map +1 -0
- package/dist/package.json +61 -0
- package/dist/platforms/claude-code/adapter.mjs +20 -0
- package/dist/platforms/claude-code/installer.d.mts +3 -0
- package/dist/platforms/claude-code/installer.mjs +173 -0
- package/dist/platforms/codex-cli/adapter.mjs +20 -0
- package/dist/platforms/codex-cli/installer.d.mts +2 -0
- package/dist/platforms/codex-cli/installer.mjs +247 -0
- package/dist/platforms/kiro-cli/adapter.mjs +21 -0
- package/dist/platforms/kiro-cli/installer.d.mts +3 -0
- package/dist/platforms/kiro-cli/installer.mjs +257 -0
- package/dist/scripts/setup-ralph-loop.sh +145 -0
- package/dist/skills/ralph-claude-cancel/SKILL.md +23 -0
- package/dist/skills/ralph-claude-interview/SKILL.md +184 -0
- package/dist/skills/ralph-claude-loop/SKILL.md +101 -0
- package/dist/skills/ralph-claude-orchestrator/SKILL.md +129 -0
- package/dist/skills/ralph-interview/SKILL.md +275 -0
- package/dist/skills/ralph-orchestrator/SKILL.md +254 -0
- package/dist/store/state-store.d.ts +17 -0
- package/dist/store/state-store.d.ts.map +1 -0
- package/dist/store/state-store.js +108 -0
- package/dist/store/state-store.js.map +1 -0
- package/hooks/stop-hook.mjs +6 -6
- package/package.json +11 -7
- package/platforms/claude-code/installer.d.mts +3 -0
- package/platforms/claude-code/installer.mjs +2 -2
- package/platforms/codex-cli/installer.d.mts +2 -0
- package/platforms/codex-cli/installer.mjs +1 -1
- package/platforms/kiro-cli/installer.d.mts +3 -0
- package/bin/loophaus.mjs +0 -521
- package/bin/uninstall.mjs +0 -255
- package/core/cost-tracker.mjs +0 -44
- package/core/engine.mjs +0 -123
- package/core/event-logger.mjs +0 -37
- package/core/events.mjs +0 -48
- package/core/io-helpers.mjs +0 -33
- package/core/loop-registry.mjs +0 -37
- package/core/loop.schema.json +0 -29
- package/core/merge-strategy.mjs +0 -72
- package/core/parallel-runner.mjs +0 -94
- package/core/policy.mjs +0 -58
- package/core/quality-scorer.mjs +0 -136
- package/core/refine-loop.mjs +0 -29
- package/core/session.mjs +0 -66
- package/core/state.schema.json +0 -24
- package/core/trace-analyzer.mjs +0 -51
- package/core/validate.mjs +0 -54
- package/core/worktree.mjs +0 -97
- package/lib/paths.mjs +0 -99
- package/lib/stop-hook-core.mjs +0 -42
- package/store/state-store.mjs +0 -106
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// loophaus CLI — install, status, stats, uninstall
|
|
3
|
+
import { resolve, dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { access } from "node:fs/promises";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const command = args[0] || "install";
|
|
11
|
+
const dryRun = args.includes("--dry-run");
|
|
12
|
+
const force = args.includes("--force");
|
|
13
|
+
const local = args.includes("--local");
|
|
14
|
+
const verbose = args.includes("--verbose");
|
|
15
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
16
|
+
const KNOWN_FLAGS = new Set([
|
|
17
|
+
"--help", "-h", "--version", "--dry-run", "--force", "--local", "--verbose",
|
|
18
|
+
"--host", "--claude", "--kiro", "--name", "--speed", "--count", "--base", "--story",
|
|
19
|
+
]);
|
|
20
|
+
const VALID_COMMANDS = [
|
|
21
|
+
"install", "uninstall", "status", "stats", "loops", "watch",
|
|
22
|
+
"replay", "compare", "worktree", "parallel", "quality",
|
|
23
|
+
"sessions", "resume", "help",
|
|
24
|
+
];
|
|
25
|
+
function validateFlags() {
|
|
26
|
+
for (const arg of args) {
|
|
27
|
+
if (arg.startsWith("--") && !KNOWN_FLAGS.has(arg)) {
|
|
28
|
+
console.error(`Unknown flag: ${arg}. Run loophaus --help for usage.`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function suggestCommand(input) {
|
|
34
|
+
let best = null;
|
|
35
|
+
let bestScore = Infinity;
|
|
36
|
+
for (const cmd of VALID_COMMANDS) {
|
|
37
|
+
const dist = levenshtein(input, cmd);
|
|
38
|
+
if (dist < bestScore && dist <= 3) {
|
|
39
|
+
bestScore = dist;
|
|
40
|
+
best = cmd;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return best;
|
|
44
|
+
}
|
|
45
|
+
function levenshtein(a, b) {
|
|
46
|
+
const m = a.length, n = b.length;
|
|
47
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
48
|
+
for (let i = 1; i <= m; i++) {
|
|
49
|
+
for (let j = 1; j <= n; j++) {
|
|
50
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
51
|
+
? dp[i - 1][j - 1]
|
|
52
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return dp[m][n];
|
|
56
|
+
}
|
|
57
|
+
function spinner(label) {
|
|
58
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
59
|
+
let i = 0;
|
|
60
|
+
const id = setInterval(() => {
|
|
61
|
+
process.stderr.write(`\r${frames[i++ % frames.length]} ${label}`);
|
|
62
|
+
}, 80);
|
|
63
|
+
return {
|
|
64
|
+
stop() {
|
|
65
|
+
clearInterval(id);
|
|
66
|
+
process.stderr.write(`\r\x1b[K`);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function getHost() {
|
|
71
|
+
if (args.includes("--claude"))
|
|
72
|
+
return "claude-code";
|
|
73
|
+
if (args.includes("--kiro"))
|
|
74
|
+
return "kiro-cli";
|
|
75
|
+
const idx = args.indexOf("--host");
|
|
76
|
+
if (idx !== -1 && args[idx + 1])
|
|
77
|
+
return args[idx + 1];
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const host = getHost();
|
|
81
|
+
function getFlag(flag) {
|
|
82
|
+
const idx = args.indexOf(flag);
|
|
83
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-"))
|
|
84
|
+
return args[idx + 1];
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
function getNumericFlag(flag, defaultVal) {
|
|
88
|
+
const raw = getFlag(flag);
|
|
89
|
+
if (raw === undefined)
|
|
90
|
+
return defaultVal;
|
|
91
|
+
const num = parseFloat(raw);
|
|
92
|
+
if (isNaN(num)) {
|
|
93
|
+
console.error(`Flag ${flag} requires a numeric value, got: "${raw}"`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
return num;
|
|
97
|
+
}
|
|
98
|
+
validateFlags();
|
|
99
|
+
if (showHelp || command === "help") {
|
|
100
|
+
console.log(`loophaus — Control plane for coding agents
|
|
101
|
+
|
|
102
|
+
Usage:
|
|
103
|
+
npx @graypark/loophaus install [--host <name>] [--force] [--dry-run]
|
|
104
|
+
npx @graypark/loophaus uninstall [--host <name>]
|
|
105
|
+
npx @graypark/loophaus status [--name <loop>]
|
|
106
|
+
npx @graypark/loophaus stats [--name <loop>]
|
|
107
|
+
npx @graypark/loophaus watch
|
|
108
|
+
npx @graypark/loophaus replay <trace-file> [--speed 2]
|
|
109
|
+
npx @graypark/loophaus compare <trace1> <trace2>
|
|
110
|
+
npx @graypark/loophaus loops
|
|
111
|
+
npx @graypark/loophaus worktree <create|remove|list>
|
|
112
|
+
npx @graypark/loophaus parallel <prd.json> [--count N] [--base branch]
|
|
113
|
+
npx @graypark/loophaus quality [--story US-001]
|
|
114
|
+
npx @graypark/loophaus sessions
|
|
115
|
+
npx @graypark/loophaus resume <session-id>
|
|
116
|
+
npx @graypark/loophaus --version
|
|
117
|
+
|
|
118
|
+
Hosts:
|
|
119
|
+
claude-code Claude Code (auto-detected via ~/.claude/)
|
|
120
|
+
codex-cli Codex CLI (auto-detected via ~/.codex/)
|
|
121
|
+
kiro-cli Kiro CLI (auto-detected via ~/.kiro/)
|
|
122
|
+
|
|
123
|
+
Install auto-detects available hosts if --host is not specified.
|
|
124
|
+
|
|
125
|
+
Options:
|
|
126
|
+
--host <name> Target a specific host
|
|
127
|
+
--claude Shorthand for --host claude-code
|
|
128
|
+
--kiro Shorthand for --host kiro-cli
|
|
129
|
+
--name <loop> Target a named loop (multi-loop)
|
|
130
|
+
--local Install to project-local .codex/ (Codex only)
|
|
131
|
+
--force Overwrite existing installation
|
|
132
|
+
--dry-run Preview changes without modifying files
|
|
133
|
+
--verbose Show full stack trace on error
|
|
134
|
+
`);
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
if (args.includes("--version")) {
|
|
138
|
+
const { getPackageVersion } = await import("../lib/paths.js");
|
|
139
|
+
console.log(getPackageVersion());
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
async function detectHosts() {
|
|
143
|
+
const hosts = [];
|
|
144
|
+
const { detect: detectClaude } = await import("../platforms/claude-code/installer.mjs");
|
|
145
|
+
const { detect: detectCodex } = await import("../platforms/codex-cli/installer.mjs");
|
|
146
|
+
const { detect: detectKiro } = await import("../platforms/kiro-cli/installer.mjs");
|
|
147
|
+
if (await detectClaude())
|
|
148
|
+
hosts.push("claude-code");
|
|
149
|
+
if (await detectCodex())
|
|
150
|
+
hosts.push("codex-cli");
|
|
151
|
+
if (await detectKiro())
|
|
152
|
+
hosts.push("kiro-cli");
|
|
153
|
+
return hosts;
|
|
154
|
+
}
|
|
155
|
+
async function runInstall() {
|
|
156
|
+
let targets = [];
|
|
157
|
+
if (host) {
|
|
158
|
+
targets = [host];
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
targets = await detectHosts();
|
|
162
|
+
if (targets.length === 0) {
|
|
163
|
+
console.log("No supported hosts detected. Install Claude Code, Codex CLI, or Kiro CLI first.");
|
|
164
|
+
console.log("Or specify a host: npx @graypark/loophaus install --host claude-code");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
console.log(`Detected hosts: ${targets.join(", ")}\n`);
|
|
168
|
+
}
|
|
169
|
+
for (const t of targets) {
|
|
170
|
+
const s = dryRun ? null : spinner(`Installing to ${t}...`);
|
|
171
|
+
try {
|
|
172
|
+
if (t === "claude-code") {
|
|
173
|
+
const { install } = await import("../platforms/claude-code/installer.mjs");
|
|
174
|
+
await install({ dryRun, force });
|
|
175
|
+
}
|
|
176
|
+
else if (t === "codex-cli") {
|
|
177
|
+
const { install } = await import("../platforms/codex-cli/installer.mjs");
|
|
178
|
+
await install({ dryRun, force, local });
|
|
179
|
+
}
|
|
180
|
+
else if (t === "kiro-cli") {
|
|
181
|
+
const { install } = await import("../platforms/kiro-cli/installer.mjs");
|
|
182
|
+
await install({ dryRun, force });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.log(`Unknown host: ${t}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
s?.stop();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function runUninstall() {
|
|
194
|
+
if (host === "claude-code" || args.includes("--claude")) {
|
|
195
|
+
const { uninstall } = await import("./uninstall.js");
|
|
196
|
+
await uninstall({ dryRun, claude: true });
|
|
197
|
+
}
|
|
198
|
+
else if (host === "kiro-cli" || args.includes("--kiro")) {
|
|
199
|
+
const { uninstall } = await import("../platforms/kiro-cli/installer.mjs");
|
|
200
|
+
await uninstall({ dryRun });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const { uninstall } = await import("./uninstall.js");
|
|
204
|
+
await uninstall({ dryRun, local });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function runStatus() {
|
|
208
|
+
const name = getFlag("--name");
|
|
209
|
+
const { read } = await import("../store/state-store.js");
|
|
210
|
+
const state = await read(undefined, name);
|
|
211
|
+
if (!state.active) {
|
|
212
|
+
console.log(name ? `No active loop: ${name}` : "No active loop.");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const iterInfo = state.maxIterations > 0
|
|
216
|
+
? `${state.currentIteration}/${state.maxIterations}`
|
|
217
|
+
: `${state.currentIteration}`;
|
|
218
|
+
console.log(`Loop Status`);
|
|
219
|
+
console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
220
|
+
if (name)
|
|
221
|
+
console.log(`Name: ${name}`);
|
|
222
|
+
console.log(`Active: yes`);
|
|
223
|
+
console.log(`Iteration: ${iterInfo}`);
|
|
224
|
+
console.log(`Promise: ${state.completionPromise || "(none)"}`);
|
|
225
|
+
try {
|
|
226
|
+
const { readFile } = await import("node:fs/promises");
|
|
227
|
+
const prd = JSON.parse(await readFile("prd.json", "utf-8"));
|
|
228
|
+
if (Array.isArray(prd.userStories)) {
|
|
229
|
+
const done = prd.userStories.filter((s) => s.passes === true).length;
|
|
230
|
+
const total = prd.userStories.length;
|
|
231
|
+
console.log("");
|
|
232
|
+
console.log("Stories");
|
|
233
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
234
|
+
for (const s of prd.userStories) {
|
|
235
|
+
const icon = s.passes ? "\u2713" : " ";
|
|
236
|
+
console.log(` ${icon} ${s.id} ${s.title}`);
|
|
237
|
+
}
|
|
238
|
+
console.log(`\n Progress: ${done}/${total} done`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch { /* no prd.json */ }
|
|
242
|
+
}
|
|
243
|
+
async function runLoops() {
|
|
244
|
+
const { listLoops } = await import("../core/loop-registry.js");
|
|
245
|
+
const loops = await listLoops();
|
|
246
|
+
if (loops.length === 0) {
|
|
247
|
+
console.log("No active loops.");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
console.log("Active Loops");
|
|
251
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
252
|
+
for (const l of loops) {
|
|
253
|
+
const status = l.active ? "active" : "done";
|
|
254
|
+
const maxIter = l.maxIterations || 0;
|
|
255
|
+
const curIter = l.currentIteration || 0;
|
|
256
|
+
const iter = maxIter > 0 ? `${curIter}/${maxIter}` : `${curIter}`;
|
|
257
|
+
console.log(` ${l.name} [${status}] iter ${iter}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function runStats() {
|
|
261
|
+
const { readTrace } = await import("../core/event-logger.js");
|
|
262
|
+
const { formatCost } = await import("../core/cost-tracker.js");
|
|
263
|
+
const events = await readTrace();
|
|
264
|
+
if (events.length === 0) {
|
|
265
|
+
console.log("No trace data found. Run a loop first.");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const iterations = events.filter((e) => e.event === "iteration").length;
|
|
269
|
+
const stops = events.filter((e) => e.event === "stop");
|
|
270
|
+
const lastStop = stops[stops.length - 1];
|
|
271
|
+
console.log(`Loop Stats`);
|
|
272
|
+
console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
273
|
+
console.log(`Total iterations: ${iterations}`);
|
|
274
|
+
console.log(`Total stops: ${stops.length}`);
|
|
275
|
+
if (lastStop) {
|
|
276
|
+
console.log(`Last stop reason: ${lastStop.reason || "unknown"}`);
|
|
277
|
+
console.log(`Last stop at: ${lastStop.ts || "unknown"}`);
|
|
278
|
+
}
|
|
279
|
+
const costEvents = events.filter((e) => e.event === "cost" || e.totalCost);
|
|
280
|
+
if (costEvents.length > 0) {
|
|
281
|
+
const totalCost = costEvents.reduce((s, e) => s + (e.totalCost || 0), 0);
|
|
282
|
+
console.log(`Estimated cost: ${formatCost(totalCost)}`);
|
|
283
|
+
}
|
|
284
|
+
console.log(`Trace file: .loophaus/trace.jsonl (${events.length} events)`);
|
|
285
|
+
}
|
|
286
|
+
async function runWatch() {
|
|
287
|
+
const { getTracePath } = await import("../core/event-logger.js");
|
|
288
|
+
const { watch: fsWatch } = await import("node:fs");
|
|
289
|
+
const { readFile, stat } = await import("node:fs/promises");
|
|
290
|
+
const tracePath = getTracePath();
|
|
291
|
+
console.log(`Watching ${tracePath}...`);
|
|
292
|
+
console.log("(Ctrl+C to stop)\n");
|
|
293
|
+
let lastSize = 0;
|
|
294
|
+
try {
|
|
295
|
+
const s = await stat(tracePath);
|
|
296
|
+
lastSize = s.size;
|
|
297
|
+
}
|
|
298
|
+
catch { /* file doesn't exist yet */ }
|
|
299
|
+
const COLORS = {
|
|
300
|
+
iteration: "\x1b[36m",
|
|
301
|
+
stop: "\x1b[31m",
|
|
302
|
+
continue: "\x1b[32m",
|
|
303
|
+
error: "\x1b[31m",
|
|
304
|
+
cost: "\x1b[33m",
|
|
305
|
+
state_change: "\x1b[35m",
|
|
306
|
+
verify_script: "\x1b[32m",
|
|
307
|
+
verify_failed: "\x1b[31m",
|
|
308
|
+
story_complete: "\x1b[32m",
|
|
309
|
+
loop_start: "\x1b[36m",
|
|
310
|
+
loop_end: "\x1b[36m",
|
|
311
|
+
};
|
|
312
|
+
const RESET = "\x1b[0m";
|
|
313
|
+
function printEvent(line) {
|
|
314
|
+
try {
|
|
315
|
+
const e = JSON.parse(line);
|
|
316
|
+
const color = COLORS[e.event] || "";
|
|
317
|
+
const time = e.ts ? new Date(e.ts).toLocaleTimeString() : "";
|
|
318
|
+
const detail = e.iteration ? ` iter=${e.iteration}` : e.reason ? ` reason=${e.reason}` : "";
|
|
319
|
+
console.log(`${color}[${time}] ${e.event}${detail}${RESET}`);
|
|
320
|
+
}
|
|
321
|
+
catch { /* skip malformed */ }
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const raw = await readFile(tracePath, "utf-8");
|
|
325
|
+
const lines = raw.trim().split("\n").slice(-20);
|
|
326
|
+
for (const line of lines)
|
|
327
|
+
printEvent(line);
|
|
328
|
+
if (lines.length > 0)
|
|
329
|
+
console.log("--- live ---\n");
|
|
330
|
+
}
|
|
331
|
+
catch { /* no file yet */ }
|
|
332
|
+
const { dirname: pathDirname } = await import("node:path");
|
|
333
|
+
const dir = pathDirname(tracePath);
|
|
334
|
+
try {
|
|
335
|
+
fsWatch(dir, { recursive: false }, async () => {
|
|
336
|
+
try {
|
|
337
|
+
const s = await stat(tracePath);
|
|
338
|
+
if (s.size > lastSize) {
|
|
339
|
+
const raw = await readFile(tracePath, "utf-8");
|
|
340
|
+
const lines = raw.trim().split("\n");
|
|
341
|
+
const newLines = [];
|
|
342
|
+
let pos = 0;
|
|
343
|
+
for (const line of lines) {
|
|
344
|
+
pos += Buffer.byteLength(line + "\n");
|
|
345
|
+
if (pos > lastSize)
|
|
346
|
+
newLines.push(line);
|
|
347
|
+
}
|
|
348
|
+
for (const line of newLines)
|
|
349
|
+
printEvent(line);
|
|
350
|
+
lastSize = s.size;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch { /* read error */ }
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
console.log("Cannot watch file. Make sure .loophaus/ directory exists.");
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
process.stdin.resume();
|
|
361
|
+
}
|
|
362
|
+
async function runReplay() {
|
|
363
|
+
const file = args[1];
|
|
364
|
+
if (!file) {
|
|
365
|
+
console.log("Usage: loophaus replay <trace-file> [--speed 2]");
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
const speed = getFlag("--speed") === "instant" ? 999999 : getNumericFlag("--speed", 1);
|
|
369
|
+
const speedLabel = speed >= 999999 ? "instant" : `${speed}x`;
|
|
370
|
+
const { readTrace } = await import("../core/event-logger.js");
|
|
371
|
+
const { replayTrace, analyzeTrace } = await import("../core/trace-analyzer.js");
|
|
372
|
+
let events;
|
|
373
|
+
if (file === ".loophaus/trace.jsonl" || file === "trace.jsonl") {
|
|
374
|
+
events = await readTrace();
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const { readFile } = await import("node:fs/promises");
|
|
378
|
+
const raw = await readFile(file, "utf-8");
|
|
379
|
+
events = raw.trim().split("\n").map((l) => { try {
|
|
380
|
+
return JSON.parse(l);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return null;
|
|
384
|
+
} }).filter(Boolean);
|
|
385
|
+
}
|
|
386
|
+
if (events.length === 0) {
|
|
387
|
+
console.log("No events found.");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const replayed = replayTrace(events, speed);
|
|
391
|
+
const analysis = analyzeTrace(events);
|
|
392
|
+
console.log(`Replaying ${events.length} events (${speedLabel})\n`);
|
|
393
|
+
const COLORS = { iteration: "\x1b[36m", stop: "\x1b[31m", continue: "\x1b[32m", error: "\x1b[31m", cost: "\x1b[33m", state_change: "\x1b[35m", verify_script: "\x1b[32m", verify_failed: "\x1b[31m", story_complete: "\x1b[32m", loop_start: "\x1b[36m", loop_end: "\x1b[36m" };
|
|
394
|
+
const RESET = "\x1b[0m";
|
|
395
|
+
let prevMs = 0;
|
|
396
|
+
for (const e of replayed) {
|
|
397
|
+
const delay = speed >= 999999 ? 0 : e.relativeMs - prevMs;
|
|
398
|
+
if (delay > 0)
|
|
399
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
400
|
+
prevMs = e.relativeMs;
|
|
401
|
+
const color = COLORS[e.event] || "";
|
|
402
|
+
const time = e.ts ? new Date(e.ts).toLocaleTimeString() : "";
|
|
403
|
+
const detail = e.iteration ? ` iter=${e.iteration}` : e.reason ? ` reason=${e.reason}` : "";
|
|
404
|
+
console.log(`${color}[${time}] ${e.event}${detail}${RESET}`);
|
|
405
|
+
}
|
|
406
|
+
console.log(`\n--- Summary ---`);
|
|
407
|
+
console.log(`Iterations: ${analysis.iterations}`);
|
|
408
|
+
console.log(`Duration: ${Math.round(analysis.durationMs / 1000)}s`);
|
|
409
|
+
if (analysis.totalCost > 0)
|
|
410
|
+
console.log(`Cost: $${analysis.totalCost.toFixed(4)}`);
|
|
411
|
+
if (analysis.lastStopReason)
|
|
412
|
+
console.log(`Stop reason: ${analysis.lastStopReason}`);
|
|
413
|
+
}
|
|
414
|
+
async function runCompare() {
|
|
415
|
+
const file1 = args[1];
|
|
416
|
+
const file2 = args[2];
|
|
417
|
+
if (!file1 || !file2) {
|
|
418
|
+
console.log("Usage: loophaus compare <trace1> <trace2>");
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
const { readFile } = await import("node:fs/promises");
|
|
422
|
+
const { compareTraces } = await import("../core/trace-analyzer.js");
|
|
423
|
+
function loadTrace(traceFile) {
|
|
424
|
+
return readFile(traceFile, "utf-8").then((raw) => raw.trim().split("\n").map((l) => { try {
|
|
425
|
+
return JSON.parse(l);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return null;
|
|
429
|
+
} }).filter(Boolean));
|
|
430
|
+
}
|
|
431
|
+
const [t1, t2] = await Promise.all([loadTrace(file1), loadTrace(file2)]);
|
|
432
|
+
const result = compareTraces(t1, t2);
|
|
433
|
+
console.log("Loop Comparison");
|
|
434
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
|
|
435
|
+
const fmt = (label, v1, v2, diff, unit = "") => {
|
|
436
|
+
const arrow = Number(diff) > 0 ? `+${diff}` : `${diff}`;
|
|
437
|
+
const color = Number(diff) > 0 ? "\x1b[31m" : Number(diff) < 0 ? "\x1b[32m" : "";
|
|
438
|
+
const reset = "\x1b[0m";
|
|
439
|
+
console.log(` ${label.padEnd(20)} ${String(v1).padStart(8)}${unit} vs ${String(v2).padStart(8)}${unit} ${color}(${arrow}${unit})${reset}`);
|
|
440
|
+
};
|
|
441
|
+
fmt("Iterations", result.trace1.iterations, result.trace2.iterations, result.diff.iterations);
|
|
442
|
+
fmt("Duration", Math.round(result.trace1.durationMs / 1000), Math.round(result.trace2.durationMs / 1000), Math.round(result.diff.durationMs / 1000), "s");
|
|
443
|
+
fmt("Stories done", result.trace1.storiesCompleted, result.trace2.storiesCompleted, result.diff.storiesCompleted);
|
|
444
|
+
if (result.trace1.totalCost || result.trace2.totalCost) {
|
|
445
|
+
fmt("Cost", result.trace1.totalCost.toFixed(4), result.trace2.totalCost.toFixed(4), result.diff.totalCost.toFixed(4), "$");
|
|
446
|
+
}
|
|
447
|
+
fmt("Errors", result.trace1.errors, result.trace2.errors, result.trace2.errors - result.trace1.errors);
|
|
448
|
+
console.log("");
|
|
449
|
+
}
|
|
450
|
+
async function runWorktree() {
|
|
451
|
+
const sub = args[1];
|
|
452
|
+
const { createWorktree, removeWorktree, listWorktrees } = await import("../core/worktree.js");
|
|
453
|
+
switch (sub) {
|
|
454
|
+
case "create": {
|
|
455
|
+
const name = args[2];
|
|
456
|
+
const base = args[3] || "HEAD";
|
|
457
|
+
if (!name) {
|
|
458
|
+
console.log("Usage: loophaus worktree create <name> [base-branch]");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const wt = await createWorktree(name, base);
|
|
462
|
+
console.log(`Created worktree: ${wt.name} at ${wt.path} (branch: ${wt.branch})`);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case "remove": {
|
|
466
|
+
const name = args[2];
|
|
467
|
+
if (!name) {
|
|
468
|
+
console.log("Usage: loophaus worktree remove <name>");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
await removeWorktree(name);
|
|
472
|
+
console.log(`Removed worktree: ${name}`);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case "list": {
|
|
476
|
+
const wts = await listWorktrees();
|
|
477
|
+
if (wts.length === 0) {
|
|
478
|
+
console.log("No loophaus worktrees.");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
console.log("Worktrees");
|
|
482
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
483
|
+
for (const wt of wts) {
|
|
484
|
+
console.log(` ${wt.name} ${wt.branch} ${wt.path}`);
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
default:
|
|
489
|
+
console.log("Usage: loophaus worktree <create|remove|list>");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function runSessions() {
|
|
493
|
+
const { listSessions } = await import("../core/session.js");
|
|
494
|
+
const sessions = await listSessions();
|
|
495
|
+
if (sessions.length === 0) {
|
|
496
|
+
console.log("No saved sessions.");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
console.log("Sessions");
|
|
500
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
501
|
+
for (const s of sessions) {
|
|
502
|
+
const age = Math.round((Date.now() - new Date(s.savedAt).getTime()) / 60000);
|
|
503
|
+
console.log(` ${s.sessionId} iter=${s.currentIteration || 0} ${age}m ago`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function runResume() {
|
|
507
|
+
const id = args[1];
|
|
508
|
+
if (!id) {
|
|
509
|
+
console.log("Usage: loophaus resume <session-id>");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const { resumeSession } = await import("../core/session.js");
|
|
513
|
+
const state = await resumeSession(id);
|
|
514
|
+
if (!state) {
|
|
515
|
+
console.log(`Session not found: ${id}`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
console.log(`Resumed session ${id} at iteration ${state.currentIteration}`);
|
|
519
|
+
console.log(`Loop is now active. The stop hook will continue from here.`);
|
|
520
|
+
}
|
|
521
|
+
async function runParallelCmd() {
|
|
522
|
+
const prdPath = args[1] || "prd.json";
|
|
523
|
+
const count = getNumericFlag("--count", 2);
|
|
524
|
+
const base = getFlag("--base") || "HEAD";
|
|
525
|
+
const { runParallel } = await import("../core/parallel-runner.js");
|
|
526
|
+
const result = await runParallel({ prdPath, count, baseBranch: base });
|
|
527
|
+
console.log(result.message);
|
|
528
|
+
if (result.worktrees) {
|
|
529
|
+
console.log("\nWorktrees:");
|
|
530
|
+
for (const wt of result.worktrees) {
|
|
531
|
+
console.log(` ${wt.name} branch:${wt.branch} stories:[${wt.stories.join(",")}]`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function runQuality() {
|
|
536
|
+
const storyId = getFlag("--story");
|
|
537
|
+
const cwd = process.cwd();
|
|
538
|
+
if (storyId) {
|
|
539
|
+
const { evaluateStory } = await import("../core/quality-scorer.js");
|
|
540
|
+
const { read } = await import("../store/state-store.js");
|
|
541
|
+
const state = await read(cwd);
|
|
542
|
+
const config = state.qualityConfig || {};
|
|
543
|
+
if (!config.typecheckCommand) {
|
|
544
|
+
try {
|
|
545
|
+
await access(join(cwd, "tsconfig.json"));
|
|
546
|
+
config.typecheckCommand = "npx tsc --noEmit";
|
|
547
|
+
}
|
|
548
|
+
catch { /* no tsconfig */ }
|
|
549
|
+
}
|
|
550
|
+
const result = await evaluateStory(storyId, cwd, config);
|
|
551
|
+
console.log(`Quality: ${storyId}`);
|
|
552
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
553
|
+
console.log(`Score: ${result.score}/100 (${result.grade})`);
|
|
554
|
+
for (const [k, v] of Object.entries(result.breakdown)) {
|
|
555
|
+
const numVal = v;
|
|
556
|
+
const bar = "\u2588".repeat(numVal) + "\u2591".repeat(10 - numVal);
|
|
557
|
+
console.log(` ${k.padEnd(10)} ${bar} ${numVal}/10`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
const { readResults } = await import("../core/quality-scorer.js");
|
|
562
|
+
const results = await readResults(cwd);
|
|
563
|
+
if (results.length === 0) {
|
|
564
|
+
console.log("No quality results yet. Run /loop-plan first.");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
console.log("Quality Results");
|
|
568
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
569
|
+
const byStory = {};
|
|
570
|
+
for (const r of results) {
|
|
571
|
+
if (!byStory[r.storyId])
|
|
572
|
+
byStory[r.storyId] = [];
|
|
573
|
+
byStory[r.storyId].push(r);
|
|
574
|
+
}
|
|
575
|
+
for (const [sid, attempts] of Object.entries(byStory)) {
|
|
576
|
+
const best = attempts.reduce((a, b) => (a.score > b.score ? a : b));
|
|
577
|
+
const icon = best.status === "keep" ? "\u2713" : best.status === "discard" ? "\u2717" : "~";
|
|
578
|
+
console.log(` ${icon} ${sid} score: ${best.score} (${attempts.length} attempts)`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
switch (command) {
|
|
584
|
+
case "install":
|
|
585
|
+
await runInstall();
|
|
586
|
+
break;
|
|
587
|
+
case "uninstall":
|
|
588
|
+
await runUninstall();
|
|
589
|
+
break;
|
|
590
|
+
case "status":
|
|
591
|
+
await runStatus();
|
|
592
|
+
break;
|
|
593
|
+
case "stats":
|
|
594
|
+
await runStats();
|
|
595
|
+
break;
|
|
596
|
+
case "loops":
|
|
597
|
+
await runLoops();
|
|
598
|
+
break;
|
|
599
|
+
case "watch":
|
|
600
|
+
await runWatch();
|
|
601
|
+
break;
|
|
602
|
+
case "replay":
|
|
603
|
+
await runReplay();
|
|
604
|
+
break;
|
|
605
|
+
case "compare":
|
|
606
|
+
await runCompare();
|
|
607
|
+
break;
|
|
608
|
+
case "worktree":
|
|
609
|
+
await runWorktree();
|
|
610
|
+
break;
|
|
611
|
+
case "parallel":
|
|
612
|
+
await runParallelCmd();
|
|
613
|
+
break;
|
|
614
|
+
case "quality":
|
|
615
|
+
await runQuality();
|
|
616
|
+
break;
|
|
617
|
+
case "sessions":
|
|
618
|
+
await runSessions();
|
|
619
|
+
break;
|
|
620
|
+
case "resume":
|
|
621
|
+
await runResume();
|
|
622
|
+
break;
|
|
623
|
+
default:
|
|
624
|
+
if (command.startsWith("-")) {
|
|
625
|
+
await runInstall();
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
const suggestion = suggestCommand(command);
|
|
629
|
+
console.log(`Unknown command: ${command}`);
|
|
630
|
+
if (suggestion)
|
|
631
|
+
console.log(`Did you mean: loophaus ${suggestion}?`);
|
|
632
|
+
console.log("Run: loophaus --help for available commands.");
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
const error = err;
|
|
639
|
+
console.error(`\u2718 ${error.message}`);
|
|
640
|
+
if (verbose && error.stack) {
|
|
641
|
+
console.error(`\n${error.stack}`);
|
|
642
|
+
}
|
|
643
|
+
if (error.message.includes("Not in a git repository")) {
|
|
644
|
+
console.error(" Hint: Run this command from a git project root.");
|
|
645
|
+
}
|
|
646
|
+
else if (error.message.includes("EACCES") || error.message.includes("permission")) {
|
|
647
|
+
console.error(" Hint: Check file permissions, or try with appropriate access.");
|
|
648
|
+
}
|
|
649
|
+
else if (error.message.includes("ENOENT")) {
|
|
650
|
+
console.error(" Hint: A required file was not found. Check your working directory.");
|
|
651
|
+
}
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=loophaus.js.map
|