@graypark/loophaus 3.5.1 → 3.6.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.
Files changed (96) hide show
  1. package/commands/loop-plan.md +22 -0
  2. package/dist/bin/install.d.ts +0 -1
  3. package/dist/bin/install.js +0 -1
  4. package/dist/bin/loophaus.d.ts +0 -1
  5. package/dist/bin/loophaus.js +111 -2
  6. package/dist/bin/uninstall.d.ts +0 -1
  7. package/dist/bin/uninstall.js +0 -1
  8. package/dist/commands/loop-plan.md +22 -0
  9. package/dist/core/benchmark.d.ts +38 -0
  10. package/dist/core/benchmark.js +207 -0
  11. package/dist/core/cleanup.d.ts +23 -0
  12. package/dist/core/cleanup.js +144 -0
  13. package/dist/core/cost-tracker.d.ts +0 -1
  14. package/dist/core/cost-tracker.js +0 -1
  15. package/dist/core/engine.d.ts +0 -1
  16. package/dist/core/engine.js +0 -1
  17. package/dist/core/event-logger.d.ts +0 -1
  18. package/dist/core/event-logger.js +0 -1
  19. package/dist/core/events.d.ts +0 -1
  20. package/dist/core/events.js +0 -1
  21. package/dist/core/io-helpers.d.ts +0 -1
  22. package/dist/core/io-helpers.js +0 -1
  23. package/dist/core/loop-registry.d.ts +0 -1
  24. package/dist/core/loop-registry.js +0 -1
  25. package/dist/core/merge-strategy.d.ts +0 -1
  26. package/dist/core/merge-strategy.js +0 -1
  27. package/dist/core/parallel-runner.d.ts +0 -1
  28. package/dist/core/parallel-runner.js +0 -1
  29. package/dist/core/policy.d.ts +0 -1
  30. package/dist/core/policy.js +0 -1
  31. package/dist/core/quality-scorer.d.ts +0 -1
  32. package/dist/core/quality-scorer.js +0 -1
  33. package/dist/core/refine-loop.d.ts +0 -1
  34. package/dist/core/refine-loop.js +0 -1
  35. package/dist/core/session.d.ts +0 -1
  36. package/dist/core/session.js +0 -1
  37. package/dist/core/trace-analyzer.d.ts +0 -1
  38. package/dist/core/trace-analyzer.js +0 -1
  39. package/dist/core/types.d.ts +0 -1
  40. package/dist/core/types.js +0 -1
  41. package/dist/core/validate.d.ts +0 -1
  42. package/dist/core/validate.js +0 -1
  43. package/dist/core/worktree.d.ts +0 -1
  44. package/dist/core/worktree.js +0 -1
  45. package/dist/lib/paths.d.ts +0 -1
  46. package/dist/lib/paths.js +0 -1
  47. package/dist/lib/stop-hook-core.d.ts +0 -1
  48. package/dist/lib/stop-hook-core.js +0 -1
  49. package/dist/package.json +3 -1
  50. package/dist/store/state-store.d.ts +0 -1
  51. package/dist/store/state-store.js +0 -1
  52. package/package.json +3 -1
  53. package/dist/bin/install.d.ts.map +0 -1
  54. package/dist/bin/install.js.map +0 -1
  55. package/dist/bin/loophaus.d.ts.map +0 -1
  56. package/dist/bin/loophaus.js.map +0 -1
  57. package/dist/bin/uninstall.d.ts.map +0 -1
  58. package/dist/bin/uninstall.js.map +0 -1
  59. package/dist/core/cost-tracker.d.ts.map +0 -1
  60. package/dist/core/cost-tracker.js.map +0 -1
  61. package/dist/core/engine.d.ts.map +0 -1
  62. package/dist/core/engine.js.map +0 -1
  63. package/dist/core/event-logger.d.ts.map +0 -1
  64. package/dist/core/event-logger.js.map +0 -1
  65. package/dist/core/events.d.ts.map +0 -1
  66. package/dist/core/events.js.map +0 -1
  67. package/dist/core/io-helpers.d.ts.map +0 -1
  68. package/dist/core/io-helpers.js.map +0 -1
  69. package/dist/core/loop-registry.d.ts.map +0 -1
  70. package/dist/core/loop-registry.js.map +0 -1
  71. package/dist/core/merge-strategy.d.ts.map +0 -1
  72. package/dist/core/merge-strategy.js.map +0 -1
  73. package/dist/core/parallel-runner.d.ts.map +0 -1
  74. package/dist/core/parallel-runner.js.map +0 -1
  75. package/dist/core/policy.d.ts.map +0 -1
  76. package/dist/core/policy.js.map +0 -1
  77. package/dist/core/quality-scorer.d.ts.map +0 -1
  78. package/dist/core/quality-scorer.js.map +0 -1
  79. package/dist/core/refine-loop.d.ts.map +0 -1
  80. package/dist/core/refine-loop.js.map +0 -1
  81. package/dist/core/session.d.ts.map +0 -1
  82. package/dist/core/session.js.map +0 -1
  83. package/dist/core/trace-analyzer.d.ts.map +0 -1
  84. package/dist/core/trace-analyzer.js.map +0 -1
  85. package/dist/core/types.d.ts.map +0 -1
  86. package/dist/core/types.js.map +0 -1
  87. package/dist/core/validate.d.ts.map +0 -1
  88. package/dist/core/validate.js.map +0 -1
  89. package/dist/core/worktree.d.ts.map +0 -1
  90. package/dist/core/worktree.js.map +0 -1
  91. package/dist/lib/paths.d.ts.map +0 -1
  92. package/dist/lib/paths.js.map +0 -1
  93. package/dist/lib/stop-hook-core.d.ts.map +0 -1
  94. package/dist/lib/stop-hook-core.js.map +0 -1
  95. package/dist/store/state-store.d.ts.map +0 -1
  96. package/dist/store/state-store.js.map +0 -1
