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,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from "node:util";
4
+ import { smokeAllAdapters } from "./agent-smoke.js";
5
+
6
+ const parsed = parseArgs({
7
+ options: {
8
+ task: { type: "string" },
9
+ jobs: { type: "string" },
10
+ harbor: { type: "string" },
11
+ environment: { type: "string", default: "modal" },
12
+ concurrency: { type: "string", default: "4" },
13
+ help: { type: "boolean", short: "h" },
14
+ },
15
+ strict: true,
16
+ });
17
+ if (parsed.values.help) {
18
+ console.log(`Attempt installation and instantiation of every pinned Harbor adapter.
19
+
20
+ Usage:
21
+ self-bench-agent-smoke --task DIRECTORY --jobs DIRECTORY [options]
22
+
23
+ Options:
24
+ --harbor PATH Harbor executable (default: harbor)
25
+ --environment docker|modal Execution environment (default: modal)
26
+ --concurrency N Concurrent adapter checks (default: 4)
27
+ -h, --help Show this help`);
28
+ process.exit(0);
29
+ }
30
+ const environment = parsed.values.environment;
31
+ if (environment !== "docker" && environment !== "modal") {
32
+ throw new Error("--environment must be docker or modal");
33
+ }
34
+ const results = await smokeAllAdapters({
35
+ taskDirectory: parsed.values.task ?? fail("--task is required"),
36
+ jobsDirectory: parsed.values.jobs ?? fail("--jobs is required"),
37
+ environment,
38
+ concurrency: positiveInteger(parsed.values.concurrency, "--concurrency"),
39
+ ...(parsed.values.harbor ? { harborPath: parsed.values.harbor } : {}),
40
+ });
41
+ console.log(
42
+ JSON.stringify(
43
+ {
44
+ adapters: results.length,
45
+ installed: results.filter((result) => result.installed).length,
46
+ failed: results.filter((result) => !result.installed).length,
47
+ },
48
+ null,
49
+ 2,
50
+ ),
51
+ );
52
+
53
+ function fail(message: string): never {
54
+ throw new Error(message);
55
+ }
56
+
57
+ function positiveInteger(value: string | undefined, label: string): number {
58
+ const parsed = Number(value);
59
+ if (!Number.isInteger(parsed) || parsed < 1) {
60
+ throw new Error(`${label} must be a positive integer`);
61
+ }
62
+ return parsed;
63
+ }
@@ -0,0 +1,132 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { parallelMap } from "./parallel.js";
4
+ import { runCommand } from "./process.js";
5
+
6
+ export const HARBOR_AGENT_ADAPTERS = [
7
+ "oracle",
8
+ "nop",
9
+ "acp",
10
+ "terminus-2",
11
+ "claude-code",
12
+ "copilot-cli",
13
+ "aider",
14
+ "cline-cli",
15
+ "codex",
16
+ "cortex-code",
17
+ "cursor-cli",
18
+ "gemini-cli",
19
+ "antigravity-cli",
20
+ "antigravity-sdk",
21
+ "rovodev-cli",
22
+ "goose",
23
+ "grok-build",
24
+ "hermes",
25
+ "kimi-code",
26
+ "kimi-cli",
27
+ "langgraph",
28
+ "deerflow",
29
+ "mini-swe-agent",
30
+ "nemo-agent",
31
+ "swe-agent",
32
+ "opencode",
33
+ "mimo",
34
+ "openclaw",
35
+ "openhands",
36
+ "openhands-sdk",
37
+ "pi",
38
+ "qwen-coder",
39
+ "devin",
40
+ "trae-agent",
41
+ "computer-1",
42
+ "eve",
43
+ "dspy-rlm",
44
+ "vibe",
45
+ ] as const;
46
+
47
+ export interface AdapterSmokeOptions {
48
+ readonly taskDirectory: string;
49
+ readonly jobsDirectory: string;
50
+ readonly harborPath?: string;
51
+ readonly environment?: "docker" | "modal";
52
+ readonly concurrency?: number;
53
+ }
54
+
55
+ export interface AdapterSmokeResult {
56
+ readonly agent: (typeof HARBOR_AGENT_ADAPTERS)[number];
57
+ readonly installed: boolean;
58
+ readonly exitCode: number;
59
+ readonly jobName: string;
60
+ readonly outputTail: string;
61
+ }
62
+
63
+ export async function smokeAllAdapters(
64
+ options: AdapterSmokeOptions,
65
+ ): Promise<readonly AdapterSmokeResult[]> {
66
+ const jobsDirectory = resolve(options.jobsDirectory);
67
+ const reportsDirectory = join(jobsDirectory, "adapter-smoke");
68
+ await mkdir(reportsDirectory, { recursive: true });
69
+ const results = await parallelMap(
70
+ HARBOR_AGENT_ADAPTERS,
71
+ options.concurrency ?? 4,
72
+ async (agent) => {
73
+ const reportPath = join(reportsDirectory, `${agent}.json`);
74
+ const existing = await readFile(reportPath, "utf8").catch(() => undefined);
75
+ if (existing) {
76
+ return JSON.parse(existing) as AdapterSmokeResult;
77
+ }
78
+ const jobName = `install-${agent}-${crypto.randomUUID().slice(0, 8)}`;
79
+ const result = await runCommand(
80
+ options.harborPath ?? "harbor",
81
+ [
82
+ "run",
83
+ "--path",
84
+ resolve(options.taskDirectory),
85
+ "--agent",
86
+ agent,
87
+ "--env",
88
+ options.environment ?? "modal",
89
+ "--job-name",
90
+ jobName,
91
+ "--jobs-dir",
92
+ jobsDirectory,
93
+ "--install-only",
94
+ "--n-concurrent",
95
+ "1",
96
+ "--max-retries",
97
+ "0",
98
+ "--delete",
99
+ "--yes",
100
+ "--quiet",
101
+ ],
102
+ { allowFailure: true, timeoutMs: 60 * 60 * 1000 },
103
+ );
104
+ const summary: AdapterSmokeResult = {
105
+ agent,
106
+ installed: result.exitCode === 0,
107
+ exitCode: result.exitCode,
108
+ jobName,
109
+ outputTail: `${result.stdout}\n${result.stderr}`.trim().slice(-2_000),
110
+ };
111
+ await writeFile(reportPath, `${JSON.stringify(summary, null, 2)}\n`, { flag: "wx" });
112
+ return summary;
113
+ },
114
+ );
115
+ await writeFile(
116
+ join(reportsDirectory, "summary.json"),
117
+ `${JSON.stringify(
118
+ {
119
+ schemaVersion: 1,
120
+ mode: "install-only",
121
+ environment: options.environment ?? "modal",
122
+ adapterCount: results.length,
123
+ installed: results.filter((result) => result.installed).length,
124
+ failed: results.filter((result) => !result.installed).length,
125
+ results,
126
+ },
127
+ null,
128
+ 2,
129
+ )}\n`,
130
+ );
131
+ return results;
132
+ }
@@ -0,0 +1,12 @@
1
+ import { startApi } from "./api.js";
2
+ import { loadConfig } from "./config.js";
3
+
4
+ const config = loadConfig();
5
+ const stop = await startApi(config);
6
+ console.log(`SelfBench API listening on http://${config.apiHost}:${config.apiPort}`);
7
+
8
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
9
+ process.once(signal, () => {
10
+ void stop().finally(() => process.exit(0));
11
+ });
12
+ }
package/src/api.ts ADDED
@@ -0,0 +1,239 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
3
+ import { pipeline } from "node:stream/promises";
4
+ import { Client } from "@temporalio/client";
5
+ import { z } from "zod";
6
+ import { createArtifactStore } from "./artifacts.js";
7
+ import type { SelfBenchConfig } from "./config.js";
8
+ import {
9
+ artifactRefSchema,
10
+ type RunPhase,
11
+ type RunStatus,
12
+ repositoryRefSchema,
13
+ runRequestSchema,
14
+ } from "./contracts.js";
15
+ import { connectTemporalClient } from "./temporal.js";
16
+ import { selfBenchRunWorkflow, statusQuery } from "./workflow.js";
17
+
18
+ const submissionSchema = z.object({
19
+ runId: z.string().regex(/^[a-z0-9][a-z0-9-]{2,62}$/),
20
+ repository: repositoryRefSchema,
21
+ provenance: artifactRefSchema,
22
+ candidateCounts: z.object({
23
+ easy: z.number().int().min(0).max(100),
24
+ medium: z.number().int().min(0).max(100),
25
+ hard: z.number().int().min(0).max(100),
26
+ }),
27
+ authoringModel: z.string().min(1).default("gpt-5.6-sol"),
28
+ selfbenchCommit: z.string().regex(/^[0-9a-f]{40}$/i),
29
+ });
30
+
31
+ export async function startApi(config: SelfBenchConfig): Promise<() => Promise<void>> {
32
+ const connection = await connectTemporalClient(config.temporal);
33
+ const client = new Client({ connection, namespace: config.temporal.namespace });
34
+ const artifacts = createArtifactStore(config.artifact);
35
+ const server = createServer(async (request, response) => {
36
+ try {
37
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
38
+ if (request.method === "GET" && url.pathname === "/healthz") {
39
+ sendJson(response, 200, { ok: true });
40
+ return;
41
+ }
42
+ if (!authorized(request, config.apiToken)) {
43
+ sendJson(response, 401, { error: "unauthorized" });
44
+ return;
45
+ }
46
+ if (request.method === "POST" && url.pathname === "/v1/provenance") {
47
+ const runId = z
48
+ .string()
49
+ .regex(/^[a-z0-9][a-z0-9-]{2,62}$/)
50
+ .parse(url.searchParams.get("runId"));
51
+ const body = await readBody(request, 100 * 1024 * 1024);
52
+ const reference = await artifacts.put(
53
+ `runs/${runId}/input/provenance.jsonl`,
54
+ body,
55
+ "application/x-ndjson",
56
+ );
57
+ sendJson(response, 201, reference);
58
+ return;
59
+ }
60
+ if (request.method === "POST" && url.pathname === "/v1/runs") {
61
+ const submission = submissionSchema.parse(
62
+ JSON.parse((await readBody(request)).toString("utf8")),
63
+ );
64
+ const workflowInput = runRequestSchema.parse({
65
+ runId: submission.runId,
66
+ repository: submission.repository,
67
+ provenance: submission.provenance,
68
+ candidateCounts: submission.candidateCounts,
69
+ authoring: {
70
+ provider: "openai-codex",
71
+ model: submission.authoringModel,
72
+ reasoningEffort: "high",
73
+ },
74
+ version: {
75
+ selfbenchCommit: config.buildCommit ?? submission.selfbenchCommit,
76
+ executionBackend: config.execution.kind,
77
+ sandboxImage: config.execution.image,
78
+ schema: 1,
79
+ },
80
+ });
81
+ await client.workflow.start(selfBenchRunWorkflow, {
82
+ workflowId: workflowInput.runId,
83
+ taskQueue: config.temporal.taskQueue,
84
+ args: [workflowInput],
85
+ workflowExecutionTimeout: "14 days",
86
+ });
87
+ sendJson(response, 202, { runId: workflowInput.runId });
88
+ return;
89
+ }
90
+ const runMatch = /^\/v1\/runs\/([a-z0-9][a-z0-9-]{2,62})(?:\/(cancel|export))?$/.exec(
91
+ url.pathname,
92
+ );
93
+ if (runMatch?.[1] && request.method === "GET" && runMatch[2] === "export") {
94
+ const status = await queryStatus(client.workflow.getHandle(runMatch[1]));
95
+ if (!("export" in status) || !status.export) {
96
+ sendJson(response, 409, { error: "run export is not ready" });
97
+ return;
98
+ }
99
+ const body = await artifacts.openRead(status.export);
100
+ response.writeHead(200, {
101
+ "content-type": status.export.contentType,
102
+ "content-length": status.export.sizeBytes,
103
+ "content-disposition": `attachment; filename="selfbench-${runMatch[1]}.tar.gz"`,
104
+ "x-content-sha256": status.export.sha256,
105
+ });
106
+ await pipeline(body, response);
107
+ return;
108
+ }
109
+ if (runMatch?.[1] && request.method === "GET" && !runMatch[2]) {
110
+ const handle = client.workflow.getHandle(runMatch[1]);
111
+ const status = await queryStatus(handle);
112
+ sendJson(response, 200, status);
113
+ return;
114
+ }
115
+ if (runMatch?.[1] && request.method === "POST" && runMatch[2] === "cancel") {
116
+ await client.workflow.getHandle(runMatch[1]).cancel();
117
+ sendJson(response, 202, { runId: runMatch[1], cancellationRequested: true });
118
+ return;
119
+ }
120
+ if (request.method === "GET" && url.pathname === "/v1/runs") {
121
+ const runs: unknown[] = [];
122
+ for await (const execution of client.workflow.list({
123
+ query: "WorkflowType = 'selfBenchRunWorkflow'",
124
+ })) {
125
+ runs.push({
126
+ runId: execution.workflowId,
127
+ status: execution.status.name,
128
+ startedAt: execution.startTime.toISOString(),
129
+ closedAt: execution.closeTime?.toISOString(),
130
+ });
131
+ }
132
+ sendJson(response, 200, runs);
133
+ return;
134
+ }
135
+ sendJson(response, 404, { error: "not found" });
136
+ } catch (error) {
137
+ if (response.headersSent) {
138
+ response.destroy(error instanceof Error ? error : new Error(String(error)));
139
+ return;
140
+ }
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ sendJson(response, error instanceof z.ZodError || error instanceof SyntaxError ? 400 : 500, {
143
+ error: message,
144
+ });
145
+ }
146
+ });
147
+
148
+ await new Promise<void>((resolve, reject) => {
149
+ server.once("error", reject);
150
+ server.listen(config.apiPort, config.apiHost, resolve);
151
+ });
152
+ return async () => {
153
+ await new Promise<void>((resolve, reject) =>
154
+ server.close((error) => (error ? reject(error) : resolve())),
155
+ );
156
+ await connection.close();
157
+ };
158
+ }
159
+
160
+ async function queryStatus(
161
+ handle: ReturnType<Client["workflow"]["getHandle"]>,
162
+ ): Promise<RunStatus | object> {
163
+ try {
164
+ const [status, description] = await Promise.all([handle.query(statusQuery), handle.describe()]);
165
+ if (description.status.name === "RUNNING" || terminalRunPhase(status.phase)) {
166
+ return status;
167
+ }
168
+ const phase = executionPhase(description.status.name);
169
+ return {
170
+ ...status,
171
+ phase,
172
+ ...(phase === "failed" && !status.error
173
+ ? { error: `Temporal workflow ${description.status.name.toLowerCase()}` }
174
+ : {}),
175
+ };
176
+ } catch {
177
+ const description = await handle.describe();
178
+ return {
179
+ runId: description.workflowId,
180
+ phase: executionPhase(description.status.name),
181
+ };
182
+ }
183
+ }
184
+
185
+ function terminalRunPhase(phase: RunPhase): boolean {
186
+ return ["complete", "blocked", "failed", "cancelled"].includes(phase);
187
+ }
188
+
189
+ function executionPhase(status: string): RunPhase {
190
+ switch (status) {
191
+ case "COMPLETED":
192
+ return "complete";
193
+ case "CANCELED":
194
+ return "cancelled";
195
+ case "RUNNING":
196
+ return "queued";
197
+ default:
198
+ return "failed";
199
+ }
200
+ }
201
+
202
+ async function readBody(request: IncomingMessage, limit = 10 * 1024 * 1024): Promise<Buffer> {
203
+ const chunks: Buffer[] = [];
204
+ let size = 0;
205
+ for await (const chunk of request) {
206
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
207
+ size += value.byteLength;
208
+ if (size > limit) {
209
+ throw new Error(`request body exceeds ${limit} bytes`);
210
+ }
211
+ chunks.push(value);
212
+ }
213
+ return Buffer.concat(chunks);
214
+ }
215
+
216
+ function authorized(request: IncomingMessage, token: string | undefined): boolean {
217
+ if (!token) {
218
+ return true;
219
+ }
220
+ const supplied = request.headers.authorization?.replace(/^Bearer\s+/i, "");
221
+ if (!supplied) {
222
+ return false;
223
+ }
224
+ const expectedBuffer = Buffer.from(token);
225
+ const suppliedBuffer = Buffer.from(supplied);
226
+ return (
227
+ expectedBuffer.length === suppliedBuffer.length &&
228
+ timingSafeEqual(expectedBuffer, suppliedBuffer)
229
+ );
230
+ }
231
+
232
+ function sendJson(response: ServerResponse, status: number, value: unknown): void {
233
+ const body = `${JSON.stringify(value, null, 2)}\n`;
234
+ response.writeHead(status, {
235
+ "content-type": "application/json; charset=utf-8",
236
+ "content-length": Buffer.byteLength(body),
237
+ });
238
+ response.end(body);
239
+ }