self-bench 0.3.4 → 0.3.6

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 (207) hide show
  1. package/README.md +16 -14
  2. package/biome.json +7 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +7 -7
  5. package/dist/api.js.map +1 -1
  6. package/dist/build-metadata.js +1 -1
  7. package/dist/cli.js +75 -15
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/contracts.d.ts +1 -0
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js +6 -5
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/extensions/discovery.d.ts.map +1 -1
  15. package/dist/extensions/discovery.js +12 -5
  16. package/dist/extensions/discovery.js.map +1 -1
  17. package/dist/harbor-task.d.ts.map +1 -1
  18. package/dist/harbor-task.js +3 -1
  19. package/dist/harbor-task.js.map +1 -1
  20. package/dist/project-paths.d.ts +2 -0
  21. package/dist/project-paths.d.ts.map +1 -0
  22. package/dist/project-paths.js +15 -0
  23. package/dist/project-paths.js.map +1 -0
  24. package/dist/provenance-associations.d.ts +49 -0
  25. package/dist/provenance-associations.d.ts.map +1 -0
  26. package/dist/provenance-associations.js +220 -0
  27. package/dist/provenance-associations.js.map +1 -0
  28. package/dist/provenance.d.ts +34 -13
  29. package/dist/provenance.d.ts.map +1 -1
  30. package/dist/provenance.js +81 -9
  31. package/dist/provenance.js.map +1 -1
  32. package/dist/reaudit-main.js +3 -3
  33. package/dist/reaudit-main.js.map +1 -1
  34. package/dist/repair-main.js +4 -4
  35. package/dist/repair-main.js.map +1 -1
  36. package/dist/{sandbox.d.ts → sandbox/contracts.d.ts} +5 -3
  37. package/dist/sandbox/contracts.d.ts.map +1 -0
  38. package/dist/sandbox/contracts.js +9 -0
  39. package/dist/sandbox/contracts.js.map +1 -0
  40. package/dist/sandbox/index.d.ts +5 -0
  41. package/dist/sandbox/index.d.ts.map +1 -0
  42. package/dist/{sandbox.js → sandbox/index.js} +6 -5
  43. package/dist/sandbox/index.js.map +1 -0
  44. package/dist/sandbox/programs/author.d.ts +3 -0
  45. package/dist/sandbox/programs/author.d.ts.map +1 -0
  46. package/dist/{sandbox-author.js → sandbox/programs/author.js} +2 -2
  47. package/dist/sandbox/programs/author.js.map +1 -0
  48. package/dist/sandbox/programs/prepare-repair.d.ts +7 -0
  49. package/dist/sandbox/programs/prepare-repair.d.ts.map +1 -0
  50. package/dist/sandbox/programs/prepare-repair.js +30 -0
  51. package/dist/sandbox/programs/prepare-repair.js.map +1 -0
  52. package/dist/sandbox/programs/repair.d.ts +3 -0
  53. package/dist/sandbox/programs/repair.d.ts.map +1 -0
  54. package/dist/{sandbox-repair.js → sandbox/programs/repair.js} +7 -28
  55. package/dist/sandbox/programs/repair.js.map +1 -0
  56. package/dist/sandbox/programs/review.d.ts +2 -0
  57. package/dist/sandbox/programs/review.d.ts.map +1 -0
  58. package/dist/{sandbox-review.js → sandbox/programs/review.js} +2 -2
  59. package/dist/sandbox/programs/review.js.map +1 -0
  60. package/dist/sandbox/programs/validation-repair.d.ts +3 -0
  61. package/dist/sandbox/programs/validation-repair.d.ts.map +1 -0
  62. package/dist/{sandbox-validation-repair.js → sandbox/programs/validation-repair.js} +7 -28
  63. package/dist/sandbox/programs/validation-repair.js.map +1 -0
  64. package/dist/{docker-executor.d.ts → sandbox/providers/docker/executor.d.ts} +3 -3
  65. package/dist/sandbox/providers/docker/executor.d.ts.map +1 -0
  66. package/dist/{docker-executor.js → sandbox/providers/docker/executor.js} +2 -2
  67. package/dist/sandbox/providers/docker/executor.js.map +1 -0
  68. package/dist/{modal-executor.d.ts → sandbox/providers/modal/executor.d.ts} +3 -3
  69. package/dist/sandbox/providers/modal/executor.d.ts.map +1 -0
  70. package/dist/{modal-executor.js → sandbox/providers/modal/executor.js} +50 -19
  71. package/dist/sandbox/providers/modal/executor.js.map +1 -0
  72. package/dist/{vercel-command.d.ts → sandbox/providers/vercel/command.d.ts} +3 -3
  73. package/dist/sandbox/providers/vercel/command.d.ts.map +1 -0
  74. package/dist/{vercel-command.js → sandbox/providers/vercel/command.js} +3 -3
  75. package/dist/sandbox/providers/vercel/command.js.map +1 -0
  76. package/dist/{vercel-executor.d.ts → sandbox/providers/vercel/executor.d.ts} +3 -3
  77. package/dist/sandbox/providers/vercel/executor.d.ts.map +1 -0
  78. package/dist/{vercel-executor.js → sandbox/providers/vercel/executor.js} +5 -5
  79. package/dist/sandbox/providers/vercel/executor.js.map +1 -0
  80. package/dist/{vercel-fetch.d.ts → sandbox/providers/vercel/fetch.d.ts} +1 -1
  81. package/dist/sandbox/providers/vercel/fetch.d.ts.map +1 -0
  82. package/dist/{vercel-fetch.js → sandbox/providers/vercel/fetch.js} +1 -1
  83. package/dist/sandbox/providers/vercel/fetch.js.map +1 -0
  84. package/dist/{sandbox-timeout.d.ts → sandbox/timeout.d.ts} +2 -2
  85. package/dist/sandbox/timeout.d.ts.map +1 -0
  86. package/dist/{sandbox-timeout.js → sandbox/timeout.js} +1 -1
  87. package/dist/sandbox/timeout.js.map +1 -0
  88. package/dist/sandbox-author.bundle.js +11 -8
  89. package/dist/sandbox-repair.bundle.js +43 -35
  90. package/dist/sandbox-review.bundle.js +2 -2
  91. package/dist/sandbox-validation-repair.bundle.js +50 -41
  92. package/dist/{vercel-cli.d.ts → setup/vercel/cli.d.ts} +2 -2
  93. package/dist/setup/vercel/cli.d.ts.map +1 -0
  94. package/dist/{vercel-cli.js → setup/vercel/cli.js} +2 -2
  95. package/dist/setup/vercel/cli.js.map +1 -0
  96. package/dist/setup/vercel/index.d.ts +4 -0
  97. package/dist/setup/vercel/index.d.ts.map +1 -0
  98. package/dist/setup/vercel/index.js +3 -0
  99. package/dist/setup/vercel/index.js.map +1 -0
  100. package/dist/{vercel-setup-probe.d.ts → setup/vercel/probe.d.ts} +2 -2
  101. package/dist/setup/vercel/probe.d.ts.map +1 -0
  102. package/dist/{vercel-setup-probe.js → setup/vercel/probe.js} +2 -2
  103. package/dist/setup/vercel/probe.js.map +1 -0
  104. package/dist/{vercel-profile.d.ts → setup/vercel/profile.d.ts} +1 -1
  105. package/dist/setup/vercel/profile.d.ts.map +1 -0
  106. package/dist/{vercel-profile.js → setup/vercel/profile.js} +2 -2
  107. package/dist/setup/vercel/profile.js.map +1 -0
  108. package/dist/{vercel-runtime-image.d.ts → setup/vercel/runtime-image.d.ts} +5 -5
  109. package/dist/setup/vercel/runtime-image.d.ts.map +1 -0
  110. package/dist/{vercel-runtime-image.js → setup/vercel/runtime-image.js} +2 -2
  111. package/dist/setup/vercel/runtime-image.js.map +1 -0
  112. package/dist/{vercel-setup.d.ts → setup/vercel/setup.d.ts} +8 -8
  113. package/dist/setup/vercel/setup.d.ts.map +1 -0
  114. package/dist/{vercel-setup.js → setup/vercel/setup.js} +8 -8
  115. package/dist/setup/vercel/setup.js.map +1 -0
  116. package/dist/temporal/activities.d.ts +12 -0
  117. package/dist/temporal/activities.d.ts.map +1 -1
  118. package/dist/temporal/activities.js +80 -33
  119. package/dist/temporal/activities.js.map +1 -1
  120. package/dist/temporal/workflow.d.ts +1 -1
  121. package/dist/temporal/workflow.d.ts.map +1 -1
  122. package/dist/temporal/workflow.js +90 -65
  123. package/dist/temporal/workflow.js.map +1 -1
  124. package/docs/operations.md +39 -5
  125. package/docs/task-construction.md +3 -1
  126. package/package.json +7 -5
  127. package/review/index.html +14 -0
  128. package/review/src/App.tsx +338 -0
  129. package/review/src/export-loader.test.ts +57 -0
  130. package/review/src/export-loader.ts +151 -0
  131. package/review/src/main.tsx +15 -0
  132. package/review/src/styles.css +162 -0
  133. package/review/src/types.ts +47 -0
  134. package/review/tsconfig.json +16 -0
  135. package/review/vite.config.ts +20 -0
  136. package/scripts/build-sandbox-programs.ts +22 -0
  137. package/scripts/verify-package.ts +52 -7
  138. package/scripts/write-build-metadata.ts +1 -1
  139. package/src/api.ts +7 -6
  140. package/src/cli.ts +99 -15
  141. package/src/config.ts +1 -1
  142. package/src/contracts.ts +7 -5
  143. package/src/extensions/discovery.ts +14 -5
  144. package/src/harbor-task.ts +6 -1
  145. package/src/project-paths.ts +15 -0
  146. package/src/provenance-associations.ts +311 -0
  147. package/src/provenance.ts +132 -28
  148. package/src/reaudit-main.ts +3 -3
  149. package/src/repair-main.ts +4 -4
  150. package/src/{sandbox.ts → sandbox/contracts.ts} +11 -20
  151. package/src/sandbox/index.ts +23 -0
  152. package/src/{sandbox-author.ts → sandbox/programs/author.ts} +1 -1
  153. package/src/sandbox/programs/prepare-repair.ts +44 -0
  154. package/src/{sandbox-repair.ts → sandbox/programs/repair.ts} +7 -31
  155. package/src/{sandbox-review.ts → sandbox/programs/review.ts} +1 -1
  156. package/src/{sandbox-validation-repair.ts → sandbox/programs/validation-repair.ts} +6 -30
  157. package/src/{docker-executor.ts → sandbox/providers/docker/executor.ts} +3 -3
  158. package/src/{modal-executor.ts → sandbox/providers/modal/executor.ts} +66 -25
  159. package/src/{vercel-command.ts → sandbox/providers/vercel/command.ts} +3 -3
  160. package/src/{vercel-executor.ts → sandbox/providers/vercel/executor.ts} +5 -5
  161. package/src/{sandbox-timeout.ts → sandbox/timeout.ts} +1 -1
  162. package/src/{vercel-cli.ts → setup/vercel/cli.ts} +1 -1
  163. package/src/setup/vercel/index.ts +3 -0
  164. package/src/{vercel-setup-probe.ts → setup/vercel/probe.ts} +5 -2
  165. package/src/{vercel-profile.ts → setup/vercel/profile.ts} +4 -1
  166. package/src/{vercel-runtime-image.ts → setup/vercel/runtime-image.ts} +5 -5
  167. package/src/{vercel-setup.ts → setup/vercel/setup.ts} +8 -8
  168. package/src/temporal/activities.ts +164 -63
  169. package/src/temporal/workflow.ts +120 -71
  170. package/tsconfig.json +1 -1
  171. package/dist/docker-executor.d.ts.map +0 -1
  172. package/dist/docker-executor.js.map +0 -1
  173. package/dist/modal-executor.d.ts.map +0 -1
  174. package/dist/modal-executor.js.map +0 -1
  175. package/dist/sandbox-author.d.ts +0 -3
  176. package/dist/sandbox-author.d.ts.map +0 -1
  177. package/dist/sandbox-author.js.map +0 -1
  178. package/dist/sandbox-repair.d.ts +0 -3
  179. package/dist/sandbox-repair.d.ts.map +0 -1
  180. package/dist/sandbox-repair.js.map +0 -1
  181. package/dist/sandbox-review.d.ts +0 -2
  182. package/dist/sandbox-review.d.ts.map +0 -1
  183. package/dist/sandbox-review.js.map +0 -1
  184. package/dist/sandbox-timeout.d.ts.map +0 -1
  185. package/dist/sandbox-timeout.js.map +0 -1
  186. package/dist/sandbox-validation-repair.d.ts +0 -3
  187. package/dist/sandbox-validation-repair.d.ts.map +0 -1
  188. package/dist/sandbox-validation-repair.js.map +0 -1
  189. package/dist/sandbox.d.ts.map +0 -1
  190. package/dist/sandbox.js.map +0 -1
  191. package/dist/vercel-cli.d.ts.map +0 -1
  192. package/dist/vercel-cli.js.map +0 -1
  193. package/dist/vercel-command.d.ts.map +0 -1
  194. package/dist/vercel-command.js.map +0 -1
  195. package/dist/vercel-executor.d.ts.map +0 -1
  196. package/dist/vercel-executor.js.map +0 -1
  197. package/dist/vercel-fetch.d.ts.map +0 -1
  198. package/dist/vercel-fetch.js.map +0 -1
  199. package/dist/vercel-profile.d.ts.map +0 -1
  200. package/dist/vercel-profile.js.map +0 -1
  201. package/dist/vercel-runtime-image.d.ts.map +0 -1
  202. package/dist/vercel-runtime-image.js.map +0 -1
  203. package/dist/vercel-setup-probe.d.ts.map +0 -1
  204. package/dist/vercel-setup-probe.js.map +0 -1
  205. package/dist/vercel-setup.d.ts.map +0 -1
  206. package/dist/vercel-setup.js.map +0 -1
  207. /package/src/{vercel-fetch.ts → sandbox/providers/vercel/fetch.ts} +0 -0