@@ -11,6 +11,28 @@ The user runs `/loop-plan` once and gets a single merged branch with all work do
11
11
 
12
12
  ---
13
13
 
14
+ ## Phase 0: Cleanup Previous Data
15
+
16
+ Before starting a new plan, apply the cleanup policy from `.loophaus/config.json`:
17
+
18
+ ```javascript
19
+ import { applyOnNewPlanPolicy } from "../core/cleanup.js";
20
+ const cleanResult = await applyOnNewPlanPolicy();
21
+ ```
22
+
23
+ | Policy | Behavior |
24
+ |--------|----------|
25
+ | `"keep"` (default) | Do nothing — old traces/results persist alongside new data |
26
+ | `"archive"` | Move trace.jsonl + results.tsv + sessions/ to `.loophaus/archive/{date}/` |
27
+ | `"delete"` | Remove trace.jsonl + results.tsv + sessions/ (benchmark.tsv always preserved) |
28
+
29
+ If cleanup was performed, briefly note it (e.g., "Archived previous loop data.") then continue.
30
+
31
+ To configure: `loophaus clean --config` to view, or create `.loophaus/config.json`:
32
+ ```json
33
+ { "cleanup": { "onNewPlan": "archive", "traceRetentionDays": 30, "sessionRetentionDays": 7 } }
34
+ ```
35
+
14
36
  ## Phase 1: Discovery Interview
15
37
 
16
38
  Ask 3-5 focused questions about $ARGUMENTS:
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=install.d.ts.map
@@ -5,4 +5,3 @@ import { fileURLToPath } from "node:url";
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const loophausCli = resolve(dirname(__filename), "loophaus.js");
7
7
  await import(loophausCli);
8
- //# sourceMappingURL=install.js.map
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=loophaus.d.ts.map
@@ -16,11 +16,12 @@ const showHelp = args.includes("--help") || args.includes("-h");
16
16
  const KNOWN_FLAGS = new Set([
17
17
  "--help", "-h", "--version", "--dry-run", "--force", "--local", "--verbose",
18
18
  "--host", "--claude", "--kiro", "--name", "--speed", "--count", "--base", "--story",
19
+ "--all", "--traces", "--sessions", "--results", "--before", "--config",
19
20
  ]);
20
21
  const VALID_COMMANDS = [
21
22
  "install", "uninstall", "status", "stats", "loops", "watch",
22
23
  "replay", "compare", "worktree", "parallel", "quality",
23
- "sessions", "resume", "help",
24
+ "sessions", "resume", "benchmark", "clean", "help",
24
25
  ];
25
26
  function validateFlags() {
26
27
  for (const arg of args) {
@@ -111,6 +112,8 @@ Usage:
111
112
  npx @graypark/loophaus worktree <create|remove|list>
112
113
  npx @graypark/loophaus parallel <prd.json> [--count N] [--base branch]
113
114
  npx @graypark/loophaus quality [--story US-001]
115
+ npx @graypark/loophaus benchmark
116
+ npx @graypark/loophaus clean [--all|--traces|--sessions|--results] [--before DATE]
114
117
  npx @graypark/loophaus sessions
115
118
  npx @graypark/loophaus resume <session-id>
116
119
  npx @graypark/loophaus --version
@@ -579,6 +582,107 @@ async function runQuality() {
579
582
  }
580
583
  }
581
584
  }
