@vercel/factory 0.0.3 → 0.0.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.
Files changed (73) hide show
  1. package/dist/approval-h5aj8HYN.d.mts +510 -0
  2. package/dist/blob/index.d.mts +32 -0
  3. package/dist/blob/index.mjs +141 -0
  4. package/dist/blob/index.mjs.map +1 -0
  5. package/dist/connectors/index.d.mts +2 -0
  6. package/dist/connectors/index.mjs +49 -0
  7. package/dist/connectors/index.mjs.map +1 -0
  8. package/dist/connectors/slack.d.mts +43 -0
  9. package/dist/connectors/slack.mjs +50 -0
  10. package/dist/connectors/slack.mjs.map +1 -0
  11. package/dist/driver-Bbd25rNM.mjs +23 -0
  12. package/dist/driver-Bbd25rNM.mjs.map +1 -0
  13. package/dist/driver-CrZsy7sm.d.mts +34 -0
  14. package/dist/eve/index.d.mts +1101 -0
  15. package/dist/eve/index.mjs +853 -0
  16. package/dist/eve/index.mjs.map +1 -0
  17. package/dist/github-3sbHe-fo.d.mts +149 -0
  18. package/dist/github.d.mts +2 -0
  19. package/dist/github.mjs +332 -0
  20. package/dist/github.mjs.map +1 -0
  21. package/dist/id-DwCihQr4.mjs +31 -0
  22. package/dist/id-DwCihQr4.mjs.map +1 -0
  23. package/dist/index-WNGZJRRH.d.mts +67 -0
  24. package/dist/index.d.mts +187 -0
  25. package/dist/index.mjs +907 -0
  26. package/dist/index.mjs.map +1 -0
  27. package/dist/next-page.d.mts +5 -0
  28. package/dist/next-page.mjs +71 -0
  29. package/dist/next-page.mjs.map +1 -0
  30. package/dist/next.d.mts +12 -0
  31. package/dist/{next.js → next.mjs} +13 -14
  32. package/dist/next.mjs.map +1 -0
  33. package/dist/postgres/index.d.mts +137 -0
  34. package/dist/postgres/index.mjs +96 -0
  35. package/dist/postgres/index.mjs.map +1 -0
  36. package/dist/signal-CHVEmzvI.d.mts +59 -0
  37. package/dist/task-Cj7Ei9Nh.mjs +336 -0
  38. package/dist/task-Cj7Ei9Nh.mjs.map +1 -0
  39. package/package.json +29 -22
  40. package/dist/approval-Z7iaWibT.d.ts +0 -541
  41. package/dist/blob/index.d.ts +0 -26
  42. package/dist/blob/index.js +0 -127
  43. package/dist/blob/index.js.map +0 -1
  44. package/dist/chunk-2JFES6BA.js +0 -41
  45. package/dist/chunk-2JFES6BA.js.map +0 -1
  46. package/dist/chunk-CEF6VJZC.js +0 -28
  47. package/dist/chunk-CEF6VJZC.js.map +0 -1
  48. package/dist/chunk-D2XMABG4.js +0 -311
  49. package/dist/chunk-D2XMABG4.js.map +0 -1
  50. package/dist/chunk-EFL4PDDE.js +0 -325
  51. package/dist/chunk-EFL4PDDE.js.map +0 -1
  52. package/dist/common-CaHaEOAS.d.ts +0 -18
  53. package/dist/connectors/index.d.ts +0 -53
  54. package/dist/connectors/index.js +0 -64
  55. package/dist/connectors/index.js.map +0 -1
  56. package/dist/driver-tRNggCCJ.d.ts +0 -32
  57. package/dist/eve/index.d.ts +0 -1053
  58. package/dist/eve/index.js +0 -752
  59. package/dist/eve/index.js.map +0 -1
  60. package/dist/github.d.ts +0 -139
  61. package/dist/github.js +0 -27
  62. package/dist/github.js.map +0 -1
  63. package/dist/index.d.ts +0 -164
  64. package/dist/index.js +0 -940
  65. package/dist/index.js.map +0 -1
  66. package/dist/next-page.d.ts +0 -5
  67. package/dist/next-page.js +0 -75
  68. package/dist/next-page.js.map +0 -1
  69. package/dist/next.d.ts +0 -12
  70. package/dist/next.js.map +0 -1
  71. package/dist/postgres/index.d.ts +0 -137
  72. package/dist/postgres/index.js +0 -118
  73. package/dist/postgres/index.js.map +0 -1