@@ -6,7 +6,7 @@ import { runCommand } from "./process.js";
6
6
  import { patchPaths } from "./repair.js";
7
7
 
8
8
  const HARBOR_SCHEMA_VERSION = "1.4";
9
- const COMPILER_REVISION = 23;
9
+ const COMPILER_REVISION = 24;
10
10
 
11
11
  export interface AuthoredTaskFiles {
12
12
  readonly definition: TaskDefinition;
@@ -70,6 +70,10 @@ export async function compileHarborTask(
70
70
  await Promise.all([
71
71
  cp(snapshot, join(environment, "repo.tar.gz")),
72
72
  cp(snapshot, join(tests, "repo.tar.gz")),
73
+ writeFile(
74
+ join(outputDirectory, "definition.json"),
75
+ `${JSON.stringify(task.definition, null, 2)}\n`,
76
+ ),
73
77
  writeFile(join(outputDirectory, "instruction.md"), `${task.definition.prompt.trim()}\n`),
74
78
  writeFile(join(solution, "gold.patch"), task.goldPatch),
75
79
  writeFile(join(solution, "solve.sh"), solutionScript()),
@@ -127,6 +131,7 @@ export async function refreshHarborTask(
127
131
  const preinstallGoldDependencies = dependencySetupPatch.length > 0;
128
132
  await writeFile(join(outputDirectory, "tests/test.sh"), testScript(definition, testPatch));
129
133
  await Promise.all([
134
+ writeFile(join(outputDirectory, "definition.json"), `${JSON.stringify(definition, null, 2)}\n`),
130
135
  writeFile(join(outputDirectory, "task.toml"), taskToml(definition)),
131
136
  writeFile(join(outputDirectory, "environment/Dockerfile"), agentDockerfile(definition)),
132
137
  writeFile(
@@ -0,0 +1,15 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, parse } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export function projectRoot(moduleUrl: string): string {
6
+ let directory = dirname(fileURLToPath(moduleUrl));
7
+ const root = parse(directory).root;
8
+ while (directory !== root) {
9
+ if (existsSync(join(directory, "package.json"))) {
10
+ return directory;
11
+ }
12
+ directory = dirname(directory);
13
+ }
14
+ throw new Error(`could not find package.json above ${fileURLToPath(moduleUrl)}`);
15
+ }
@@ -0,0 +1,311 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { assertPullRequestBelongsToRepository, githubRepository } from "./github.js";
4
+ import { sha256 } from "./hash.js";
5
+ import { runCommand } from "./process.js";
6
+ import type {
7
+ LocalSessionMetadata,
8
+ ProvenanceMessage,
9
+ SessionProvenanceFormat,
10
+ } from "./provenance.js";
11
+
12
+ const localSourceTypeSchema = z.enum(["pi", "claude-code", "codex"]);
13
+ type LocalSourceType = z.infer<typeof localSourceTypeSchema>;
14
+
15
+ const associationMessageSchema = z
16
+ .object({
17
+ sourceType: localSourceTypeSchema,
18
+ sessionId: z.string().min(1),
19
+ messageIndex: z.number().int().nonnegative(),
20
+ contentSha256: z.string().regex(/^[0-9a-f]{64}$/),
21
+ })
22
+ .strict();
23
+
24
+ export const provenanceAssociationManifestSchema = z
25
+ .object({
26
+ schemaVersion: z.literal(1),
27
+ repository: z.string().regex(/^[^/]+\/[^/]+$/),
28
+ sourcePr: z.number().int().positive(),
29
+ sourceUrl: z.string().url(),
30
+ messages: z.array(associationMessageSchema).min(1),
31
+ })
32
+ .strict();
33
+
34
+ export type ProvenanceAssociationManifest = z.infer<typeof provenanceAssociationManifestSchema>;
35
+
36
+ export interface AssociationSessionSummary {
37
+ readonly selector: string;
38
+ readonly sourceType: LocalSourceType;
39
+ readonly sessionId: string;
40
+ readonly messageCount: number;
41
+ readonly modifiedAt?: string;
42
+ readonly paths?: readonly string[];
43
+ }
44
+
45
+ export interface MergedPullRequest {
46
+ readonly sourcePr: number;
47
+ readonly sourceUrl: string;
48
+ }
49
+
50
+ export function associationSessionSummaries(
51
+ messages: readonly ProvenanceMessage[],
52
+ metadata: readonly LocalSessionMetadata[] = [],
53
+ ): AssociationSessionSummary[] {
54
+ const metadataBySelector = new Map<string, { modifiedAt: string; paths: Set<string> }>();
55
+ for (const session of metadata) {
56
+ if (!isLocalSourceType(session.sourceType)) {
57
+ continue;
58
+ }
59
+ const selector = sessionSelector(session.sourceType, session.sessionId);
60
+ const existing = metadataBySelector.get(selector);
61
+ metadataBySelector.set(selector, {
62
+ modifiedAt:
63
+ !existing || session.modifiedAt > existing.modifiedAt
64
+ ? session.modifiedAt
65
+ : existing.modifiedAt,
66
+ paths: new Set([...(existing?.paths ?? []), session.path]),
67
+ });
68
+ }
69
+
70
+ const counts = new Map<string, AssociationSessionSummary>();
71
+ for (const message of messages) {
72
+ if (!isLocalSourceType(message.sourceType)) {
73
+ continue;
74
+ }
75
+ const selector = sessionSelector(message.sourceType, message.sessionId);
76
+ const existing = counts.get(selector);
77
+ const sessionMetadata = metadataBySelector.get(selector);
78
+ counts.set(selector, {
79
+ selector,
80
+ sourceType: message.sourceType,
81
+ sessionId: message.sessionId,
82
+ messageCount: (existing?.messageCount ?? 0) + 1,
83
+ ...(sessionMetadata
84
+ ? {
85
+ modifiedAt: sessionMetadata.modifiedAt,
86
+ paths: [...sessionMetadata.paths].sort(),
87
+ }
88
+ : {}),
89
+ });
90
+ }
91
+ return [...counts.values()].sort(
92
+ (left, right) =>
93
+ (right.modifiedAt ?? "").localeCompare(left.modifiedAt ?? "") ||
94
+ left.selector.localeCompare(right.selector),
95
+ );
96
+ }
97
+
98
+ export function createProvenanceAssociationManifest(input: {
99
+ readonly repositoryUrl: string;
100
+ readonly pullRequest: MergedPullRequest;
101
+ readonly messages: readonly ProvenanceMessage[];
102
+ readonly sessionSelectors: readonly string[];
103
+ }): ProvenanceAssociationManifest {
104
+ const repository = githubRepository(input.repositoryUrl);
105
+ assertPullRequestBelongsToRepository(
106
+ input.repositoryUrl,
107
+ input.pullRequest.sourceUrl,
108
+ input.pullRequest.sourcePr,
109
+ );
110
+ if (input.sessionSelectors.length === 0) {
111
+ throw new Error("at least one --session is required to create an association manifest");
112
+ }
113
+
114
+ const selected = new Set(input.sessionSelectors.map(parseSessionSelector));
115
+ if (selected.size !== input.sessionSelectors.length) {
116
+ throw new Error("each --session selector must be unique");
117
+ }
118
+ const available = new Set(
119
+ associationSessionSummaries(input.messages).map(({ selector }) => selector),
120
+ );
121
+ for (const selector of selected) {
122
+ if (!available.has(selector)) {
123
+ throw new Error(`local session ${selector} was not found for this repository`);
124
+ }
125
+ }
126
+
127
+ const messageKeys = new Set<string>();
128
+ const messages = input.messages
129
+ .filter(
130
+ (message): message is ProvenanceMessage & { sourceType: LocalSourceType } =>
131
+ isLocalSourceType(message.sourceType) &&
132
+ selected.has(sessionSelector(message.sourceType, message.sessionId)),
133
+ )
134
+ .map((message) => {
135
+ const key = messageKey(message.sourceType, message.sessionId, message.messageIndex);
136
+ if (messageKeys.has(key)) {
137
+ throw new Error(`local provenance contains duplicate message identity ${key}`);
138
+ }
139
+ messageKeys.add(key);
140
+ return {
141
+ sourceType: message.sourceType,
142
+ sessionId: message.sessionId,
143
+ messageIndex: message.messageIndex,
144
+ contentSha256: sha256(message.content),
145
+ };
146
+ })
147
+ .sort(compareAssociationMessages);
148
+
149
+ return provenanceAssociationManifestSchema.parse({
150
+ schemaVersion: 1,
151
+ repository,
152
+ sourcePr: input.pullRequest.sourcePr,
153
+ sourceUrl: input.pullRequest.sourceUrl,
154
+ messages,
155
+ });
156
+ }
157
+
158
+ export async function writeProvenanceAssociationManifest(
159
+ path: string,
160
+ manifest: ProvenanceAssociationManifest,
161
+ ): Promise<void> {
162
+ await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx", mode: 0o600 });
163
+ }
164
+
165
+ export async function applyProvenanceAssociationManifests(
166
+ messages: readonly ProvenanceMessage[],
167
+ repositoryUrl: string,
168
+ paths: readonly string[],
169
+ ): Promise<ProvenanceMessage[]> {
170
+ if (paths.length === 0) {
171
+ return [...messages];
172
+ }
173
+ const repository = githubRepository(repositoryUrl);
174
+ const manifests = await Promise.all(
175
+ paths.map(async (path) => {
176
+ const value = JSON.parse(await readFile(path, "utf8"));
177
+ return { path, manifest: provenanceAssociationManifestSchema.parse(value) };
178
+ }),
179
+ );
180
+ const messageByKey = new Map<string, ProvenanceMessage>();
181
+ for (const message of messages) {
182
+ const key = messageKey(message.sourceType, message.sessionId, message.messageIndex);
183
+ if (messageByKey.has(key)) {
184
+ throw new Error(`local provenance contains duplicate message identity ${key}`);
185
+ }
186
+ messageByKey.set(key, message);
187
+ }
188
+
189
+ const associations = new Map<string, MergedPullRequest>();
190
+ for (const { path, manifest } of manifests) {
191
+ if (manifest.repository.toLowerCase() !== repository) {
192
+ throw new Error(
193
+ `association manifest ${path} belongs to ${manifest.repository}, not ${repository}`,
194
+ );
195
+ }
196
+ assertPullRequestBelongsToRepository(repositoryUrl, manifest.sourceUrl, manifest.sourcePr);
197
+ for (const selector of new Set(
198
+ manifest.messages.map((message) => sessionSelector(message.sourceType, message.sessionId)),
199
+ )) {
200
+ const expected = manifest.messages
201
+ .filter((message) => sessionSelector(message.sourceType, message.sessionId) === selector)
202
+ .sort(compareAssociationMessages);
203
+ const current = messages
204
+ .filter(
205
+ (message): message is ProvenanceMessage & { sourceType: LocalSourceType } =>
206
+ isLocalSourceType(message.sourceType) &&
207
+ sessionSelector(message.sourceType, message.sessionId) === selector,
208
+ )
209
+ .map((message) => ({
210
+ sourceType: message.sourceType,
211
+ sessionId: message.sessionId,
212
+ messageIndex: message.messageIndex,
213
+ contentSha256: sha256(message.content),
214
+ }))
215
+ .sort(compareAssociationMessages);
216
+ if (JSON.stringify(current) !== JSON.stringify(expected)) {
217
+ throw new Error(`association manifest ${path} does not match local session ${selector}`);
218
+ }
219
+ }
220
+ for (const reference of manifest.messages) {
221
+ const key = messageKey(reference.sourceType, reference.sessionId, reference.messageIndex);
222
+ const message = messageByKey.get(key);
223
+ if (!message || sha256(message.content) !== reference.contentSha256) {
224
+ throw new Error(`association manifest ${path} does not match local message ${key}`);
225
+ }
226
+ if (associations.has(key)) {
227
+ throw new Error(`local message ${key} is associated more than once`);
228
+ }
229
+ associations.set(key, {
230
+ sourcePr: manifest.sourcePr,
231
+ sourceUrl: manifest.sourceUrl,
232
+ });
233
+ }
234
+ }
235
+
236
+ return messages.map((message) => {
237
+ const association = associations.get(
238
+ messageKey(message.sourceType, message.sessionId, message.messageIndex),
239
+ );
240
+ return association ? { ...message, ...association } : message;
241
+ });
242
+ }
243
+
244
+ export async function resolveMergedPullRequest(
245
+ repositoryUrl: string,
246
+ sourcePr: number,
247
+ ): Promise<MergedPullRequest> {
248
+ const repository = githubRepository(repositoryUrl);
249
+ const result = await runCommand("gh", [
250
+ "pr",
251
+ "view",
252
+ String(sourcePr),
253
+ "--repo",
254
+ repository,
255
+ "--json",
256
+ "number,url,state,mergedAt",
257
+ ]);
258
+ const parsed = z
259
+ .object({
260
+ number: z.number().int().positive(),
261
+ url: z.string().url(),
262
+ state: z.string(),
263
+ mergedAt: z.string().nullable(),
264
+ })
265
+ .parse(JSON.parse(result.stdout));
266
+ if (parsed.number !== sourcePr) {
267
+ throw new Error(`GitHub returned pull request ${parsed.number}; expected ${sourcePr}`);
268
+ }
269
+ assertPullRequestBelongsToRepository(repositoryUrl, parsed.url, sourcePr);
270
+ if (parsed.state !== "MERGED" || !parsed.mergedAt) {
271
+ throw new Error(`pull request ${repository}#${sourcePr} is not merged`);
272
+ }
273
+ return { sourcePr, sourceUrl: parsed.url };
274
+ }
275
+
276
+ function parseSessionSelector(value: string): string {
277
+ const separator = value.indexOf(":");
278
+ if (separator < 1 || separator === value.length - 1) {
279
+ throw new Error(`invalid session selector ${JSON.stringify(value)}; expected TYPE:SESSION_ID`);
280
+ }
281
+ const sourceType = value.slice(0, separator);
282
+ localSourceTypeSchema.parse(sourceType);
283
+ return sessionSelector(sourceType as LocalSourceType, value.slice(separator + 1));
284
+ }
285
+
286
+ function sessionSelector(sourceType: LocalSourceType, sessionId: string): string {
287
+ return `${sourceType}:${sessionId}`;
288
+ }
289
+
290
+ function messageKey(
291
+ sourceType: SessionProvenanceFormat | "github-pull-request",
292
+ sessionId: string,
293
+ messageIndex: number,
294
+ ): string {
295
+ return `${sourceType}:${sessionId}:${messageIndex}`;
296
+ }
297
+
298
+ function compareAssociationMessages(
299
+ left: z.infer<typeof associationMessageSchema>,
300
+ right: z.infer<typeof associationMessageSchema>,
301
+ ): number {
302
+ return (
303
+ left.sourceType.localeCompare(right.sourceType) ||
304
+ left.sessionId.localeCompare(right.sessionId) ||
305
+ left.messageIndex - right.messageIndex
306
+ );
307
+ }
308
+
309
+ function isLocalSourceType(value: string): value is LocalSourceType {
310
+ return value === "pi" || value === "claude-code" || value === "codex";
311
+ }
package/src/provenance.ts CHANGED
@@ -1,23 +1,50 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
- import { basename, join } from "node:path";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { basename, join, relative } from "node:path";
3
+ import { z } from "zod";
3
4
  import { assertPullRequestBelongsToRepository, githubRepository } from "./github.js";
4
5
  import { runCommand } from "./process.js";
5
6
 
6
7
  export type SessionProvenanceFormat = "codex" | "claude-code" | "pi" | "generic";
7
8
 
8
- interface ProvenanceMessageBase {
9
+ const provenanceMessageBaseSchema = z.object({
10
+ sessionId: z.string().min(1),
11
+ messageIndex: z.number().int().nonnegative(),
12
+ content: z.string().min(1),
13
+ });
14
+
15
+ const localProvenanceMessageSchema = provenanceMessageBaseSchema
16
+ .extend({
17
+ sourceType: z.enum(["codex", "claude-code", "pi", "generic"]),
18
+ sourcePr: z.number().int().positive().optional(),
19
+ sourceUrl: z.string().url().optional(),
20
+ })
21
+ .refine(
22
+ (message) => (message.sourcePr === undefined) === (message.sourceUrl === undefined),
23
+ "sourcePr and sourceUrl must be supplied together",
24
+ );
25
+
26
+ export const provenanceMessageSchema = z.union([
27
+ localProvenanceMessageSchema,
28
+ provenanceMessageBaseSchema.extend({
29
+ sourceType: z.literal("github-pull-request"),
30
+ sourcePr: z.number().int().positive(),
31
+ sourceUrl: z.string().url(),
32
+ }),
33
+ ]);
34
+
35
+ export type ProvenanceMessage = z.infer<typeof provenanceMessageSchema>;
36
+
37
+ export interface LocalSessionMetadata {
38
+ readonly sourceType: SessionProvenanceFormat;
9
39
  readonly sessionId: string;
10
- readonly messageIndex: number;
11
- readonly content: string;
40
+ readonly path: string;
41
+ readonly modifiedAt: string;
12
42
  }
13
43
 
14
- export type ProvenanceMessage =
15
- | (ProvenanceMessageBase & { readonly sourceType: SessionProvenanceFormat })
16
- | (ProvenanceMessageBase & {
17
- readonly sourceType: "github-pull-request";
18
- readonly sourcePr: number;
19
- readonly sourceUrl: string;
20
- });
44
+ export interface RepositoryProvenanceCollection {
45
+ readonly messages: readonly ProvenanceMessage[];
46
+ readonly sessions: readonly LocalSessionMetadata[];
47
+ }
21
48
 
22
49
  interface JsonRecord {
23
50
  readonly [key: string]: unknown;
@@ -51,9 +78,19 @@ export async function collectRepositoryProvenance(
51
78
  repositoryPath: string,
52
79
  homeDirectory: string,
53
80
  ): Promise<ProvenanceMessage[]> {
81
+ return [
82
+ ...(await collectRepositoryProvenanceWithMetadata(repositoryPath, homeDirectory)).messages,
83
+ ];
84
+ }
85
+
86
+ export async function collectRepositoryProvenanceWithMetadata(
87
+ repositoryPath: string,
88
+ homeDirectory: string,
89
+ ): Promise<RepositoryProvenanceCollection> {
54
90
  const worktrees = await repositoryWorktrees(repositoryPath);
55
91
  const encodedWorktrees = worktrees.flatMap((path) => [path, encodeSessionPath(path)]);
56
92
  const messages: ProvenanceMessage[] = [];
93
+ const sessions: LocalSessionMetadata[] = [];
57
94
 
58
95
  for (const [format, relativeRoot] of SOURCE_ROOTS) {
59
96
  const root = join(homeDirectory, relativeRoot);
@@ -65,29 +102,54 @@ export async function collectRepositoryProvenance(
65
102
  ) {
66
103
  continue;
67
104
  }
68
- messages.push(...extractProvenanceMessages(raw, format, basename(path)));
105
+ const extracted = extractProvenanceMessages(raw, format, basename(path));
106
+ if (extracted.length === 0) {
107
+ continue;
108
+ }
109
+ messages.push(...extracted);
110
+ const file = await stat(path).catch(() => undefined);
111
+ if (file) {
112
+ const sessionIds = new Set(extracted.map((message) => message.sessionId));
113
+ for (const sessionId of sessionIds) {
114
+ sessions.push({
115
+ sourceType: format,
116
+ sessionId,
117
+ path: displaySessionPath(path, homeDirectory),
118
+ modifiedAt: file.mtime.toISOString(),
119
+ });
120
+ }
121
+ }
69
122
  }
70
123
  }
71
124
 
72
- return deduplicateMessages(messages);
125
+ return { messages: deduplicateMessages(messages), sessions };
73
126
  }
74
127
 
75
128
  export async function collectGitHubPullRequestProvenance(
76
129
  repositoryUrl: string,
130
+ token?: string,
131
+ signal?: AbortSignal,
77
132
  ): Promise<ProvenanceMessage[]> {
78
133
  const repository = githubRepository(repositoryUrl);
79
- const result = await runCommand("gh", [
80
- "pr",
81
- "list",
82
- "--repo",
83
- repository,
84
- "--state",
85
- "merged",
86
- "--limit",
87
- String(GITHUB_PULL_REQUEST_LIMIT),
88
- "--json",
89
- "number,title,body,url,author,isDraft,additions,deletions,changedFiles",
90
- ]);
134
+ const result = await runCommand(
135
+ "gh",
136
+ [
137
+ "pr",
138
+ "list",
139
+ "--repo",
140
+ repository,
141
+ "--state",
142
+ "merged",
143
+ "--limit",
144
+ String(GITHUB_PULL_REQUEST_LIMIT),
145
+ "--json",
146
+ "number,title,body,url,author,isDraft,additions,deletions,changedFiles",
147
+ ],
148
+ {
149
+ env: token ? { ...process.env, GH_TOKEN: token } : process.env,
150
+ ...(signal ? { signal } : {}),
151
+ },
152
+ );
91
153
  return extractGitHubPullRequestProvenance(result.stdout, repositoryUrl);
92
154
  }
93
155
 
@@ -130,19 +192,55 @@ export function extractGitHubPullRequestProvenance(
130
192
  return messages;
131
193
  }
132
194
 
195
+ export function combineRunProvenance(
196
+ repositoryUrl: string,
197
+ local: readonly ProvenanceMessage[],
198
+ github: readonly ProvenanceMessage[],
199
+ ): ProvenanceMessage[] {
200
+ const explicitlyAssociatedPrs = new Set<number>();
201
+ for (const message of local) {
202
+ if (message.sourcePr === undefined || message.sourceUrl === undefined) {
203
+ continue;
204
+ }
205
+ assertPullRequestBelongsToRepository(repositoryUrl, message.sourceUrl, message.sourcePr);
206
+ explicitlyAssociatedPrs.add(message.sourcePr);
207
+ }
208
+ return [
209
+ ...local,
210
+ ...github.filter(
211
+ (message) =>
212
+ message.sourceType !== "github-pull-request" ||
213
+ !explicitlyAssociatedPrs.has(message.sourcePr),
214
+ ),
215
+ ];
216
+ }
217
+
133
218
  export function assertProvenanceMatchesPullRequest(
134
219
  message: ProvenanceMessage,
135
220
  sourcePr: number,
136
221
  sourceUrl: string,
222
+ provenance: readonly ProvenanceMessage[] = [message],
137
223
  ): void {
138
224
  if (
139
- message.sourceType === "github-pull-request" &&
225
+ (message.sourcePr !== undefined || message.sourceUrl !== undefined) &&
140
226
  (message.sourcePr !== sourcePr || message.sourceUrl !== sourceUrl)
141
227
  ) {
142
228
  throw new Error(
143
229
  `pull request ${sourceUrl}#${sourcePr} does not match provenance ${message.sourceUrl}#${message.sourcePr}`,
144
230
  );
145
231
  }
232
+ const hasExplicitLocalAssociation = provenance.some(
233
+ (item) => item.sourceType !== "github-pull-request" && item.sourcePr === sourcePr,
234
+ );
235
+ if (
236
+ hasExplicitLocalAssociation &&
237
+ message.sourceType !== "github-pull-request" &&
238
+ (message.sourcePr !== sourcePr || message.sourceUrl !== sourceUrl)
239
+ ) {
240
+ throw new Error(
241
+ `pull request ${sourceUrl}#${sourcePr} has explicit local provenance; unbound local provenance cannot be selected`,
242
+ );
243
+ }
146
244
  }
147
245
 
148
246
  export function extractProvenanceMessages(
@@ -418,13 +516,19 @@ async function listJsonFiles(root: string): Promise<string[]> {
418
516
  const entries = await readdir(root, { recursive: true, withFileTypes: true }).catch(() => []);
419
517
  return entries
420
518
  .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
421
- .map((entry) => join(entry.parentPath, entry.name));
519
+ .map((entry) => join(entry.parentPath, entry.name))
520
+ .sort();
422
521
  }
423
522
 
424
523
  function encodeSessionPath(path: string): string {
425
524
  return path.replaceAll("/", "-");
426
525
  }
427
526
 
527
+ function displaySessionPath(path: string, homeDirectory: string): string {
528
+ const fromHome = relative(homeDirectory, path);
529
+ return fromHome && !fromHome.startsWith("..") ? `~/${fromHome}` : path;
530
+ }
531
+
428
532
  function deduplicateMessages(messages: readonly ProvenanceMessage[]): ProvenanceMessage[] {
429
533
  const seen = new Set<string>();
430
534
  return messages.filter((message) => {
@@ -3,7 +3,6 @@
3
3
  import { access, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
6
  import { parseArgs } from "node:util";
8
7
  import {
9
8
  COUPLING_REVIEW_MODEL,
@@ -19,7 +18,8 @@ import {
19
18
  } from "./coupling.js";
20
19
  import { parallelMap } from "./parallel.js";
21
20
  import { runCommand } from "./process.js";
22
- import { createSandboxExecutor, type SandboxExecutor } from "./sandbox.js";
21
+ import { projectRoot } from "./project-paths.js";
22
+ import { createSandboxExecutor, type SandboxExecutor } from "./sandbox/index.js";
23
23
  import { loadPiModelAuth } from "./subscription-auth.js";
24
24
 
25
25
  const parsed = parseArgs({
@@ -182,7 +182,7 @@ async function auditTask(
182
182
  }
183
183
 
184
184
  function assetRoot(): string {
185
- return resolve(dirname(fileURLToPath(import.meta.url)), "..");
185
+ return projectRoot(import.meta.url);
186
186
  }
187
187
 
188
188
  function positiveInteger(value: string | undefined, label: string): number {
@@ -2,13 +2,13 @@
2
2
 
3
3
  import { access, cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
- import { basename, dirname, join, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
5
+ import { basename, join, resolve } from "node:path";
7
6
  import { parseArgs } from "node:util";
8
7
  import { defaultStandaloneConcurrency, loadWorkerConfig } from "./config.js";
9
8
  import { parallelMap } from "./parallel.js";
10
9
  import { runCommand } from "./process.js";
11
- import { createSandboxExecutor } from "./sandbox.js";
10
+ import { projectRoot } from "./project-paths.js";
11
+ import { createSandboxExecutor } from "./sandbox/index.js";
12
12
  import { loadCodexModelAuth } from "./subscription-auth.js";
13
13
 
14
14
  const parsed = parseArgs({
@@ -189,7 +189,7 @@ function reviewReport(value: unknown): { readonly tasks: readonly ReviewTask[] }
189
189
  }
190
190
 
191
191
  function assetRoot(): string {
192
- return resolve(dirname(fileURLToPath(import.meta.url)), "..");
192
+ return projectRoot(import.meta.url);
193
193
  }
194
194
 
195
195
  function positiveInteger(value: string | undefined, label: string): number {
@@ -1,9 +1,3 @@
1
- import type { SelfBenchWorkerConfig } from "./config.js";
2
- import { DockerSandboxExecutor } from "./docker-executor.js";
3
- import { ModalSandboxExecutor } from "./modal-executor.js";
4
- import { TimeoutCappedSandboxExecutor } from "./sandbox-timeout.js";
5
- import { VercelSandboxExecutor } from "./vercel-executor.js";
6
-
7
1
  export interface SandboxFile {
8
2
  readonly path: string;
9
3
  readonly contents: Uint8Array | string;
@@ -41,21 +35,18 @@ export interface SandboxResult {
41
35
  readonly outputs: Readonly<Record<string, Uint8Array>>;
42
36
  }
43
37
 
38
+ export class SandboxExecutionError extends Error {
39
+ constructor(
40
+ message: string,
41
+ readonly result: SandboxResult,
42
+ options?: ErrorOptions,
43
+ ) {
44
+ super(message, options);
45
+ this.name = "SandboxExecutionError";
46
+ }
47
+ }
48
+
44
49
  export interface SandboxExecutor {
45
50
  run(request: SandboxRequest, options?: SandboxRunOptions): Promise<SandboxResult>;
46
51
  close(): void;
47
52
  }
48
-
49
- export function createSandboxExecutor(config: SelfBenchWorkerConfig["execution"]): SandboxExecutor {
50
- switch (config.kind) {
51
- case "docker":
52
- return new DockerSandboxExecutor(config);
53
- case "modal":
54
- return new ModalSandboxExecutor(config);
55
- case "vercel":
56
- return new TimeoutCappedSandboxExecutor(
57
- new VercelSandboxExecutor(config),
58
- config.timeoutCapMs,
59
- );
60
- }
61
- }