585
+ async function runBenchmarkCmd() {
586
+ const { runBenchmark, logBenchmark, readBenchmarkHistory, scoreBenchmark } = await import("../core/benchmark.js");
587
+ const s = spinner("Running benchmark...");
588
+ let result;
589
+ try {
590
+ result = await runBenchmark();
591
+ }
592
+ finally {
593
+ s.stop();
594
+ }
595
+ await logBenchmark(result);
596
+ console.log("Project Benchmark");
597
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
598
+ console.log(` Score: ${result.score}/100 (${result.grade})\n`);
599
+ const labels = {
600
+ tests: "Tests",
601
+ typecheck: "Typecheck",
602
+ build: "Build",
603
+ testTime: "Test Time",
604
+ coverage: "Coverage",
605
+ pkgSize: "Pkg Size",
606
+ };
607
+ for (const [key, info] of Object.entries(result.breakdown)) {
608
+ const bar = "\u2588".repeat(info.score) + "\u2591".repeat(10 - info.score);
609
+ const label = (labels[key] || key).padEnd(12);
610
+ console.log(` ${label} ${bar} ${info.score}/10`);
611
+ }
612
+ // Trend
613
+ const history = await readBenchmarkHistory();
614
+ if (history.length > 1) {
615
+ const prev = history[history.length - 2];
616
+ const diff = result.score - prev.score;
617
+ const arrow = diff > 0 ? `\x1b[32m+${diff}\x1b[0m` : diff < 0 ? `\x1b[31m${diff}\x1b[0m` : "0";
618
+ console.log(`\n Trend: ${prev.score} → ${result.score} (${arrow})`);
619
+ console.log(` Prev: v${prev.version} @ ${prev.commit} (${prev.ts.split("T")[0]})`);
620
+ }
621
+ console.log(`\n Recorded to .loophaus/benchmark.tsv (${history.length} entries)`);
622
+ }
623
+ async function runCleanCmd() {
624
+ const { cleanAll, cleanTraces, cleanSessions, cleanResults, readConfig } = await import("../core/cleanup.js");
625
+ const hasFlag = (f) => args.includes(f);
626
+ if (hasFlag("--config")) {
627
+ const config = await readConfig();
628
+ console.log("Cleanup Config (.loophaus/config.json)");
629
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
630
+ console.log(` onNewPlan: ${config.cleanup.onNewPlan}`);
631
+ console.log(` traceRetentionDays: ${config.cleanup.traceRetentionDays}`);
632
+ console.log(` sessionRetentionDays: ${config.cleanup.sessionRetentionDays}`);
633
+ return;
634
+ }
635
+ if (!hasFlag("--all") && !hasFlag("--traces") && !hasFlag("--sessions") && !hasFlag("--results")) {
636
+ console.log(`Usage: loophaus clean [options]
637
+
638
+ Options:
639
+ --all Remove traces + results + sessions (not benchmark.tsv)
640
+ --traces Remove trace.jsonl only
641
+ --sessions Remove session checkpoints
642
+ --results Remove results.tsv only
643
+ --before DATE Only remove data before this date (sessions only)
644
+ --config Show current cleanup policy`);
645
+ return;
646
+ }
647
+ const beforeRaw = getFlag("--before");
648
+ const before = beforeRaw ? new Date(beforeRaw) : undefined;
649
+ let result;
650
+ if (hasFlag("--all")) {
651
+ result = await cleanAll();
652
+ }
653
+ else {
654
+ result = { removed: [], archived: [], skipped: [] };
655
+ if (hasFlag("--traces")) {
656
+ const r = await cleanTraces();
657
+ result.removed.push(...r.removed);
658
+ result.skipped.push(...r.skipped);
659
+ }
660
+ if (hasFlag("--results")) {
661
+ const r = await cleanResults();
662
+ result.removed.push(...r.removed);
663
+ result.skipped.push(...r.skipped);
664
+ }
665
+ if (hasFlag("--sessions")) {
666
+ const r = await cleanSessions({ before });
667
+ result.removed.push(...r.removed);
668
+ result.skipped.push(...r.skipped);
669
+ }
670
+ }
671
+ console.log("Clean Complete");
672
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
673
+ if (result.removed.length > 0) {
674
+ console.log(` Removed: ${result.removed.join(", ")}`);
675
+ }
676
+ if (result.archived.length > 0) {
677
+ console.log(` Archived: ${result.archived.join(", ")}`);
678
+ }
679
+ if (result.skipped.length > 0) {
680
+ console.log(` Skipped: ${result.skipped.join(", ")}`);
681
+ }
682
+ if (result.removed.length === 0 && result.archived.length === 0) {
683
+ console.log(" Nothing to clean.");
684
+ }
685
+ }
582
686
  try {
583
687
  switch (command) {
584
688
  case "install":
@@ -614,6 +718,12 @@ try {
614
718
  case "quality":
615
719
  await runQuality();
616
720
  break;
721
+ case "benchmark":
722
+ await runBenchmarkCmd();
723
+ break;
724
+ case "clean":
725
+ await runCleanCmd();
726
+ break;
617
727
  case "sessions":
618
728
  await runSessions();
619
729
  break;
@@ -651,4 +761,3 @@ catch (err) {
651
761
  }
652
762
  process.exit(1);
653
763
  }
654
- //# sourceMappingURL=loophaus.js.map
@@ -5,4 +5,3 @@ export interface UninstallOptions {
5
5
  claude?: boolean;
6
6
  }
7
7
  export declare function uninstall({ dryRun, local, claude, }?: UninstallOptions): Promise<void>;
8
- //# sourceMappingURL=uninstall.d.ts.map
@@ -206,4 +206,3 @@ if (isDirectRun) {
206
206
  process.exit(1);
207
207
  });
208
208
  }
