self-bench 0.3.0 → 0.3.2

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 (66) hide show
  1. package/.dockerignore +10 -0
  2. package/Dockerfile +37 -0
  3. package/Dockerfile.sandbox +24 -0
  4. package/README.md +34 -26
  5. package/biome.json +18 -0
  6. package/bun.lock +1182 -0
  7. package/compose.yaml +85 -0
  8. package/dist/agent-smoke-main.js +1 -1
  9. package/dist/build-metadata.d.ts +2 -0
  10. package/dist/build-metadata.d.ts.map +1 -0
  11. package/dist/build-metadata.js +2 -0
  12. package/dist/build-metadata.js.map +1 -0
  13. package/dist/cli.js +18 -8
  14. package/dist/cli.js.map +1 -1
  15. package/dist/eval-main.js +1 -1
  16. package/dist/reaudit-main.js +1 -1
  17. package/dist/repair-main.js +1 -1
  18. package/dist/validate-main.js +1 -1
  19. package/docs/evaluations.md +67 -0
  20. package/docs/operations.md +178 -0
  21. package/docs/task-construction.md +94 -0
  22. package/package.json +28 -15
  23. package/scripts/verify-package.ts +57 -0
  24. package/scripts/write-build-metadata.ts +27 -0
  25. package/src/activities.ts +1236 -0
  26. package/src/agent-smoke-main.ts +63 -0
  27. package/src/agent-smoke.ts +132 -0
  28. package/src/api-main.ts +12 -0
  29. package/src/api.ts +239 -0
  30. package/src/artifacts.ts +361 -0
  31. package/src/audit.ts +106 -0
  32. package/src/build-metadata.ts +3 -0
  33. package/src/cli.ts +350 -0
  34. package/src/codex-review.ts +220 -0
  35. package/src/config.ts +117 -0
  36. package/src/contracts.ts +209 -0
  37. package/src/coupling.ts +259 -0
  38. package/src/docker-executor.ts +115 -0
  39. package/src/eval-main.ts +92 -0
  40. package/src/evaluate.ts +293 -0
  41. package/src/github.ts +26 -0
  42. package/src/harbor-results.ts +142 -0
  43. package/src/harbor-task.ts +528 -0
  44. package/src/hash.ts +5 -0
  45. package/src/modal-auth.ts +11 -0
  46. package/src/modal-executor.ts +176 -0
  47. package/src/parallel.ts +24 -0
  48. package/src/process.ts +165 -0
  49. package/src/provenance.ts +458 -0
  50. package/src/reaudit-main.ts +192 -0
  51. package/src/repair-main.ts +203 -0
  52. package/src/repair.ts +55 -0
  53. package/src/run-wait.ts +40 -0
  54. package/src/sandbox-author.ts +19 -0
  55. package/src/sandbox-repair.ts +160 -0
  56. package/src/sandbox-review.ts +17 -0
  57. package/src/sandbox-validation-repair.ts +174 -0
  58. package/src/sandbox.ts +51 -0
  59. package/src/subscription-auth.ts +67 -0
  60. package/src/temporal.ts +23 -0
  61. package/src/validate-main.ts +171 -0
  62. package/src/validation-repair.ts +94 -0
  63. package/src/worker-main.ts +32 -0
  64. package/src/workflow.ts +519 -0
  65. package/tsconfig.build.json +13 -0
  66. package/tsconfig.json +21 -0
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseArgs } from "node:util";
8
+ import { loadConfig } from "./config.js";
9
+ import { parallelMap } from "./parallel.js";
10
+ import { runCommand } from "./process.js";
11
+ import { createSandboxExecutor } from "./sandbox.js";
12
+ import { loadCodexSubscriptionAuth } from "./subscription-auth.js";
13
+
14
+ const parsed = parseArgs({
15
+ options: {
16
+ tasks: { type: "string" },
17
+ review: { type: "string" },
18
+ output: { type: "string" },
19
+ task: { type: "string" },
20
+ concurrency: { type: "string", default: "9" },
21
+ model: { type: "string", default: "gpt-5.6-sol" },
22
+ help: { type: "boolean", short: "h" },
23
+ },
24
+ strict: true,
25
+ });
26
+ if (parsed.values.help) {
27
+ console.log(`Repair coupled tests in expanded Harbor tasks.
28
+
29
+ Usage:
30
+ self-bench-repair --tasks DIRECTORY --review REPORT.json --output DIRECTORY [options]
31
+
32
+ Options:
33
+ --concurrency N Concurrent repair sandboxes (default: 9)
34
+ --model MODEL Codex subscription model (default: gpt-5.6-sol)
35
+ --task ID Repair only one coupled task
36
+ -h, --help Show this help`);
37
+ process.exit(0);
38
+ }
39
+
40
+ const tasksDirectory = resolve(parsed.values.tasks ?? fail("--tasks is required"));
41
+ const reviewPath = resolve(parsed.values.review ?? fail("--review is required"));
42
+ const outputDirectory = resolve(parsed.values.output ?? fail("--output is required"));
43
+ if (outputDirectory === tasksDirectory || outputDirectory.startsWith(`${tasksDirectory}/`)) {
44
+ throw new Error("--output must be outside --tasks");
45
+ }
46
+ const concurrency = positiveInteger(parsed.values.concurrency, "--concurrency");
47
+ const rawReview = JSON.parse(await readFile(reviewPath, "utf8")) as unknown;
48
+ const review = reviewReport(rawReview);
49
+ const coupled = new Map(
50
+ review.tasks
51
+ .filter((task) => task.resolution.verdict === "coupled")
52
+ .filter((task) => parsed.values.task === undefined || task.taskId === parsed.values.task)
53
+ .map((task) => [task.taskId, task]),
54
+ );
55
+ if (parsed.values.task !== undefined && coupled.size === 0) {
56
+ throw new Error(`coupled task not found in review: ${parsed.values.task}`);
57
+ }
58
+ const sourceTasks = (
59
+ await Promise.all(
60
+ (
61
+ await readdir(tasksDirectory)
62
+ ).map(async (taskId) => {
63
+ const directory = join(tasksDirectory, taskId);
64
+ return await access(join(directory, "instruction.md")).then(
65
+ () => directory,
66
+ () => undefined,
67
+ );
68
+ }),
69
+ )
70
+ ).filter((directory): directory is string => directory !== undefined);
71
+
72
+ await rm(outputDirectory, { recursive: true, force: true });
73
+ await mkdir(outputDirectory, { recursive: true });
74
+ for (const source of sourceTasks) {
75
+ if (parsed.values.task === undefined && !coupled.has(basename(source))) {
76
+ await cp(source, join(outputDirectory, basename(source)), { recursive: true });
77
+ }
78
+ }
79
+
80
+ const config = loadConfig();
81
+ const sandbox = createSandboxExecutor(config.execution);
82
+ const [authJson, repairer] = await Promise.all([
83
+ loadCodexSubscriptionAuth(),
84
+ readFile(join(assetRoot(), "dist/sandbox-repair.bundle.js")),
85
+ ]);
86
+ const results = await parallelMap([...coupled.values()], concurrency, async (taskReview) => {
87
+ const taskId = taskReview.taskId;
88
+ const source = join(tasksDirectory, taskId);
89
+ console.error(`repairing ${taskId}`);
90
+ const scratch = await mkdtemp(join(tmpdir(), `selfbench-repair-${taskId}-`));
91
+ try {
92
+ const archive = join(scratch, "task.tar.gz");
93
+ await runCommand("tar", ["-czf", archive, "-C", source, "."]);
94
+ const result = await sandbox.run({
95
+ runId: "selfbench-repair-v1",
96
+ stage: taskId,
97
+ timeoutMs: 2 * 60 * 60 * 1000,
98
+ cpu: 4,
99
+ memoryMiB: 8192,
100
+ files: [
101
+ { path: "/work/task.tar.gz", contents: await readFile(archive) },
102
+ { path: "/work/review.json", contents: `${JSON.stringify(taskReview, null, 2)}\n` },
103
+ { path: "/work/sandbox-repair.js", contents: repairer },
104
+ ],
105
+ outputPaths: ["/work/repaired-task.tar.gz", "/work/repair-report.json"],
106
+ secrets: { SELFBENCH_CODEX_AUTH_JSON: authJson },
107
+ environment: { SELFBENCH_REPAIR_MODEL: parsed.values.model ?? "gpt-5.6-sol" },
108
+ command: [
109
+ "node",
110
+ "/work/sandbox-repair.js",
111
+ "/work/task.tar.gz",
112
+ "/work/review.json",
113
+ "/work/repaired-task.tar.gz",
114
+ "/work/repair-report.json",
115
+ ],
116
+ });
117
+ const repaired = result.outputs["/work/repaired-task.tar.gz"];
118
+ const report = result.outputs["/work/repair-report.json"];
119
+ if (result.exitCode !== 0 || !repaired || !report) {
120
+ throw new Error(
121
+ `repair sandbox ${result.sandboxId} failed: ${result.stderr.trim() || result.stdout.trim()}`,
122
+ );
123
+ }
124
+ const destination = join(outputDirectory, taskId);
125
+ await mkdir(destination);
126
+ const repairedArchive = join(scratch, "repaired-task.tar.gz");
127
+ await writeFile(repairedArchive, repaired);
128
+ await runCommand("tar", ["-xzf", repairedArchive, "-C", destination]);
129
+ const parsedReport = JSON.parse(Buffer.from(report).toString("utf8")) as Record<
130
+ string,
131
+ unknown
132
+ >;
133
+ console.error(`${taskId}: repaired in ${result.sandboxId}`);
134
+ return { taskId, status: "repaired" as const, sandboxId: result.sandboxId, ...parsedReport };
135
+ } catch (error) {
136
+ const message = error instanceof Error ? error.message : String(error);
137
+ console.error(`${taskId}: error: ${message}`);
138
+ return { taskId, status: "error" as const, error: message };
139
+ } finally {
140
+ await rm(scratch, { recursive: true, force: true });
141
+ }
142
+ });
143
+ sandbox.close();
144
+ const reportOutput = {
145
+ schemaVersion: 1,
146
+ generatedAt: new Date().toISOString(),
147
+ sourceTasks: tasksDirectory,
148
+ sourceReview: reviewPath,
149
+ outputTasks: outputDirectory,
150
+ model: parsed.values.model,
151
+ copiedClean: parsed.values.task === undefined ? sourceTasks.length - coupled.size : 0,
152
+ repaired: results.filter((result) => result.status === "repaired").length,
153
+ errors: results.filter((result) => result.status === "error").length,
154
+ tasks: results,
155
+ };
156
+ await writeFile(
157
+ join(outputDirectory, "repair-report.json"),
158
+ `${JSON.stringify(reportOutput, null, 2)}\n`,
159
+ );
160
+ console.log(JSON.stringify(reportOutput, null, 2));
161
+
162
+ interface ReviewTask {
163
+ readonly taskId: string;
164
+ readonly resolution: { readonly verdict: "clean" | "coupled" };
165
+ readonly [key: string]: unknown;
166
+ }
167
+
168
+ function reviewReport(value: unknown): { readonly tasks: readonly ReviewTask[] } {
169
+ if (!isRecord(value) || !Array.isArray(value.tasks)) {
170
+ throw new Error("invalid coupling review report");
171
+ }
172
+ const tasks = value.tasks.filter(
173
+ (task): task is ReviewTask =>
174
+ isRecord(task) &&
175
+ typeof task.taskId === "string" &&
176
+ isRecord(task.resolution) &&
177
+ (task.resolution.verdict === "clean" || task.resolution.verdict === "coupled"),
178
+ );
179
+ if (tasks.length !== value.tasks.length) {
180
+ throw new Error("coupling review report contains invalid task entries");
181
+ }
182
+ return { tasks };
183
+ }
184
+
185
+ function assetRoot(): string {
186
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..");
187
+ }
188
+
189
+ function positiveInteger(value: string | undefined, label: string): number {
190
+ const number = Number(value);
191
+ if (!Number.isInteger(number) || number < 1) {
192
+ throw new Error(`${label} must be a positive integer`);
193
+ }
194
+ return number;
195
+ }
196
+
197
+ function isRecord(value: unknown): value is Record<string, unknown> {
198
+ return typeof value === "object" && value !== null && !Array.isArray(value);
199
+ }
200
+
201
+ function fail(message: string): never {
202
+ throw new Error(message);
203
+ }
package/src/repair.ts ADDED
@@ -0,0 +1,55 @@
1
+ export function patchPaths(patch: string): readonly string[] {
2
+ const paths = new Set<string>();
3
+ for (const line of patch.split("\n")) {
4
+ const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
5
+ if (match?.[2]) {
6
+ paths.add(match[2]);
7
+ }
8
+ }
9
+ return [...paths].sort();
10
+ }
11
+
12
+ export function assertRepairPaths(
13
+ originalTestPatch: string,
14
+ changedPaths: readonly string[],
15
+ ): void {
16
+ const allowed = new Set(patchPaths(originalTestPatch));
17
+ if (allowed.size === 0) {
18
+ throw new Error("original held-out test patch changes no files");
19
+ }
20
+ const outside = changedPaths.filter((path) => !allowed.has(path));
21
+ if (outside.length > 0) {
22
+ throw new Error(`repair changed files outside the held-out tests: ${outside.join(", ")}`);
23
+ }
24
+ }
25
+
26
+ export function repairPrompt(input: {
27
+ readonly taskId: string;
28
+ readonly authenticRequest: string;
29
+ readonly couplingReport: string;
30
+ readonly allowedPaths: readonly string[];
31
+ }): string {
32
+ return `Repair the held-out tests for SelfBench task ${input.taskId}.
33
+
34
+ The repository is the exact base snapshot with the current held-out test patch already applied. You may edit only these existing test files:
35
+
36
+ ${input.allowedPaths.map((path) => `- ${path}`).join("\n")}
37
+
38
+ The authentic engineer request is:
39
+
40
+ <authentic_request>
41
+ ${input.authenticRequest.trim()}
42
+ </authentic_request>
43
+
44
+ The independent coupling report is:
45
+
46
+ <coupling_report>
47
+ ${input.couplingReport.trim()}
48
+ </coupling_report>
49
+
50
+ Rewrite the tests so they verify every material requested behavior through stable public boundaries without requiring the gold patch's exact internal names, helper structure, error prose, mock input shape, response presentation, or incidental implementation choices. Preserve meaningful negative, authorization, compatibility, and regression coverage. Do not edit application code, the request, or the gold patch. Do not delete assertions merely to silence the report. A coherent alternative implementation must be able to pass, while the unchanged base must still fail the fail-to-pass tests and the gold implementation must still pass.
51
+
52
+ The final working tree must retain a non-empty test patch relative to HEAD and every originally added fail-to-pass test must still have an equivalent behavioral test. Do not reset, revert, delete, or commit the test patch. If an exact public response field is the only stable seam, prefer asserting the underlying externally visible behavior through existing endpoints, persistence, or follow-up actions. If no rigorous uncoupled test is possible within the allowed files, leave the current tests in place and explain the blocker in your final message instead of removing coverage.
53
+
54
+ Inspect /work/task/solution/gold.patch only to understand the intended behavior and available seams, never to copy its private structure into assertions. Inspect /work/task/tests/test.sh for the verifier command. Run focused tests when feasible. Finish only after the working tree contains the repaired test files and no other changes.`;
55
+ }
@@ -0,0 +1,40 @@
1
+ export interface PolledRunStatus {
2
+ readonly phase: string;
3
+ readonly error?: unknown;
4
+ readonly [key: string]: unknown;
5
+ }
6
+
7
+ export interface WaitForRunOptions {
8
+ readonly poll: () => Promise<PolledRunStatus>;
9
+ readonly onPhase?: (status: PolledRunStatus) => void;
10
+ readonly intervalMs?: number;
11
+ readonly delay?: (milliseconds: number) => Promise<void>;
12
+ }
13
+
14
+ const FAILED_PHASES = new Set(["blocked", "failed", "cancelled"]);
15
+
16
+ export async function waitForRun(options: WaitForRunOptions): Promise<PolledRunStatus> {
17
+ const intervalMs = options.intervalMs ?? 2_000;
18
+ const delay = options.delay ?? defaultDelay;
19
+ let previousPhase: string | undefined;
20
+
21
+ while (true) {
22
+ const status = await options.poll();
23
+ if (status.phase !== previousPhase) {
24
+ options.onPhase?.(status);
25
+ previousPhase = status.phase;
26
+ }
27
+ if (status.phase === "complete") {
28
+ return status;
29
+ }
30
+ if (FAILED_PHASES.has(status.phase)) {
31
+ const detail = typeof status.error === "string" ? `: ${status.error}` : "";
32
+ throw new Error(`SelfBench run ${status.phase}${detail}`);
33
+ }
34
+ await delay(intervalMs);
35
+ }
36
+ }
37
+
38
+ function defaultDelay(milliseconds: number): Promise<void> {
39
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
40
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readdir } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { compileHarborTask } from "./harbor-task.js";
6
+
7
+ const [tasksRoot, repositoryDirectory, outputDirectory] = process.argv.slice(2);
8
+ if (!tasksRoot || !repositoryDirectory || !outputDirectory) {
9
+ throw new Error("usage: sandbox-author TASKS_ROOT REPOSITORY OUTPUT");
10
+ }
11
+
12
+ const entries = await readdir(tasksRoot, { withFileTypes: true });
13
+ const taskDirectories = entries
14
+ .filter((entry) => entry.isDirectory())
15
+ .map((entry) => join(tasksRoot, entry.name));
16
+ if (taskDirectories.length !== 1) {
17
+ throw new Error(`authoring must produce exactly one task; found ${taskDirectories.length}`);
18
+ }
19
+ await compileHarborTask(taskDirectories[0] as string, repositoryDirectory, outputDirectory);
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { sha256 } from "./hash.js";
7
+ import { runCommand } from "./process.js";
8
+ import { assertRepairPaths, patchPaths, repairPrompt } from "./repair.js";
9
+
10
+ const [archivePath, reviewPath, outputArchive, outputReport] = process.argv.slice(2);
11
+ if (!archivePath || !reviewPath || !outputArchive || !outputReport) {
12
+ throw new Error("usage: sandbox-repair TASK.tar.gz REVIEW.json OUTPUT.tar.gz OUTPUT-REPORT.json");
13
+ }
14
+
15
+ const extractedDirectory = "/work/task";
16
+ const repositoryDirectory = "/work/repo";
17
+ await Promise.all([mkdir(extractedDirectory, { recursive: true }), mkdir(repositoryDirectory)]);
18
+ await runCommand("tar", ["-xzf", archivePath, "-C", extractedDirectory]);
19
+ const taskDirectory = await access(join(extractedDirectory, "instruction.md")).then(
20
+ () => extractedDirectory,
21
+ () => join(extractedDirectory, "harbor-task"),
22
+ );
23
+
24
+ const [instruction, originalPatch, review] = await Promise.all([
25
+ readFile(join(taskDirectory, "instruction.md"), "utf8"),
26
+ readFile(join(taskDirectory, "tests/test.patch"), "utf8"),
27
+ readFile(reviewPath, "utf8"),
28
+ ]);
29
+ const manifestPath = join(taskDirectory, ".selfbench-manifest.json");
30
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, unknown>;
31
+ const taskId = typeof manifest.taskId === "string" ? manifest.taskId : "unknown-task";
32
+ const allowedPaths = patchPaths(originalPatch);
33
+
34
+ await runCommand("tar", [
35
+ "-xzf",
36
+ join(taskDirectory, "tests/repo.tar.gz"),
37
+ "-C",
38
+ repositoryDirectory,
39
+ ]);
40
+ await runCommand("git", ["-C", repositoryDirectory, "init", "-q"]);
41
+ await runCommand("git", ["-C", repositoryDirectory, "config", "user.name", "SelfBench"]);
42
+ await runCommand("git", ["-C", repositoryDirectory, "config", "user.email", "selfbench@local"]);
43
+ await runCommand("git", ["-C", repositoryDirectory, "add", "-A"]);
44
+ await runCommand("git", ["-C", repositoryDirectory, "commit", "-qm", "base"]);
45
+ await runCommand("git", [
46
+ "-C",
47
+ repositoryDirectory,
48
+ "apply",
49
+ join(taskDirectory, "tests/test.patch"),
50
+ ]);
51
+ await runCommand("git", ["-C", repositoryDirectory, "add", "-N", "--all"]);
52
+
53
+ const codexHome = join(homedir(), ".codex");
54
+ const authPath = join(codexHome, "auth.json");
55
+ await mkdir(codexHome, { recursive: true });
56
+ await writeFile(
57
+ authPath,
58
+ process.env.SELFBENCH_CODEX_AUTH_JSON ?? fail("SELFBENCH_CODEX_AUTH_JSON is required"),
59
+ );
60
+ await chmod(authPath, 0o600);
61
+ const promptPath = join(tmpdir(), `selfbench-repair-${taskId}.md`);
62
+ await writeFile(
63
+ promptPath,
64
+ repairPrompt({
65
+ taskId,
66
+ authenticRequest: instruction,
67
+ couplingReport: review,
68
+ allowedPaths,
69
+ }),
70
+ );
71
+
72
+ const codex = await runCommand(
73
+ "codex",
74
+ [
75
+ "exec",
76
+ "--model",
77
+ process.env.SELFBENCH_REPAIR_MODEL ?? "gpt-5.6-sol",
78
+ "--dangerously-bypass-approvals-and-sandbox",
79
+ "--ephemeral",
80
+ "--ignore-user-config",
81
+ "--json",
82
+ "-C",
83
+ repositoryDirectory,
84
+ "-",
85
+ ],
86
+ {
87
+ allowFailure: true,
88
+ timeoutMs: 90 * 60 * 1000,
89
+ env: withoutApiKey(process.env),
90
+ input: await readFile(promptPath, "utf8"),
91
+ onOutput: (stream, chunk) => {
92
+ (stream === "stdout" ? process.stdout : process.stderr).write(chunk);
93
+ },
94
+ },
95
+ );
96
+ if (codex.exitCode !== 0) {
97
+ throw new Error(`Codex repair exited ${codex.exitCode}: ${codex.stderr.slice(-2_000)}`);
98
+ }
99
+
100
+ const [tracked, untracked] = await Promise.all([
101
+ runCommand("git", ["-C", repositoryDirectory, "diff", "--name-only", "HEAD"]),
102
+ runCommand("git", ["-C", repositoryDirectory, "ls-files", "--others", "--exclude-standard"]),
103
+ ]);
104
+ const changedPaths = [...tracked.stdout.split("\n"), ...untracked.stdout.split("\n")]
105
+ .filter(Boolean)
106
+ .sort();
107
+ assertRepairPaths(originalPatch, changedPaths);
108
+ const repaired = await runCommand("git", ["-C", repositoryDirectory, "diff", "--binary", "HEAD"]);
109
+ if (!repaired.stdout.startsWith("diff --git ")) {
110
+ throw new Error(
111
+ `repair produced no held-out test patch; status=${JSON.stringify(changedPaths)}; Codex tail=${codex.stdout.slice(-4_000)}`,
112
+ );
113
+ }
114
+ if (repaired.stdout === originalPatch) {
115
+ throw new Error("repair left the held-out test patch unchanged");
116
+ }
117
+
118
+ await writeFile(join(taskDirectory, "tests/test.patch"), repaired.stdout);
119
+ await writeFile(
120
+ manifestPath,
121
+ `${JSON.stringify(
122
+ {
123
+ ...manifest,
124
+ testPatchSha256: sha256(repaired.stdout),
125
+ repair: {
126
+ model: process.env.SELFBENCH_REPAIR_MODEL ?? "gpt-5.6-sol",
127
+ originalTestPatchSha256: sha256(originalPatch),
128
+ },
129
+ },
130
+ null,
131
+ 2,
132
+ )}\n`,
133
+ );
134
+ await runCommand("tar", ["-czf", outputArchive, "-C", extractedDirectory, "."]);
135
+ await writeFile(
136
+ outputReport,
137
+ `${JSON.stringify(
138
+ {
139
+ schemaVersion: 1,
140
+ taskId,
141
+ model: process.env.SELFBENCH_REPAIR_MODEL ?? "gpt-5.6-sol",
142
+ changedPaths,
143
+ originalTestPatchSha256: sha256(originalPatch),
144
+ repairedTestPatchSha256: sha256(repaired.stdout),
145
+ codexOutputTail: codex.stdout.slice(-4_000),
146
+ },
147
+ null,
148
+ 2,
149
+ )}\n`,
150
+ );
151
+
152
+ function withoutApiKey(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
153
+ const output = { ...environment };
154
+ delete output.OPENAI_API_KEY;
155
+ return output;
156
+ }
157
+
158
+ function fail(message: string): never {
159
+ throw new Error(message);
160
+ }
@@ -0,0 +1,17 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { reviewCouplingWithCodex } from "./codex-review.js";
3
+
4
+ const authJson = process.env.SELFBENCH_PI_AUTH_JSON;
5
+ const outputPath = process.env.SELFBENCH_REVIEW_OUTPUT;
6
+ if (!authJson) {
7
+ throw new Error("SELFBENCH_PI_AUTH_JSON is required");
8
+ }
9
+ if (!outputPath) {
10
+ throw new Error("SELFBENCH_REVIEW_OUTPUT is required");
11
+ }
12
+
13
+ const review = await reviewCouplingWithCodex({
14
+ authJson,
15
+ prompt: await readFile("/work/review-input.md", "utf8"),
16
+ });
17
+ await writeFile(outputPath, `${JSON.stringify(review, null, 2)}\n`, { flag: "wx" });
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { taskDefinitionSchema } from "./contracts.js";
7
+ import { runCommand } from "./process.js";
8
+ import {
9
+ assertValidationRepair,
10
+ validationRepairPaths,
11
+ validationRepairPrompt,
12
+ } from "./validation-repair.js";
13
+
14
+ const [archivePath, definitionPath, diagnosticsPath, outputDefinition, outputPatch, outputReport] =
15
+ process.argv.slice(2);
16
+ if (
17
+ !archivePath ||
18
+ !definitionPath ||
19
+ !diagnosticsPath ||
20
+ !outputDefinition ||
21
+ !outputPatch ||
22
+ !outputReport
23
+ ) {
24
+ throw new Error(
25
+ "usage: sandbox-validation-repair TASK.tar.gz DEFINITION.json DIAGNOSTICS.txt OUTPUT-DEFINITION.json OUTPUT-PATCH OUTPUT-REPORT.json",
26
+ );
27
+ }
28
+
29
+ const extractedDirectory = "/work/task";
30
+ const repositoryDirectory = "/work/repo";
31
+ await Promise.all([mkdir(extractedDirectory, { recursive: true }), mkdir(repositoryDirectory)]);
32
+ await runCommand("tar", ["-xzf", archivePath, "-C", extractedDirectory]);
33
+ const taskDirectory = await access(join(extractedDirectory, "instruction.md")).then(
34
+ () => extractedDirectory,
35
+ () => join(extractedDirectory, "harbor-task"),
36
+ );
37
+ const [instruction, originalPatch, diagnostics, originalDefinitionBytes] = await Promise.all([
38
+ readFile(join(taskDirectory, "instruction.md"), "utf8"),
39
+ readFile(join(taskDirectory, "tests/test.patch"), "utf8"),
40
+ readFile(diagnosticsPath, "utf8"),
41
+ readFile(definitionPath, "utf8"),
42
+ ]);
43
+ const originalDefinition = taskDefinitionSchema.parse(JSON.parse(originalDefinitionBytes));
44
+ const allowedPaths = validationRepairPaths(originalPatch);
45
+
46
+ await runCommand("tar", [
47
+ "-xzf",
48
+ join(taskDirectory, "tests/repo.tar.gz"),
49
+ "-C",
50
+ repositoryDirectory,
51
+ ]);
52
+ await runCommand("git", ["-C", repositoryDirectory, "init", "-q"]);
53
+ await runCommand("git", ["-C", repositoryDirectory, "config", "user.name", "SelfBench"]);
54
+ await runCommand("git", ["-C", repositoryDirectory, "config", "user.email", "selfbench@local"]);
55
+ await runCommand("git", ["-C", repositoryDirectory, "add", "-A"]);
56
+ await runCommand("git", ["-C", repositoryDirectory, "commit", "-qm", "base"]);
57
+ await runCommand("git", [
58
+ "-C",
59
+ repositoryDirectory,
60
+ "apply",
61
+ join(taskDirectory, "tests/test.patch"),
62
+ ]);
63
+ await runCommand("git", ["-C", repositoryDirectory, "add", "-N", "--all"]);
64
+ await Promise.all([
65
+ writeFile("/work/definition.json", `${JSON.stringify(originalDefinition, null, 2)}\n`),
66
+ writeFile("/work/gold.patch", await readFile(join(taskDirectory, "solution/gold.patch"))),
67
+ writeFile("/work/test.sh", await readFile(join(taskDirectory, "tests/test.sh"))),
68
+ ]);
69
+
70
+ const piHome = join(homedir(), ".pi/agent");
71
+ const piAuth = process.env.SELFBENCH_PI_AUTH_JSON ?? fail("SELFBENCH_PI_AUTH_JSON is required");
72
+ await mkdir(piHome, { recursive: true });
73
+ await Promise.all([
74
+ writeFile(join(piHome, "auth.json"), piAuth).then(() => chmod(join(piHome, "auth.json"), 0o600)),
75
+ writeFile(join(piHome, "settings.json"), `${JSON.stringify({ transport: "auto" })}\n`),
76
+ ]);
77
+ const promptPath = join(tmpdir(), `selfbench-validation-repair-${originalDefinition.taskId}.md`);
78
+ await writeFile(
79
+ promptPath,
80
+ validationRepairPrompt({
81
+ definition: originalDefinition,
82
+ authenticRequest: instruction,
83
+ diagnostics,
84
+ allowedPaths,
85
+ }),
86
+ );
87
+
88
+ const pi = await runCommand(
89
+ "pi",
90
+ [
91
+ "--print",
92
+ "--mode",
93
+ "json",
94
+ "--no-session",
95
+ "--no-approve",
96
+ "--no-skills",
97
+ "--no-prompt-templates",
98
+ "--no-context-files",
99
+ "--no-extensions",
100
+ "--provider",
101
+ "openai-codex",
102
+ "--model",
103
+ process.env.SELFBENCH_REPAIR_MODEL ?? "gpt-5.6-sol",
104
+ "--thinking",
105
+ "high",
106
+ "--tools",
107
+ "read,bash,grep,find,ls",
108
+ `@${promptPath}`,
109
+ ],
110
+ {
111
+ allowFailure: true,
112
+ timeoutMs: 90 * 60 * 1000,
113
+ cwd: repositoryDirectory,
114
+ env: withoutApiKey(process.env),
115
+ onOutput: (stream, chunk) => {
116
+ (stream === "stdout" ? process.stdout : process.stderr).write(chunk);
117
+ },
118
+ },
119
+ );
120
+ if (pi.exitCode !== 0) {
121
+ throw new Error(`Pi validation repair exited ${pi.exitCode}: ${pi.stderr.slice(-2_000)}`);
122
+ }
123
+
124
+ const repairedDefinition = taskDefinitionSchema.parse(
125
+ JSON.parse(await readFile("/work/definition.json", "utf8")),
126
+ );
127
+ const [tracked, untracked] = await Promise.all([
128
+ runCommand("git", ["-C", repositoryDirectory, "diff", "--name-only", "HEAD"]),
129
+ runCommand("git", ["-C", repositoryDirectory, "ls-files", "--others", "--exclude-standard"]),
130
+ ]);
131
+ const changedPaths = [...tracked.stdout.split("\n"), ...untracked.stdout.split("\n")]
132
+ .filter(Boolean)
133
+ .sort();
134
+ assertValidationRepair(originalDefinition, repairedDefinition, originalPatch, changedPaths);
135
+ const repaired = await runCommand("git", ["-C", repositoryDirectory, "diff", "--binary", "HEAD"]);
136
+ const repairedPatch = repaired.stdout.startsWith("diff --git ") ? repaired.stdout : originalPatch;
137
+ if (
138
+ repairedPatch === originalPatch &&
139
+ JSON.stringify(repairedDefinition) === JSON.stringify(originalDefinition)
140
+ ) {
141
+ throw new Error("validation repair left the task unchanged");
142
+ }
143
+
144
+ await Promise.all([
145
+ writeFile(outputDefinition, `${JSON.stringify(repairedDefinition, null, 2)}\n`),
146
+ writeFile(outputPatch, repairedPatch),
147
+ writeFile(
148
+ outputReport,
149
+ `${JSON.stringify(
150
+ {
151
+ schemaVersion: 1,
152
+ taskId: originalDefinition.taskId,
153
+ model: process.env.SELFBENCH_REPAIR_MODEL ?? "gpt-5.6-sol",
154
+ changedPaths,
155
+ definitionChanged:
156
+ JSON.stringify(repairedDefinition) !== JSON.stringify(originalDefinition),
157
+ testsChanged: repairedPatch !== originalPatch,
158
+ piOutputTail: pi.stdout.slice(-4_000),
159
+ },
160
+ null,
161
+ 2,
162
+ )}\n`,
163
+ ),
164
+ ]);
165
+
166
+ function withoutApiKey(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
167
+ const output = { ...environment };
168
+ delete output.OPENAI_API_KEY;
169
+ return output;
170
+ }
171
+
172
+ function fail(message: string): never {
173
+ throw new Error(message);
174
+ }