@otto-code/brain 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/otto-brain +2 -0
- package/dist/bench/context-corpus.d.ts +58 -0
- package/dist/bench/context-corpus.js +210 -0
- package/dist/bench/corpus.d.ts +31 -0
- package/dist/bench/corpus.js +379 -0
- package/dist/bench/curated-repos.d.ts +35 -0
- package/dist/bench/curated-repos.js +41 -0
- package/dist/bench/health.d.ts +50 -0
- package/dist/bench/health.js +112 -0
- package/dist/bench/index.d.ts +103 -0
- package/dist/bench/index.js +209 -0
- package/dist/bench/mine.d.ts +32 -0
- package/dist/bench/mine.js +101 -0
- package/dist/bench/repo-task.d.ts +93 -0
- package/dist/bench/repo-task.js +380 -0
- package/dist/bench/repo.d.ts +125 -0
- package/dist/bench/repo.js +195 -0
- package/dist/bench/rescore.d.ts +41 -0
- package/dist/bench/rescore.js +127 -0
- package/dist/bench/tasks.d.ts +124 -0
- package/dist/bench/tasks.js +1249 -0
- package/dist/bench/verify.d.ts +142 -0
- package/dist/bench/verify.js +350 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +72 -0
- package/dist/commands/bench.d.ts +20 -0
- package/dist/commands/bench.js +222 -0
- package/dist/commands/calibrate.d.ts +19 -0
- package/dist/commands/calibrate.js +75 -0
- package/dist/commands/catalog.d.ts +32 -0
- package/dist/commands/catalog.js +48 -0
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +90 -0
- package/dist/commands/lifecycle.d.ts +29 -0
- package/dist/commands/lifecycle.js +194 -0
- package/dist/commands/pull.d.ts +22 -0
- package/dist/commands/pull.js +135 -0
- package/dist/commands/report.d.ts +10 -0
- package/dist/commands/report.js +34 -0
- package/dist/commands/rescore.d.ts +11 -0
- package/dist/commands/rescore.js +48 -0
- package/dist/commands/runtime.d.ts +21 -0
- package/dist/commands/runtime.js +62 -0
- package/dist/commands/scan.d.ts +24 -0
- package/dist/commands/scan.js +55 -0
- package/dist/commands/search.d.ts +31 -0
- package/dist/commands/search.js +135 -0
- package/dist/commands/share.d.ts +26 -0
- package/dist/commands/share.js +148 -0
- package/dist/commands/sweep.d.ts +21 -0
- package/dist/commands/sweep.js +69 -0
- package/dist/commands/ui.d.ts +9 -0
- package/dist/commands/ui.js +28 -0
- package/dist/config/env.d.ts +10 -0
- package/dist/config/env.js +48 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +9 -0
- package/dist/config/otto-home.d.ts +2 -0
- package/dist/config/otto-home.js +24 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/private-files.d.ts +3 -0
- package/dist/config/private-files.js +32 -0
- package/dist/config/profiles.d.ts +34 -0
- package/dist/config/profiles.js +116 -0
- package/dist/config/schema.d.ts +1035 -0
- package/dist/config/schema.js +191 -0
- package/dist/config/store.d.ts +11 -0
- package/dist/config/store.js +73 -0
- package/dist/gguf.d.ts +61 -0
- package/dist/gguf.js +231 -0
- package/dist/gpu.d.ts +12 -0
- package/dist/gpu.js +49 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/models/dirs.d.ts +9 -0
- package/dist/models/dirs.js +21 -0
- package/dist/models/download.d.ts +31 -0
- package/dist/models/download.js +105 -0
- package/dist/models/enrich.d.ts +36 -0
- package/dist/models/enrich.js +68 -0
- package/dist/models/hf.d.ts +62 -0
- package/dist/models/hf.js +114 -0
- package/dist/models/index.d.ts +15 -0
- package/dist/models/index.js +49 -0
- package/dist/models/manage.d.ts +27 -0
- package/dist/models/manage.js +112 -0
- package/dist/models/pick.d.ts +3 -0
- package/dist/models/pick.js +36 -0
- package/dist/models/scan.d.ts +13 -0
- package/dist/models/scan.js +185 -0
- package/dist/ops/archive.d.ts +34 -0
- package/dist/ops/archive.js +105 -0
- package/dist/ops/calibrate.d.ts +49 -0
- package/dist/ops/calibrate.js +74 -0
- package/dist/ops/report.d.ts +26 -0
- package/dist/ops/report.js +532 -0
- package/dist/ops/results.d.ts +207 -0
- package/dist/ops/results.js +250 -0
- package/dist/ops/sweep.d.ts +77 -0
- package/dist/ops/sweep.js +126 -0
- package/dist/output/index.d.ts +5 -0
- package/dist/output/index.js +5 -0
- package/dist/output/render.d.ts +4 -0
- package/dist/output/render.js +96 -0
- package/dist/output/types.d.ts +52 -0
- package/dist/output/types.js +9 -0
- package/dist/output/with-output.d.ts +3 -0
- package/dist/output/with-output.js +42 -0
- package/dist/run.d.ts +5 -0
- package/dist/run.js +19 -0
- package/dist/runtime/args.d.ts +21 -0
- package/dist/runtime/args.js +71 -0
- package/dist/runtime/index.d.ts +13 -0
- package/dist/runtime/index.js +41 -0
- package/dist/runtime/lmstudio.d.ts +10 -0
- package/dist/runtime/lmstudio.js +88 -0
- package/dist/runtime/managed.d.ts +26 -0
- package/dist/runtime/managed.js +148 -0
- package/dist/service/model-selector.d.ts +36 -0
- package/dist/service/model-selector.js +123 -0
- package/dist/service/pid-lock.d.ts +18 -0
- package/dist/service/pid-lock.js +50 -0
- package/dist/service/router.d.ts +144 -0
- package/dist/service/router.js +746 -0
- package/dist/service/scheduler.d.ts +74 -0
- package/dist/service/scheduler.js +110 -0
- package/dist/service/serve.d.ts +41 -0
- package/dist/service/serve.js +278 -0
- package/dist/service/supervisor.d.ts +67 -0
- package/dist/service/supervisor.js +222 -0
- package/dist/service/tailscale.d.ts +19 -0
- package/dist/service/tailscale.js +62 -0
- package/dist/service/tls.d.ts +77 -0
- package/dist/service/tls.js +192 -0
- package/dist/sysmon.d.ts +50 -0
- package/dist/sysmon.js +103 -0
- package/dist/tui/app.d.ts +208 -0
- package/dist/tui/app.js +1801 -0
- package/dist/tui/screen.d.ts +72 -0
- package/dist/tui/screen.js +176 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +7 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/dist/vram.d.ts +67 -0
- package/dist/vram.js +132 -0
- package/package.json +58 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Repo } from "./repo.js";
|
|
2
|
+
import type { TestResult } from "./repo.js";
|
|
3
|
+
import type { MinedTask } from "./mine.js";
|
|
4
|
+
import type { Task, Tool } from "./tasks.js";
|
|
5
|
+
/**
|
|
6
|
+
* Turn a mined commit into a real, runnable agentic-coding task.
|
|
7
|
+
*
|
|
8
|
+
* This is the SWE-bench-style oracle wired end to end: reset the working copy to
|
|
9
|
+
* the buggy parent, bring in ONLY the author's tests as the oracle (source stays
|
|
10
|
+
* broken), then hand the model real read/write/run tools against that working
|
|
11
|
+
* copy and score on whether the previously-failing tests actually pass afterward.
|
|
12
|
+
*
|
|
13
|
+
* Two properties are load-bearing:
|
|
14
|
+
* - Nothing here is simulated. `run_tests` builds and runs the workspace suite
|
|
15
|
+
* through {@link Repo}; the score is a delta between two real test runs, not a
|
|
16
|
+
* regex over the model's prose.
|
|
17
|
+
* - The oracle test files are read-only to the model. A write that lands on a
|
|
18
|
+
* test path is refused, so a model cannot "pass" by rewriting the assertions.
|
|
19
|
+
*
|
|
20
|
+
* The scorer ({@link scoreTestDeltas}) is a pure function of four id sets and is
|
|
21
|
+
* unit-tested directly; the process-spawning half (build/test/fs) lives only in
|
|
22
|
+
* the task's run() and is never exercised by tests.
|
|
23
|
+
*/
|
|
24
|
+
/** The tools a repo task exposes to the model, in OpenAI function shape. */
|
|
25
|
+
export declare const REPO_TOOLS: Tool[];
|
|
26
|
+
/**
|
|
27
|
+
* Oracle test outcomes from one {@link Repo.test} run, restricted to the oracle
|
|
28
|
+
* files. `importFailed` carries oracle test files that could not even be
|
|
29
|
+
* imported - a state distinct from "its assertions failed" and scored as such.
|
|
30
|
+
*/
|
|
31
|
+
export interface OracleTestRun {
|
|
32
|
+
passed: Set<string>;
|
|
33
|
+
failed: Set<string>;
|
|
34
|
+
importFailed: Set<string>;
|
|
35
|
+
}
|
|
36
|
+
/** The pass/fail delta between a baseline run and a post-edit run. */
|
|
37
|
+
export interface TestDelta {
|
|
38
|
+
score: number;
|
|
39
|
+
targets: number;
|
|
40
|
+
fixed: number;
|
|
41
|
+
regressions: number;
|
|
42
|
+
keptPassing: number;
|
|
43
|
+
fixedTests: string[];
|
|
44
|
+
regressedTests: string[];
|
|
45
|
+
degenerate: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Score a fix by the fraction of previously-failing oracle tests that now pass,
|
|
49
|
+
* discounted by any previously-passing tests it regressed.
|
|
50
|
+
*
|
|
51
|
+
* The target set is every oracle test that failed at baseline, plus - for oracle
|
|
52
|
+
* files that could not be imported at baseline - every test that surfaced in the
|
|
53
|
+
* file once it imported after the fix. That captures the common missing-symbol
|
|
54
|
+
* bug, where the buggy source has no assertions to fail because the test file
|
|
55
|
+
* never loaded. A file that newly fails to import after the fix counts its
|
|
56
|
+
* previously-passing tests as regressed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function scoreTestDeltas(before: OracleTestRun, after: OracleTestRun): TestDelta;
|
|
59
|
+
/** Restrict a {@link TestResult} to the oracle test files. */
|
|
60
|
+
export declare function collectOracleRun(result: TestResult, oracleFiles: Set<string>): OracleTestRun;
|
|
61
|
+
/** Everything a mined task needs beyond the mined record itself. */
|
|
62
|
+
export interface RepoTaskOptions {
|
|
63
|
+
repo: Repo;
|
|
64
|
+
weight?: number;
|
|
65
|
+
maxTurns?: number;
|
|
66
|
+
testTimeoutMs?: number;
|
|
67
|
+
maxFileChars?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Adapt one {@link MinedTask} into a runnable {@link Task}. The bound {@link Repo}
|
|
71
|
+
* is reused across tasks (only one model fits in VRAM, so tasks already
|
|
72
|
+
* serialise on a single working copy).
|
|
73
|
+
*/
|
|
74
|
+
export declare function makeRepoTask(mined: MinedTask, options: RepoTaskOptions): Task;
|
|
75
|
+
/** Options for {@link loadRepoTasks}. */
|
|
76
|
+
export interface LoadRepoTasksOptions {
|
|
77
|
+
dir: string;
|
|
78
|
+
workspace: string;
|
|
79
|
+
workspaceDir: string;
|
|
80
|
+
ref?: string;
|
|
81
|
+
limit?: number;
|
|
82
|
+
maxTasks?: number;
|
|
83
|
+
onlyFixes?: boolean;
|
|
84
|
+
maxTurns?: number;
|
|
85
|
+
weight?: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Mine a target repo and wrap the top mined commits as runnable tasks, all bound
|
|
89
|
+
* to a single reused {@link Repo}. Returns an empty array when nothing mines -
|
|
90
|
+
* the caller decides whether that is fatal.
|
|
91
|
+
*/
|
|
92
|
+
export declare function loadRepoTasks(options: LoadRepoTasksOptions): Promise<Task[]>;
|
|
93
|
+
//# sourceMappingURL=repo-task.d.ts.map
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Repo } from "./repo.js";
|
|
4
|
+
import { mine } from "./mine.js";
|
|
5
|
+
/**
|
|
6
|
+
* Turn a mined commit into a real, runnable agentic-coding task.
|
|
7
|
+
*
|
|
8
|
+
* This is the SWE-bench-style oracle wired end to end: reset the working copy to
|
|
9
|
+
* the buggy parent, bring in ONLY the author's tests as the oracle (source stays
|
|
10
|
+
* broken), then hand the model real read/write/run tools against that working
|
|
11
|
+
* copy and score on whether the previously-failing tests actually pass afterward.
|
|
12
|
+
*
|
|
13
|
+
* Two properties are load-bearing:
|
|
14
|
+
* - Nothing here is simulated. `run_tests` builds and runs the workspace suite
|
|
15
|
+
* through {@link Repo}; the score is a delta between two real test runs, not a
|
|
16
|
+
* regex over the model's prose.
|
|
17
|
+
* - The oracle test files are read-only to the model. A write that lands on a
|
|
18
|
+
* test path is refused, so a model cannot "pass" by rewriting the assertions.
|
|
19
|
+
*
|
|
20
|
+
* The scorer ({@link scoreTestDeltas}) is a pure function of four id sets and is
|
|
21
|
+
* unit-tested directly; the process-spawning half (build/test/fs) lives only in
|
|
22
|
+
* the task's run() and is never exercised by tests.
|
|
23
|
+
*/
|
|
24
|
+
/** The tools a repo task exposes to the model, in OpenAI function shape. */
|
|
25
|
+
export const REPO_TOOLS = [
|
|
26
|
+
{
|
|
27
|
+
type: "function",
|
|
28
|
+
function: {
|
|
29
|
+
name: "read_file",
|
|
30
|
+
description: "Read a source file from the repository working copy.",
|
|
31
|
+
parameters: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { path: { type: "string", description: "Repo-relative path" } },
|
|
34
|
+
required: ["path"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "function",
|
|
40
|
+
function: {
|
|
41
|
+
name: "write_file",
|
|
42
|
+
description: "Overwrite a source file with new contents. The oracle test files are read-only.",
|
|
43
|
+
parameters: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
path: { type: "string", description: "Repo-relative path" },
|
|
47
|
+
content: { type: "string", description: "Complete new file contents" },
|
|
48
|
+
},
|
|
49
|
+
required: ["path", "content"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "function",
|
|
55
|
+
function: {
|
|
56
|
+
name: "list_directory",
|
|
57
|
+
description: "List the entries of a directory in the repository working copy.",
|
|
58
|
+
parameters: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: { path: { type: "string", description: "Repo-relative path" } },
|
|
61
|
+
required: ["path"],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: "function",
|
|
67
|
+
function: {
|
|
68
|
+
name: "run_tests",
|
|
69
|
+
description: "Build the workspace and run the oracle test suite. Returns pass/fail counts and the names of failing tests.",
|
|
70
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
/** A test id is `${file}::${fullName}`; recover the file half. */
|
|
75
|
+
function fileOf(id) {
|
|
76
|
+
const idx = id.indexOf("::");
|
|
77
|
+
return idx === -1 ? id : id.slice(0, idx);
|
|
78
|
+
}
|
|
79
|
+
function clamp01(n) {
|
|
80
|
+
return Math.max(0, Math.min(1, n));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Score a fix by the fraction of previously-failing oracle tests that now pass,
|
|
84
|
+
* discounted by any previously-passing tests it regressed.
|
|
85
|
+
*
|
|
86
|
+
* The target set is every oracle test that failed at baseline, plus - for oracle
|
|
87
|
+
* files that could not be imported at baseline - every test that surfaced in the
|
|
88
|
+
* file once it imported after the fix. That captures the common missing-symbol
|
|
89
|
+
* bug, where the buggy source has no assertions to fail because the test file
|
|
90
|
+
* never loaded. A file that newly fails to import after the fix counts its
|
|
91
|
+
* previously-passing tests as regressed.
|
|
92
|
+
*/
|
|
93
|
+
export function scoreTestDeltas(before, after) {
|
|
94
|
+
const targets = new Set(before.failed);
|
|
95
|
+
if (before.importFailed.size) {
|
|
96
|
+
for (const id of [...after.passed, ...after.failed]) {
|
|
97
|
+
if (before.importFailed.has(fileOf(id)))
|
|
98
|
+
targets.add(id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const fixedTests = [...targets].filter((id) => after.passed.has(id));
|
|
102
|
+
const regressedTests = [...before.passed].filter((id) => after.failed.has(id) || after.importFailed.has(fileOf(id)));
|
|
103
|
+
const keptPassing = before.passed.size - regressedTests.length;
|
|
104
|
+
// Nothing failed at baseline means the oracle never reproduced the bug on the
|
|
105
|
+
// buggy parent - the task is degenerate and cannot credit a fix.
|
|
106
|
+
const degenerate = before.failed.size === 0 && before.importFailed.size === 0;
|
|
107
|
+
let score = 0;
|
|
108
|
+
if (!degenerate && targets.size > 0) {
|
|
109
|
+
const fixedFraction = fixedTests.length / targets.size;
|
|
110
|
+
const regressionGuard = before.passed.size ? keptPassing / before.passed.size : 1;
|
|
111
|
+
score = clamp01(fixedFraction * regressionGuard);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
score,
|
|
115
|
+
targets: targets.size,
|
|
116
|
+
fixed: fixedTests.length,
|
|
117
|
+
regressions: regressedTests.length,
|
|
118
|
+
keptPassing,
|
|
119
|
+
fixedTests,
|
|
120
|
+
regressedTests,
|
|
121
|
+
degenerate,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Restrict a {@link TestResult} to the oracle test files. */
|
|
125
|
+
export function collectOracleRun(result, oracleFiles) {
|
|
126
|
+
return {
|
|
127
|
+
passed: new Set([...result.passed].filter((id) => oracleFiles.has(fileOf(id)))),
|
|
128
|
+
failed: new Set([...result.failed].filter((id) => oracleFiles.has(fileOf(id)))),
|
|
129
|
+
importFailed: new Set(result.fileFailures.map((f) => f.file).filter((file) => oracleFiles.has(file))),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function normalizeRepoPath(p) {
|
|
133
|
+
return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
134
|
+
}
|
|
135
|
+
/** Resolve a model-supplied path inside the repo, refusing to escape it. */
|
|
136
|
+
function resolveInRepo(repoDir, p) {
|
|
137
|
+
const resolved = path.resolve(repoDir, p);
|
|
138
|
+
const rel = path.relative(repoDir, resolved);
|
|
139
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
|
|
140
|
+
return null;
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
/** Build the workspace, then run only the oracle test files. Build failure is
|
|
144
|
+
* non-fatal: vitest transpiles per file and still runs when a tsc build has type
|
|
145
|
+
* errors. Scoping to `oracleFiles` avoids running the whole workspace suite. */
|
|
146
|
+
async function runOracleTests(repo, timeoutMs, oracleFiles) {
|
|
147
|
+
await repo.build();
|
|
148
|
+
return repo.test({ timeout: timeoutMs, files: oracleFiles });
|
|
149
|
+
}
|
|
150
|
+
/** Compact one oracle run into a line for the model. */
|
|
151
|
+
function describeRun(run) {
|
|
152
|
+
const failing = [...run.failed];
|
|
153
|
+
const importFailed = [...run.importFailed];
|
|
154
|
+
const bits = [`${run.passed.size} passing, ${run.failed.size} failing`];
|
|
155
|
+
if (importFailed.length)
|
|
156
|
+
bits.push(`${importFailed.length} file(s) failed to import`);
|
|
157
|
+
const names = failing.slice(0, 12).map((id) => id.split("::").slice(1).join("::") || id);
|
|
158
|
+
if (names.length)
|
|
159
|
+
bits.push(`failing: ${names.join("; ")}`);
|
|
160
|
+
if (importFailed.length)
|
|
161
|
+
bits.push(`import-failed files: ${importFailed.join(", ")}`);
|
|
162
|
+
return bits.join(". ");
|
|
163
|
+
}
|
|
164
|
+
function parseArgs(call) {
|
|
165
|
+
let args = call.function?.arguments ?? call.input ?? {};
|
|
166
|
+
if (typeof args === "string") {
|
|
167
|
+
try {
|
|
168
|
+
args = JSON.parse(args);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
args = {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return args && typeof args === "object" ? args : {};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Adapt one {@link MinedTask} into a runnable {@link Task}. The bound {@link Repo}
|
|
178
|
+
* is reused across tasks (only one model fits in VRAM, so tasks already
|
|
179
|
+
* serialise on a single working copy).
|
|
180
|
+
*/
|
|
181
|
+
export function makeRepoTask(mined, options) {
|
|
182
|
+
const { repo } = options;
|
|
183
|
+
const weight = options.weight ?? 3;
|
|
184
|
+
const maxTurns = options.maxTurns ?? 12;
|
|
185
|
+
const testTimeoutMs = options.testTimeoutMs ?? 600000;
|
|
186
|
+
const maxFileChars = options.maxFileChars ?? 20000;
|
|
187
|
+
const oracleFiles = new Set(mined.testPaths.map(normalizeRepoPath));
|
|
188
|
+
return {
|
|
189
|
+
id: mined.id,
|
|
190
|
+
category: "SWE-bench repo",
|
|
191
|
+
weight,
|
|
192
|
+
description: `Make the mined oracle tests pass: ${mined.subject}`,
|
|
193
|
+
async run({ chat }) {
|
|
194
|
+
// 1. Buggy parent source + author's oracle tests.
|
|
195
|
+
await repo.reset(mined.parent);
|
|
196
|
+
await repo.checkoutPaths(mined.fix, mined.testPaths);
|
|
197
|
+
await repo.ensureDependencies();
|
|
198
|
+
const baselineResult = await runOracleTests(repo, testTimeoutMs, mined.testPaths);
|
|
199
|
+
const baseline = collectOracleRun(baselineResult, oracleFiles);
|
|
200
|
+
const preDelta = scoreTestDeltas(baseline, baseline);
|
|
201
|
+
if (preDelta.degenerate) {
|
|
202
|
+
return {
|
|
203
|
+
score: 0,
|
|
204
|
+
summary: `oracle did not reproduce on the buggy parent (${mined.id})`,
|
|
205
|
+
detail: { baseline: describeRun(baseline), mined: mined.id },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// 2. Real agentic repair loop against the working copy.
|
|
209
|
+
const messages = [
|
|
210
|
+
{
|
|
211
|
+
role: "user",
|
|
212
|
+
content: `A bug in this TypeScript repository is described by a failing test suite.\n\n` +
|
|
213
|
+
`Task: ${mined.subject}\n\n` +
|
|
214
|
+
`Oracle test file(s) (do NOT edit these): ${mined.testPaths.join(", ")}\n` +
|
|
215
|
+
`Source file(s) that likely need the fix: ${mined.sourcePaths.join(", ")}\n\n` +
|
|
216
|
+
`Current state - ${describeRun(baseline)}\n\n` +
|
|
217
|
+
`Fix the SOURCE so the failing tests pass. Use read_file and list_directory to ` +
|
|
218
|
+
`explore, write_file to save your fix, and run_tests to check progress. When the ` +
|
|
219
|
+
`oracle tests pass, stop.`,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
let turns = 0;
|
|
223
|
+
let ranTests = false;
|
|
224
|
+
let wrote = false;
|
|
225
|
+
let stalled = null;
|
|
226
|
+
while (turns < maxTurns) {
|
|
227
|
+
turns += 1;
|
|
228
|
+
const response = await chat({
|
|
229
|
+
messages,
|
|
230
|
+
tools: REPO_TOOLS,
|
|
231
|
+
max_tokens: 4096,
|
|
232
|
+
temperature: 0.3,
|
|
233
|
+
});
|
|
234
|
+
const message = response.choices?.[0]?.message || {};
|
|
235
|
+
const calls = message.tool_calls || [];
|
|
236
|
+
if (!calls.length) {
|
|
237
|
+
if (!message.content)
|
|
238
|
+
stalled = "produced neither content nor a tool call";
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
messages.push({
|
|
242
|
+
role: "assistant",
|
|
243
|
+
content: message.content || null,
|
|
244
|
+
tool_calls: message.tool_calls,
|
|
245
|
+
});
|
|
246
|
+
for (const call of calls) {
|
|
247
|
+
const name = call.function?.name || call.name;
|
|
248
|
+
const args = parseArgs(call);
|
|
249
|
+
let toolResult = "ok";
|
|
250
|
+
if (name === "read_file") {
|
|
251
|
+
const target = resolveInRepo(repo.dir, String(args.path ?? ""));
|
|
252
|
+
if (!target)
|
|
253
|
+
toolResult = "error: path is outside the repository";
|
|
254
|
+
else {
|
|
255
|
+
try {
|
|
256
|
+
toolResult = fs.readFileSync(target, "utf8").slice(0, maxFileChars);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
toolResult = `error: ${error instanceof Error ? error.message : String(error)}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (name === "write_file") {
|
|
264
|
+
const rel = normalizeRepoPath(String(args.path ?? ""));
|
|
265
|
+
const target = resolveInRepo(repo.dir, rel);
|
|
266
|
+
if (!target)
|
|
267
|
+
toolResult = "error: path is outside the repository";
|
|
268
|
+
else if (oracleFiles.has(rel))
|
|
269
|
+
toolResult = "error: the oracle test files are read-only";
|
|
270
|
+
else {
|
|
271
|
+
try {
|
|
272
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
273
|
+
fs.writeFileSync(target, String(args.content ?? ""), "utf8");
|
|
274
|
+
wrote = true;
|
|
275
|
+
toolResult = `wrote ${rel}`;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
toolResult = `error: ${error instanceof Error ? error.message : String(error)}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (name === "list_directory") {
|
|
283
|
+
const target = resolveInRepo(repo.dir, String(args.path ?? "."));
|
|
284
|
+
if (!target)
|
|
285
|
+
toolResult = "error: path is outside the repository";
|
|
286
|
+
else {
|
|
287
|
+
try {
|
|
288
|
+
toolResult =
|
|
289
|
+
fs
|
|
290
|
+
.readdirSync(target, { withFileTypes: true })
|
|
291
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
|
|
292
|
+
.join("\n") || "(empty)";
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
toolResult = `error: ${error instanceof Error ? error.message : String(error)}`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (name === "run_tests") {
|
|
300
|
+
ranTests = true;
|
|
301
|
+
const current = collectOracleRun(await runOracleTests(repo, testTimeoutMs, mined.testPaths), oracleFiles);
|
|
302
|
+
toolResult = describeRun(current);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
toolResult = `error: unknown tool "${String(name)}"`;
|
|
306
|
+
}
|
|
307
|
+
messages.push({
|
|
308
|
+
role: "tool",
|
|
309
|
+
tool_call_id: call.id || "call_0",
|
|
310
|
+
content: toolResult,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// 3. Authoritative final run and delta scoring.
|
|
315
|
+
const finalResult = await runOracleTests(repo, testTimeoutMs, mined.testPaths);
|
|
316
|
+
const after = collectOracleRun(finalResult, oracleFiles);
|
|
317
|
+
const delta = scoreTestDeltas(baseline, after);
|
|
318
|
+
const summaryBits = [`fixed ${delta.fixed}/${delta.targets} oracle tests`];
|
|
319
|
+
if (delta.regressions)
|
|
320
|
+
summaryBits.push(`${delta.regressions} regressions`);
|
|
321
|
+
if (!wrote)
|
|
322
|
+
summaryBits.push("no edit written");
|
|
323
|
+
if (stalled)
|
|
324
|
+
summaryBits.push(stalled);
|
|
325
|
+
if (!ranTests)
|
|
326
|
+
summaryBits.push("never ran tests");
|
|
327
|
+
return {
|
|
328
|
+
score: delta.score,
|
|
329
|
+
summary: summaryBits.join(", "),
|
|
330
|
+
detail: {
|
|
331
|
+
turns,
|
|
332
|
+
wrote,
|
|
333
|
+
ranTests,
|
|
334
|
+
stalled,
|
|
335
|
+
baseline: describeRun(baseline),
|
|
336
|
+
after: describeRun(after),
|
|
337
|
+
delta: {
|
|
338
|
+
targets: delta.targets,
|
|
339
|
+
fixed: delta.fixed,
|
|
340
|
+
regressions: delta.regressions,
|
|
341
|
+
fixedTests: delta.fixedTests.slice(0, 20),
|
|
342
|
+
regressedTests: delta.regressedTests.slice(0, 20),
|
|
343
|
+
},
|
|
344
|
+
mined: {
|
|
345
|
+
id: mined.id,
|
|
346
|
+
fix: mined.fix,
|
|
347
|
+
parent: mined.parent,
|
|
348
|
+
subject: mined.subject,
|
|
349
|
+
testPaths: mined.testPaths,
|
|
350
|
+
sourcePaths: mined.sourcePaths,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Mine a target repo and wrap the top mined commits as runnable tasks, all bound
|
|
359
|
+
* to a single reused {@link Repo}. Returns an empty array when nothing mines -
|
|
360
|
+
* the caller decides whether that is fatal.
|
|
361
|
+
*/
|
|
362
|
+
export async function loadRepoTasks(options) {
|
|
363
|
+
const mined = await mine({
|
|
364
|
+
dir: options.dir,
|
|
365
|
+
workspaceDir: options.workspaceDir,
|
|
366
|
+
limit: options.limit,
|
|
367
|
+
ref: options.ref,
|
|
368
|
+
});
|
|
369
|
+
const pool = options.onlyFixes ? mined.filter((m) => m.looksLikeFix) : mined;
|
|
370
|
+
const chosen = pool.slice(0, options.maxTasks ?? 5);
|
|
371
|
+
if (!chosen.length)
|
|
372
|
+
return [];
|
|
373
|
+
const repo = new Repo({
|
|
374
|
+
dir: options.dir,
|
|
375
|
+
workspace: options.workspace,
|
|
376
|
+
workspaceDir: options.workspaceDir,
|
|
377
|
+
});
|
|
378
|
+
return chosen.map((m) => makeRepoTask(m, { repo, maxTurns: options.maxTurns, weight: options.weight }));
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=repo-task.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Outcome of running a child process. */
|
|
2
|
+
export interface RunResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
code: number | string;
|
|
5
|
+
timedOut: boolean;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
}
|
|
9
|
+
interface RunOptions {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}
|
|
14
|
+
export declare function run(command: string, args: string[], { cwd, timeout, env }?: RunOptions): Promise<RunResult>;
|
|
15
|
+
interface RepoOptions {
|
|
16
|
+
dir: string;
|
|
17
|
+
workspace: string;
|
|
18
|
+
workspaceDir: string;
|
|
19
|
+
}
|
|
20
|
+
/** One failing test file that could not even be imported. */
|
|
21
|
+
export interface FileFailure {
|
|
22
|
+
file: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
/** Aggregate pass/fail counts from a vitest report. */
|
|
26
|
+
export interface VitestTotals {
|
|
27
|
+
total: number;
|
|
28
|
+
passed: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
}
|
|
31
|
+
/** Pass/fail sets parsed out of a vitest JSON report. */
|
|
32
|
+
export interface ParsedVitest {
|
|
33
|
+
passed: Set<string>;
|
|
34
|
+
failed: Set<string>;
|
|
35
|
+
fileFailures: FileFailure[];
|
|
36
|
+
totals: VitestTotals;
|
|
37
|
+
}
|
|
38
|
+
/** Result of running a workspace test suite. */
|
|
39
|
+
export interface TestResult {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
parsed: boolean;
|
|
42
|
+
passed: Set<string>;
|
|
43
|
+
failed: Set<string>;
|
|
44
|
+
fileFailures: FileFailure[];
|
|
45
|
+
totals?: VitestTotals;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
/** A single vitest assertion result. */
|
|
49
|
+
interface VitestAssertion {
|
|
50
|
+
fullName: string;
|
|
51
|
+
status: string;
|
|
52
|
+
}
|
|
53
|
+
/** A vitest suite (one test file) in the JSON report. */
|
|
54
|
+
interface VitestSuite {
|
|
55
|
+
name?: string;
|
|
56
|
+
status?: string;
|
|
57
|
+
message?: string;
|
|
58
|
+
assertionResults?: VitestAssertion[];
|
|
59
|
+
}
|
|
60
|
+
/** The subset of the vitest JSON report we read. */
|
|
61
|
+
interface VitestReport {
|
|
62
|
+
testResults?: VitestSuite[];
|
|
63
|
+
numTotalTests?: number;
|
|
64
|
+
numPassedTests?: number;
|
|
65
|
+
numFailedTests?: number;
|
|
66
|
+
}
|
|
67
|
+
export declare class Repo {
|
|
68
|
+
dir: string;
|
|
69
|
+
workspace: string;
|
|
70
|
+
workspaceDir: string;
|
|
71
|
+
lastLockHash: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* @param {object} options
|
|
74
|
+
* @param {string} options.dir working copy
|
|
75
|
+
* @param {string} options.workspace npm workspace name, e.g. @otto-code/protocol
|
|
76
|
+
* @param {string} options.workspaceDir its path relative to dir
|
|
77
|
+
*/
|
|
78
|
+
constructor({ dir, workspace, workspaceDir }: RepoOptions);
|
|
79
|
+
git(args: string[], options?: Partial<RunOptions>): Promise<RunResult>;
|
|
80
|
+
/** Discard all local modifications and untracked files, keeping ignored ones. */
|
|
81
|
+
reset(sha: string): Promise<boolean>;
|
|
82
|
+
/** Bring specific paths in from another commit - used to apply the oracle. */
|
|
83
|
+
checkoutPaths(sha: string, paths: string[]): Promise<boolean>;
|
|
84
|
+
headSha(): Promise<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Install only when the lockfile actually changed since the last install.
|
|
87
|
+
* Across neighbouring commits this is almost always a no-op.
|
|
88
|
+
*/
|
|
89
|
+
ensureDependencies(): Promise<{
|
|
90
|
+
installed: boolean;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Build the workspace. Required, not optional: some tests import through the
|
|
94
|
+
* package's own `exports` map into dist/, and fail at import time otherwise.
|
|
95
|
+
*/
|
|
96
|
+
build(): Promise<{
|
|
97
|
+
ok: boolean;
|
|
98
|
+
output: string;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* Run the workspace test suite and return per-test outcomes.
|
|
102
|
+
*
|
|
103
|
+
* `files` restricts the run to those test paths (passed to vitest as filters).
|
|
104
|
+
* The SWE-bench oracle only ever scores its own test files, so scoping to them
|
|
105
|
+
* avoids running the whole workspace suite - faster, and it keeps the harness
|
|
106
|
+
* within the "never run the full suite" rule.
|
|
107
|
+
*
|
|
108
|
+
* vitest's --outputFile needs a native path; a POSIX-style /tmp path silently
|
|
109
|
+
* produces no file on Windows.
|
|
110
|
+
*/
|
|
111
|
+
test({ timeout, files, }?: {
|
|
112
|
+
timeout?: number;
|
|
113
|
+
files?: string[];
|
|
114
|
+
}): Promise<TestResult>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Turn a vitest JSON report into pass/fail sets keyed by a stable test id.
|
|
118
|
+
*
|
|
119
|
+
* A file that fails to import has no assertion results at all - those are
|
|
120
|
+
* recorded separately, because "no tests ran" is not the same as "tests failed"
|
|
121
|
+
* and must not be mistaken for a passing baseline.
|
|
122
|
+
*/
|
|
123
|
+
export declare function parseVitest(report: VitestReport, repoDir: string): ParsedVitest;
|
|
124
|
+
export {};
|
|
125
|
+
//# sourceMappingURL=repo.d.ts.map
|