@@ -0,0 +1,853 @@
1
+ import { i as repositoryIdSchema, o as signalIdSchema, p as vocabularySchema, s as taskIdSchema, t as changeIdSchema } from "../id-DwCihQr4.mjs";
2
+ import { S as findingFingerprint, b as requiresApproval, g as dispatchTask, p as levelSchema, s as taskQuestionSchema, u as changeEvidenceSchema, w as findingSchema, y as defaultApprovalPolicy } from "../task-Cj7Ei9Nh.mjs";
3
+ import { answerReviewComment, readPullRequestHead, readReviewComment } from "../github.mjs";
4
+ import { z } from "zod";
5
+ import { defineHook } from "eve/hooks";
6
+ import { defineTool } from "eve/tools";
7
+ //#region src/eve/conflict.ts
8
+ const gitShaSchema = z.string().regex(/^[0-9a-f]{40}$/);
9
+ const gitBranchSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/).refine((value) => !value.includes(".."));
10
+ const mergeConflictPayloadSchema = z.strictObject({
11
+ repository: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
12
+ prNumber: z.number().int().positive(),
13
+ prUrl: z.url(),
14
+ headBranch: gitBranchSchema.refine((value) => value.startsWith("factory/")),
15
+ headSha: gitShaSchema,
16
+ baseBranch: gitBranchSchema,
17
+ baseSha: gitShaSchema
18
+ });
19
+ function mergeConflictTools(stores, options) {
20
+ const repoPath = options.repoPath ?? "repo";
21
+ async function context(taskId) {
22
+ const task = await stores.tasks.get(taskId);
23
+ if (task === null) throw new Error(`task ${taskId} does not exist`);
24
+ if (task.kind !== "merge_conflict") throw new Error(`task ${taskId} is ${task.kind}, not merge_conflict`);
25
+ if (task.state !== "running") throw new Error(`task ${taskId} is ${task.state}, not running`);
26
+ if (task.origin.signalId === void 0) throw new Error(`task ${taskId} has no conflict signal`);
27
+ const signal = await stores.signals.get(task.origin.signalId);
28
+ if (signal === null || signal.source !== "github_merge_conflict") throw new Error(`task ${taskId} has no GitHub merge conflict signal`);
29
+ return {
30
+ task,
31
+ conflict: mergeConflictPayloadSchema.parse(signal.payload)
32
+ };
33
+ }
34
+ return {
35
+ fetch_merge_conflict: defineTool({
36
+ description: "Fetch the conflicted factory pull request and its current base, check out the PR head, and start the merge. Resolve every file returned in conflictedFiles, then test and commit the merge.",
37
+ inputSchema: z.object({ taskId: taskIdSchema }),
38
+ async execute({ taskId }, ctx) {
39
+ const { conflict } = await context(taskId);
40
+ const sandbox = await ctx.getSandbox();
41
+ const git = `git -C ${repoPath}`;
42
+ const headRef = `refs/remotes/origin/conflict-pr-${conflict.prNumber}`;
43
+ const baseRef = `refs/remotes/origin/conflict-base-${conflict.prNumber}`;
44
+ const fetch = await options.withCredential(sandbox, conflict.repository, `${git} fetch --depth 100 origin +refs/pull/${conflict.prNumber}/head:${headRef} +refs/heads/${conflict.baseBranch}:${baseRef}`);
45
+ if (typeof fetch.exitCode === "number" && fetch.exitCode !== 0) throw new Error(`could not fetch pull request: ${(fetch.stderr ?? "").trim()}`);
46
+ const [headSha, baseSha] = (await sandbox.run({ command: `${git} rev-parse ${headRef} && ${git} rev-parse ${baseRef}` })).stdout.trim().split("\n");
47
+ if (headSha !== conflict.headSha || baseSha !== conflict.baseSha) throw new Error("pull request or base changed while conflict resolution was starting; retry from a new task");
48
+ await sandbox.run({ command: `${git} checkout -B conflict-pr ${headRef}` });
49
+ const merge = await sandbox.run({ command: `${git} merge --no-commit --no-ff ${baseRef}` });
50
+ const conflictedFiles = (await sandbox.run({ command: `${git} diff --name-only --diff-filter=U` })).stdout.trim().split("\n").filter(Boolean);
51
+ if (conflictedFiles.length === 0) throw new Error(typeof merge.exitCode === "number" && merge.exitCode !== 0 ? `merge failed without file conflicts: ${(merge.stderr ?? "").trim()}` : "pull request no longer has merge conflicts");
52
+ return {
53
+ ...conflict,
54
+ conflictedFiles
55
+ };
56
+ }
57
+ }),
58
+ publish_merge_resolution: defineTool({
59
+ description: "Publish a tested, committed conflict resolution to the existing pull request branch. Fails if the remote branch changed since this task started.",
60
+ inputSchema: z.object({
61
+ taskId: taskIdSchema,
62
+ summary: z.string().min(1).max(2e3)
63
+ }),
64
+ async execute({ taskId, summary }, ctx) {
65
+ const { conflict } = await context(taskId);
66
+ const sandbox = await ctx.getSandbox();
67
+ const git = `git -C ${repoPath}`;
68
+ if ((await sandbox.run({ command: `${git} status --porcelain` })).stdout.trim() !== "") throw new Error("commit the conflict resolution before publishing");
69
+ if ((await sandbox.run({ command: `${git} diff --name-only --diff-filter=U` })).stdout.trim() !== "") throw new Error("merge conflicts remain unresolved");
70
+ const push = await options.withCredential(sandbox, conflict.repository, `${git} push --force-with-lease=refs/heads/${conflict.headBranch}:${conflict.headSha} origin HEAD:refs/heads/${conflict.headBranch}`);
71
+ if (typeof push.exitCode === "number" && push.exitCode !== 0) throw new Error(`could not update pull request branch: ${(push.stderr ?? "").trim()}`);
72
+ await stores.tasks.transition(taskId, "verifying", { reason: summary });
73
+ await stores.tasks.transition(taskId, "succeeded", { reason: summary });
74
+ return {
75
+ updated: true,
76
+ prNumber: conflict.prNumber,
77
+ prUrl: conflict.prUrl
78
+ };
79
+ }
80
+ })
81
+ };
82
+ }
83
+ //#endregion
84
+ //#region src/eve/finding.ts
85
+ function isLostRaceError(error) {
86
+ return error instanceof Error && (error.name === "VersionConflictError" || error.name === "InvalidTransitionError");
87
+ }
88
+ /**
89
+ * Turn one finding into a signal and a fix task. The fingerprint is the
90
+ * signal's delivery id, so the same problem reported twice is a no-op. The
91
+ * signal is accepted on the reporter's own verification (a human gate follows
92
+ * when the approval policy says so), and the task carries the approval
93
+ * decision from the moment it exists. Shared by record_finding and scanner
94
+ * tools that record on the agent's behalf.
95
+ */
96
+ async function recordFinding(stores, repositoryId, finding, options = {}) {
97
+ const source = options.source ?? "security_scan";
98
+ const policy = options.approval ?? defaultApprovalPolicy;
99
+ if (await stores.repositories.get(repositoryId) === null) return {
100
+ recorded: false,
101
+ reason: `repository ${repositoryId} does not exist`
102
+ };
103
+ const fingerprint = findingFingerprint(repositoryId, finding);
104
+ const signal = await stores.signals.create({
105
+ repositoryId,
106
+ source,
107
+ deliveryId: fingerprint,
108
+ replyTo: {
109
+ channel: "finding",
110
+ address: fingerprint
111
+ },
112
+ payload: finding
113
+ });
114
+ if (signal.outcome !== "pending") return {
115
+ recorded: false,
116
+ duplicate: true,
117
+ signalId: signal.id,
118
+ reason: `already recorded as ${signal.id}`
119
+ };
120
+ try {
121
+ await stores.signals.decide(signal.id, "accepted", {
122
+ detail: finding.priority,
123
+ reason: `${finding.tool ?? "scanner"} reported it, ${finding.verification.level} by ${finding.verification.method}`
124
+ });
125
+ } catch (error) {
126
+ if (isLostRaceError(error)) {
127
+ const current = await stores.signals.get(signal.id);
128
+ if (current !== null && current.outcome !== "pending") return {
129
+ recorded: false,
130
+ duplicate: true,
131
+ signalId: signal.id,
132
+ reason: `already recorded as ${signal.id}`
133
+ };
134
+ }
135
+ throw error;
136
+ }
137
+ const approval = requiresApproval(policy, {
138
+ source,
139
+ kind: "code_change",
140
+ priority: finding.priority
141
+ });
142
+ const task = await stores.tasks.create({
143
+ repositoryIds: [repositoryId],
144
+ kind: "code_change",
145
+ origin: { signalId: signal.id },
146
+ replyTo: signal.replyTo,
147
+ ...approval ? { approval: "required" } : {}
148
+ });
149
+ return {
150
+ recorded: true,
151
+ signalId: signal.id,
152
+ taskId: task.id,
153
+ approval: approval ? "required" : "not_required"
154
+ };
155
+ }
156
+ /**
157
+ * The scanner's outcome tool: turns one finding into a signal and a fix task
158
+ * in a single deterministic step. The finding's fingerprint is the signal's
159
+ * delivery id, so reporting the same problem twice is a no-op. The signal is
160
+ * accepted on the reporter's own verification (a human gate follows when the
161
+ * approval policy says so), and the task carries the approval decision from
162
+ * the moment it exists.
163
+ */
164
+ function recordFindingTool(stores, options = {}) {
165
+ return defineTool({
166
+ description: "Record one problem you found in the repository. Creates a fix task (which may wait for human approval) unless the same finding was already recorded. Report every finding separately; never batch.",
167
+ inputSchema: z.object({
168
+ repositoryId: repositoryIdSchema,
169
+ finding: findingSchema
170
+ }),
171
+ execute({ repositoryId, finding }) {
172
+ return recordFinding(stores, repositoryId, finding, options);
173
+ }
174
+ });
175
+ }
176
+ //#endregion
177
+ //#region src/eve/deepsec.ts
178
+ /** deepsec's exported finding shape, as far as we depend on it. */
179
+ const deepsecFindingSchema = z.looseObject({
180
+ title: z.string().min(1),
181
+ description: z.string().min(1),
182
+ severity: z.string().min(1),
183
+ metadata: z.looseObject({
184
+ filePath: z.string().min(1).optional(),
185
+ lineNumbers: z.array(z.number().int().min(1)).optional(),
186
+ vulnSlug: z.string().min(1).optional(),
187
+ confidence: z.string().optional(),
188
+ runId: z.string().optional()
189
+ }).optional()
190
+ });
191
+ const deepsecExportSchema = z.union([z.array(deepsecFindingSchema), z.looseObject({ findings: z.array(deepsecFindingSchema) }).transform((value) => value.findings)]);
192
+ const priorityBySeverity = {
193
+ CRITICAL: "P0",
194
+ HIGH: "P1",
195
+ MEDIUM: "P2",
196
+ HIGH_BUG: "P2",
197
+ BUG: "P3",
198
+ LOW: "P3"
199
+ };
200
+ /** Convert one deepsec finding into the factory's Finding. Pure; exported for tests. */
201
+ function findingFromDeepsec(raw) {
202
+ const meta = raw.metadata ?? {};
203
+ const confidence = (meta.confidence ?? "").toLowerCase();
204
+ const line = meta.lineNumbers?.[0];
205
+ const endLine = meta.lineNumbers === void 0 ? void 0 : Math.max(...meta.lineNumbers);
206
+ return findingSchema.parse({
207
+ title: raw.title.replace(/^\[[A-Z_]+\]\s*/u, "").slice(0, 200),
208
+ body: raw.description.slice(0, 8e3),
209
+ ...meta.filePath === void 0 ? {} : { path: meta.filePath.slice(0, 500) },
210
+ ...line === void 0 ? {} : { line },
211
+ ...endLine === void 0 || endLine === line ? {} : { endLine },
212
+ priority: priorityBySeverity[raw.severity.toUpperCase()] ?? "P3",
213
+ category: "security",
214
+ verification: {
215
+ level: confidence === "high" ? "supported" : "unverified",
216
+ method: "static_analysis",
217
+ summary: `deepsec ${meta.vulnSlug ?? "finding"} with ${confidence || "unknown"} confidence${meta.runId === void 0 ? "" : ` (run ${meta.runId})`}.`,
218
+ limitations: "AI-assisted static analysis of the checkout; not reproduced at runtime."
219
+ },
220
+ tool: "deepsec",
221
+ ...meta.vulnSlug === void 0 ? {} : { ruleId: meta.vulnSlug }
222
+ });
223
+ }
224
+ const findingsPath = "/tmp/deepsec-findings.json";
225
+ /** Parse "N file(s) with candidates" out of deepsec scan output; null when absent. */
226
+ function candidateCount(output) {
227
+ const match = /(\d+)\s+file\(s\) with candidates/u.exec(output);
228
+ return match?.[1] === void 0 ? null : Number(match[1]);
229
+ }
230
+ /** The last few meaningful lines of a command's output, ANSI stripped, for a receipt. */
231
+ function tail(output, lines = 4) {
232
+ return output.replaceAll(/\u001b\[[0-9;]*m/gu, "").split("\n").map((line) => line.trim()).filter((line) => line !== "").slice(-lines).join(" | ").slice(0, 600);
233
+ }
234
+ /**
235
+ * Runs deepsec (vercel-labs/deepsec) against the sandbox checkout and records
236
+ * what it finds through the same path as record_finding, so every deepsec
237
+ * finding becomes a fix task subject to the approval policy. Trusted code
238
+ * owns the command sequence and the credential; the agent chooses when to
239
+ * scan and reports the outcome.
240
+ */
241
+ function deepsecScanTool(stores, options) {
242
+ const repoPath = options.repoPath ?? "repo";
243
+ const workspace = options.workspacePath ?? "deepsec";
244
+ const version = options.version ?? "2.3.8";
245
+ const agent = options.agent ?? "claude";
246
+ const model = options.model ?? "claude-sonnet-4-6";
247
+ const thinkingLevel = options.thinkingLevel ?? "medium";
248
+ const concurrency = options.concurrency ?? 4;
249
+ const prefix = options.commandPrefix === void 0 ? [] : [options.commandPrefix];
250
+ const filesPerRun = options.filesPerRun ?? 40;
251
+ return defineTool({
252
+ description: "Run deepsec, the AI-powered vulnerability scanner, over the repository checkout and record every finding it reports as a fix task. Takes minutes. Call once per scan; then summarize the result.",
253
+ inputSchema: z.object({
254
+ repositoryId: repositoryIdSchema,
255
+ limit: z.number().int().min(1).max(500).optional().describe("Candidate files to investigate this run (default 40; deepsec resumes where it left off next time).")
256
+ }),
257
+ async execute({ repositoryId, limit }, ctx) {
258
+ const repository = await stores.repositories.get(repositoryId);
259
+ if (repository === null) return {
260
+ scanned: false,
261
+ reason: `repository ${repositoryId} does not exist`
262
+ };
263
+ const sandbox = await ctx.getSandbox();
264
+ const projectId = repository.slug.replace(/[^a-z0-9]+/gu, "-");
265
+ const startedAt = Date.now();
266
+ const phase = async (state, reason) => {
267
+ await stores.receipts.append({
268
+ repositoryIds: [repositoryId],
269
+ kind: "deepsec",
270
+ state,
271
+ phase: "security_scan",
272
+ reason: `${reason} (${Math.round((Date.now() - startedAt) / 1e3)}s in)`
273
+ });
274
+ };
275
+ await phase("started", `deepsec ${version} on ${repository.slug}, ${agent}/${model}`);
276
+ const prepare = await sandbox.run({ command: [
277
+ ...prefix,
278
+ "set -e",
279
+ `if [ ! -f ${workspace}/deepsec.config.ts ]; then`,
280
+ ` npx -y deepsec@${version} init ./${workspace} ./${repoPath} --id ${projectId} --scaffold-only --headless --yes --skip-install`,
281
+ ` (cd ${workspace} && npm install --no-audit --no-fund --loglevel=error)`,
282
+ "fi",
283
+ `cd ${workspace}`,
284
+ `./node_modules/.bin/deepsec scan --project-id ${projectId}`
285
+ ].join("\n") });
286
+ if (typeof prepare.exitCode === "number" && prepare.exitCode !== 0) {
287
+ const detail = (prepare.stderr ?? prepare.stdout).trim().slice(-1500);
288
+ await phase("failed", `scan phase failed: ${detail.slice(-300)}`);
289
+ return {
290
+ scanned: false,
291
+ reason: `deepsec scan failed: ${detail}`
292
+ };
293
+ }
294
+ const candidates = candidateCount(prepare.stdout);
295
+ await phase("scanned", `${candidates === null ? "scan output" : `${candidates} files with candidates`}: ${tail(`${prepare.stdout}\n${prepare.stderr ?? ""}`)}`);
296
+ const investigate = await options.withModelCredential(sandbox, [
297
+ ...prefix,
298
+ "set -e",
299
+ `cd ${workspace}`,
300
+ `./node_modules/.bin/deepsec process --project-id ${projectId} --agent ${agent} --model ${model} --thinking-level ${thinkingLevel} --concurrency ${concurrency} --limit ${limit ?? filesPerRun}`,
301
+ `./node_modules/.bin/deepsec export --project-id ${projectId} --format json --out ${findingsPath}`
302
+ ].join("\n"));
303
+ if (typeof investigate.exitCode === "number" && investigate.exitCode !== 0) {
304
+ const detail = (investigate.stderr ?? investigate.stdout).trim().slice(-1500);
305
+ await phase("failed", `process phase failed: ${detail.slice(-300)}`);
306
+ return {
307
+ scanned: false,
308
+ reason: `deepsec process failed: ${detail}`
309
+ };
310
+ }
311
+ const exported = await sandbox.run({ command: `cat ${findingsPath}` });
312
+ let parsed;
313
+ try {
314
+ parsed = JSON.parse(exported.stdout);
315
+ } catch {
316
+ return {
317
+ scanned: false,
318
+ reason: "deepsec export was not valid JSON"
319
+ };
320
+ }
321
+ const raw = deepsecExportSchema.safeParse(parsed);
322
+ if (!raw.success) return {
323
+ scanned: false,
324
+ reason: `deepsec export had an unexpected shape: ${raw.error.issues[0]?.message ?? "unknown"}`
325
+ };
326
+ const results = [];
327
+ for (const entry of raw.data) {
328
+ const finding = findingFromDeepsec(entry);
329
+ results.push({
330
+ finding,
331
+ result: await recordFinding(stores, repositoryId, finding, options)
332
+ });
333
+ }
334
+ const recorded = results.filter(({ result }) => result.recorded);
335
+ await phase("completed", `${results.length} findings, ${recorded.length} new. process output: ${tail(`${investigate.stdout}\n${investigate.stderr ?? ""}`, 6)}`);
336
+ return {
337
+ scanned: true,
338
+ tool: "deepsec",
339
+ found: results.length,
340
+ recorded: recorded.length,
341
+ duplicates: results.length - recorded.length,
342
+ awaitingApproval: recorded.filter(({ result }) => result.recorded && result.approval === "required").length,
343
+ findings: results.map(({ finding, result }) => ({
344
+ title: finding.title,
345
+ priority: finding.priority,
346
+ path: finding.path,
347
+ line: finding.line,
348
+ ...result.recorded ? {
349
+ taskId: result.taskId,
350
+ approval: result.approval
351
+ } : { duplicate: true }
352
+ }))
353
+ };
354
+ }
355
+ });
356
+ }
357
+ //#endregion
358
+ //#region src/eve/fetch.ts
359
+ /**
360
+ * The verifier's way in to the change under review. The sandbox has no
361
+ * GitHub credential while the agent runs, so trusted code does the one fetch
362
+ * the agent cannot: the pull request number and repository come from the
363
+ * change record, never from the agent, and the credential is gone again
364
+ * before control returns. Mirrors how publication reads commits out of the
365
+ * sandbox instead of letting the agent push.
366
+ */
367
+ function fetchPullRequestTool(stores, options) {
368
+ const repoPath = options.repoPath ?? "repo";
369
+ const depth = options.depth ?? 50;
370
+ return defineTool({
371
+ description: "Fetch the pull request under review into the checkout and check it out as the local branch `pr`. Call this after get_task, before inspecting or testing the change. Returns the PR number, URL, and the commit you are now on.",
372
+ inputSchema: z.object({ changeId: changeIdSchema.describe("The change under review, from your starting message.") }),
373
+ async execute({ changeId }, ctx) {
374
+ const change = await stores.changes.get(changeId);
375
+ if (change === null) return {
376
+ fetched: false,
377
+ reason: `change ${changeId} does not exist`
378
+ };
379
+ if (change.state !== "verifying" && change.state !== "ready") return {
380
+ fetched: false,
381
+ reason: `change is ${change.state}, not open for review; nothing to fetch`
382
+ };
383
+ const repository = await stores.repositories.get(change.repositoryId);
384
+ if (repository === null) return {
385
+ fetched: false,
386
+ reason: `repository ${change.repositoryId} does not exist`
387
+ };
388
+ const sandbox = await ctx.getSandbox();
389
+ const tracking = `refs/remotes/origin/pr-${change.prNumber}`;
390
+ const git = `git -C ${repoPath}`;
391
+ const result = await options.withCredential(sandbox, repository.slug, `${git} fetch --depth ${depth} origin +refs/pull/${change.prNumber}/head:${tracking} && ${git} checkout -B pr ${tracking}`);
392
+ if (typeof result.exitCode === "number" && result.exitCode !== 0) return {
393
+ fetched: false,
394
+ reason: `git fetch of pull request ${change.prNumber} failed: ${(result.stderr ?? "").trim()}`
395
+ };
396
+ const head = await sandbox.run({ command: `${git} rev-parse HEAD` });
397
+ return {
398
+ fetched: true,
399
+ prNumber: change.prNumber,
400
+ prUrl: change.prUrl,
401
+ branch: "pr",
402
+ headSha: head.stdout.trim()
403
+ };
404
+ }
405
+ });
406
+ }
407
+ //#endregion
408
+ //#region src/eve/invoke.ts
409
+ /**
410
+ * A tool that lets one agent hand work to another, NMA-style: the caller
411
+ * decides to invoke; trusted code owns the how. The handoff is recorded as a
412
+ * factory task (single-winner dispatch, receipts), then a fresh session is
413
+ * started on the target agent carrying only the task id — never the caller's
414
+ * context. The session start is create-once on the task id, so a retried
415
+ * tool call cannot start a second session.
416
+ */
417
+ function invokeAgentTool(stores, options) {
418
+ const doFetch = options.fetch ?? fetch;
419
+ return defineTool({
420
+ description: `${options.description} Hands the work to a separate agent in a fresh session; you will not see its reasoning. Returns the handoff task id.`,
421
+ inputSchema: z.object({
422
+ repositoryIds: z.array(repositoryIdSchema).min(1),
423
+ parentTaskId: taskIdSchema.optional().describe("Your own task, when this work continues it."),
424
+ changeId: changeIdSchema.optional().describe("The change the target agent should work on."),
425
+ channel: z.string().min(1).describe("Reply connector, e.g. github."),
426
+ address: z.string().min(1).describe("Reply address, e.g. acme/web#41."),
427
+ brief: z.string().min(1).max(4e3).describe("What the target agent needs to know. Facts and references only, not your reasoning.")
428
+ }),
429
+ async execute(input) {
430
+ const task = await stores.tasks.create({
431
+ repositoryIds: input.repositoryIds,
432
+ kind: options.kind,
433
+ origin: input.parentTaskId === void 0 ? { operator: "factory" } : { parentTaskId: input.parentTaskId },
434
+ replyTo: {
435
+ channel: input.channel,
436
+ address: input.address
437
+ }
438
+ });
439
+ const running = await dispatchTask(stores, task.id, {
440
+ maxLaunchAttempts: 1,
441
+ launch: async (dispatched) => {
442
+ const headers = {
443
+ "content-type": "application/json",
444
+ ...await options.headers?.()
445
+ };
446
+ const token = await options.token?.();
447
+ if (token !== void 0) headers.authorization = `Bearer ${token}`;
448
+ const message = [
449
+ `You are assigned factory task ${dispatched.id}.`,
450
+ input.changeId === void 0 ? null : `Change under review: ${input.changeId}.`,
451
+ `Reply address: ${input.channel} ${input.address}.`,
452
+ "",
453
+ input.brief,
454
+ "",
455
+ "Read the task with get_task first, do the work, and record the outcome with your factory tools."
456
+ ].filter((line) => line !== null).join("\n");
457
+ const response = await doFetch(`${options.targetUrl()}/eve/v1/session`, {
458
+ method: "POST",
459
+ headers,
460
+ body: JSON.stringify({
461
+ message,
462
+ operationId: `factory-task:${dispatched.id}`
463
+ })
464
+ });
465
+ if (!response.ok) throw new Error(`target agent refused the session: ${response.status}`);
466
+ }
467
+ });
468
+ return {
469
+ taskId: running.id,
470
+ kind: running.kind,
471
+ state: running.state
472
+ };
473
+ }
474
+ });
475
+ }
476
+ //#endregion
477
+ //#region src/eve/review.ts
478
+ function reviewCommentTools(stores, options) {
479
+ const repoPath = options.repoPath ?? "repo";
480
+ const commentInputSchema = z.object({
481
+ changeId: changeIdSchema.describe("The change under review, from your starting message."),
482
+ commentId: z.number().int().positive().describe("The GitHub review comment id.")
483
+ });
484
+ return {
485
+ read_review_comment: defineTool({
486
+ description: "Read an inline review comment on a change under verification or ready for human review. By default this refuses to return comments from people without write access to the repository.",
487
+ inputSchema: commentInputSchema,
488
+ async execute({ changeId, commentId }) {
489
+ const change = await stores.changes.get(changeId);
490
+ if (change === null) throw new Error(`change ${changeId} does not exist`);
491
+ if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
492
+ const repository = await stores.repositories.get(change.repositoryId);
493
+ if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
494
+ return readReviewComment({
495
+ repository: repository.slug,
496
+ token: await options.tokenForRepository(repository.slug),
497
+ fetch: options.fetch,
498
+ commentId
499
+ });
500
+ }
501
+ }),
502
+ answer_review_comment: defineTool({
503
+ description: "Reply to an inline review comment on a change under verification or ready for human review. Use for findings and low-confidence responses, not for a concrete medium- or high-confidence fix.",
504
+ inputSchema: commentInputSchema.extend({ body: z.string().min(1).max(5e3).describe("The Markdown reply to post.") }),
505
+ async execute({ changeId, commentId, body }) {
506
+ const change = await stores.changes.get(changeId);
507
+ if (change === null) throw new Error(`change ${changeId} does not exist`);
508
+ if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
509
+ const repository = await stores.repositories.get(change.repositoryId);
510
+ if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
511
+ return answerReviewComment({
512
+ repository: repository.slug,
513
+ token: await options.tokenForRepository(repository.slug),
514
+ fetch: options.fetch,
515
+ pullRequestNumber: change.prNumber,
516
+ commentId,
517
+ body
518
+ });
519
+ }
520
+ }),
521
+ suggest_review_fix: defineTool({
522
+ description: "Post a concrete, apply-able GitHub suggested change for a medium-confidence fix. The replacement must cover exactly the lines attached to the inline review comment.",
523
+ inputSchema: commentInputSchema.extend({
524
+ confidence: z.literal("medium"),
525
+ replacement: z.string().min(1).max(5e3).describe("Exact replacement text for the commented lines."),
526
+ summary: z.string().min(1).max(1e3).describe("A brief explanation of the suggested fix.")
527
+ }),
528
+ async execute({ changeId, commentId, replacement, summary }) {
529
+ const change = await stores.changes.get(changeId);
530
+ if (change === null) throw new Error(`change ${changeId} does not exist`);
531
+ if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
532
+ const repository = await stores.repositories.get(change.repositoryId);
533
+ if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
534
+ return answerReviewComment({
535
+ repository: repository.slug,
536
+ token: await options.tokenForRepository(repository.slug),
537
+ fetch: options.fetch,
538
+ pullRequestNumber: change.prNumber,
539
+ commentId,
540
+ body: `${summary}\n\n\`\`\`suggestion\n${replacement}\n\`\`\``
541
+ });
542
+ }
543
+ }),
544
+ apply_review_fix: defineTool({
545
+ description: "Publish a tested, committed high-confidence fix to the existing same-repository pull request branch. Fails if the checkout is dirty, the PR changed, or the PR comes from a fork.",
546
+ inputSchema: commentInputSchema.extend({
547
+ confidence: z.literal("high"),
548
+ summary: z.string().min(1).max(1e3).describe("What was fixed and verified.")
549
+ }),
550
+ async execute({ changeId, commentId, summary }, ctx) {
551
+ const change = await stores.changes.get(changeId);
552
+ if (change === null) throw new Error(`change ${changeId} does not exist`);
553
+ if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
554
+ const repository = await stores.repositories.get(change.repositoryId);
555
+ if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
556
+ if (options.withCredential === void 0) throw new Error("applying review fixes is not configured");
557
+ const token = await options.tokenForRepository(repository.slug);
558
+ await readReviewComment({
559
+ repository: repository.slug,
560
+ token,
561
+ fetch: options.fetch,
562
+ commentId
563
+ });
564
+ const head = await readPullRequestHead({
565
+ repository: repository.slug,
566
+ token,
567
+ fetch: options.fetch,
568
+ pullRequestNumber: change.prNumber
569
+ });
570
+ if (head.repository !== repository.slug.toLowerCase()) throw new Error("cannot apply a fix to a pull request from a fork");
571
+ const sandbox = await ctx.getSandbox();
572
+ const git = `git -C ${repoPath}`;
573
+ if ((await sandbox.run({ command: `${git} status --porcelain` })).stdout.trim() !== "") throw new Error("commit the review fix before publishing");
574
+ if ((await sandbox.run({ command: `${git} merge-base --is-ancestor ${head.sha} HEAD` })).exitCode !== 0) throw new Error("pull request changed after it was fetched; fetch it again before applying the fix");
575
+ if ((await sandbox.run({ command: `${git} rev-parse HEAD` })).stdout.trim() === head.sha) throw new Error("no committed review fix to publish");
576
+ const push = await options.withCredential(sandbox, repository.slug, `${git} push --force-with-lease=refs/heads/${head.branch}:${head.sha} origin HEAD:refs/heads/${head.branch}`);
577
+ if (typeof push.exitCode === "number" && push.exitCode !== 0) throw new Error(`could not update pull request branch: ${(push.stderr ?? "").trim()}`);
578
+ const reply = await answerReviewComment({
579
+ repository: repository.slug,
580
+ token,
581
+ fetch: options.fetch,
582
+ pullRequestNumber: change.prNumber,
583
+ commentId,
584
+ body: summary
585
+ });
586
+ return {
587
+ updated: true,
588
+ prNumber: change.prNumber,
589
+ prUrl: change.prUrl,
590
+ reply
591
+ };
592
+ }
593
+ })
594
+ };
595
+ }
596
+ //#endregion
597
+ //#region src/eve/verification.ts
598
+ /**
599
+ * The verifier's outcome tool. Writes evidence onto the change and moves it
600
+ * to ready only when the verdict is pass; a failed verdict leaves the change
601
+ * verifying with the evidence recorded, and the verification task itself
602
+ * ends with a receipt either way. The change's own code_change task follows
603
+ * the verdict too: succeeded on pass, failed on fail with the verifier's
604
+ * summary as the reason, so the ledger never shows a verified-and-rejected fix
605
+ * as still in progress. Fail closed: the agent cannot mark a change ready
606
+ * without supplying every evidence field.
607
+ */
608
+ function recordVerificationTool(stores) {
609
+ return defineTool({
610
+ description: "Record the outcome of a verification task: the evidence you gathered and whether the change passed. Pass moves the change to ready for human review; fail keeps it out of review.",
611
+ inputSchema: z.object({
612
+ taskId: taskIdSchema.describe("Your verification task."),
613
+ changeId: changeIdSchema.describe("The change you verified."),
614
+ verdict: z.enum(["pass", "fail"]),
615
+ evidence: changeEvidenceSchema,
616
+ confidence: levelSchema.describe("How certain you are in the verdict."),
617
+ summary: z.string().min(1).max(2e3).describe("What you checked and what you found.")
618
+ }),
619
+ async execute({ taskId, changeId, verdict, evidence, confidence, summary }) {
620
+ const change = await stores.changes.get(changeId);
621
+ if (change === null) return {
622
+ recorded: false,
623
+ reason: `change ${changeId} does not exist`
624
+ };
625
+ if (change.state !== "verifying") return {
626
+ recorded: false,
627
+ reason: `change is ${change.state}, not verifying; nothing to record`
628
+ };
629
+ const task = await stores.tasks.get(taskId);
630
+ if (task === null || task.kind !== "verification") return {
631
+ recorded: false,
632
+ reason: `${taskId} is not a verification task`
633
+ };
634
+ let updatedChange = change;
635
+ if (verdict === "pass") updatedChange = await stores.changes.transition(changeId, "ready", {
636
+ patch: {
637
+ evidence,
638
+ confidence
639
+ },
640
+ reason: summary
641
+ });
642
+ let finished;
643
+ if (verdict === "pass") {
644
+ await stores.tasks.transition(taskId, "verifying", { reason: summary });
645
+ finished = await stores.tasks.transition(taskId, "succeeded");
646
+ } else finished = await stores.tasks.transition(taskId, "failed", { reason: summary });
647
+ if (change.taskId !== void 0) {
648
+ if ((await stores.tasks.get(change.taskId))?.state === "verifying") await stores.tasks.transition(change.taskId, verdict === "pass" ? "succeeded" : "failed", verdict === "pass" ? {} : { reason: `verification failed: ${summary}` }).catch(() => void 0);
649
+ }
650
+ return {
651
+ recorded: true,
652
+ verdict,
653
+ changeState: updatedChange.state,
654
+ taskState: finished.state
655
+ };
656
+ }
657
+ });
658
+ }
659
+ //#endregion
660
+ //#region src/eve/index.ts
661
+ const decidedOutcomeSchema = z.enum([
662
+ "accepted",
663
+ "duplicate",
664
+ "unactionable",
665
+ "low_confidence",
666
+ "ignored"
667
+ ]);
668
+ const recordChangeInputSchema = z.object({
669
+ taskId: taskIdSchema,
670
+ repositoryId: repositoryIdSchema,
671
+ prNumber: z.number().int().min(1),
672
+ prUrl: z.url(),
673
+ risk: levelSchema.describe("Blast radius of the change if it is wrong."),
674
+ confidence: levelSchema.describe("How certain you are the change is correct."),
675
+ summary: z.string().min(1).describe("One-sentence description of the change.")
676
+ });
677
+ /**
678
+ * The factory's store operations wrapped as eve tools. Expose a tool to an
679
+ * agent by re-exporting it from a file under agent/tools/, e.g.
680
+ * `export default factoryTools(stores).get_task;`. Every write goes through
681
+ * the engine, so agents inherit transition legality and receipts.
682
+ */
683
+ function factoryTools(stores, options = {}) {
684
+ const recordChangeGate = options.recordChangeGate;
685
+ return {
686
+ get_task: defineTool({
687
+ description: "Read one factory task by id: its kind, state, repositories, origin, and results.",
688
+ inputSchema: z.object({ taskId: taskIdSchema }),
689
+ async execute({ taskId }) {
690
+ return await stores.tasks.get(taskId) ?? {
691
+ found: false,
692
+ taskId
693
+ };
694
+ }
695
+ }),
696
+ decide_signal: defineTool({
697
+ description: "Record the triage decision for a pending signal. A signal is decided exactly once.",
698
+ inputSchema: z.object({
699
+ signalId: signalIdSchema,
700
+ outcome: decidedOutcomeSchema,
701
+ reason: z.string().min(1).describe("Why this outcome was chosen."),
702
+ detail: z.string().min(1).optional().describe("Severity if accepted, or the duplicate-of signal id.")
703
+ }),
704
+ execute({ signalId, outcome, reason, detail }) {
705
+ return stores.signals.decide(signalId, outcome, {
706
+ reason,
707
+ detail
708
+ });
709
+ }
710
+ }),
711
+ record_change: defineTool({
712
+ description: "Record the pull request produced for a code_change task. Creates the change, links it to the task, and moves the task to verifying.",
713
+ inputSchema: recordChangeInputSchema,
714
+ ...recordChangeGate === void 0 ? {} : { approval: async ({ toolInput }) => {
715
+ const parsed = recordChangeInputSchema.safeParse(toolInput);
716
+ if (!parsed.success) return {
717
+ type: "denied",
718
+ reason: "record_change input is malformed; fix the arguments and retry"
719
+ };
720
+ const verdict = await recordChangeGate(parsed.data);
721
+ return verdict.ok ? "not-applicable" : {
722
+ type: "denied",
723
+ reason: verdict.reason
724
+ };
725
+ } },
726
+ async execute({ taskId, repositoryId, prNumber, prUrl, risk, confidence, summary }) {
727
+ const change = await stores.changes.create({
728
+ repositoryId,
729
+ taskId,
730
+ prNumber,
731
+ prUrl,
732
+ author: "factory",
733
+ risk,
734
+ confidence,
735
+ evidence: {
736
+ reproVerified: false,
737
+ testsAdded: 0,
738
+ checksGreen: false,
739
+ attackPassed: false,
740
+ scopeMatches: false
741
+ }
742
+ });
743
+ return {
744
+ change,
745
+ task: await stores.tasks.transition(taskId, "verifying", {
746
+ patch: { changeId: change.id },
747
+ reason: summary
748
+ })
749
+ };
750
+ }
751
+ }),
752
+ finish_task: defineTool({
753
+ description: "Mark your task finished with a summary, for task kinds that have no other outcome tool (for example a security_scan). Not for code_change or verification tasks: those end with record_change and record_verification.",
754
+ inputSchema: z.object({
755
+ taskId: taskIdSchema,
756
+ summary: z.string().min(1).max(4e3).describe("What you did and found, for the ledger.")
757
+ }),
758
+ async execute({ taskId, summary }) {
759
+ const task = await stores.tasks.get(taskId);
760
+ if (task === null) return {
761
+ finished: false,
762
+ reason: `${taskId} does not exist`
763
+ };
764
+ if (task.kind === "code_change" || task.kind === "verification") return {
765
+ finished: false,
766
+ reason: `${task.kind} tasks end with their own outcome tool`
767
+ };
768
+ if (task.state !== "running") return {
769
+ finished: false,
770
+ reason: `task is ${task.state}, not running`
771
+ };
772
+ await stores.tasks.transition(taskId, "verifying", { reason: summary });
773
+ return {
774
+ finished: true,
775
+ taskState: (await stores.tasks.transition(taskId, "succeeded", { reason: summary })).state
776
+ };
777
+ }
778
+ }),
779
+ report_blocked: defineTool({
780
+ description: "Mark a running task as needing a human because the work cannot continue without help.",
781
+ inputSchema: z.object({
782
+ taskId: taskIdSchema,
783
+ reason: z.string().min(1).describe("What is blocking, written for the human.")
784
+ }),
785
+ execute({ taskId, reason }) {
786
+ return stores.tasks.transition(taskId, "needs_human", { reason });
787
+ }
788
+ }),
789
+ ask_question: defineTool({
790
+ description: "Raise a question a human operator must answer. Creates a question task that waits for the answer.",
791
+ inputSchema: z.object({
792
+ repositoryIds: z.array(repositoryIdSchema).min(1),
793
+ parentTaskId: taskIdSchema.optional().describe("Task the answer unblocks, when there is one."),
794
+ channel: z.string().min(1).describe("Reply connector, e.g. github."),
795
+ address: z.string().min(1).describe("Reply address, e.g. acme/web#41."),
796
+ question: taskQuestionSchema.omit({
797
+ answer: true,
798
+ answeredBy: true
799
+ })
800
+ }),
801
+ async execute({ repositoryIds, parentTaskId, channel, address, question }) {
802
+ const task = await stores.tasks.create({
803
+ repositoryIds,
804
+ kind: "question",
805
+ origin: parentTaskId === void 0 ? { operator: "factory" } : { parentTaskId },
806
+ replyTo: {
807
+ channel,
808
+ address
809
+ },
810
+ question
811
+ });
812
+ return stores.tasks.transition(task.id, "needs_human");
813
+ }
814
+ }),
815
+ append_receipt: defineTool({
816
+ description: "Append one immutable audit receipt for an event worth remembering. Receipts can never be edited or deleted.",
817
+ inputSchema: z.object({
818
+ repositoryIds: z.array(repositoryIdSchema).min(1),
819
+ taskId: taskIdSchema.optional(),
820
+ changeId: changeIdSchema.optional(),
821
+ kind: vocabularySchema.describe("What the receipt is about, e.g. investigation."),
822
+ state: vocabularySchema.describe("What happened, e.g. flag_ramped."),
823
+ reason: z.string().min(1).max(2e3).optional(),
824
+ dedupeKey: z.string().min(1).optional().describe("Same key twice records one receipt instead of two.")
825
+ }),
826
+ execute(input) {
827
+ return stores.receipts.append(input);
828
+ }
829
+ })
830
+ };
831
+ }
832
+ /**
833
+ * Observe-only eve hooks that keep the ledger honest about session outcomes.
834
+ * Mount by re-exporting from agent/hooks/, e.g.
835
+ * `export default factoryHooks(stores).factory;`.
836
+ *
837
+ * turn.completed: a turn that ends while its task is still running means the
838
+ * agent finished talking without recording an outcome; the task moves to
839
+ * needs_human instead of rotting. The task is found by the session's
840
+ * channel address, which dispatch set from the task's replyTo.
841
+ */
842
+ function factoryHooks(stores) {
843
+ return { factory: defineHook({ events: { async "turn.completed"(_event, ctx) {
844
+ const address = ctx.channel.continuationToken;
845
+ if (address === void 0) return;
846
+ const abandoned = (await stores.tasks.list()).filter((task) => task.state === "running" && task.replyTo.address === address);
847
+ for (const task of abandoned) await stores.tasks.transition(task.id, "needs_human", { reason: "agent turn ended without recording an outcome" }).catch(() => void 0);
848
+ } } }) };
849
+ }
850
+ //#endregion
851
+ export { deepsecScanTool, factoryHooks, factoryTools, fetchPullRequestTool, findingFromDeepsec, invokeAgentTool, mergeConflictPayloadSchema, mergeConflictTools, recordFinding, recordFindingTool, recordVerificationTool, reviewCommentTools };
852
+
853
+ //# sourceMappingURL=index.mjs.map