209
- //# sourceMappingURL=uninstall.js.map
@@ -11,6 +11,28 @@ The user runs `/loop-plan` once and gets a single merged branch with all work do
11
11
 
12
12
  ---
13
13
 
14
+ ## Phase 0: Cleanup Previous Data
15
+
16
+ Before starting a new plan, apply the cleanup policy from `.loophaus/config.json`:
17
+
18
+ ```javascript
19
+ import { applyOnNewPlanPolicy } from "../core/cleanup.js";
20
+ const cleanResult = await applyOnNewPlanPolicy();
21
+ ```
22
+
23
+ | Policy | Behavior |
24
+ |--------|----------|
25
+ | `"keep"` (default) | Do nothing — old traces/results persist alongside new data |
26
+ | `"archive"` | Move trace.jsonl + results.tsv + sessions/ to `.loophaus/archive/{date}/` |
27
+ | `"delete"` | Remove trace.jsonl + results.tsv + sessions/ (benchmark.tsv always preserved) |
28
+
29
+ If cleanup was performed, briefly note it (e.g., "Archived previous loop data.") then continue.
30
+
31
+ To configure: `loophaus clean --config` to view, or create `.loophaus/config.json`:
32
+ ```json
33
+ { "cleanup": { "onNewPlan": "archive", "traceRetentionDays": 30, "sessionRetentionDays": 7 } }
34
+ ```
35
+
14
36
  ## Phase 1: Discovery Interview
15
37
 
16
38
  Ask 3-5 focused questions about $ARGUMENTS:
