@vercel/factory 0.0.0 → 0.0.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.
package/dist/eve/index.js CHANGED
@@ -1,56 +1,476 @@
1
1
  import {
2
+ changeEvidenceSchema,
2
3
  changeIdSchema,
4
+ defaultApprovalPolicy,
5
+ dispatchTask,
6
+ findingFingerprint,
7
+ findingSchema,
3
8
  levelSchema,
4
9
  repositoryIdSchema,
10
+ requiresApproval,
5
11
  signalIdSchema,
6
12
  taskIdSchema,
7
13
  taskQuestionSchema,
8
14
  vocabularySchema
9
- } from "../chunk-BSDWPFKE.js";
15
+ } from "../chunk-LBEYCOSR.js";
16
+ import "../chunk-CEF6VJZC.js";
10
17
 
11
18
  // src/eve/index.ts
19
+ import { defineHook } from "eve/hooks";
20
+ import { defineTool as defineTool6 } from "eve/tools";
21
+ import { z as z6 } from "zod";
22
+
23
+ // src/eve/deepsec.ts
24
+ import { defineTool as defineTool2 } from "eve/tools";
25
+ import { z as z2 } from "zod";
26
+
27
+ // src/eve/finding.ts
12
28
  import { defineTool } from "eve/tools";
13
29
  import { z } from "zod";
