@vercel/factory 0.0.10 → 0.0.12

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.
@@ -263,7 +263,10 @@ interface CreateTaskInput {
263
263
  kind: Task["kind"];
264
264
  origin: Task["origin"];
265
265
  replyTo: ReplyTo;
266
+ /** Same key creates one task and returns it on workflow retries. */
267
+ dedupeKey?: string;
266
268
  area?: string;
269
+ changeId?: ChangeId;
267
270
  dependsOn?: TaskId[];
268
271
  question?: Task["question"];
269
272
  /** Stamp the task as needing a human decision before dispatch. */
@@ -516,4 +519,4 @@ interface ApprovalSubject {
516
519
  declare function requiresApproval(policy: ApprovalPolicy, subject: ApprovalSubject): boolean;
517
520
  //#endregion
518
521
  export { Cost as $, FactoryStores as A, repositoryIdsSchema as At, standardTaskKinds as B, ChangePatch as C, RepositoryId as Ct, CreateStoresOptions as D, factoryIdSchema as Dt, CreateSignalInput as E, changeIdSchema as Et, TaskApproval as F, taskOriginSchema as G, taskDevboxSchema as H, TaskKind as I, taskStateSchema as J, taskQuestionSchema as K, TaskState as L, TaskTransitionOptions as M, taskIdSchema as Mt, createStores as N, CreateTaskInput as O, receiptIdSchema as Ot, Task as P, repositorySlugSchema as Q, coordinationRepositoryIdsSchema as R, ChangeAssociationError as S, ReceiptId as St, CreateChangeInput as T, TaskId as Tt, taskExecutionRepositoryIdsSchema as U, taskApprovalSchema as V, taskKindSchema as W, RepositorySlug as X, Repository as Y, repositorySchema as Z, findingVerificationMethodSchema as _, changeSchema as _t, approvalRuleSchema as a, receiptKindSchema as at, ApprovalDecision as b, ChangeId as bt, Finding as c, receiptStateSchema as ct, FindingVerification as d, standardReceiptStates as dt, Receipt as et, findingCategorySchema as f, Change as ft, findingVerificationLevelSchema as g, changeEvidenceSchema as gt, findingSchema as h, Level as ht, approvalPolicySchema as i, costSchema as it, TaskPatch as j, signalIdSchema as jt, DecideSignalOptions as k, repositoryIdSchema as kt, FindingCategory as l, standardReceiptKinds as lt, findingPrioritySchema as m, ChangeState as mt, ApprovalRule as n, ReceiptPhase as nt, defaultApprovalPolicy as o, receiptPhaseSchema as ot, findingFingerprint as p, ChangeEvidence as pt, taskSchema as q, ApprovalSubject as r, ReceiptState as rt, requiresApproval as s, receiptSchema as st, ApprovalPolicy as t, ReceiptKind as tt, FindingPriority as u, standardReceiptPhases as ut, findingVerificationSchema as v, changeStateSchema as vt, ChangeTransitionOptions as w, SignalId as wt, ApprovalError as x, FactoryId as xt, AppendReceiptInput as y, levelSchema as yt, harnessSchema as z };
519
- //# sourceMappingURL=approval-CDiBco2V.d.mts.map
522
+ //# sourceMappingURL=approval-CIXB8IF8.d.mts.map
@@ -63,7 +63,7 @@ const taskSchema = z.strictObject({
63
63
  reservedUsd: usdSchema.optional().describe("Budget reserved before dispatch."),
64
64
  settledUsd: usdSchema.optional().describe("Measured cost settled after execution."),
65
65
  question: taskQuestionSchema.optional(),
66
- changeId: changeIdSchema.optional().describe("Change this code_change task produced."),
66
+ changeId: changeIdSchema.optional().describe("Change produced or verified by this task."),
67
67
  reportRef: z.string().min(1).optional().describe("Reference to the report artifact an investigation produced."),
68
68
  createdAt: isoDateTimeSchema,
69
69
  updatedAt: isoDateTimeSchema
@@ -104,10 +104,10 @@ const taskSchema = z.strictObject({
104
104
  path: ["devbox"],
105
105
  message: "coordination tasks never run in a devbox"
106
106
  });
107
- if (task.changeId !== void 0 && task.kind !== "code_change") ctx.addIssue({
107
+ if (task.changeId !== void 0 && task.kind !== "code_change" && task.kind !== "verification") ctx.addIssue({
108
108
  code: "custom",
109
109
  path: ["changeId"],
110
- message: "changeId is only valid on code_change tasks"
110
+ message: "changeId is only valid on code_change or verification tasks"
111
111
  });
112
112
  if (task.reportRef !== void 0 && task.kind !== "investigation") ctx.addIssue({
113
113
  code: "custom",
@@ -227,4 +227,4 @@ const changeSchema = z.strictObject({
227
227
  //#endregion
228
228
  export { taskSchema as _, DEFAULT_MAX_LAUNCH_ATTEMPTS as a, coordinationRepositoryIdsSchema as c, taskApprovalSchema as d, taskDevboxSchema as f, taskQuestionSchema as g, taskOriginSchema as h, levelSchema as i, harnessSchema as l, taskKindSchema as m, changeSchema as n, DispatchRefusedError as o, taskExecutionRepositoryIdsSchema as p, changeStateSchema as r, dispatchTask as s, changeEvidenceSchema as t, standardTaskKinds as u, taskStateSchema as v };
229
229
 
230
- //# sourceMappingURL=change-BPYhWKuL.mjs.map
230
+ //# sourceMappingURL=change-CFNGXVqD.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"change-CFNGXVqD.mjs","names":[],"sources":["../src/schema/task.ts","../src/dispatch.ts","../src/schema/change.ts"],"sourcesContent":["import { z } from \"zod\";\nimport {\n isoDateTimeSchema,\n replyToSchema,\n schemaVersionSchema,\n usdSchema,\n vocabularySchema,\n} from \"./common\";\nimport {\n changeIdSchema,\n repositoryIdSchema,\n repositoryIdsSchema,\n signalIdSchema,\n taskIdSchema,\n} from \"./id\";\n\n/**\n * Kinds the standard loop understands. Any vocabulary value is valid; the\n * kind-scoped rules below apply only to these standard kinds.\n */\nexport const standardTaskKinds = [\n \"coordination\",\n \"code_change\",\n \"verification\",\n \"investigation\",\n \"question\",\n] as const;\n\nexport const taskKindSchema = vocabularySchema.describe(\n \"What kind of work this is, e.g. code_change, question, or a registered custom kind.\",\n);\n\nexport const taskStateSchema = z\n .enum([\"queued\", \"running\", \"verifying\", \"succeeded\", \"failed\", \"needs_human\", \"cancelled\"])\n .describe(\"Lifecycle state. Transitions are fixed; see taskTransitions.\");\n\nexport const taskApprovalSchema = z\n .enum([\"required\", \"granted\", \"denied\"])\n .describe(\n \"Human approval status. Absent means no approval is needed. Stamped required at creation by policy; granted or denied by an operator.\",\n );\n\nexport const taskExecutionRepositoryIdsSchema = z\n .tuple([repositoryIdSchema])\n .describe(\"The one repository used to execute a task.\");\n\nexport const coordinationRepositoryIdsSchema = z\n .tuple([repositoryIdSchema, repositoryIdSchema])\n .rest(repositoryIdSchema)\n .refine((repositoryIds) => new Set(repositoryIds).size === repositoryIds.length, {\n message: \"Coordination tasks require distinct repositories.\",\n })\n .describe(\"The distinct repositories coordinated by a non-executable parent task.\");\n\nexport const harnessSchema = z\n .string()\n .min(1)\n .describe(\"Coding harness used in the devbox, e.g. claude-code or codex.\");\n\nexport const taskOriginSchema = z\n .strictObject({\n signalId: signalIdSchema.optional().describe(\"Signal that led to this task.\"),\n parentTaskId: taskIdSchema.optional().describe(\"Parent task that planned this child.\"),\n operator: z.string().min(1).optional().describe(\"Human operator who requested it.\"),\n })\n .refine(\n (origin) =>\n origin.signalId !== undefined ||\n origin.parentTaskId !== undefined ||\n origin.operator !== undefined,\n { message: \"origin requires a signalId, parentTaskId, or operator\" },\n );\n\nexport const taskDevboxSchema = z.strictObject({\n devboxId: z.string().min(1).describe(\"Devbox running or having run this task.\"),\n remoteId: z.string().min(1).describe(\"Provider-side id of the execution.\"),\n harness: harnessSchema,\n});\n\nexport const taskQuestionSchema = z.strictObject({\n text: z.string().min(1).describe(\"The question a human must answer.\"),\n options: z.array(z.string().min(1)).describe(\"Suggested answers; free-text is also allowed.\"),\n answer: z.string().min(1).optional(),\n answeredBy: z.string().min(1).optional().describe(\"Authenticated operator who answered.\"),\n});\n\nexport const taskSchema = z\n .strictObject({\n schemaVersion: schemaVersionSchema,\n id: taskIdSchema,\n repositoryIds: repositoryIdsSchema.describe(\"Repositories the task concerns.\"),\n kind: taskKindSchema,\n area: z\n .string()\n .min(1)\n .optional()\n .describe(\"Investigation area, e.g. onboarding or security. Investigation tasks only.\"),\n state: taskStateSchema,\n approval: taskApprovalSchema.optional(),\n origin: taskOriginSchema,\n dependsOn: z\n .array(taskIdSchema)\n .min(1)\n .optional()\n .describe(\"Sibling tasks that must produce a change before this one may run.\"),\n replyTo: replyToSchema,\n devbox: taskDevboxSchema.optional(),\n attempt: z\n .number()\n .int()\n .min(1)\n .describe(\"Execution attempt, starting at 1; bumps on retry.\"),\n reservedUsd: usdSchema.optional().describe(\"Budget reserved before dispatch.\"),\n settledUsd: usdSchema.optional().describe(\"Measured cost settled after execution.\"),\n question: taskQuestionSchema.optional(),\n changeId: changeIdSchema.optional().describe(\"Change produced or verified by this task.\"),\n reportRef: z\n .string()\n .min(1)\n .optional()\n .describe(\"Reference to the report artifact an investigation produced.\"),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n })\n .superRefine((task, ctx) => {\n if (\n task.kind === \"coordination\" &&\n !coordinationRepositoryIdsSchema.safeParse(task.repositoryIds).success\n ) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"repositoryIds\"],\n message: \"coordination tasks require at least two distinct repositories\",\n });\n }\n if (\n (task.kind === \"code_change\" || task.kind === \"verification\") &&\n task.repositoryIds.length !== 1\n ) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"repositoryIds\"],\n message: `${task.kind} tasks require exactly one repository`,\n });\n }\n if (task.area !== undefined && task.kind !== \"investigation\") {\n ctx.addIssue({\n code: \"custom\",\n path: [\"area\"],\n message: \"area is only valid on investigation tasks\",\n });\n }\n if (task.kind === \"question\") {\n if (task.question === undefined) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"question\"],\n message: \"question tasks require a question\",\n });\n }\n if (task.devbox !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"devbox\"],\n message: \"question tasks never run in a devbox\",\n });\n }\n } else if (task.question !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"question\"],\n message: \"question is only valid on question tasks\",\n });\n }\n if (task.kind === \"coordination\" && task.devbox !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"devbox\"],\n message: \"coordination tasks never run in a devbox\",\n });\n }\n if (\n task.changeId !== undefined &&\n task.kind !== \"code_change\" &&\n task.kind !== \"verification\"\n ) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"changeId\"],\n message: \"changeId is only valid on code_change or verification tasks\",\n });\n }\n if (task.reportRef !== undefined && task.kind !== \"investigation\") {\n ctx.addIssue({\n code: \"custom\",\n path: [\"reportRef\"],\n message: \"reportRef is only valid on investigation tasks\",\n });\n }\n });\n\nexport type TaskApproval = z.infer<typeof taskApprovalSchema>;\nexport type TaskKind = z.infer<typeof taskKindSchema>;\nexport type TaskState = z.infer<typeof taskStateSchema>;\nexport type Task = z.infer<typeof taskSchema>;\n","import type { TaskId } from \"./schema/id\";\nimport { taskExecutionRepositoryIdsSchema, type Task } from \"./schema/task\";\nimport { RecordNotFoundError } from \"./store/driver\";\nimport type { FactoryStores } from \"./store/engine\";\n\nexport class DispatchRefusedError extends Error {\n readonly taskId: TaskId;\n readonly reason: string;\n\n constructor(taskId: TaskId, reason: string) {\n super(`dispatch of \"${taskId}\" refused: ${reason}`);\n this.name = \"DispatchRefusedError\";\n this.taskId = taskId;\n this.reason = reason;\n }\n}\n\nexport interface DispatchOptions {\n /**\n * Starts the durable execution for the task (an Eve agent run). Runs after\n * the task has won the queued -> running transition. The execution owns its\n * own provisioning: it creates its Devbox as its first step and patches the\n * task record with the result.\n */\n launch: (task: Task) => Promise<void>;\n /** How many launches to try before the task fails for good; a failure re-queues for the next sweep. */\n maxLaunchAttempts?: number;\n}\n\n// A fresh Vercel project rejects its own agents' sessions until its identity\n// finishes provisioning; at one sweep per minute the budget must outlast that.\nexport const DEFAULT_MAX_LAUNCH_ATTEMPTS = 45;\n\n/**\n * The handshake between recorded work and execution: win the store-gated\n * transition (at most one dispatcher can succeed), then start the run. A\n * launch failure re-queues the task (bumping its attempt) until the attempt\n * limit, then fails it; a crash between the transition and the launch leaves\n * a running task with no execution, which the sweep reaps.\n */\nexport async function dispatchTask(\n stores: FactoryStores,\n taskId: TaskId,\n options: DispatchOptions,\n): Promise<Task> {\n const task = await stores.tasks.get(taskId);\n if (task === null) {\n throw new RecordNotFoundError(\"tasks\", taskId);\n }\n if (task.kind === \"question\") {\n throw new DispatchRefusedError(taskId, \"question tasks are answered, not dispatched\");\n }\n if (task.kind === \"coordination\") {\n throw new DispatchRefusedError(taskId, \"coordination tasks are reconciled, not dispatched\");\n }\n if (!taskExecutionRepositoryIdsSchema.safeParse(task.repositoryIds).success) {\n throw new DispatchRefusedError(taskId, \"task execution requires exactly one repository\");\n }\n if (task.approval === \"required\") {\n throw new DispatchRefusedError(taskId, \"awaiting human approval\");\n }\n if (task.approval === \"denied\") {\n throw new DispatchRefusedError(taskId, \"approval was denied\");\n }\n for (const dependencyId of task.dependsOn ?? []) {\n const dependency = await stores.tasks.get(dependencyId);\n if (dependency === null) {\n throw new DispatchRefusedError(taskId, `dependency \"${dependencyId}\" does not exist`);\n }\n if (dependency.changeId === undefined) {\n throw new DispatchRefusedError(\n taskId,\n `dependency \"${dependencyId}\" has not produced a change`,\n );\n }\n }\n\n const running = await stores.tasks.transition(taskId, \"running\");\n try {\n await options.launch(running);\n } catch (error) {\n const reason = (error instanceof Error ? error.message : String(error)).slice(0, 500);\n const limit = options.maxLaunchAttempts ?? DEFAULT_MAX_LAUNCH_ATTEMPTS;\n if (running.attempt < limit) {\n const retryReason = `launch failed (attempt ${running.attempt} of ${limit}), will retry: ${reason}`;\n await stores.tasks.transition(taskId, \"queued\", { reason: retryReason }).catch(() => undefined);\n // The queued transition writes no receipt; leave one so the retry is visible.\n if (running.repositoryIds !== undefined) {\n await stores.receipts\n .append({\n repositoryIds: running.repositoryIds,\n taskId,\n kind: \"task\",\n state: \"launch_retry\",\n reason: retryReason,\n dedupeKey: `task:${taskId}:launch_retry:${running.attempt}`,\n })\n .catch(() => undefined);\n }\n } else {\n await stores.tasks\n .transition(taskId, \"failed\", { reason: `launch failed after ${running.attempt} attempts: ${reason}` })\n .catch(() => undefined);\n }\n throw error;\n }\n return running;\n}\n","import { z } from \"zod\";\nimport { isoDateTimeSchema, schemaVersionSchema } from \"./common\";\nimport { changeIdSchema, repositoryIdSchema, taskIdSchema } from \"./id\";\n\nexport const levelSchema = z.enum([\"low\", \"medium\", \"high\"]);\n\nexport const changeStateSchema = z.enum([\n \"verifying\",\n \"ready\",\n \"merged\",\n \"deployed\",\n \"rolled_out\",\n \"rolled_back\",\n \"closed\",\n]);\n\nconst postMergeStates: readonly string[] = [\"merged\", \"deployed\", \"rolled_out\", \"rolled_back\"];\n\nexport const changeEvidenceSchema = z.strictObject({\n reproVerified: z.boolean(),\n testsAdded: z.number().int().min(0),\n checksGreen: z.boolean(),\n attackPassed: z.boolean(),\n scopeMatches: z.boolean(),\n previewUrl: z.url().optional(),\n});\n\nexport const changeSchema = z\n .strictObject({\n schemaVersion: schemaVersionSchema,\n id: changeIdSchema,\n repositoryId: repositoryIdSchema,\n taskId: taskIdSchema.optional(),\n groupId: taskIdSchema.optional(),\n prNumber: z.number().int().min(1),\n prUrl: z.url(),\n author: z.string().min(1),\n risk: levelSchema,\n confidence: levelSchema,\n evidence: changeEvidenceSchema,\n state: changeStateSchema,\n mergeSha: z\n .string()\n .regex(/^[0-9a-f]{40}$/)\n .optional()\n .describe(\"Merge commit sha recorded when the pull request merges, matched against production deployments.\"),\n decidedBy: z.string().min(1).optional(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n })\n .superRefine((change, ctx) => {\n if (postMergeStates.includes(change.state) && change.decidedBy === undefined) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"decidedBy\"],\n message: `${change.state} changes require decidedBy`,\n });\n }\n });\n\nexport type Level = z.infer<typeof levelSchema>;\nexport type ChangeState = z.infer<typeof changeStateSchema>;\nexport type ChangeEvidence = z.infer<typeof changeEvidenceSchema>;\nexport type Change = z.infer<typeof changeSchema>;\n"],"mappings":"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"}
@@ -38,11 +38,11 @@ interface SlackMessageEvent {
38
38
  actorId: string;
39
39
  text: string;
40
40
  replyAddress: string;
41
- repositorySlug: string;
41
+ repositorySlugs: readonly [string, ...string[]];
42
42
  payload: unknown;
43
43
  }
44
44
  interface SlackMessageSignal extends NormalizedSignal<"slack"> {
45
- repositorySlug: string;
45
+ repositorySlugs: readonly [string, ...string[]];
46
46
  message: {
47
47
  teamId: string;
48
48
  channelId: string;
@@ -52,7 +52,7 @@ interface SlackMessageSignal extends NormalizedSignal<"slack"> {
52
52
  text: string;
53
53
  };
54
54
  }
55
- /** Normalize an Eve-verified Slack message after the caller resolves its repository. */
55
+ /** Normalize an Eve-verified Slack message after the caller resolves its repositories. */
56
56
  declare function normalizeSlackMessage(event: SlackMessageEvent): SlackMessageSignal;
57
57
  type SlackRepositorySelection = {
58
58
  status: "selected";
@@ -63,11 +63,19 @@ type SlackRepositorySelection = {
63
63
  status: "ambiguous";
64
64
  repositorySlugs: readonly string[];
65
65
  };
66
+ type SlackRepositoriesSelection = {
67
+ status: "selected";
68
+ repositorySlugs: readonly [string, ...string[]];
69
+ } | {
70
+ status: "required";
71
+ };
72
+ /** Select every explicitly named configured repository, preserving configuration order. */
73
+ declare function selectSlackRepositories(text: string, repositories: readonly string[]): SlackRepositoriesSelection;
66
74
  /**
67
75
  * Select the only configured repository, or require an explicit owner/name
68
76
  * when a Slack surface fronts more than one repository.
69
77
  */
70
78
  declare function selectSlackRepository(text: string, repositories: readonly string[]): SlackRepositorySelection;
71
79
  //#endregion
72
- export { SlackCommunicationAdapter, SlackCommunicationAdapterOptions, SlackCommunicationDelivery, SlackMessageEvent, SlackMessageSignal, SlackReplyTarget, SlackRepositorySelection, createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackRepository };
80
+ export { SlackCommunicationAdapter, SlackCommunicationAdapterOptions, SlackCommunicationDelivery, SlackMessageEvent, SlackMessageSignal, SlackReplyTarget, SlackRepositoriesSelection, SlackRepositorySelection, createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackRepositories, selectSlackRepository };
73
81
  //# sourceMappingURL=slack.d.mts.map
@@ -89,12 +89,12 @@ function createSlackCommunicationAdapter(options) {
89
89
  }
90
90
  });
91
91
  }
92
- /** Normalize an Eve-verified Slack message after the caller resolves its repository. */
92
+ /** Normalize an Eve-verified Slack message after the caller resolves its repositories. */
93
93
  function normalizeSlackMessage(event) {
94
94
  return {
95
95
  source: "slack",
96
96
  deliveryId: `${event.teamId}:${event.channelId}:${event.messageTs}`,
97
- repositorySlug: event.repositorySlug.toLowerCase(),
97
+ repositorySlugs: [event.repositorySlugs[0].toLowerCase(), ...event.repositorySlugs.slice(1).map((slug) => slug.toLowerCase())],
98
98
  replyTo: {
99
99
  channel: "slack",
100
100
  address: event.replyAddress
@@ -110,6 +110,22 @@ function normalizeSlackMessage(event) {
110
110
  payload: event.payload
111
111
  };
112
112
  }
113
+ /** Select every explicitly named configured repository, preserving configuration order. */
114
+ function selectSlackRepositories(text, repositories) {
115
+ const normalized = [...new Set(repositories.map((slug) => slug.toLowerCase()))];
116
+ if (normalized.length === 1) return {
117
+ status: "selected",
118
+ repositorySlugs: [normalized[0]]
119
+ };
120
+ const matches = normalized.filter((slug) => {
121
+ const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
122
+ return new RegExp(`(^|[^a-z0-9._/-])${escaped}($|[^a-z0-9._/-])`, "i").test(text);
123
+ });
124
+ return matches.length === 0 ? { status: "required" } : {
125
+ status: "selected",
126
+ repositorySlugs: [matches[0], ...matches.slice(1)]
127
+ };
128
+ }
113
129
  /**
114
130
  * Select the only configured repository, or require an explicit owner/name
115
131
  * when a Slack surface fronts more than one repository.
@@ -135,6 +151,6 @@ function selectSlackRepository(text, repositories) {
135
151
  return { status: "required" };
136
152
  }
137
153
  //#endregion
138
- export { createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackRepository };
154
+ export { createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackRepositories, selectSlackRepository };
139
155
 
140
156
  //# sourceMappingURL=slack.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack.mjs","names":[],"sources":["../../src/connectors/slack.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n communicationEventSchema,\n type CommunicationAdapter,\n type CommunicationEvent,\n type CommunicationOutcome,\n type NormalizedSignal,\n} from \"./index\";\n\nconst slackOutcomeLabels: Record<CommunicationOutcome, string> = {\n accepted: \"Request accepted\",\n started: \"Work started\",\n succeeded: \"Work completed\",\n failed: \"Work failed\",\n needs_human: \"Human action required\",\n question: \"Question requires an answer\",\n digest: \"Factory digest\",\n rolled_back: \"Change rolled back\",\n budget_warning: \"Factory budget warning\",\n budget_exhausted: \"Factory budget exhausted\",\n message: \"Factory update\",\n};\n\nconst slackDetailFields = [\"title\", \"message\", \"summary\", \"question\", \"requestedAction\", \"reason\", \"url\"] as const;\nconst slackListFields = [\"items\", \"options\"] as const;\nconst slackTextLimit = 12_000;\nconst slackOwnerOutcomes = new Set<CommunicationOutcome>([\n \"digest\",\n \"budget_warning\",\n \"budget_exhausted\",\n]);\n\nexport interface SlackCommunicationDelivery {\n eventId: string;\n clientMessageId: string;\n text: string;\n target:\n | { type: \"owner\" }\n | { type: \"thread\"; channelId: string; threadTs: string };\n}\n\nexport interface SlackCommunicationAdapter extends CommunicationAdapter {\n readonly channel: \"slack\";\n render(event: CommunicationEvent): string;\n}\n\nexport interface SlackCommunicationAdapterOptions {\n send(delivery: SlackCommunicationDelivery): Promise<void>;\n render?: (event: CommunicationEvent) => string;\n}\n\nexport interface SlackReplyTarget {\n channelId: string;\n threadTs: string;\n}\n\n/** Parse Eve's Slack continuation token into its channel and thread target. */\nexport function parseSlackReplyAddress(address: string): SlackReplyTarget {\n const separator = address.indexOf(\":\");\n const channelId = address.slice(0, separator);\n const threadTs = address.slice(separator + 1);\n if (separator <= 0 || !/^[A-Z][A-Z0-9]+$/.test(channelId) || !/^\\d+\\.\\d+$/.test(threadTs)) {\n throw new Error(`Invalid Slack reply address: ${address}.`);\n }\n return { channelId, threadTs };\n}\n\n/** Render a provider-neutral communication event as plain Slack text. */\nexport function renderSlackCommunicationEvent(input: CommunicationEvent): string {\n const event = communicationEventSchema.parse(input);\n const headline = slackOutcomeLabels[event.outcome];\n const details = slackDetailFields.flatMap((field) => {\n const value = event.data[field];\n return typeof value === \"string\" && value.trim() !== \"\" ? [value.trim()] : [];\n });\n const lists = slackListFields.flatMap((field) => {\n const value = event.data[field];\n if (!Array.isArray(value)) return [];\n return value.flatMap((item) =>\n typeof item === \"string\" && item.trim() !== \"\" ? [`- ${item.trim()}`] : []\n );\n });\n const text = [headline, ...new Set([...details, ...lists])].join(\"\\n\");\n return text.length <= slackTextLimit ? text : `${text.slice(0, slackTextLimit - 1)}…`;\n}\n\nfunction slackClientMessageId(eventId: string): string {\n const digest = createHash(\"sha256\").update(eventId).digest(\"hex\").slice(0, 32);\n return [\n digest.slice(0, 8),\n digest.slice(8, 12),\n digest.slice(12, 16),\n digest.slice(16, 20),\n digest.slice(20),\n ].join(\"-\");\n}\n\n/** Bind Slack rendering and delivery to the channel-neutral communication contract. */\nexport function createSlackCommunicationAdapter(\n options: SlackCommunicationAdapterOptions,\n): SlackCommunicationAdapter {\n const render = options.render ?? renderSlackCommunicationEvent;\n return Object.freeze({\n channel: \"slack\" as const,\n render,\n async deliver(input: CommunicationEvent): Promise<void> {\n const event = communicationEventSchema.parse(input);\n if (event.replyTo.channel !== \"slack\") {\n throw new Error(`Slack communication adapter cannot deliver to ${event.replyTo.channel}.`);\n }\n const target = slackOwnerOutcomes.has(event.outcome)\n ? { type: \"owner\" as const }\n : { type: \"thread\" as const, ...parseSlackReplyAddress(event.replyTo.address) };\n await options.send({\n eventId: event.eventId,\n clientMessageId: slackClientMessageId(event.eventId),\n text: render(event),\n target,\n });\n },\n });\n}\n\nexport interface SlackMessageEvent {\n teamId: string;\n channelId: string;\n messageTs: string;\n threadTs: string;\n actorId: string;\n text: string;\n replyAddress: string;\n repositorySlug: string;\n payload: unknown;\n}\n\nexport interface SlackMessageSignal extends NormalizedSignal<\"slack\"> {\n repositorySlug: string;\n message: {\n teamId: string;\n channelId: string;\n messageTs: string;\n threadTs: string;\n actorId: string;\n text: string;\n };\n}\n\n/** Normalize an Eve-verified Slack message after the caller resolves its repository. */\nexport function normalizeSlackMessage(event: SlackMessageEvent): SlackMessageSignal {\n return {\n source: \"slack\",\n deliveryId: `${event.teamId}:${event.channelId}:${event.messageTs}`,\n repositorySlug: event.repositorySlug.toLowerCase(),\n replyTo: { channel: \"slack\", address: event.replyAddress },\n message: {\n teamId: event.teamId,\n channelId: event.channelId,\n messageTs: event.messageTs,\n threadTs: event.threadTs,\n actorId: event.actorId,\n text: event.text,\n },\n payload: event.payload,\n };\n}\n\nexport type SlackRepositorySelection =\n | { status: \"selected\"; repositorySlug: string }\n | { status: \"required\" }\n | { status: \"ambiguous\"; repositorySlugs: readonly string[] };\n\n/**\n * Select the only configured repository, or require an explicit owner/name\n * when a Slack surface fronts more than one repository.\n */\nexport function selectSlackRepository(\n text: string,\n repositories: readonly string[],\n): SlackRepositorySelection {\n const normalized = [...new Set(repositories.map((slug) => slug.toLowerCase()))];\n if (normalized.length === 1) {\n return { status: \"selected\", repositorySlug: normalized[0]! };\n }\n\n const matches = normalized.filter((slug) => {\n const escaped = slug.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`(^|[^a-z0-9._/-])${escaped}($|[^a-z0-9._/-])`, \"i\").test(text);\n });\n if (matches.length === 1) {\n return { status: \"selected\", repositorySlug: matches[0]! };\n }\n if (matches.length > 1) {\n return { status: \"ambiguous\", repositorySlugs: matches };\n }\n return { status: \"required\" };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"slack.mjs","names":[],"sources":["../../src/connectors/slack.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n communicationEventSchema,\n type CommunicationAdapter,\n type CommunicationEvent,\n type CommunicationOutcome,\n type NormalizedSignal,\n} from \"./index\";\n\nconst slackOutcomeLabels: Record<CommunicationOutcome, string> = {\n accepted: \"Request accepted\",\n started: \"Work started\",\n succeeded: \"Work completed\",\n failed: \"Work failed\",\n needs_human: \"Human action required\",\n question: \"Question requires an answer\",\n digest: \"Factory digest\",\n rolled_back: \"Change rolled back\",\n budget_warning: \"Factory budget warning\",\n budget_exhausted: \"Factory budget exhausted\",\n message: \"Factory update\",\n};\n\nconst slackDetailFields = [\"title\", \"message\", \"summary\", \"question\", \"requestedAction\", \"reason\", \"url\"] as const;\nconst slackListFields = [\"items\", \"options\"] as const;\nconst slackTextLimit = 12_000;\nconst slackOwnerOutcomes = new Set<CommunicationOutcome>([\n \"digest\",\n \"budget_warning\",\n \"budget_exhausted\",\n]);\n\nexport interface SlackCommunicationDelivery {\n eventId: string;\n clientMessageId: string;\n text: string;\n target:\n | { type: \"owner\" }\n | { type: \"thread\"; channelId: string; threadTs: string };\n}\n\nexport interface SlackCommunicationAdapter extends CommunicationAdapter {\n readonly channel: \"slack\";\n render(event: CommunicationEvent): string;\n}\n\nexport interface SlackCommunicationAdapterOptions {\n send(delivery: SlackCommunicationDelivery): Promise<void>;\n render?: (event: CommunicationEvent) => string;\n}\n\nexport interface SlackReplyTarget {\n channelId: string;\n threadTs: string;\n}\n\n/** Parse Eve's Slack continuation token into its channel and thread target. */\nexport function parseSlackReplyAddress(address: string): SlackReplyTarget {\n const separator = address.indexOf(\":\");\n const channelId = address.slice(0, separator);\n const threadTs = address.slice(separator + 1);\n if (separator <= 0 || !/^[A-Z][A-Z0-9]+$/.test(channelId) || !/^\\d+\\.\\d+$/.test(threadTs)) {\n throw new Error(`Invalid Slack reply address: ${address}.`);\n }\n return { channelId, threadTs };\n}\n\n/** Render a provider-neutral communication event as plain Slack text. */\nexport function renderSlackCommunicationEvent(input: CommunicationEvent): string {\n const event = communicationEventSchema.parse(input);\n const headline = slackOutcomeLabels[event.outcome];\n const details = slackDetailFields.flatMap((field) => {\n const value = event.data[field];\n return typeof value === \"string\" && value.trim() !== \"\" ? [value.trim()] : [];\n });\n const lists = slackListFields.flatMap((field) => {\n const value = event.data[field];\n if (!Array.isArray(value)) return [];\n return value.flatMap((item) =>\n typeof item === \"string\" && item.trim() !== \"\" ? [`- ${item.trim()}`] : []\n );\n });\n const text = [headline, ...new Set([...details, ...lists])].join(\"\\n\");\n return text.length <= slackTextLimit ? text : `${text.slice(0, slackTextLimit - 1)}…`;\n}\n\nfunction slackClientMessageId(eventId: string): string {\n const digest = createHash(\"sha256\").update(eventId).digest(\"hex\").slice(0, 32);\n return [\n digest.slice(0, 8),\n digest.slice(8, 12),\n digest.slice(12, 16),\n digest.slice(16, 20),\n digest.slice(20),\n ].join(\"-\");\n}\n\n/** Bind Slack rendering and delivery to the channel-neutral communication contract. */\nexport function createSlackCommunicationAdapter(\n options: SlackCommunicationAdapterOptions,\n): SlackCommunicationAdapter {\n const render = options.render ?? renderSlackCommunicationEvent;\n return Object.freeze({\n channel: \"slack\" as const,\n render,\n async deliver(input: CommunicationEvent): Promise<void> {\n const event = communicationEventSchema.parse(input);\n if (event.replyTo.channel !== \"slack\") {\n throw new Error(`Slack communication adapter cannot deliver to ${event.replyTo.channel}.`);\n }\n const target = slackOwnerOutcomes.has(event.outcome)\n ? { type: \"owner\" as const }\n : { type: \"thread\" as const, ...parseSlackReplyAddress(event.replyTo.address) };\n await options.send({\n eventId: event.eventId,\n clientMessageId: slackClientMessageId(event.eventId),\n text: render(event),\n target,\n });\n },\n });\n}\n\nexport interface SlackMessageEvent {\n teamId: string;\n channelId: string;\n messageTs: string;\n threadTs: string;\n actorId: string;\n text: string;\n replyAddress: string;\n repositorySlugs: readonly [string, ...string[]];\n payload: unknown;\n}\n\nexport interface SlackMessageSignal extends NormalizedSignal<\"slack\"> {\n repositorySlugs: readonly [string, ...string[]];\n message: {\n teamId: string;\n channelId: string;\n messageTs: string;\n threadTs: string;\n actorId: string;\n text: string;\n };\n}\n\n/** Normalize an Eve-verified Slack message after the caller resolves its repositories. */\nexport function normalizeSlackMessage(event: SlackMessageEvent): SlackMessageSignal {\n return {\n source: \"slack\",\n deliveryId: `${event.teamId}:${event.channelId}:${event.messageTs}`,\n repositorySlugs: [\n event.repositorySlugs[0].toLowerCase(),\n ...event.repositorySlugs.slice(1).map((slug) => slug.toLowerCase()),\n ],\n replyTo: { channel: \"slack\", address: event.replyAddress },\n message: {\n teamId: event.teamId,\n channelId: event.channelId,\n messageTs: event.messageTs,\n threadTs: event.threadTs,\n actorId: event.actorId,\n text: event.text,\n },\n payload: event.payload,\n };\n}\n\nexport type SlackRepositorySelection =\n | { status: \"selected\"; repositorySlug: string }\n | { status: \"required\" }\n | { status: \"ambiguous\"; repositorySlugs: readonly string[] };\n\nexport type SlackRepositoriesSelection =\n | { status: \"selected\"; repositorySlugs: readonly [string, ...string[]] }\n | { status: \"required\" };\n\n/** Select every explicitly named configured repository, preserving configuration order. */\nexport function selectSlackRepositories(\n text: string,\n repositories: readonly string[],\n): SlackRepositoriesSelection {\n const normalized = [...new Set(repositories.map((slug) => slug.toLowerCase()))];\n if (normalized.length === 1) {\n return { status: \"selected\", repositorySlugs: [normalized[0]!] };\n }\n const matches = normalized.filter((slug) => {\n const escaped = slug.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`(^|[^a-z0-9._/-])${escaped}($|[^a-z0-9._/-])`, \"i\").test(text);\n });\n return matches.length === 0\n ? { status: \"required\" }\n : { status: \"selected\", repositorySlugs: [matches[0]!, ...matches.slice(1)] };\n}\n\n/**\n * Select the only configured repository, or require an explicit owner/name\n * when a Slack surface fronts more than one repository.\n */\nexport function selectSlackRepository(\n text: string,\n repositories: readonly string[],\n): SlackRepositorySelection {\n const normalized = [...new Set(repositories.map((slug) => slug.toLowerCase()))];\n if (normalized.length === 1) {\n return { status: \"selected\", repositorySlug: normalized[0]! };\n }\n\n const matches = normalized.filter((slug) => {\n const escaped = slug.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`(^|[^a-z0-9._/-])${escaped}($|[^a-z0-9._/-])`, \"i\").test(text);\n });\n if (matches.length === 1) {\n return { status: \"selected\", repositorySlug: matches[0]! };\n }\n if (matches.length > 1) {\n return { status: \"ambiguous\", repositorySlugs: matches };\n }\n return { status: \"required\" };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { A as FactoryStores, c as Finding, t as ApprovalPolicy } from "../approval-CDiBco2V.mjs";
1
+ import { A as FactoryStores, P as Task, c as Finding, t as ApprovalPolicy } from "../approval-CIXB8IF8.mjs";
2
2
  import { T as PullRequestReviewComment, b as GitHubReviewComment, j as SubmittedPullRequestReview } from "../github-wThBfbm-.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/eve/fetch.d.ts
@@ -341,7 +341,7 @@ interface InvokeAgentOptions {
341
341
  /** Bearer token for the target agent's eve channel, when it requires auth. */
342
342
  token?: () => Promise<string | undefined>;
343
343
  /** Extra headers for the session request, e.g. a deployment protection bypass. */
344
- headers?: () => Record<string, string> | Promise<Record<string, string>>;
344
+ headers?: (task: Task) => Record<string, string> | Promise<Record<string, string>>;
345
345
  fetch?: typeof fetch;
346
346
  }
347
347
  /**
@@ -1,6 +1,6 @@
1
1
  import { i as repositoryIdSchema, o as signalIdSchema, p as vocabularySchema, s as taskIdSchema, t as changeIdSchema } from "../id-DwCihQr4.mjs";
2
2
  import { c as findingSchema, i as requiresApproval, o as findingFingerprint, r as defaultApprovalPolicy } from "../approval-DdvEKLiE.mjs";
3
- import { g as taskQuestionSchema, i as levelSchema, s as dispatchTask, t as changeEvidenceSchema } from "../change-BPYhWKuL.mjs";
3
+ import { g as taskQuestionSchema, i as levelSchema, s as dispatchTask, t as changeEvidenceSchema } from "../change-CFNGXVqD.mjs";
4
4
  import { answerReviewComment, readPullRequestHead, readReviewComment, submitPullRequestReview } from "../github.mjs";
5
5
  import { z } from "zod";
6
6
  import { defineHook } from "eve/hooks";
@@ -442,7 +442,7 @@ function invokeAgentTool(stores, options) {
442
442
  launch: async (dispatched) => {
443
443
  const headers = {
444
444
  "content-type": "application/json",
445
- ...await options.headers?.()
445
+ ...await options.headers?.(dispatched)
446
446
  };
447
447
  const token = await options.token?.();
448
448
  if (token !== void 0) headers.authorization = `Bearer ${token}`;