@@ -0,0 +1,38 @@
1
+ export interface BenchmarkMetrics {
2
+ testsPassed: number;
3
+ testsFailed: number;
4
+ testsTotal: number;
5
+ testTimeMs: number;
6
+ typecheckErrors: number;
7
+ buildSuccess: boolean;
8
+ coveragePct: number;
9
+ pkgSizeKb: number;
10
+ }
11
+ export interface BenchmarkResult {
12
+ score: number;
13
+ grade: string;
14
+ breakdown: Record<string, {
15
+ value: number;
16
+ max: number;
17
+ score: number;
18
+ }>;
19
+ metrics: BenchmarkMetrics;
20
+ }
21
+ export interface BenchmarkEntry {
22
+ ts: string;
23
+ commit: string;
24
+ version: string;
25
+ score: number;
26
+ grade: string;
27
+ testsPassed: number;
28
+ testsTotal: number;
29
+ testTimeMs: number;
30
+ typecheckErrors: number;
31
+ buildSuccess: boolean;
32
+ coveragePct: number;
33
+ pkgSizeKb: number;
34
+ }
35
+ export declare function scoreBenchmark(metrics: BenchmarkMetrics): BenchmarkResult;
36
+ export declare function runBenchmark(cwd?: string): Promise<BenchmarkResult>;
37
+ export declare function logBenchmark(result: BenchmarkResult, cwd?: string): Promise<void>;
38
+ export declare function readBenchmarkHistory(cwd?: string): Promise<BenchmarkEntry[]>;
@@ -0,0 +1,207 @@
1
+ // core/benchmark.ts
2
+ // Project-level quality measurement (autoresearch pattern: val_bpb → project score)
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { readFile, appendFile, mkdir, stat } from "node:fs/promises";
6
+ import { join, dirname } from "node:path";
7
+ const execFileAsync = promisify(execFile);
8
+ export function scoreBenchmark(metrics) {
9
+ const breakdown = {};
10
+ // Tests: 0-10 based on pass rate
11
+ const testRate = metrics.testsTotal > 0 ? metrics.testsPassed / metrics.testsTotal : 0;
12
+ breakdown.tests = { value: metrics.testsPassed, max: metrics.testsTotal, score: Math.round(testRate * 10) };
13
+ // Typecheck: 10 if 0 errors, degrade
14
+ const tcScore = metrics.typecheckErrors === 0 ? 10 : metrics.typecheckErrors <= 5 ? 6 : metrics.typecheckErrors <= 20 ? 3 : 0;
15
+ breakdown.typecheck = { value: metrics.typecheckErrors, max: 0, score: tcScore };
16
+ // Build: binary
17
+ breakdown.build = { value: metrics.buildSuccess ? 1 : 0, max: 1, score: metrics.buildSuccess ? 10 : 0 };
18
+ // Test time: < 2s = 10, < 5s = 8, < 10s = 6, < 30s = 4, else 2
19
+ const ttScore = metrics.testTimeMs < 2000 ? 10 : metrics.testTimeMs < 5000 ? 8 : metrics.testTimeMs < 10000 ? 6 : metrics.testTimeMs < 30000 ? 4 : 2;
20
+ breakdown.testTime = { value: metrics.testTimeMs, max: 2000, score: ttScore };
21
+ // Coverage: direct percentage mapping to 0-10
22
+ const covScore = Math.min(10, Math.round(metrics.coveragePct / 10));
23
+ breakdown.coverage = { value: metrics.coveragePct, max: 100, score: covScore };
24
+ // Package size: < 50KB = 10, < 100KB = 8, < 200KB = 6, < 500KB = 4, else 2
25
+ const sizeScore = metrics.pkgSizeKb < 50 ? 10 : metrics.pkgSizeKb < 100 ? 8 : metrics.pkgSizeKb < 200 ? 6 : metrics.pkgSizeKb < 500 ? 4 : 2;
26
+ breakdown.pkgSize = { value: metrics.pkgSizeKb, max: 50, score: sizeScore };
27
+ // Weighted average (tests 3x, typecheck 2.5x, build 1.5x, coverage 2x, testTime 0.5x, pkgSize 0.5x)
28
+ const weights = { tests: 3, typecheck: 2.5, build: 1.5, coverage: 2, testTime: 0.5, pkgSize: 0.5 };
29
+ let weightedSum = 0;
30
+ let totalWeight = 0;
31
+ for (const [key, w] of Object.entries(weights)) {
32
+ weightedSum += breakdown[key].score * w;
33
+ totalWeight += 10 * w;
34
+ }
35
+ const score = Math.round((weightedSum / totalWeight) * 100);
36
+ const grade = score >= 90 ? "A+" : score >= 85 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
37
+ return { score, grade, breakdown, metrics };
38
+ }
39
+ export async function runBenchmark(cwd) {
40
+ const dir = cwd || process.cwd();
41
+ const metrics = {
42
+ testsPassed: 0,
43
+ testsFailed: 0,
44
+ testsTotal: 0,
45
+ testTimeMs: 0,
46
+ typecheckErrors: 0,
47
+ buildSuccess: false,
48
+ coveragePct: 0,
49
+ pkgSizeKb: 0,
50
+ };
51
+ // 1. Tests
52
+ const testStart = Date.now();
53
+ try {
54
+ const { stdout } = await execFileAsync("npx", ["vitest", "run", "--reporter=json"], { cwd: dir, timeout: 120_000 });
55
+ metrics.testTimeMs = Date.now() - testStart;
56
+ try {
57
+ const json = JSON.parse(stdout);
58
+ metrics.testsPassed = json.numPassedTests ?? 0;
59
+ metrics.testsFailed = json.numFailedTests ?? 0;
60
+ metrics.testsTotal = json.numTotalTests ?? 0;
61
+ }
62
+ catch {
63
+ // JSON parse failed, try regex fallback
64
+ const passMatch = stdout.match(/(\d+) passed/);
65
+ const failMatch = stdout.match(/(\d+) failed/);
66
+ if (passMatch)
67
+ metrics.testsPassed = parseInt(passMatch[1]);
68
+ if (failMatch)
69
+ metrics.testsFailed = parseInt(failMatch[1]);
70
+ metrics.testsTotal = metrics.testsPassed + metrics.testsFailed;
71
+ }
72
+ }
73
+ catch (err) {
74
+ metrics.testTimeMs = Date.now() - testStart;
75
+ const output = err.stdout || "";
76
+ const passMatch = output.match(/(\d+) passed/);
77
+ if (passMatch)
78
+ metrics.testsPassed = parseInt(passMatch[1]);
79
+ const failMatch = output.match(/(\d+) failed/);
80
+ if (failMatch)
81
+ metrics.testsFailed = parseInt(failMatch[1]);
82
+ metrics.testsTotal = metrics.testsPassed + metrics.testsFailed;
83
+ }
84
+ // 2. Typecheck
85
+ try {
86
+ await execFileAsync("npx", ["tsc", "--noEmit"], { cwd: dir, timeout: 60_000 });
87
+ metrics.typecheckErrors = 0;
88
+ }
89
+ catch (err) {
90
+ const output = err.stdout || err.stderr || "";
91
+ const errorCount = (output.match(/error TS/g) || []).length;
92
+ metrics.typecheckErrors = errorCount || 1;
93
+ }
94
+ // 3. Build
95
+ try {
96
+ await execFileAsync("npm", ["run", "build"], { cwd: dir, timeout: 60_000 });
97
+ metrics.buildSuccess = true;
98
+ }
99
+ catch {
100
+ metrics.buildSuccess = false;
101
+ }
102
+ // 4. Coverage
103
+ try {
104
+ const summaryPath = join(dir, "coverage", "coverage-summary.json");
105
+ const raw = await readFile(summaryPath, "utf-8");
106
+ const summary = JSON.parse(raw);
107
+ metrics.coveragePct = summary.total?.lines?.pct ?? 0;
108
+ }
109
+ catch {
110
+ // Run coverage if summary doesn't exist
111
+ try {
112
+ await execFileAsync("npx", ["vitest", "run", "--coverage"], { cwd: dir, timeout: 120_000 });
113
+ const summaryPath = join(dir, "coverage", "coverage-summary.json");
114
+ const raw = await readFile(summaryPath, "utf-8");
115
+ const summary = JSON.parse(raw);
116
+ metrics.coveragePct = summary.total?.lines?.pct ?? 0;
117
+ }
118
+ catch {
119
+ metrics.coveragePct = 0;
120
+ }
121
+ }
122
+ // 5. Package size
123
+ try {
124
+ const distDir = join(dir, "dist");
125
+ const s = await stat(distDir);
126
+ if (s.isDirectory()) {
127
+ const { stdout } = await execFileAsync("du", ["-sk", distDir], { timeout: 10_000 });
128
+ const match = stdout.match(/^(\d+)/);
129
+ metrics.pkgSizeKb = match ? parseInt(match[1]) : 0;
130
+ }
131
+ }
132
+ catch {
133
+ metrics.pkgSizeKb = 0;
134
+ }
135
+ return scoreBenchmark(metrics);
136
+ }
137
+ function getBenchmarkPath(cwd) {
138
+ return join(cwd || process.cwd(), ".loophaus", "benchmark.tsv");
139
+ }
140
+ const HEADER = "ts\tcommit\tversion\tscore\tgrade\ttests_passed\ttests_total\ttest_time_ms\ttypecheck_errors\tbuild_ok\tcoverage_pct\tpkg_size_kb\n";
141
+ export async function logBenchmark(result, cwd) {
142
+ const benchPath = getBenchmarkPath(cwd);
143
+ await mkdir(dirname(benchPath), { recursive: true });
144
+ let commitHash = "unknown";
145
+ try {
146
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { timeout: 5_000 });
147
+ commitHash = stdout.trim();
148
+ }
149
+ catch { /* not in git */ }
150
+ let version = "unknown";
151
+ try {
152
+ const pkgPath = join(cwd || process.cwd(), "package.json");
153
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
154
+ version = pkg.version || "unknown";
155
+ }
156
+ catch { /* no package.json */ }
157
+ // Write header if file is new
158
+ try {
159
+ await stat(benchPath);
160
+ }
161
+ catch {
162
+ await appendFile(benchPath, HEADER, "utf-8");
163
+ }
164
+ const m = result.metrics;
165
+ const line = [
166
+ new Date().toISOString(),
167
+ commitHash,
168
+ version,
169
+ result.score,
170
+ result.grade,
171
+ m.testsPassed,
172
+ m.testsTotal,
173
+ m.testTimeMs,
174
+ m.typecheckErrors,
175
+ m.buildSuccess ? 1 : 0,
176
+ m.coveragePct.toFixed(1),
177
+ m.pkgSizeKb,
178
+ ].join("\t") + "\n";
179
+ await appendFile(benchPath, line, "utf-8");
180
+ }
181
+ export async function readBenchmarkHistory(cwd) {
182
+ const benchPath = getBenchmarkPath(cwd);
183
+ try {
184
+ const raw = await readFile(benchPath, "utf-8");
185
+ const lines = raw.trim().split("\n").slice(1); // skip header
186
+ return lines.map(line => {
187
+ const cols = line.split("\t");
188
+ return {
189
+ ts: cols[0] || "",
190
+ commit: cols[1] || "",
191
+ version: cols[2] || "",
192
+ score: parseInt(cols[3]) || 0,
193
+ grade: cols[4] || "",
194
+ testsPassed: parseInt(cols[5]) || 0,
195
+ testsTotal: parseInt(cols[6]) || 0,
196
+ testTimeMs: parseInt(cols[7]) || 0,
197
+ typecheckErrors: parseInt(cols[8]) || 0,
198
+ buildSuccess: cols[9] === "1",
199
+ coveragePct: parseFloat(cols[10]) || 0,
200
+ pkgSizeKb: parseInt(cols[11]) || 0,
201
+ };
202
+ });
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ }
@@ -0,0 +1,23 @@
1
+ export interface CleanupConfig {
2
+ cleanup: {
3
+ onNewPlan: "archive" | "delete" | "keep";
4
+ traceRetentionDays: number;
5
+ sessionRetentionDays: number;
6
+ };
7
+ }
8
+ export declare function readConfig(cwd?: string): Promise<CleanupConfig>;
9
+ export declare function writeConfig(config: CleanupConfig, cwd?: string): Promise<void>;
10
+ export interface CleanResult {
11
+ removed: string[];
12
+ archived: string[];
13
+ skipped: string[];
14
+ }
15
+ export declare function cleanTraces(cwd?: string): Promise<CleanResult>;
16
+ export declare function cleanResults(cwd?: string): Promise<CleanResult>;
17
+ export declare function cleanSessions(options?: {
18
+ cwd?: string;
19
+ before?: Date;
20
+ }): Promise<CleanResult>;
21
+ export declare function cleanAll(cwd?: string): Promise<CleanResult>;
22
+ export declare function archiveCurrentData(cwd?: string): Promise<CleanResult>;
23
+ export declare function applyOnNewPlanPolicy(cwd?: string): Promise<CleanResult>;
@@ -0,0 +1,144 @@
1
+ // core/cleanup.ts
2
+ // Data lifecycle management for .loophaus/ directory
3
+ import { readFile, writeFile, readdir, rm, rename, mkdir, stat } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ const DEFAULT_CONFIG = {
6
+ cleanup: {
7
+ onNewPlan: "keep",
8
+ traceRetentionDays: 30,
9
+ sessionRetentionDays: 7,
10
+ },
11
+ };
12
+ // Files that are NEVER deleted by any clean operation
13
+ const PROTECTED_FILES = new Set(["benchmark.tsv", "config.json"]);
14
+ function getLoophausDir(cwd) {
15
+ return join(cwd || process.cwd(), ".loophaus");
16
+ }
17
+ export async function readConfig(cwd) {
18
+ const configPath = join(getLoophausDir(cwd), "config.json");
19
+ try {
20
+ const raw = await readFile(configPath, "utf-8");
21
+ const parsed = JSON.parse(raw);
22
+ return {
23
+ cleanup: { ...DEFAULT_CONFIG.cleanup, ...parsed.cleanup },
24
+ };
25
+ }
26
+ catch {
27
+ return { ...DEFAULT_CONFIG };
28
+ }
29
+ }
30
+ export async function writeConfig(config, cwd) {
31
+ const dir = getLoophausDir(cwd);
32
+ await mkdir(dir, { recursive: true });
33
+ await writeFile(join(dir, "config.json"), JSON.stringify(config, null, 2), "utf-8");
34
+ }
35
+ export async function cleanTraces(cwd) {
36
+ const dir = getLoophausDir(cwd);
37
+ const result = { removed: [], archived: [], skipped: [] };
38
+ const tracePath = join(dir, "trace.jsonl");
39
+ try {
40
+ await rm(tracePath);
41
+ result.removed.push("trace.jsonl");
42
+ }
43
+ catch {
44
+ result.skipped.push("trace.jsonl");
45
+ }
46
+ return result;
47
+ }
48
+ export async function cleanResults(cwd) {
49
+ const dir = getLoophausDir(cwd);
50
+ const result = { removed: [], archived: [], skipped: [] };
51
+ const resultsPath = join(dir, "results.tsv");
52
+ try {
53
+ await rm(resultsPath);
54
+ result.removed.push("results.tsv");
55
+ }
56
+ catch {
57
+ result.skipped.push("results.tsv");
58
+ }
59
+ return result;
60
+ }
61
+ export async function cleanSessions(options) {
62
+ const dir = join(getLoophausDir(options?.cwd), "sessions");
63
+ const result = { removed: [], archived: [], skipped: [] };
64
+ try {
65
+ const files = await readdir(dir);
66
+ for (const file of files) {
67
+ if (!file.endsWith(".json"))
68
+ continue;
69
+ const filePath = join(dir, file);
70
+ if (options?.before) {
71
+ const s = await stat(filePath);
72
+ if (s.mtime >= options.before) {
73
+ result.skipped.push(file);
74
+ continue;
75
+ }
76
+ }
77
+ await rm(filePath);
78
+ result.removed.push(file);
79
+ }
80
+ }
81
+ catch {
82
+ // sessions/ doesn't exist
83
+ }
84
+ return result;
85
+ }
86
+ export async function cleanAll(cwd) {
87
+ const result = { removed: [], archived: [], skipped: [] };
88
+ const r1 = await cleanTraces(cwd);
89
+ const r2 = await cleanResults(cwd);
90
+ const r3 = await cleanSessions({ cwd });
91
+ result.removed.push(...r1.removed, ...r2.removed, ...r3.removed);
92
+ result.skipped.push(...r1.skipped, ...r2.skipped, ...r3.skipped);
93
+ // Explicitly note protected files
94
+ result.skipped.push("benchmark.tsv (protected)", "config.json (protected)");
95
+ return result;
96
+ }
97
+ export async function archiveCurrentData(cwd) {
98
+ const dir = getLoophausDir(cwd);
99
+ const result = { removed: [], archived: [], skipped: [] };
100
+ const dateStr = new Date().toISOString().split("T")[0];
101
+ const archiveDir = join(dir, "archive", dateStr);
102
+ await mkdir(archiveDir, { recursive: true });
103
+ const filesToArchive = ["trace.jsonl", "results.tsv"];
104
+ for (const file of filesToArchive) {
105
+ const src = join(dir, file);
106
+ const dest = join(archiveDir, file);
107
+ try {
108
+ await rename(src, dest);
109
+ result.archived.push(`${file} → archive/${dateStr}/${file}`);
110
+ }
111
+ catch {
112
+ result.skipped.push(file);
113
+ }
114
+ }
115
+ // Archive sessions
116
+ const sessionsDir = join(dir, "sessions");
117
+ try {
118
+ const files = await readdir(sessionsDir);
119
+ if (files.length > 0) {
120
+ const sessArchive = join(archiveDir, "sessions");
121
+ await mkdir(sessArchive, { recursive: true });
122
+ for (const file of files) {
123
+ await rename(join(sessionsDir, file), join(sessArchive, file));
124
+ result.archived.push(`sessions/${file}`);
125
+ }
126
+ }
127
+ }
128
+ catch {
129
+ // no sessions
130
+ }
131
+ return result;
132
+ }
133
+ export async function applyOnNewPlanPolicy(cwd) {
134
+ const config = await readConfig(cwd);
135
+ switch (config.cleanup.onNewPlan) {
136
+ case "archive":
137
+ return archiveCurrentData(cwd);
138
+ case "delete":
139
+ return cleanAll(cwd);
140
+ case "keep":
141
+ default:
142
+ return { removed: [], archived: [], skipped: ["policy: keep (no cleanup)"] };
143
+ }
144
+ }
@@ -30,4 +30,3 @@ export declare function estimateCost(model: string, inputTokens: number, outputT
30
30
  export declare function formatCost(cost: number): string;
31
31
  export declare function createTracker(): CostTracker;
32
32
  export { MODEL_PRICES };
33
- //# sourceMappingURL=cost-tracker.d.ts.map
@@ -38,4 +38,3 @@ export function createTracker() {
38
38
  };
39
39
  }
40
40
  export { MODEL_PRICES };
41
- //# sourceMappingURL=cost-tracker.js.map
@@ -1,4 +1,3 @@
1
1
  import type { LoopState, StopHookInput, StopHookResult } from "./types.js";
2
2
  export declare function evaluateStopHook(input: StopHookInput, state: LoopState): StopHookResult;
3
3
  export declare function extractPromise(text: string, promisePhrase: string): boolean;
4
- //# sourceMappingURL=engine.d.ts.map
@@ -106,4 +106,3 @@ export function extractPromise(text, promisePhrase) {
106
106
  function escapeRegex(str) {
107
107
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
108
  }
109
- //# sourceMappingURL=engine.js.map
@@ -2,4 +2,3 @@ import type { LoopEvent } from "./types.js";
2
2
  export declare function getTracePath(cwd?: string): string;
3
3
  export declare function logEvents(events: LoopEvent[], metadata?: Record<string, unknown>, cwd?: string): Promise<void>;
4
4
  export declare function readTrace(cwd?: string): Promise<Record<string, unknown>[]>;
5
- //# sourceMappingURL=event-logger.d.ts.map