@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.
- package/commands/loop-plan.md +22 -0
- package/dist/bin/install.d.ts +0 -1
- package/dist/bin/install.js +0 -1
- package/dist/bin/loophaus.d.ts +0 -1
- package/dist/bin/loophaus.js +111 -2
- package/dist/bin/uninstall.d.ts +0 -1
- package/dist/bin/uninstall.js +0 -1
- package/dist/commands/loop-plan.md +22 -0
- package/dist/core/benchmark.d.ts +38 -0
- package/dist/core/benchmark.js +207 -0
- package/dist/core/cleanup.d.ts +23 -0
- package/dist/core/cleanup.js +144 -0
- package/dist/core/cost-tracker.d.ts +0 -1
- package/dist/core/cost-tracker.js +0 -1
- package/dist/core/engine.d.ts +0 -1
- package/dist/core/engine.js +0 -1
- package/dist/core/event-logger.d.ts +0 -1
- package/dist/core/event-logger.js +0 -1
- package/dist/core/events.d.ts +0 -1
- package/dist/core/events.js +0 -1
- package/dist/core/io-helpers.d.ts +0 -1
- package/dist/core/io-helpers.js +0 -1
- package/dist/core/loop-registry.d.ts +0 -1
- package/dist/core/loop-registry.js +0 -1
- package/dist/core/merge-strategy.d.ts +0 -1
- package/dist/core/merge-strategy.js +0 -1
- package/dist/core/parallel-runner.d.ts +0 -1
- package/dist/core/parallel-runner.js +0 -1
- package/dist/core/policy.d.ts +0 -1
- package/dist/core/policy.js +0 -1
- package/dist/core/quality-scorer.d.ts +0 -1
- package/dist/core/quality-scorer.js +0 -1
- package/dist/core/refine-loop.d.ts +0 -1
- package/dist/core/refine-loop.js +0 -1
- package/dist/core/session.d.ts +0 -1
- package/dist/core/session.js +0 -1
- package/dist/core/trace-analyzer.d.ts +0 -1
- package/dist/core/trace-analyzer.js +0 -1
- package/dist/core/types.d.ts +0 -1
- package/dist/core/types.js +0 -1
- package/dist/core/validate.d.ts +0 -1
- package/dist/core/validate.js +0 -1
- package/dist/core/worktree.d.ts +0 -1
- package/dist/core/worktree.js +0 -1
- package/dist/lib/paths.d.ts +0 -1
- package/dist/lib/paths.js +0 -1
- package/dist/lib/stop-hook-core.d.ts +0 -1
- package/dist/lib/stop-hook-core.js +0 -1
- package/dist/package.json +3 -1
- package/dist/store/state-store.d.ts +0 -1
- package/dist/store/state-store.js +0 -1
- package/package.json +3 -1
- package/dist/bin/install.d.ts.map +0 -1
- package/dist/bin/install.js.map +0 -1
- package/dist/bin/loophaus.d.ts.map +0 -1
- package/dist/bin/loophaus.js.map +0 -1
- package/dist/bin/uninstall.d.ts.map +0 -1
- package/dist/bin/uninstall.js.map +0 -1
- package/dist/core/cost-tracker.d.ts.map +0 -1
- package/dist/core/cost-tracker.js.map +0 -1
- package/dist/core/engine.d.ts.map +0 -1
- package/dist/core/engine.js.map +0 -1
- package/dist/core/event-logger.d.ts.map +0 -1
- package/dist/core/event-logger.js.map +0 -1
- package/dist/core/events.d.ts.map +0 -1
- package/dist/core/events.js.map +0 -1
- package/dist/core/io-helpers.d.ts.map +0 -1
- package/dist/core/io-helpers.js.map +0 -1
- package/dist/core/loop-registry.d.ts.map +0 -1
- package/dist/core/loop-registry.js.map +0 -1
- package/dist/core/merge-strategy.d.ts.map +0 -1
- package/dist/core/merge-strategy.js.map +0 -1
- package/dist/core/parallel-runner.d.ts.map +0 -1
- package/dist/core/parallel-runner.js.map +0 -1
- package/dist/core/policy.d.ts.map +0 -1
- package/dist/core/policy.js.map +0 -1
- package/dist/core/quality-scorer.d.ts.map +0 -1
- package/dist/core/quality-scorer.js.map +0 -1
- package/dist/core/refine-loop.d.ts.map +0 -1
- package/dist/core/refine-loop.js.map +0 -1
- package/dist/core/session.d.ts.map +0 -1
- package/dist/core/session.js.map +0 -1
- package/dist/core/trace-analyzer.d.ts.map +0 -1
- package/dist/core/trace-analyzer.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/validate.d.ts.map +0 -1
- package/dist/core/validate.js.map +0 -1
- package/dist/core/worktree.d.ts.map +0 -1
- package/dist/core/worktree.js.map +0 -1
- package/dist/lib/paths.d.ts.map +0 -1
- package/dist/lib/paths.js.map +0 -1
- package/dist/lib/stop-hook-core.d.ts.map +0 -1
- package/dist/lib/stop-hook-core.js.map +0 -1
- package/dist/store/state-store.d.ts.map +0 -1
- package/dist/store/state-store.js.map +0 -1
package/commands/loop-plan.md
CHANGED
|
@@ -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:
|
package/dist/bin/install.d.ts
CHANGED
package/dist/bin/install.js
CHANGED
package/dist/bin/loophaus.d.ts
CHANGED
package/dist/bin/loophaus.js
CHANGED
|
@@ -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
|
package/dist/bin/uninstall.d.ts
CHANGED
package/dist/bin/uninstall.js
CHANGED
|
@@ -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
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -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
|
package/dist/core/engine.js
CHANGED
|
@@ -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
|