14
- var decidedOutcomeSchema = z.enum([
30
+ async function recordFinding(stores, repositoryId, finding, options = {}) {
31
+ const source = options.source ?? "security_scan";
32
+ const policy = options.approval ?? defaultApprovalPolicy;
33
+ const repository = await stores.repositories.get(repositoryId);
34
+ if (repository === null) {
35
+ return { recorded: false, reason: `repository ${repositoryId} does not exist` };
36
+ }
37
+ const fingerprint = findingFingerprint(repositoryId, finding);
38
+ const signal = await stores.signals.create({
39
+ repositoryId,
40
+ source,
41
+ deliveryId: fingerprint,
42
+ replyTo: { channel: "finding", address: fingerprint },
43
+ payload: finding
44
+ });
45
+ if (signal.outcome !== "pending") {
46
+ return { recorded: false, duplicate: true, signalId: signal.id, reason: `already recorded as ${signal.id}` };
47
+ }
48
+ await stores.signals.decide(signal.id, "accepted", {
49
+ detail: finding.priority,
50
+ reason: `${finding.tool ?? "scanner"} reported it, ${finding.verification.level} by ${finding.verification.method}`
51
+ });
52
+ const approval = requiresApproval(policy, { source, kind: "code_change", priority: finding.priority });
53
+ const task = await stores.tasks.create({
54
+ repositoryIds: [repositoryId],
55
+ kind: "code_change",
56
+ origin: { signalId: signal.id },
57
+ replyTo: signal.replyTo,
58
+ ...approval ? { approval: "required" } : {}
59
+ });
60
+ return { recorded: true, signalId: signal.id, taskId: task.id, approval: approval ? "required" : "not_required" };
61
+ }
62
+ function recordFindingTool(stores, options = {}) {
63
+ return defineTool({
64
+ 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.",
65
+ inputSchema: z.object({
66
+ repositoryId: repositoryIdSchema,
67
+ finding: findingSchema
68
+ }),
69
+ execute({ repositoryId, finding }) {
70
+ return recordFinding(stores, repositoryId, finding, options);
71
+ }
72
+ });
73
+ }
74
+
75
+ // src/eve/deepsec.ts
76
+ var deepsecFindingSchema = z2.looseObject({
77
+ title: z2.string().min(1),
78
+ description: z2.string().min(1),
79
+ severity: z2.string().min(1),
80
+ metadata: z2.looseObject({
81
+ filePath: z2.string().min(1).optional(),
82
+ lineNumbers: z2.array(z2.number().int().min(1)).optional(),
83
+ vulnSlug: z2.string().min(1).optional(),
84
+ confidence: z2.string().optional(),
85
+ runId: z2.string().optional()
86
+ }).optional()
87
+ });
88
+ var deepsecExportSchema = z2.union([
89
+ z2.array(deepsecFindingSchema),
90
+ z2.looseObject({ findings: z2.array(deepsecFindingSchema) }).transform((value) => value.findings)
91
+ ]);
92
+ var priorityBySeverity = {
93
+ CRITICAL: "P0",
94
+ HIGH: "P1",
95
+ MEDIUM: "P2",
96
+ HIGH_BUG: "P2",
97
+ BUG: "P3",
98
+ LOW: "P3"
99
+ };
100
+ function findingFromDeepsec(raw) {
101
+ const meta = raw.metadata ?? {};
102
+ const confidence = (meta.confidence ?? "").toLowerCase();
103
+ const line = meta.lineNumbers?.[0];
104
+ const endLine = meta.lineNumbers === void 0 ? void 0 : Math.max(...meta.lineNumbers);
105
+ return findingSchema.parse({
106
+ title: raw.title.replace(/^\[[A-Z_]+\]\s*/u, "").slice(0, 200),
107
+ body: raw.description.slice(0, 8e3),
108
+ ...meta.filePath === void 0 ? {} : { path: meta.filePath.slice(0, 500) },
109
+ ...line === void 0 ? {} : { line },
110
+ ...endLine === void 0 || endLine === line ? {} : { endLine },
111
+ priority: priorityBySeverity[raw.severity.toUpperCase()] ?? "P3",
112
+ category: "security",
113
+ verification: {
114
+ // deepsec's agent reads the code; it does not run it. Say so.
115
+ level: confidence === "high" ? "supported" : "unverified",
116
+ method: "static_analysis",
117
+ summary: `deepsec ${meta.vulnSlug ?? "finding"} with ${confidence || "unknown"} confidence${meta.runId === void 0 ? "" : ` (run ${meta.runId})`}.`,
118
+ limitations: "AI-assisted static analysis of the checkout; not reproduced at runtime."
119
+ },
120
+ tool: "deepsec",
121
+ ...meta.vulnSlug === void 0 ? {} : { ruleId: meta.vulnSlug }
122
+ });
123
+ }
124
+ var findingsPath = "/tmp/deepsec-findings.json";
125
+ function candidateCount(output) {
126
+ const match = /(\d+)\s+file\(s\) with candidates/u.exec(output);
127
+ return match?.[1] === void 0 ? null : Number(match[1]);
128
+ }
129
+ function tail(output, lines = 4) {
130
+ const ansi = /\u001b\[[0-9;]*m/gu;
131
+ return output.replaceAll(ansi, "").split("\n").map((line) => line.trim()).filter((line) => line !== "").slice(-lines).join(" | ").slice(0, 600);
132
+ }
133
+ function deepsecScanTool(stores, options) {
134
+ const repoPath = options.repoPath ?? "repo";
135
+ const workspace = options.workspacePath ?? "deepsec";
136
+ const version = options.version ?? "2.3.8";
137
+ const agent = options.agent ?? "claude";
138
+ const model = options.model ?? "claude-sonnet-4-6";
139
+ const thinkingLevel = options.thinkingLevel ?? "medium";
140
+ const concurrency = options.concurrency ?? 4;
141
+ const prefix = options.commandPrefix === void 0 ? [] : [options.commandPrefix];
142
+ const filesPerRun = options.filesPerRun ?? 40;
143
+ return defineTool2({
144
+ 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.",
145
+ inputSchema: z2.object({
146
+ repositoryId: repositoryIdSchema,
147
+ limit: z2.number().int().min(1).max(500).optional().describe("Candidate files to investigate this run (default 40; deepsec resumes where it left off next time).")
148
+ }),
149
+ async execute({ repositoryId, limit }, ctx) {
150
+ const repository = await stores.repositories.get(repositoryId);
151
+ if (repository === null) {
152
+ return { scanned: false, reason: `repository ${repositoryId} does not exist` };
153
+ }
154
+ const sandbox = await ctx.getSandbox();
155
+ const projectId = repository.slug.replace(/[^a-z0-9]+/gu, "-");
156
+ const startedAt = Date.now();
157
+ const phase = async (state, reason) => {
158
+ await stores.receipts.append({
159
+ repositoryIds: [repositoryId],
160
+ kind: "deepsec",
161
+ state,
162
+ phase: "security_scan",
163
+ reason: `${reason} (${Math.round((Date.now() - startedAt) / 1e3)}s in)`
164
+ });
165
+ };
166
+ await phase("started", `deepsec ${version} on ${repository.slug}, ${agent}/${model}`);
167
+ const prepare = await sandbox.run({
168
+ command: [
169
+ ...prefix,
170
+ "set -e",
171
+ `if [ ! -f ${workspace}/deepsec.config.ts ]; then`,
172
+ ` npx -y deepsec@${version} init ./${workspace} ./${repoPath} --id ${projectId} --scaffold-only --headless --yes --skip-install`,
173
+ ` (cd ${workspace} && npm install --no-audit --no-fund --loglevel=error)`,
174
+ "fi",
175
+ // deepsec resolves deepsec.config.ts from the current directory, so
176
+ // every deepsec command runs inside the workspace. No --root: it is a
177
+ // one-off override that skips the project's file records, leaving
178
+ // `process` with nothing to investigate.
179
+ `cd ${workspace}`,
180
+ `./node_modules/.bin/deepsec scan --project-id ${projectId}`
181
+ ].join("\n")
182
+ });
183
+ if (typeof prepare.exitCode === "number" && prepare.exitCode !== 0) {
184
+ const detail = (prepare.stderr ?? prepare.stdout).trim().slice(-1500);
185
+ await phase("failed", `scan phase failed: ${detail.slice(-300)}`);
186
+ return { scanned: false, reason: `deepsec scan failed: ${detail}` };
187
+ }
188
+ const candidates = candidateCount(prepare.stdout);
189
+ await phase(
190
+ "scanned",
191
+ `${candidates === null ? "scan output" : `${candidates} files with candidates`}: ${tail(`${prepare.stdout}
192
+ ${prepare.stderr ?? ""}`)}`
193
+ );
194
+ const investigate = await options.withModelCredential(
195
+ sandbox,
196
+ [
197
+ ...prefix,
198
+ "set -e",
199
+ `cd ${workspace}`,
200
+ `./node_modules/.bin/deepsec process --project-id ${projectId} --agent ${agent} --model ${model} --thinking-level ${thinkingLevel} --concurrency ${concurrency} --limit ${limit ?? filesPerRun}`,
201
+ `./node_modules/.bin/deepsec export --project-id ${projectId} --format json --out ${findingsPath}`
202
+ ].join("\n")
203
+ );
204
+ if (typeof investigate.exitCode === "number" && investigate.exitCode !== 0) {
205
+ const detail = (investigate.stderr ?? investigate.stdout).trim().slice(-1500);
206
+ await phase("failed", `process phase failed: ${detail.slice(-300)}`);
207
+ return { scanned: false, reason: `deepsec process failed: ${detail}` };
208
+ }
209
+ const exported = await sandbox.run({ command: `cat ${findingsPath}` });
210
+ let parsed;
211
+ try {
212
+ parsed = JSON.parse(exported.stdout);
213
+ } catch {
214
+ return { scanned: false, reason: "deepsec export was not valid JSON" };
215
+ }
216
+ const raw = deepsecExportSchema.safeParse(parsed);
217
+ if (!raw.success) {
218
+ return { scanned: false, reason: `deepsec export had an unexpected shape: ${raw.error.issues[0]?.message ?? "unknown"}` };
219
+ }
220
+ const results = [];
221
+ for (const entry of raw.data) {
222
+ const finding = findingFromDeepsec(entry);
223
+ results.push({ finding, result: await recordFinding(stores, repositoryId, finding, options) });
224
+ }
225
+ const recorded = results.filter(({ result }) => result.recorded);
226
+ await phase(
227
+ "completed",
228
+ `${results.length} findings, ${recorded.length} new. process output: ${tail(`${investigate.stdout}
229
+ ${investigate.stderr ?? ""}`, 6)}`
230
+ );
231
+ return {
232
+ scanned: true,
233
+ tool: "deepsec",
234
+ found: results.length,
235
+ recorded: recorded.length,
236
+ duplicates: results.length - recorded.length,
237
+ awaitingApproval: recorded.filter(({ result }) => result.recorded && result.approval === "required").length,
238
+ findings: results.map(({ finding, result }) => ({
239
+ title: finding.title,
240
+ priority: finding.priority,
241
+ path: finding.path,
242
+ line: finding.line,
243
+ ...result.recorded ? { taskId: result.taskId, approval: result.approval } : { duplicate: true }
244
+ }))
245
+ };
246
+ }
247
+ });
248
+ }
249
+
250
+ // src/eve/fetch.ts
251
+ import { defineTool as defineTool3 } from "eve/tools";
252
+ import { z as z3 } from "zod";
253
+ function fetchPullRequestTool(stores, options) {
254
+ const repoPath = options.repoPath ?? "repo";
255
+ const depth = options.depth ?? 50;
256
+ return defineTool3({
257
+ 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.",
258
+ inputSchema: z3.object({
259
+ changeId: changeIdSchema.describe("The change under review, from your starting message.")
260
+ }),
261
+ async execute({ changeId }, ctx) {
262
+ const change = await stores.changes.get(changeId);
263
+ if (change === null) {
264
+ return { fetched: false, reason: `change ${changeId} does not exist` };
265
+ }
266
+ if (change.state !== "verifying") {
267
+ return { fetched: false, reason: `change is ${change.state}, not verifying; nothing to fetch` };
268
+ }
269
+ const repository = await stores.repositories.get(change.repositoryId);
270
+ if (repository === null) {
271
+ return { fetched: false, reason: `repository ${change.repositoryId} does not exist` };
272
+ }
273
+ const sandbox = await ctx.getSandbox();
274
+ const tracking = `refs/remotes/origin/pr-${change.prNumber}`;
275
+ const git = `git -C ${repoPath}`;
276
+ const result = await options.withCredential(
277
+ sandbox,
278
+ repository.slug,
279
+ `${git} fetch --depth ${depth} origin +refs/pull/${change.prNumber}/head:${tracking} && ${git} checkout -B pr ${tracking}`
280
+ );
281
+ if (typeof result.exitCode === "number" && result.exitCode !== 0) {
282
+ return {
283
+ fetched: false,
284
+ reason: `git fetch of pull request ${change.prNumber} failed: ${(result.stderr ?? "").trim()}`
285
+ };
286
+ }
287
+ const head = await sandbox.run({ command: `${git} rev-parse HEAD` });
288
+ return {
289
+ fetched: true,
290
+ prNumber: change.prNumber,
291
+ prUrl: change.prUrl,
292
+ branch: "pr",
293
+ headSha: head.stdout.trim()
294
+ };
295
+ }
296
+ });
297
+ }
298
+
299
+ // src/eve/invoke.ts
300
+ import { defineTool as defineTool4 } from "eve/tools";
301
+ import { z as z4 } from "zod";
302
+ function invokeAgentTool(stores, options) {
303
+ const doFetch = options.fetch ?? fetch;
304
+ return defineTool4({
305
+ 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.`,
306
+ inputSchema: z4.object({
307
+ repositoryIds: z4.array(repositoryIdSchema).min(1),
308
+ parentTaskId: taskIdSchema.optional().describe("Your own task, when this work continues it."),
309
+ changeId: changeIdSchema.optional().describe("The change the target agent should work on."),
310
+ channel: z4.string().min(1).describe("Reply connector, e.g. github."),
311
+ address: z4.string().min(1).describe("Reply address, e.g. acme/web#41."),
312
+ brief: z4.string().min(1).max(4e3).describe("What the target agent needs to know. Facts and references only, not your reasoning.")
313
+ }),
314
+ async execute(input) {
315
+ const task = await stores.tasks.create({
316
+ repositoryIds: input.repositoryIds,
317
+ kind: options.kind,
318
+ origin: input.parentTaskId === void 0 ? { operator: "factory" } : { parentTaskId: input.parentTaskId },
319
+ replyTo: { channel: input.channel, address: input.address }
320
+ });
321
+ const running = await dispatchTask(stores, task.id, {
322
+ launch: async (dispatched) => {
323
+ const headers = {
324
+ "content-type": "application/json",
325
+ ...await options.headers?.()
326
+ };
327
+ const token = await options.token?.();
328
+ if (token !== void 0) {
329
+ headers.authorization = `Bearer ${token}`;
330
+ }
331
+ const message = [
332
+ `You are assigned factory task ${dispatched.id}.`,
333
+ input.changeId === void 0 ? null : `Change under review: ${input.changeId}.`,
334
+ `Reply address: ${input.channel} ${input.address}.`,
335
+ "",
336
+ input.brief,
337
+ "",
338
+ "Read the task with get_task first, do the work, and record the outcome with your factory tools."
339
+ ].filter((line) => line !== null).join("\n");
340
+ const response = await doFetch(`${options.targetUrl()}/eve/v1/session`, {
341
+ method: "POST",
342
+ headers,
343
+ body: JSON.stringify({ message, operationId: `factory-task:${dispatched.id}` })
344
+ });
345
+ if (!response.ok) {
346
+ throw new Error(`target agent refused the session: ${response.status}`);
347
+ }
348
+ }
349
+ });
350
+ return { taskId: running.id, kind: running.kind, state: running.state };
351
+ }
352
+ });
353
+ }
354
+
355
+ // src/eve/verification.ts
356
+ import { defineTool as defineTool5 } from "eve/tools";
357
+ import { z as z5 } from "zod";
358
+ function recordVerificationTool(stores) {
359
+ return defineTool5({
360
+ 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.",
361
+ inputSchema: z5.object({
362
+ taskId: taskIdSchema.describe("Your verification task."),
363
+ changeId: changeIdSchema.describe("The change you verified."),
364
+ verdict: z5.enum(["pass", "fail"]),
365
+ evidence: changeEvidenceSchema,
366
+ confidence: levelSchema.describe("How certain you are in the verdict."),
367
+ summary: z5.string().min(1).max(2e3).describe("What you checked and what you found.")
368
+ }),
369
+ async execute({ taskId, changeId, verdict, evidence, confidence, summary }) {
370
+ const change = await stores.changes.get(changeId);
371
+ if (change === null) {
372
+ return { recorded: false, reason: `change ${changeId} does not exist` };
373
+ }
374
+ if (change.state !== "verifying") {
375
+ return {
376
+ recorded: false,
377
+ reason: `change is ${change.state}, not verifying; nothing to record`
378
+ };
379
+ }
380
+ const task = await stores.tasks.get(taskId);
381
+ if (task === null || task.kind !== "verification") {
382
+ return { recorded: false, reason: `${taskId} is not a verification task` };
383
+ }
384
+ let updatedChange = change;
385
+ if (verdict === "pass") {
386
+ updatedChange = await stores.changes.transition(changeId, "ready", {
387
+ patch: { evidence, confidence },
388
+ reason: summary
389
+ });
390
+ }
391
+ let finished;
392
+ if (verdict === "pass") {
393
+ await stores.tasks.transition(taskId, "verifying", { reason: summary });
394
+ finished = await stores.tasks.transition(taskId, "succeeded");
395
+ } else {
396
+ finished = await stores.tasks.transition(taskId, "failed", { reason: summary });
397
+ }
398
+ if (change.taskId !== void 0) {
399
+ const parent = await stores.tasks.get(change.taskId);
400
+ if (parent?.state === "verifying") {
401
+ await stores.tasks.transition(
402
+ change.taskId,
403
+ verdict === "pass" ? "succeeded" : "failed",
404
+ verdict === "pass" ? {} : { reason: `verification failed: ${summary}` }
405
+ ).catch(() => void 0);
406
+ }
407
+ }
408
+ return {
409
+ recorded: true,
410
+ verdict,
411
+ changeState: updatedChange.state,
412
+ taskState: finished.state
413
+ };
414
+ }
415
+ });
416
+ }
417
+
418
+ // src/eve/index.ts
419
+ var decidedOutcomeSchema = z6.enum([
15
420
  "accepted",
16
421
  "duplicate",
17
422
  "unactionable",
18
423
  "low_confidence",
19
424
  "ignored"
20
425
  ]);
21
- function factoryTools(stores) {
426
+ var recordChangeInputSchema = z6.object({
427
+ taskId: taskIdSchema,
428
+ repositoryId: repositoryIdSchema,
429
+ prNumber: z6.number().int().min(1),
430
+ prUrl: z6.url(),
431
+ risk: levelSchema.describe("Blast radius of the change if it is wrong."),
432
+ confidence: levelSchema.describe("How certain you are the change is correct."),
433
+ summary: z6.string().min(1).describe("One-sentence description of the change.")
434
+ });
435
+ function factoryTools(stores, options = {}) {
436
+ const recordChangeGate = options.recordChangeGate;
22
437
  return {
23
- get_task: defineTool({
438
+ get_task: defineTool6({
24
439
  description: "Read one factory task by id: its kind, state, repositories, origin, and results.",
25
- inputSchema: z.object({ taskId: taskIdSchema }),
440
+ inputSchema: z6.object({ taskId: taskIdSchema }),
26
441
  async execute({ taskId }) {
27
442
  const task = await stores.tasks.get(taskId);
28
443
  return task ?? { found: false, taskId };
29
444
  }
30
445
  }),
31
- decide_signal: defineTool({
446
+ decide_signal: defineTool6({
32
447
  description: "Record the triage decision for a pending signal. A signal is decided exactly once.",
33
- inputSchema: z.object({
448
+ inputSchema: z6.object({
34
449
  signalId: signalIdSchema,
35
450
  outcome: decidedOutcomeSchema,
36
- reason: z.string().min(1).describe("Why this outcome was chosen."),
37
- detail: z.string().min(1).optional().describe("Severity if accepted, or the duplicate-of signal id.")
451
+ reason: z6.string().min(1).describe("Why this outcome was chosen."),
452
+ detail: z6.string().min(1).optional().describe("Severity if accepted, or the duplicate-of signal id.")
38
453
  }),
39
454
  execute({ signalId, outcome, reason, detail }) {
40
455
  return stores.signals.decide(signalId, outcome, { reason, detail });
41
456
  }
42
457
  }),
43
- record_change: defineTool({
458
+ record_change: defineTool6({
44
459
  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.",
45
- inputSchema: z.object({
46
- taskId: taskIdSchema,
47
- repositoryId: repositoryIdSchema,
48
- prNumber: z.number().int().min(1),
49
- prUrl: z.url(),
50
- risk: levelSchema.describe("Blast radius of the change if it is wrong."),
51
- confidence: levelSchema.describe("How certain you are the change is correct."),
52
- summary: z.string().min(1).describe("One-sentence description of the change.")
53
- }),
460
+ inputSchema: recordChangeInputSchema,
461
+ ...recordChangeGate === void 0 ? {} : {
462
+ approval: async ({ toolInput }) => {
463
+ const parsed = recordChangeInputSchema.safeParse(toolInput);
464
+ if (!parsed.success) {
465
+ return {
466
+ type: "denied",
467
+ reason: "record_change input is malformed; fix the arguments and retry"
468
+ };
469
+ }
470
+ const verdict = await recordChangeGate(parsed.data);
471
+ return verdict.ok ? "not-applicable" : { type: "denied", reason: verdict.reason };
472
+ }
473
+ },
54
474
  async execute({ taskId, repositoryId, prNumber, prUrl, risk, confidence, summary }) {
55
475
  const change = await stores.changes.create({
56
476
  repositoryId,
@@ -75,23 +495,45 @@ function factoryTools(stores) {
75
495
  return { change, task };
76
496
  }
77
497
  }),
78
- report_blocked: defineTool({
498
+ finish_task: defineTool6({
499
+ 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.",
500
+ inputSchema: z6.object({
501
+ taskId: taskIdSchema,
502
+ summary: z6.string().min(1).max(4e3).describe("What you did and found, for the ledger.")
503
+ }),
504
+ async execute({ taskId, summary }) {
505
+ const task = await stores.tasks.get(taskId);
506
+ if (task === null) {
507
+ return { finished: false, reason: `${taskId} does not exist` };
508
+ }
509
+ if (task.kind === "code_change" || task.kind === "verification") {
510
+ return { finished: false, reason: `${task.kind} tasks end with their own outcome tool` };
511
+ }
512
+ if (task.state !== "running") {
513
+ return { finished: false, reason: `task is ${task.state}, not running` };
514
+ }
515
+ await stores.tasks.transition(taskId, "verifying", { reason: summary });
516
+ const finished = await stores.tasks.transition(taskId, "succeeded", { reason: summary });
517
+ return { finished: true, taskState: finished.state };
518
+ }
519
+ }),
520
+ report_blocked: defineTool6({
79
521
  description: "Mark a running task as needing a human because the work cannot continue without help.",
80
- inputSchema: z.object({
522
+ inputSchema: z6.object({
81
523
  taskId: taskIdSchema,
82
- reason: z.string().min(1).describe("What is blocking, written for the human.")
524
+ reason: z6.string().min(1).describe("What is blocking, written for the human.")
83
525
  }),
84
526
  execute({ taskId, reason }) {
85
527
  return stores.tasks.transition(taskId, "needs_human", { reason });
86
528
  }
87
529
  }),
88
- ask_question: defineTool({
530
+ ask_question: defineTool6({
89
531
  description: "Raise a question a human operator must answer. Creates a question task that waits for the answer.",
90
- inputSchema: z.object({
91
- repositoryIds: z.array(repositoryIdSchema).min(1),
532
+ inputSchema: z6.object({
533
+ repositoryIds: z6.array(repositoryIdSchema).min(1),
92
534
  parentTaskId: taskIdSchema.optional().describe("Task the answer unblocks, when there is one."),
93
- channel: z.string().min(1).describe("Reply connector, e.g. github."),
94
- address: z.string().min(1).describe("Reply address, e.g. acme/web#41."),
535
+ channel: z6.string().min(1).describe("Reply connector, e.g. github."),
536
+ address: z6.string().min(1).describe("Reply address, e.g. acme/web#41."),
95
537
  question: taskQuestionSchema.omit({ answer: true, answeredBy: true })
96
538
  }),
97
539
  async execute({ repositoryIds, parentTaskId, channel, address, question }) {
@@ -105,16 +547,16 @@ function factoryTools(stores) {
105
547
  return stores.tasks.transition(task.id, "needs_human");
106
548
  }
107
549
  }),
108
- append_receipt: defineTool({
550
+ append_receipt: defineTool6({
109
551
  description: "Append one immutable audit receipt for an event worth remembering. Receipts can never be edited or deleted.",
110
- inputSchema: z.object({
111
- repositoryIds: z.array(repositoryIdSchema).min(1),
552
+ inputSchema: z6.object({
553
+ repositoryIds: z6.array(repositoryIdSchema).min(1),
112
554
  taskId: taskIdSchema.optional(),
113
555
  changeId: changeIdSchema.optional(),
114
556
  kind: vocabularySchema.describe("What the receipt is about, e.g. investigation."),
115
557
  state: vocabularySchema.describe("What happened, e.g. flag_ramped."),
116
- reason: z.string().min(1).max(2e3).optional(),
117
- dedupeKey: z.string().min(1).optional().describe("Same key twice records one receipt instead of two.")
558
+ reason: z6.string().min(1).max(2e3).optional(),
559
+ dedupeKey: z6.string().min(1).optional().describe("Same key twice records one receipt instead of two.")
118
560
  }),
119
561
  execute(input) {
120
562
  return stores.receipts.append(input);
@@ -122,7 +564,38 @@ function factoryTools(stores) {
122
564
  })
123
565
  };
124
566
  }
567
+ function factoryHooks(stores) {
568
+ return {
569
+ factory: defineHook({
570
+ events: {
571
+ async "turn.completed"(_event, ctx) {
572
+ const address = ctx.channel.continuationToken;
573
+ if (address === void 0) {
574
+ return;
575
+ }
576
+ const tasks = await stores.tasks.list();
577
+ const abandoned = tasks.filter(
578
+ (task) => task.state === "running" && task.replyTo.address === address
579
+ );
580
+ for (const task of abandoned) {
581
+ await stores.tasks.transition(task.id, "needs_human", {
582
+ reason: "agent turn ended without recording an outcome"
583
+ }).catch(() => void 0);
584
+ }
585
+ }
586
+ }
587
+ })
588
+ };
589
+ }
125
590
  export {
126
- factoryTools
591
+ deepsecScanTool,
592
+ factoryHooks,
593
+ factoryTools,
594
+ fetchPullRequestTool,
595
+ findingFromDeepsec,
596
+ invokeAgentTool,
597
+ recordFinding,
598
+ recordFindingTool,
599
+ recordVerificationTool
127
600
  };
128
601
  //# sourceMappingURL=index.js.map