@vercel/factory 0.0.12 → 0.0.14

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 (57) hide show
  1. package/README.md +1 -1
  2. package/dist/api.d.mts +225 -0
  3. package/dist/api.mjs +312 -0
  4. package/dist/api.mjs.map +1 -0
  5. package/dist/{approval-DdvEKLiE.mjs → approval-JX2Px9IC.mjs} +2 -2
  6. package/dist/{approval-DdvEKLiE.mjs.map → approval-JX2Px9IC.mjs.map} +1 -1
  7. package/dist/blob/index.d.mts +4 -3
  8. package/dist/blob/index.mjs +21 -15
  9. package/dist/blob/index.mjs.map +1 -1
  10. package/dist/budget-DzSiIA4t.mjs +42 -0
  11. package/dist/budget-DzSiIA4t.mjs.map +1 -0
  12. package/dist/build-factory.d.mts +1 -1
  13. package/dist/build-factory.mjs +1 -1
  14. package/dist/change-D2U6v7Ek.mjs +554 -0
  15. package/dist/change-D2U6v7Ek.mjs.map +1 -0
  16. package/dist/communication-Kd9nU5RP.mjs +30 -0
  17. package/dist/communication-Kd9nU5RP.mjs.map +1 -0
  18. package/dist/connectors/index.d.mts +3 -2
  19. package/dist/connectors/index.mjs +53 -26
  20. package/dist/connectors/index.mjs.map +1 -1
  21. package/dist/connectors/slack.d.mts +17 -6
  22. package/dist/connectors/slack.mjs +99 -32
  23. package/dist/connectors/slack.mjs.map +1 -1
  24. package/dist/coordination-Cq08hb1l.mjs +76 -0
  25. package/dist/coordination-Cq08hb1l.mjs.map +1 -0
  26. package/dist/dispatch-BaZS3PT8.d.mts +76 -0
  27. package/dist/{driver-CrZsy7sm.d.mts → driver-BC0Mt5KG.d.mts} +2 -2
  28. package/dist/driver-Bbd25rNM.mjs.map +1 -1
  29. package/dist/engine-DFHU2Pe-.d.mts +781 -0
  30. package/dist/engine-WaUM2cGA.mjs +1068 -0
  31. package/dist/engine-WaUM2cGA.mjs.map +1 -0
  32. package/dist/eve/index.d.mts +289 -153
  33. package/dist/eve/index.mjs +378 -89
  34. package/dist/eve/index.mjs.map +1 -1
  35. package/dist/{factory-config-ifd3JxiU.mjs → factory-config-BoRXOw43.mjs} +23 -22
  36. package/dist/factory-config-BoRXOw43.mjs.map +1 -0
  37. package/dist/{factory-config-Bu4mklm9.d.mts → factory-config-D2yU3G0c.d.mts} +21 -2
  38. package/dist/{github-wThBfbm-.d.mts → github-C5HNBN6p.d.mts} +34 -3
  39. package/dist/github.d.mts +2 -2
  40. package/dist/github.mjs +61 -1
  41. package/dist/github.mjs.map +1 -1
  42. package/dist/{id-DwCihQr4.mjs → id-BLIkCVue.mjs} +4 -2
  43. package/dist/{id-DwCihQr4.mjs.map → id-BLIkCVue.mjs.map} +1 -1
  44. package/dist/index-Bw6hggoH.d.mts +40 -0
  45. package/dist/index.d.mts +291 -61
  46. package/dist/index.mjs +140 -783
  47. package/dist/index.mjs.map +1 -1
  48. package/dist/postgres/index.d.mts +1 -1
  49. package/dist/task-CcOyOJ3k.mjs +154 -0
  50. package/dist/task-CcOyOJ3k.mjs.map +1 -0
  51. package/package.json +5 -1
  52. package/dist/approval-CIXB8IF8.d.mts +0 -522
  53. package/dist/change-CFNGXVqD.mjs +0 -230
  54. package/dist/change-CFNGXVqD.mjs.map +0 -1
  55. package/dist/factory-config-ifd3JxiU.mjs.map +0 -1
  56. package/dist/index-Cy4jfXh5.d.mts +0 -67
  57. package/dist/signal-CHVEmzvI.d.mts +0 -59
@@ -1,2 +1,3 @@
1
- import { a as NormalizedSignal, c as communicationEventSchema, d as createCommunicationRouter, i as CommunicationRouter, l as communicationOutcomeSchema, n as CommunicationEvent, o as SignalNormalizer, r as CommunicationOutcome, s as WebhookEvent, t as CommunicationAdapter, u as communicationOutcomes } from "../index-Cy4jfXh5.mjs";
2
- export { CommunicationAdapter, CommunicationEvent, CommunicationOutcome, CommunicationRouter, NormalizedSignal, SignalNormalizer, WebhookEvent, communicationEventSchema, communicationOutcomeSchema, communicationOutcomes, createCommunicationRouter };
1
+ import { ct as CommunicationOutcome, dt as communicationOutcomes, lt as communicationEventSchema, st as CommunicationEvent, ut as communicationOutcomeSchema } from "../engine-DFHU2Pe-.mjs";
2
+ import { a as WebhookEvent, c as OutboxSweepResult, i as SignalNormalizer, l as createOutboxCommunicationRouter, n as CommunicationRouter, o as createCommunicationRouter, r as NormalizedSignal, s as OutboxDeliveryOptions, t as CommunicationAdapter, u as sweepCommunicationOutbox } from "../index-Bw6hggoH.mjs";
3
+ export { CommunicationAdapter, type CommunicationEvent, type CommunicationOutcome, CommunicationRouter, NormalizedSignal, OutboxDeliveryOptions, OutboxSweepResult, SignalNormalizer, WebhookEvent, communicationEventSchema, communicationOutcomeSchema, communicationOutcomes, createCommunicationRouter, createOutboxCommunicationRouter, sweepCommunicationOutbox };
@@ -1,29 +1,56 @@
1
- import { a as repositoryIdsSchema, s as taskIdSchema, t as changeIdSchema, u as replyToSchema } from "../id-DwCihQr4.mjs";
2
- import { z } from "zod";
3
- //#region src/connectors/index.ts
4
- const communicationOutcomes = [
5
- "accepted",
6
- "started",
7
- "succeeded",
8
- "failed",
9
- "needs_human",
10
- "question",
11
- "digest",
12
- "rolled_back",
13
- "budget_warning",
14
- "budget_exhausted",
15
- "message"
1
+ import { n as communicationOutcomeSchema, r as communicationOutcomes, t as communicationEventSchema } from "../communication-Kd9nU5RP.mjs";
2
+ //#region src/connectors/outbox.ts
3
+ const defaultRetryDelayMs = (attempt) => Math.min(6e4 * 2 ** Math.max(0, attempt - 1), 36e5);
4
+ const credentialPatterns = [
5
+ /github_pat_[A-Za-z0-9_]+/gu,
6
+ /gh[psoru]_[A-Za-z0-9_]+/gu,
7
+ /xox[baprs]-[A-Za-z0-9-]+/gu,
8
+ /(?:Basic|Bearer) [A-Za-z0-9+/_=.:-]+/gu
16
9
  ];
17
- const communicationOutcomeSchema = z.enum(communicationOutcomes).describe("Lifecycle outcome a connector should render and deliver.");
18
- const communicationEventSchema = z.strictObject({
19
- eventId: z.string().min(1).describe("Stable idempotency key for this delivery."),
20
- repositoryIds: repositoryIdsSchema,
21
- taskId: taskIdSchema.optional(),
22
- changeId: changeIdSchema.optional(),
23
- replyTo: replyToSchema,
24
- outcome: communicationOutcomeSchema,
25
- data: z.record(z.string(), z.unknown()).describe("Outcome-specific data rendered by the selected connector.")
26
- });
10
+ function errorMessage(error) {
11
+ let message = error instanceof Error ? error.message : "Unknown communication delivery error";
12
+ for (const pattern of credentialPatterns) message = message.replace(pattern, (match) => {
13
+ return `${match.startsWith("Basic ") ? "Basic " : match.startsWith("Bearer ") ? "Bearer " : ""}[REDACTED]`;
14
+ });
15
+ return message;
16
+ }
17
+ async function deliverOne(stores, router, id, options) {
18
+ const claimed = await stores.outbox.claim(id, options.leaseMs);
19
+ if (claimed === null) return "skipped";
20
+ try {
21
+ await router.deliver(claimed.event);
22
+ return await stores.outbox.complete(claimed.id, claimed.attempt) === null ? "skipped" : "delivered";
23
+ } catch (error) {
24
+ const delay = (options.retryDelayMs ?? defaultRetryDelayMs)(claimed.attempt);
25
+ return await stores.outbox.retry(claimed.id, claimed.attempt, errorMessage(error), delay) === null ? "skipped" : "retried";
26
+ }
27
+ }
28
+ async function sweepCommunicationOutbox(stores, router, options = {}) {
29
+ const limit = options.limit ?? 100;
30
+ const messages = await stores.outbox.pending(limit);
31
+ const result = {
32
+ delivered: 0,
33
+ retried: 0,
34
+ skipped: 0
35
+ };
36
+ for (const message of messages) {
37
+ const outcome = await deliverOne(stores, router, message.id, options);
38
+ result[outcome] += 1;
39
+ }
40
+ return result;
41
+ }
42
+ function createOutboxCommunicationRouter(stores, router, options = {}) {
43
+ return Object.freeze({
44
+ channels: router.channels,
45
+ async deliver(event) {
46
+ const message = await stores.outbox.enqueue(event);
47
+ if (message.state === "delivered") return;
48
+ await deliverOne(stores, router, message.id, options);
49
+ }
50
+ });
51
+ }
52
+ //#endregion
53
+ //#region src/connectors/index.ts
27
54
  function createCommunicationRouter(adapters) {
28
55
  const byChannel = /* @__PURE__ */ new Map();
29
56
  for (const adapter of adapters) {
@@ -44,6 +71,6 @@ function createCommunicationRouter(adapters) {
44
71
  });
45
72
  }
46
73
  //#endregion
47
- export { communicationEventSchema, communicationOutcomeSchema, communicationOutcomes, createCommunicationRouter };
74
+ export { communicationEventSchema, communicationOutcomeSchema, communicationOutcomes, createCommunicationRouter, createOutboxCommunicationRouter, sweepCommunicationOutbox };
48
75
 
49
76
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/connectors/index.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { changeIdSchema, repositoryIdsSchema, taskIdSchema } from \"../schema/id\";\nimport { replyToSchema } from \"../schema/common\";\nimport type { ReplyTo } from \"../schema/common\";\nimport type { SignalSource } from \"../schema/signal\";\n\nexport interface WebhookEvent {\n deliveryId: string;\n event: string;\n payload: unknown;\n}\n\nexport interface NormalizedSignal<TSource extends SignalSource = SignalSource> {\n source: TSource;\n deliveryId: string;\n replyTo: ReplyTo;\n payload: unknown;\n}\n\nexport type SignalNormalizer<TSignal extends NormalizedSignal = NormalizedSignal> = (\n event: WebhookEvent,\n) => TSignal | null;\n\nexport const communicationOutcomes = [\n \"accepted\",\n \"started\",\n \"succeeded\",\n \"failed\",\n \"needs_human\",\n \"question\",\n \"digest\",\n \"rolled_back\",\n \"budget_warning\",\n \"budget_exhausted\",\n \"message\",\n] as const;\n\nexport const communicationOutcomeSchema = z\n .enum(communicationOutcomes)\n .describe(\"Lifecycle outcome a connector should render and deliver.\");\n\nexport const communicationEventSchema = z.strictObject({\n eventId: z.string().min(1).describe(\"Stable idempotency key for this delivery.\"),\n repositoryIds: repositoryIdsSchema,\n taskId: taskIdSchema.optional(),\n changeId: changeIdSchema.optional(),\n replyTo: replyToSchema,\n outcome: communicationOutcomeSchema,\n data: z\n .record(z.string(), z.unknown())\n .describe(\"Outcome-specific data rendered by the selected connector.\"),\n});\n\nexport type CommunicationOutcome = z.infer<typeof communicationOutcomeSchema>;\nexport type CommunicationEvent = z.infer<typeof communicationEventSchema>;\n\nexport interface CommunicationAdapter {\n readonly channel: string;\n deliver(event: CommunicationEvent): Promise<void>;\n}\n\nexport interface CommunicationRouter {\n readonly channels: readonly string[];\n deliver(event: CommunicationEvent): Promise<void>;\n}\n\nexport function createCommunicationRouter(\n adapters: readonly CommunicationAdapter[],\n): CommunicationRouter {\n const byChannel = new Map<string, CommunicationAdapter>();\n for (const adapter of adapters) {\n const channel = adapter.channel.trim();\n if (channel.length === 0) {\n throw new Error(\"Communication adapter channel is required.\");\n }\n if (byChannel.has(channel)) {\n throw new Error(`Communication adapter ${channel} is registered more than once.`);\n }\n byChannel.set(channel, adapter);\n }\n const channels = Object.freeze([...byChannel.keys()]);\n\n return Object.freeze({\n channels,\n async deliver(input: CommunicationEvent): Promise<void> {\n const event = communicationEventSchema.parse(input);\n const adapter = byChannel.get(event.replyTo.channel);\n if (adapter === undefined) {\n throw new Error(`No communication adapter is registered for ${event.replyTo.channel}.`);\n }\n await adapter.deliver(event);\n },\n });\n}\n"],"mappings":";;;AAuBA,MAAa,wBAAwB;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAa,6BAA6B,EACvC,KAAK,qBAAqB,CAAC,CAC3B,SAAS,0DAA0D;AAEtE,MAAa,2BAA2B,EAAE,aAAa;CACrD,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,2CAA2C;CAC/E,eAAe;CACf,QAAQ,aAAa,SAAS;CAC9B,UAAU,eAAe,SAAS;CAClC,SAAS;CACT,SAAS;CACT,MAAM,EACH,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,CAAC,CAC/B,SAAS,2DAA2D;AACzE,CAAC;AAeD,SAAgB,0BACd,UACqB;CACrB,MAAM,4BAAY,IAAI,IAAkC;CACxD,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,UAAU,QAAQ,QAAQ,KAAK;EACrC,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MAAM,4CAA4C;EAE9D,IAAI,UAAU,IAAI,OAAO,GACvB,MAAM,IAAI,MAAM,yBAAyB,QAAQ,+BAA+B;EAElF,UAAU,IAAI,SAAS,OAAO;CAChC;CACA,MAAM,WAAW,OAAO,OAAO,CAAC,GAAG,UAAU,KAAK,CAAC,CAAC;CAEpD,OAAO,OAAO,OAAO;EACnB;EACA,MAAM,QAAQ,OAA0C;GACtD,MAAM,QAAQ,yBAAyB,MAAM,KAAK;GAClD,MAAM,UAAU,UAAU,IAAI,MAAM,QAAQ,OAAO;GACnD,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MAAM,8CAA8C,MAAM,QAAQ,QAAQ,EAAE;GAExF,MAAM,QAAQ,QAAQ,KAAK;EAC7B;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/connectors/outbox.ts","../../src/connectors/index.ts"],"sourcesContent":["import type { OutboxId } from \"../schema/id\";\nimport type { FactoryStores } from \"../store/engine\";\nimport type { CommunicationEvent, CommunicationRouter } from \"./index\";\n\nexport interface OutboxSweepResult {\n delivered: number;\n retried: number;\n skipped: number;\n}\n\nexport interface OutboxDeliveryOptions {\n leaseMs?: number;\n limit?: number;\n retryDelayMs?: (attempt: number) => number;\n}\n\nconst defaultRetryDelayMs = (attempt: number) =>\n Math.min(60_000 * 2 ** Math.max(0, attempt - 1), 60 * 60_000);\n\nconst credentialPatterns = [\n /github_pat_[A-Za-z0-9_]+/gu,\n /gh[psoru]_[A-Za-z0-9_]+/gu,\n /xox[baprs]-[A-Za-z0-9-]+/gu,\n /(?:Basic|Bearer) [A-Za-z0-9+/_=.:-]+/gu,\n] as const;\n\nfunction errorMessage(error: unknown): string {\n let message = error instanceof Error ? error.message : \"Unknown communication delivery error\";\n for (const pattern of credentialPatterns) {\n message = message.replace(pattern, (match) => {\n const scheme = match.startsWith(\"Basic \")\n ? \"Basic \"\n : match.startsWith(\"Bearer \")\n ? \"Bearer \"\n : \"\";\n return `${scheme}[REDACTED]`;\n });\n }\n return message;\n}\n\nasync function deliverOne(\n stores: FactoryStores,\n router: CommunicationRouter,\n id: OutboxId,\n options: OutboxDeliveryOptions,\n): Promise<\"delivered\" | \"retried\" | \"skipped\"> {\n const claimed = await stores.outbox.claim(id, options.leaseMs);\n if (claimed === null) return \"skipped\";\n try {\n await router.deliver(claimed.event);\n const completed = await stores.outbox.complete(claimed.id, claimed.attempt);\n return completed === null ? \"skipped\" : \"delivered\";\n } catch (error) {\n const delay = (options.retryDelayMs ?? defaultRetryDelayMs)(claimed.attempt);\n const retried = await stores.outbox.retry(\n claimed.id,\n claimed.attempt,\n errorMessage(error),\n delay,\n );\n return retried === null ? \"skipped\" : \"retried\";\n }\n}\n\nexport async function sweepCommunicationOutbox(\n stores: FactoryStores,\n router: CommunicationRouter,\n options: OutboxDeliveryOptions = {},\n): Promise<OutboxSweepResult> {\n const limit = options.limit ?? 100;\n const messages = await stores.outbox.pending(limit);\n const result: OutboxSweepResult = { delivered: 0, retried: 0, skipped: 0 };\n for (const message of messages) {\n const outcome = await deliverOne(stores, router, message.id, options);\n result[outcome] += 1;\n }\n return result;\n}\n\nexport function createOutboxCommunicationRouter(\n stores: FactoryStores,\n router: CommunicationRouter,\n options: OutboxDeliveryOptions = {},\n): CommunicationRouter {\n return Object.freeze({\n channels: router.channels,\n async deliver(event: CommunicationEvent): Promise<void> {\n const message = await stores.outbox.enqueue(event);\n if (message.state === \"delivered\") return;\n await deliverOne(stores, router, message.id, options);\n },\n });\n}\n","import { communicationEventSchema, type CommunicationEvent } from \"../schema/communication\";\nimport type { ReplyTo } from \"../schema/common\";\nimport type { SignalSource } from \"../schema/signal\";\n\nexport {\n communicationEventSchema,\n communicationOutcomeSchema,\n communicationOutcomes,\n type CommunicationEvent,\n type CommunicationOutcome,\n} from \"../schema/communication\";\n\nexport interface WebhookEvent {\n deliveryId: string;\n event: string;\n payload: unknown;\n}\n\nexport interface NormalizedSignal<TSource extends SignalSource = SignalSource> {\n source: TSource;\n deliveryId: string;\n replyTo: ReplyTo;\n payload: unknown;\n}\n\nexport type SignalNormalizer<TSignal extends NormalizedSignal = NormalizedSignal> = (\n event: WebhookEvent,\n) => TSignal | null;\n\nexport interface CommunicationAdapter {\n readonly channel: string;\n deliver(event: CommunicationEvent): Promise<void>;\n}\n\nexport interface CommunicationRouter {\n readonly channels: readonly string[];\n deliver(event: CommunicationEvent): Promise<void>;\n}\n\nexport function createCommunicationRouter(\n adapters: readonly CommunicationAdapter[],\n): CommunicationRouter {\n const byChannel = new Map<string, CommunicationAdapter>();\n for (const adapter of adapters) {\n const channel = adapter.channel.trim();\n if (channel.length === 0) {\n throw new Error(\"Communication adapter channel is required.\");\n }\n if (byChannel.has(channel)) {\n throw new Error(`Communication adapter ${channel} is registered more than once.`);\n }\n byChannel.set(channel, adapter);\n }\n const channels = Object.freeze([...byChannel.keys()]);\n\n return Object.freeze({\n channels,\n async deliver(input: CommunicationEvent): Promise<void> {\n const event = communicationEventSchema.parse(input);\n const adapter = byChannel.get(event.replyTo.channel);\n if (adapter === undefined) {\n throw new Error(`No communication adapter is registered for ${event.replyTo.channel}.`);\n }\n await adapter.deliver(event);\n },\n });\n}\n\nexport * from \"./outbox\";\n"],"mappings":";;AAgBA,MAAM,uBAAuB,YAC3B,KAAK,IAAI,MAAS,KAAK,KAAK,IAAI,GAAG,UAAU,CAAC,GAAG,IAAW;AAE9D,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;AACF;AAEA,SAAS,aAAa,OAAwB;CAC5C,IAAI,UAAU,iBAAiB,QAAQ,MAAM,UAAU;CACvD,KAAK,MAAM,WAAW,oBACpB,UAAU,QAAQ,QAAQ,UAAU,UAAU;EAM5C,OAAO,GALQ,MAAM,WAAW,QAAQ,IACpC,WACA,MAAM,WAAW,SAAS,IACxB,YACA,GACW;CACnB,CAAC;CAEH,OAAO;AACT;AAEA,eAAe,WACb,QACA,QACA,IACA,SAC8C;CAC9C,MAAM,UAAU,MAAM,OAAO,OAAO,MAAM,IAAI,QAAQ,OAAO;CAC7D,IAAI,YAAY,MAAM,OAAO;CAC7B,IAAI;EACF,MAAM,OAAO,QAAQ,QAAQ,KAAK;EAElC,OAAO,MADiB,OAAO,OAAO,SAAS,QAAQ,IAAI,QAAQ,OAAO,MACrD,OAAO,YAAY;CAC1C,SAAS,OAAO;EACd,MAAM,SAAS,QAAQ,gBAAgB,oBAAA,CAAqB,QAAQ,OAAO;EAO3E,OAAO,MANe,OAAO,OAAO,MAClC,QAAQ,IACR,QAAQ,SACR,aAAa,KAAK,GAClB,KACF,MACmB,OAAO,YAAY;CACxC;AACF;AAEA,eAAsB,yBACpB,QACA,QACA,UAAiC,CAAC,GACN;CAC5B,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,WAAW,MAAM,OAAO,OAAO,QAAQ,KAAK;CAClD,MAAM,SAA4B;EAAE,WAAW;EAAG,SAAS;EAAG,SAAS;CAAE;CACzE,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,UAAU,MAAM,WAAW,QAAQ,QAAQ,QAAQ,IAAI,OAAO;EACpE,OAAO,YAAY;CACrB;CACA,OAAO;AACT;AAEA,SAAgB,gCACd,QACA,QACA,UAAiC,CAAC,GACb;CACrB,OAAO,OAAO,OAAO;EACnB,UAAU,OAAO;EACjB,MAAM,QAAQ,OAA0C;GACtD,MAAM,UAAU,MAAM,OAAO,OAAO,QAAQ,KAAK;GACjD,IAAI,QAAQ,UAAU,aAAa;GACnC,MAAM,WAAW,QAAQ,QAAQ,QAAQ,IAAI,OAAO;EACtD;CACF,CAAC;AACH;;;ACtDA,SAAgB,0BACd,UACqB;CACrB,MAAM,4BAAY,IAAI,IAAkC;CACxD,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,UAAU,QAAQ,QAAQ,KAAK;EACrC,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MAAM,4CAA4C;EAE9D,IAAI,UAAU,IAAI,OAAO,GACvB,MAAM,IAAI,MAAM,yBAAyB,QAAQ,+BAA+B;EAElF,UAAU,IAAI,SAAS,OAAO;CAChC;CACA,MAAM,WAAW,OAAO,OAAO,CAAC,GAAG,UAAU,KAAK,CAAC,CAAC;CAEpD,OAAO,OAAO,OAAO;EACnB;EACA,MAAM,QAAQ,OAA0C;GACtD,MAAM,QAAQ,yBAAyB,MAAM,KAAK;GAClD,MAAM,UAAU,UAAU,IAAI,MAAM,QAAQ,OAAO;GACnD,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MAAM,8CAA8C,MAAM,QAAQ,QAAQ,EAAE;GAExF,MAAM,QAAQ,QAAQ,KAAK;EAC7B;CACF,CAAC;AACH"}
@@ -1,4 +1,5 @@
1
- import { a as NormalizedSignal, n as CommunicationEvent, t as CommunicationAdapter } from "../index-Cy4jfXh5.mjs";
1
+ import { st as CommunicationEvent } from "../engine-DFHU2Pe-.mjs";
2
+ import { r as NormalizedSignal, t as CommunicationAdapter } from "../index-Bw6hggoH.mjs";
2
3
  //#region src/connectors/slack.d.ts
3
4
  interface SlackCommunicationDelivery {
4
5
  eventId: string;
@@ -24,9 +25,19 @@ interface SlackReplyTarget {
24
25
  channelId: string;
25
26
  threadTs: string;
26
27
  }
28
+ type SlackPullRequestSelection = {
29
+ status: "none";
30
+ } | {
31
+ status: "ambiguous";
32
+ } | {
33
+ status: "selected";
34
+ pullRequestNumber: number;
35
+ };
36
+ /** Resolve an explicit pull-request reference after the repository has been selected. */
37
+ declare function selectSlackPullRequest(text: string, repositorySlug: string): SlackPullRequestSelection;
27
38
  /** Parse Eve's Slack continuation token into its channel and thread target. */
28
39
  declare function parseSlackReplyAddress(address: string): SlackReplyTarget;
29
- /** Render a provider-neutral communication event as plain Slack text. */
40
+ /** Render a provider-neutral communication event as Slack mrkdwn. */
30
41
  declare function renderSlackCommunicationEvent(input: CommunicationEvent): string;
31
42
  /** Bind Slack rendering and delivery to the channel-neutral communication contract. */
32
43
  declare function createSlackCommunicationAdapter(options: SlackCommunicationAdapterOptions): SlackCommunicationAdapter;
@@ -69,13 +80,13 @@ type SlackRepositoriesSelection = {
69
80
  } | {
70
81
  status: "required";
71
82
  };
72
- /** Select every explicitly named configured repository, preserving configuration order. */
83
+ /** Select every named configured repository, preserving configuration order. */
73
84
  declare function selectSlackRepositories(text: string, repositories: readonly string[]): SlackRepositoriesSelection;
74
85
  /**
75
- * Select the only configured repository, or require an explicit owner/name
76
- * when a Slack surface fronts more than one repository.
86
+ * Select the only configured repository, or match a full slug, unique short
87
+ * name, or GitHub repository URL when a Slack surface fronts multiple repositories.
77
88
  */
78
89
  declare function selectSlackRepository(text: string, repositories: readonly string[]): SlackRepositorySelection;
79
90
  //#endregion
80
- export { SlackCommunicationAdapter, SlackCommunicationAdapterOptions, SlackCommunicationDelivery, SlackMessageEvent, SlackMessageSignal, SlackReplyTarget, SlackRepositoriesSelection, SlackRepositorySelection, createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackRepositories, selectSlackRepository };
91
+ export { SlackCommunicationAdapter, SlackCommunicationAdapterOptions, SlackCommunicationDelivery, SlackMessageEvent, SlackMessageSignal, SlackPullRequestSelection, SlackReplyTarget, SlackRepositoriesSelection, SlackRepositorySelection, createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackPullRequest, selectSlackRepositories, selectSlackRepository };
81
92
  //# sourceMappingURL=slack.d.mts.map
@@ -1,4 +1,5 @@
1
- import { communicationEventSchema } from "./index.mjs";
1
+ import { t as communicationEventSchema } from "../communication-Kd9nU5RP.mjs";
2
+ import "./index.mjs";
2
3
  import { createHash } from "node:crypto";
3
4
  //#region src/connectors/slack.ts
4
5
  const slackOutcomeLabels = {
@@ -30,6 +31,33 @@ const slackOwnerOutcomes = /* @__PURE__ */ new Set([
30
31
  "budget_warning",
31
32
  "budget_exhausted"
32
33
  ]);
34
+ function escapeSlackText(value) {
35
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
36
+ }
37
+ function slackLink(url, label) {
38
+ try {
39
+ const parsed = new URL(url);
40
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
41
+ return `<${escapeSlackText(parsed.toString())}|${escapeSlackText(label)}>`;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Resolve an explicit pull-request reference after the repository has been selected. */
47
+ function selectSlackPullRequest(text, repositorySlug) {
48
+ const escapedSlug = repositorySlug.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
49
+ const references = [...text.matchAll(new RegExp(`https://github\\.com/${escapedSlug}/pull/(\\d+)`, "giu")), ...text.matchAll(/\b(?:pr|pull\s+request)\s*#?\s*(\d+)\b/giu)].flatMap((match) => {
50
+ const value = Number(match[1]);
51
+ return Number.isSafeInteger(value) && value > 0 ? [value] : [];
52
+ });
53
+ const distinct = [...new Set(references)];
54
+ if (distinct.length === 0) return { status: "none" };
55
+ if (distinct.length > 1) return { status: "ambiguous" };
56
+ return {
57
+ status: "selected",
58
+ pullRequestNumber: distinct[0]
59
+ };
60
+ }
33
61
  /** Parse Eve's Slack continuation token into its channel and thread target. */
34
62
  function parseSlackReplyAddress(address) {
35
63
  const separator = address.indexOf(":");
@@ -41,20 +69,35 @@ function parseSlackReplyAddress(address) {
41
69
  threadTs
42
70
  };
43
71
  }
44
- /** Render a provider-neutral communication event as plain Slack text. */
72
+ /** Render a provider-neutral communication event as Slack mrkdwn. */
45
73
  function renderSlackCommunicationEvent(input) {
46
74
  const event = communicationEventSchema.parse(input);
47
- const headline = slackOutcomeLabels[event.outcome];
48
- const details = slackDetailFields.flatMap((field) => {
75
+ const headline = `*${slackOutcomeLabels[event.outcome]}*`;
76
+ const details = new Map(slackDetailFields.flatMap((field) => {
49
77
  const value = event.data[field];
50
- return typeof value === "string" && value.trim() !== "" ? [value.trim()] : [];
51
- });
78
+ return typeof value === "string" && value.trim() !== "" ? [[field, value.trim()]] : [];
79
+ }));
80
+ const primaryField = [
81
+ "title",
82
+ "question",
83
+ "requestedAction"
84
+ ].find((field) => details.has(field));
85
+ const primary = primaryField === void 0 ? void 0 : details.get(primaryField);
86
+ const url = details.get("url");
87
+ const linkedPrimary = primary !== void 0 && url !== void 0 ? slackLink(url, primary) : null;
88
+ if (primaryField !== void 0) details.delete(primaryField);
89
+ if (linkedPrimary !== null) details.delete("url");
90
+ const detailLines = [.../* @__PURE__ */ new Set([...linkedPrimary !== null ? [linkedPrimary] : primary === void 0 ? [] : [escapeSlackText(primary)], ...[...details.values()].map(escapeSlackText)])];
52
91
  const lists = slackListFields.flatMap((field) => {
53
92
  const value = event.data[field];
54
93
  if (!Array.isArray(value)) return [];
55
- return value.flatMap((item) => typeof item === "string" && item.trim() !== "" ? [`- ${item.trim()}`] : []);
94
+ return value.flatMap((item) => typeof item === "string" && item.trim() !== "" ? [`• ${escapeSlackText(item.trim())}`] : []);
56
95
  });
57
- const text = [headline, .../* @__PURE__ */ new Set([...details, ...lists])].join("\n");
96
+ const text = [
97
+ headline,
98
+ ...detailLines,
99
+ ...lists.length === 0 ? [] : [lists.join("\n")]
100
+ ].join("\n\n");
58
101
  return text.length <= slackTextLimit ? text : `${text.slice(0, 11999)}…`;
59
102
  }
60
103
  function slackClientMessageId(eventId) {
@@ -110,47 +153,71 @@ function normalizeSlackMessage(event) {
110
153
  payload: event.payload
111
154
  };
112
155
  }
113
- /** Select every explicitly named configured repository, preserving configuration order. */
114
- function selectSlackRepositories(text, repositories) {
156
+ function escapedPattern(value) {
157
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
158
+ }
159
+ function isDelimitedTextMentioned(text, value) {
160
+ return new RegExp(`(^|[^a-z0-9._/-])${escapedPattern(value)}($|[^a-z0-9._/-])`, "i").test(text);
161
+ }
162
+ function isRepositoryReferenceMentioned(text, slug) {
163
+ if (isDelimitedTextMentioned(text, slug)) return true;
164
+ return new RegExp(`https?://github\\.com/${escapedPattern(slug)}(?:\\.git)?/?(?=$|[\\s?#|>)\\],.!])`, "i").test(text);
165
+ }
166
+ function repositoryName(slug) {
167
+ return slug.slice(slug.lastIndexOf("/") + 1);
168
+ }
169
+ function resolveSlackRepositoryMentions(text, repositories) {
115
170
  const normalized = [...new Set(repositories.map((slug) => slug.toLowerCase()))];
116
- if (normalized.length === 1) return {
171
+ const entries = normalized.map((slug) => ({
172
+ slug,
173
+ name: repositoryName(slug)
174
+ }));
175
+ const counts = /* @__PURE__ */ new Map();
176
+ for (const { name } of entries) counts.set(name, (counts.get(name) ?? 0) + 1);
177
+ const mentionedNames = new Set([...counts.keys()].filter((name) => isDelimitedTextMentioned(text, name)));
178
+ const matches = new Set(entries.flatMap(({ slug, name }) => isRepositoryReferenceMentioned(text, slug) || counts.get(name) === 1 && mentionedNames.has(name) ? [slug] : []));
179
+ const ambiguous = new Set(entries.flatMap(({ slug, name }) => counts.get(name) !== 1 && mentionedNames.has(name) ? [slug] : []));
180
+ return {
181
+ repositories: normalized,
182
+ matches: normalized.filter((slug) => matches.has(slug)),
183
+ ambiguous: normalized.filter((slug) => ambiguous.has(slug)),
184
+ candidates: normalized.filter((slug) => matches.has(slug) || ambiguous.has(slug))
185
+ };
186
+ }
187
+ /** Select every named configured repository, preserving configuration order. */
188
+ function selectSlackRepositories(text, repositories) {
189
+ const mentions = resolveSlackRepositoryMentions(text, repositories);
190
+ if (mentions.repositories.length === 1) return {
117
191
  status: "selected",
118
- repositorySlugs: [normalized[0]]
192
+ repositorySlugs: [mentions.repositories[0]]
119
193
  };
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" } : {
194
+ if (mentions.ambiguous.length > 0) return { status: "required" };
195
+ return mentions.matches.length === 0 ? { status: "required" } : {
125
196
  status: "selected",
126
- repositorySlugs: [matches[0], ...matches.slice(1)]
197
+ repositorySlugs: [mentions.matches[0], ...mentions.matches.slice(1)]
127
198
  };
128
199
  }
129
200
  /**
130
- * Select the only configured repository, or require an explicit owner/name
131
- * when a Slack surface fronts more than one repository.
201
+ * Select the only configured repository, or match a full slug, unique short
202
+ * name, or GitHub repository URL when a Slack surface fronts multiple repositories.
132
203
  */
133
204
  function selectSlackRepository(text, repositories) {
134
- const normalized = [...new Set(repositories.map((slug) => slug.toLowerCase()))];
135
- if (normalized.length === 1) return {
205
+ const mentions = resolveSlackRepositoryMentions(text, repositories);
206
+ if (mentions.repositories.length === 1) return {
136
207
  status: "selected",
137
- repositorySlug: normalized[0]
208
+ repositorySlug: mentions.repositories[0]
138
209
  };
139
- const matches = normalized.filter((slug) => {
140
- const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
- return new RegExp(`(^|[^a-z0-9._/-])${escaped}($|[^a-z0-9._/-])`, "i").test(text);
142
- });
143
- if (matches.length === 1) return {
210
+ if (mentions.candidates.length === 1) return {
144
211
  status: "selected",
145
- repositorySlug: matches[0]
212
+ repositorySlug: mentions.candidates[0]
146
213
  };
147
- if (matches.length > 1) return {
214
+ if (mentions.candidates.length > 1) return {
148
215
  status: "ambiguous",
149
- repositorySlugs: matches
216
+ repositorySlugs: mentions.candidates
150
217
  };
151
218
  return { status: "required" };
152
219
  }
153
220
  //#endregion
154
- export { createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackRepositories, selectSlackRepository };
221
+ export { createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, selectSlackPullRequest, selectSlackRepositories, selectSlackRepository };
155
222
 
156
223
  //# 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 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
+ {"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\nfunction escapeSlackText(value: string): string {\n return value.replaceAll(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\");\n}\n\nfunction slackLink(url: string, label: string): string | null {\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return null;\n return `<${escapeSlackText(parsed.toString())}|${escapeSlackText(label)}>`;\n } catch {\n return null;\n }\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\nexport type SlackPullRequestSelection =\n | { status: \"none\" }\n | { status: \"ambiguous\" }\n | { status: \"selected\"; pullRequestNumber: number };\n\n/** Resolve an explicit pull-request reference after the repository has been selected. */\nexport function selectSlackPullRequest(\n text: string,\n repositorySlug: string,\n): SlackPullRequestSelection {\n const escapedSlug = repositorySlug.replace(/[.*+?^${}()|[\\]\\\\]/gu, \"\\\\$&\");\n const references = [\n ...text.matchAll(new RegExp(`https://github\\\\.com/${escapedSlug}/pull/(\\\\d+)`, \"giu\")),\n ...text.matchAll(/\\b(?:pr|pull\\s+request)\\s*#?\\s*(\\d+)\\b/giu),\n ].flatMap((match) => {\n const value = Number(match[1]);\n return Number.isSafeInteger(value) && value > 0 ? [value] : [];\n });\n const distinct = [...new Set(references)];\n if (distinct.length === 0) return { status: \"none\" };\n if (distinct.length > 1) return { status: \"ambiguous\" };\n return { status: \"selected\", pullRequestNumber: distinct[0]! };\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 Slack mrkdwn. */\nexport function renderSlackCommunicationEvent(input: CommunicationEvent): string {\n const event = communicationEventSchema.parse(input);\n const headline = `*${slackOutcomeLabels[event.outcome]}*`;\n const details = new Map(slackDetailFields.flatMap((field) => {\n const value = event.data[field];\n return typeof value === \"string\" && value.trim() !== \"\" ? [[field, value.trim()] as const] : [];\n }));\n const primaryField = ([\"title\", \"question\", \"requestedAction\"] as const)\n .find((field) => details.has(field));\n const primary = primaryField === undefined ? undefined : details.get(primaryField);\n const url = details.get(\"url\");\n const linkedPrimary = primary !== undefined && url !== undefined ? slackLink(url, primary) : null;\n if (primaryField !== undefined) details.delete(primaryField);\n if (linkedPrimary !== null) details.delete(\"url\");\n\n const detailLines = [...new Set([\n ...(linkedPrimary !== null ? [linkedPrimary] : primary === undefined ? [] : [escapeSlackText(primary)]),\n ...[...details.values()].map(escapeSlackText),\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() !== \"\" ? [`• ${escapeSlackText(item.trim())}`] : []\n );\n });\n const text = [headline, ...detailLines, ...(lists.length === 0 ? [] : [lists.join(\"\\n\")])]\n .join(\"\\n\\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\nfunction escapedPattern(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction isDelimitedTextMentioned(text: string, value: string): boolean {\n return new RegExp(\n `(^|[^a-z0-9._/-])${escapedPattern(value)}($|[^a-z0-9._/-])`,\n \"i\",\n ).test(text);\n}\n\nfunction isRepositoryReferenceMentioned(text: string, slug: string): boolean {\n if (isDelimitedTextMentioned(text, slug)) return true;\n return new RegExp(\n `https?://github\\\\.com/${escapedPattern(slug)}(?:\\\\.git)?/?(?=$|[\\\\s?#|>)\\\\],.!])`,\n \"i\",\n ).test(text);\n}\n\nfunction repositoryName(slug: string): string {\n return slug.slice(slug.lastIndexOf(\"/\") + 1);\n}\n\ninterface SlackRepositoryMentions {\n repositories: readonly string[];\n matches: readonly string[];\n ambiguous: readonly string[];\n candidates: readonly string[];\n}\n\nfunction resolveSlackRepositoryMentions(\n text: string,\n repositories: readonly string[],\n): SlackRepositoryMentions {\n const normalized = [...new Set(repositories.map((slug) => slug.toLowerCase()))];\n const entries = normalized.map((slug) => ({ slug, name: repositoryName(slug) }));\n const counts = new Map<string, number>();\n for (const { name } of entries) {\n counts.set(name, (counts.get(name) ?? 0) + 1);\n }\n const mentionedNames = new Set(\n [...counts.keys()].filter((name) => isDelimitedTextMentioned(text, name)),\n );\n const matches = new Set(entries.flatMap(({ slug, name }) =>\n isRepositoryReferenceMentioned(text, slug) || (\n counts.get(name) === 1 && mentionedNames.has(name)\n )\n ? [slug]\n : []\n ));\n const ambiguous = new Set(entries.flatMap(({ slug, name }) =>\n counts.get(name) !== 1 && mentionedNames.has(name) ? [slug] : []\n ));\n\n return {\n repositories: normalized,\n matches: normalized.filter((slug) => matches.has(slug)),\n ambiguous: normalized.filter((slug) => ambiguous.has(slug)),\n candidates: normalized.filter((slug) => matches.has(slug) || ambiguous.has(slug)),\n };\n}\n\n/** Select every named configured repository, preserving configuration order. */\nexport function selectSlackRepositories(\n text: string,\n repositories: readonly string[],\n): SlackRepositoriesSelection {\n const mentions = resolveSlackRepositoryMentions(text, repositories);\n if (mentions.repositories.length === 1) {\n return { status: \"selected\", repositorySlugs: [mentions.repositories[0]!] };\n }\n if (mentions.ambiguous.length > 0) return { status: \"required\" };\n return mentions.matches.length === 0\n ? { status: \"required\" }\n : {\n status: \"selected\",\n repositorySlugs: [mentions.matches[0]!, ...mentions.matches.slice(1)],\n };\n}\n\n/**\n * Select the only configured repository, or match a full slug, unique short\n * name, or GitHub repository URL when a Slack surface fronts multiple repositories.\n */\nexport function selectSlackRepository(\n text: string,\n repositories: readonly string[],\n): SlackRepositorySelection {\n const mentions = resolveSlackRepositoryMentions(text, repositories);\n if (mentions.repositories.length === 1) {\n return { status: \"selected\", repositorySlug: mentions.repositories[0]! };\n }\n if (mentions.candidates.length === 1) {\n return { status: \"selected\", repositorySlug: mentions.candidates[0]! };\n }\n if (mentions.candidates.length > 1) {\n return { status: \"ambiguous\", repositorySlugs: mentions.candidates };\n }\n return { status: \"required\" };\n}\n"],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import { n as RecordNotFoundError } from "./driver-Bbd25rNM.mjs";
2
+ //#region src/coordination.ts
3
+ /** Create one non-executable parent and one independently executable child per repository. */
4
+ async function createCoordination(stores, input) {
5
+ const repositoryIds = [...new Set(input.repositoryIds)];
6
+ if (repositoryIds.length < 2) throw new Error("coordination requires at least two distinct repositories");
7
+ const parent = await stores.tasks.create({
8
+ repositoryIds,
9
+ kind: "coordination",
10
+ origin: input.signalId === void 0 ? { operator: input.operator } : { signalId: input.signalId },
11
+ replyTo: input.replyTo,
12
+ dedupeKey: input.dedupeKey
13
+ });
14
+ return {
15
+ parent: parent.state === "queued" ? await stores.tasks.transition(parent.id, "running", { reason: "repository-scoped child tasks created" }) : parent,
16
+ children: await Promise.all(repositoryIds.map((repositoryId) => stores.tasks.create({
17
+ repositoryIds: [repositoryId],
18
+ kind: "code_change",
19
+ origin: { parentTaskId: parent.id },
20
+ replyTo: input.replyTo,
21
+ dedupeKey: input.dedupeKey === void 0 ? void 0 : `${input.dedupeKey}:${repositoryId}`
22
+ })))
23
+ };
24
+ }
25
+ /** Reconcile a coordination parent from its direct children without ever dispatching the parent. */
26
+ async function reconcileCoordination(stores, parentTaskId, options) {
27
+ const parent = await stores.tasks.get(parentTaskId);
28
+ if (parent === null) throw new RecordNotFoundError("tasks", parentTaskId);
29
+ if (parent.kind !== "coordination") throw new Error(`task ${parentTaskId} is ${parent.kind}, not coordination`);
30
+ const children = (await stores.tasks.list()).filter((task) => task.origin.parentTaskId === parentTaskId);
31
+ if (children.length !== parent.repositoryIds.length) return {
32
+ status: "pending",
33
+ parent,
34
+ children
35
+ };
36
+ if (parent.state === "succeeded" || parent.state === "failed") return {
37
+ status: parent.state,
38
+ transitioned: false,
39
+ parent,
40
+ children
41
+ };
42
+ if (children.some((child) => child.state === "failed" || child.state === "cancelled")) {
43
+ await options.present({
44
+ status: "failed",
45
+ parent,
46
+ children
47
+ });
48
+ return {
49
+ status: "failed",
50
+ transitioned: true,
51
+ parent: await stores.tasks.transition(parent.id, "failed", { reason: "one or more repository tasks failed" }),
52
+ children
53
+ };
54
+ }
55
+ if (!children.every((child) => child.state === "succeeded")) return {
56
+ status: "pending",
57
+ parent,
58
+ children
59
+ };
60
+ await options.present({
61
+ status: "succeeded",
62
+ parent,
63
+ children
64
+ });
65
+ await stores.tasks.transition(parent.id, "verifying", { reason: "all repository tasks succeeded" });
66
+ return {
67
+ status: "succeeded",
68
+ transitioned: true,
69
+ parent: await stores.tasks.transition(parent.id, "succeeded", { reason: "aggregate multi-repository handoff is ready" }),
70
+ children
71
+ };
72
+ }
73
+ //#endregion
74
+ export { reconcileCoordination as n, createCoordination as t };
75
+
76
+ //# sourceMappingURL=coordination-Cq08hb1l.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coordination-Cq08hb1l.mjs","names":[],"sources":["../src/coordination.ts"],"sourcesContent":["import type { ReplyTo } from \"./schema/common\";\nimport type { RepositoryId, SignalId, TaskId } from \"./schema/id\";\nimport type { Task } from \"./schema/task\";\nimport { RecordNotFoundError } from \"./store/driver\";\nimport type { FactoryStores } from \"./store/engine\";\n\ninterface CreateCoordinationInputBase {\n repositoryIds: readonly [RepositoryId, RepositoryId, ...RepositoryId[]];\n replyTo: ReplyTo;\n /** Same key recreates neither the parent nor its repository-scoped children. */\n dedupeKey?: string;\n}\n\nexport type CreateCoordinationInput = CreateCoordinationInputBase &\n ({ signalId: SignalId; operator?: never } | { operator: string; signalId?: never });\n\nexport interface CoordinationPlan {\n parent: Task;\n children: readonly Task[];\n}\n\nexport type CoordinationReconciliation =\n | { status: \"pending\"; parent: Task; children: readonly Task[] }\n | { status: \"succeeded\" | \"failed\"; transitioned: boolean; parent: Task; children: readonly Task[] };\n\nexport interface ReconcileCoordinationOptions {\n present(input: {\n status: \"succeeded\" | \"failed\";\n parent: Task;\n children: readonly Task[];\n }): Promise<void>;\n}\n\n/** Create one non-executable parent and one independently executable child per repository. */\nexport async function createCoordination(\n stores: FactoryStores,\n input: CreateCoordinationInput,\n): Promise<CoordinationPlan> {\n const repositoryIds = [...new Set(input.repositoryIds)];\n if (repositoryIds.length < 2) {\n throw new Error(\"coordination requires at least two distinct repositories\");\n }\n const parent = await stores.tasks.create({\n repositoryIds,\n kind: \"coordination\",\n origin: input.signalId === undefined ? { operator: input.operator } : { signalId: input.signalId },\n replyTo: input.replyTo,\n dedupeKey: input.dedupeKey,\n });\n const running = parent.state === \"queued\"\n ? await stores.tasks.transition(parent.id, \"running\", {\n reason: \"repository-scoped child tasks created\",\n })\n : parent;\n const children = await Promise.all(\n repositoryIds.map((repositoryId) =>\n stores.tasks.create({\n repositoryIds: [repositoryId],\n kind: \"code_change\",\n origin: { parentTaskId: parent.id },\n replyTo: input.replyTo,\n dedupeKey: input.dedupeKey === undefined ? undefined : `${input.dedupeKey}:${repositoryId}`,\n }),\n ),\n );\n return { parent: running, children };\n}\n\n/** Reconcile a coordination parent from its direct children without ever dispatching the parent. */\nexport async function reconcileCoordination(\n stores: FactoryStores,\n parentTaskId: TaskId,\n options: ReconcileCoordinationOptions,\n): Promise<CoordinationReconciliation> {\n const parent = await stores.tasks.get(parentTaskId);\n if (parent === null) throw new RecordNotFoundError(\"tasks\", parentTaskId);\n if (parent.kind !== \"coordination\") {\n throw new Error(`task ${parentTaskId} is ${parent.kind}, not coordination`);\n }\n const children = (await stores.tasks.list()).filter(\n (task) => task.origin.parentTaskId === parentTaskId,\n );\n if (children.length !== parent.repositoryIds.length) {\n return { status: \"pending\", parent, children };\n }\n if (parent.state === \"succeeded\" || parent.state === \"failed\") {\n return { status: parent.state, transitioned: false, parent, children };\n }\n if (children.some((child) => child.state === \"failed\" || child.state === \"cancelled\")) {\n await options.present({ status: \"failed\", parent, children });\n const failed = await stores.tasks.transition(parent.id, \"failed\", {\n reason: \"one or more repository tasks failed\",\n });\n return { status: \"failed\", transitioned: true, parent: failed, children };\n }\n if (!children.every((child) => child.state === \"succeeded\")) {\n return { status: \"pending\", parent, children };\n }\n await options.present({ status: \"succeeded\", parent, children });\n await stores.tasks.transition(parent.id, \"verifying\", {\n reason: \"all repository tasks succeeded\",\n });\n const succeeded = await stores.tasks.transition(parent.id, \"succeeded\", {\n reason: \"aggregate multi-repository handoff is ready\",\n });\n return { status: \"succeeded\", transitioned: true, parent: succeeded, children };\n}\n"],"mappings":";;;AAkCA,eAAsB,mBACpB,QACA,OAC2B;CAC3B,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,MAAM,aAAa,CAAC;CACtD,IAAI,cAAc,SAAS,GACzB,MAAM,IAAI,MAAM,0DAA0D;CAE5E,MAAM,SAAS,MAAM,OAAO,MAAM,OAAO;EACvC;EACA,MAAM;EACN,QAAQ,MAAM,aAAa,KAAA,IAAY,EAAE,UAAU,MAAM,SAAS,IAAI,EAAE,UAAU,MAAM,SAAS;EACjG,SAAS,MAAM;EACf,WAAW,MAAM;CACnB,CAAC;CAiBD,OAAO;EAAE,QAhBO,OAAO,UAAU,WAC7B,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI,WAAW,EAClD,QAAQ,wCACV,CAAC,IACD;EAYsB,UAAA,MAXH,QAAQ,IAC7B,cAAc,KAAK,iBACjB,OAAO,MAAM,OAAO;GAClB,eAAe,CAAC,YAAY;GAC5B,MAAM;GACN,QAAQ,EAAE,cAAc,OAAO,GAAG;GAClC,SAAS,MAAM;GACf,WAAW,MAAM,cAAc,KAAA,IAAY,KAAA,IAAY,GAAG,MAAM,UAAU,GAAG;EAC/E,CAAC,CACH,CACF;CACmC;AACrC;;AAGA,eAAsB,sBACpB,QACA,cACA,SACqC;CACrC,MAAM,SAAS,MAAM,OAAO,MAAM,IAAI,YAAY;CAClD,IAAI,WAAW,MAAM,MAAM,IAAI,oBAAoB,SAAS,YAAY;CACxE,IAAI,OAAO,SAAS,gBAClB,MAAM,IAAI,MAAM,QAAQ,aAAa,MAAM,OAAO,KAAK,mBAAmB;CAE5E,MAAM,YAAY,MAAM,OAAO,MAAM,KAAK,EAAA,CAAG,QAC1C,SAAS,KAAK,OAAO,iBAAiB,YACzC;CACA,IAAI,SAAS,WAAW,OAAO,cAAc,QAC3C,OAAO;EAAE,QAAQ;EAAW;EAAQ;CAAS;CAE/C,IAAI,OAAO,UAAU,eAAe,OAAO,UAAU,UACnD,OAAO;EAAE,QAAQ,OAAO;EAAO,cAAc;EAAO;EAAQ;CAAS;CAEvE,IAAI,SAAS,MAAM,UAAU,MAAM,UAAU,YAAY,MAAM,UAAU,WAAW,GAAG;EACrF,MAAM,QAAQ,QAAQ;GAAE,QAAQ;GAAU;GAAQ;EAAS,CAAC;EAI5D,OAAO;GAAE,QAAQ;GAAU,cAAc;GAAM,QAAQ,MAHlC,OAAO,MAAM,WAAW,OAAO,IAAI,UAAU,EAChE,QAAQ,sCACV,CAAC;GAC8D;EAAS;CAC1E;CACA,IAAI,CAAC,SAAS,OAAO,UAAU,MAAM,UAAU,WAAW,GACxD,OAAO;EAAE,QAAQ;EAAW;EAAQ;CAAS;CAE/C,MAAM,QAAQ,QAAQ;EAAE,QAAQ;EAAa;EAAQ;CAAS,CAAC;CAC/D,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI,aAAa,EACpD,QAAQ,iCACV,CAAC;CAID,OAAO;EAAE,QAAQ;EAAa,cAAc;EAAM,QAAQ,MAHlC,OAAO,MAAM,WAAW,OAAO,IAAI,aAAa,EACtE,QAAQ,8CACV,CAAC;EACoE;CAAS;AAChF"}
@@ -0,0 +1,76 @@
1
+ import { Tt as Task, Yt as TaskId, h as FactoryStores, on as BudgetLimits, wt as ExecutionReference } from "./engine-DFHU2Pe-.mjs";
2
+ import { z } from "zod";
3
+ //#region src/agent-routes.d.ts
4
+ declare const agentRouteSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ agent: z.ZodString;
7
+ taskKind: z.ZodString;
8
+ description: z.ZodString;
9
+ permissions: z.ZodDefault<z.ZodArray<z.ZodString>>;
10
+ }, z.core.$strict>;
11
+ type AgentRoute = z.infer<typeof agentRouteSchema>;
12
+ type AgentRouteInput = z.input<typeof agentRouteSchema>;
13
+ interface AgentRouteRegistry {
14
+ readonly routes: readonly AgentRoute[];
15
+ get(id: string): AgentRoute | undefined;
16
+ forTaskKind(kind: string): AgentRoute | undefined;
17
+ }
18
+ /**
19
+ * Define the application-owned routes an agent or workflow may select. Route
20
+ * descriptions inform judgment; this validated registry remains the authority.
21
+ */
22
+ declare function defineAgentRoutes(inputs: readonly AgentRouteInput[]): AgentRouteRegistry;
23
+ //#endregion
24
+ //#region src/dispatch.d.ts
25
+ declare class DispatchRefusedError extends Error {
26
+ readonly taskId: TaskId;
27
+ readonly reason: string;
28
+ constructor(taskId: TaskId, reason: string);
29
+ }
30
+ /**
31
+ * An exhausted budget is definitive, not a retryable refusal: the task has
32
+ * already been failed and receipted when this throws, so it never re-enters
33
+ * the sweep. Operator retry recovers it once budget frees up.
34
+ */
35
+ declare class BudgetExhaustedError extends Error {
36
+ readonly taskId: TaskId;
37
+ readonly month: string;
38
+ readonly spendUsd: number;
39
+ constructor(taskId: TaskId, month: string, spendUsd: number);
40
+ }
41
+ interface DispatchBudget {
42
+ limits: Pick<BudgetLimits, "totalUsd" | "perTaskReservationUsd">;
43
+ /** Defaults to the current UTC month. */
44
+ month?: string;
45
+ now?: () => Date;
46
+ }
47
+ interface DispatchOptions {
48
+ /**
49
+ * Starts the durable execution for the task (an Eve agent run). Runs after
50
+ * the task has won the queued -> running transition. Returning the
51
+ * provider's execution reference records it on the task so the reconciler
52
+ * can find the run; returning nothing leaves the task reachable only by the
53
+ * stall window.
54
+ */
55
+ launch: (task: Task) => Promise<ExecutionReference | void>;
56
+ /** How many launches to try before the task fails for good; a failure re-queues for the next sweep. */
57
+ maxLaunchAttempts?: number;
58
+ /**
59
+ * Reserve-before-execution. When set, dispatch refuses definitively once
60
+ * the month's derived spend cannot fit another reservation, and otherwise
61
+ * stamps the reservation in the same write that wins queued -> running.
62
+ */
63
+ budget?: DispatchBudget;
64
+ }
65
+ declare const DEFAULT_MAX_LAUNCH_ATTEMPTS = 45;
66
+ /**
67
+ * The handshake between recorded work and execution: win the store-gated
68
+ * transition (at most one dispatcher can succeed), then start the run. A
69
+ * launch failure re-queues the task (bumping its attempt) until the attempt
70
+ * limit, then fails it; a crash between the transition and the launch leaves
71
+ * a running task with no execution, which the sweep reaps.
72
+ */
73
+ declare function dispatchTask(stores: FactoryStores, taskId: TaskId, options: DispatchOptions): Promise<Task>;
74
+ //#endregion
75
+ export { DispatchRefusedError as a, AgentRouteInput as c, defineAgentRoutes as d, DispatchOptions as i, AgentRouteRegistry as l, DEFAULT_MAX_LAUNCH_ATTEMPTS as n, dispatchTask as o, DispatchBudget as r, AgentRoute as s, BudgetExhaustedError as t, agentRouteSchema as u };
76
+ //# sourceMappingURL=dispatch-BaZS3PT8.d.mts.map
@@ -1,5 +1,5 @@
1
1
  //#region src/store/driver.d.ts
2
- type CollectionName = "repositories" | "signals" | "tasks" | "changes" | "receipts";
2
+ type CollectionName = "repositories" | "signals" | "tasks" | "changes" | "receipts" | "budgets" | "answers" | "sessions" | "outboxQueues" | "outboxDeliveries";
3
3
  interface StoredRecord {
4
4
  readonly value: unknown;
5
5
  readonly version: number;
@@ -31,4 +31,4 @@ interface StoreDriver {
31
31
  }
32
32
  //#endregion
33
33
  export { StoredEntry as a, StoreDriver as i, RecordExistsError as n, StoredRecord as o, RecordNotFoundError as r, VersionConflictError as s, CollectionName as t };
34
- //# sourceMappingURL=driver-CrZsy7sm.d.mts.map
34
+ //# sourceMappingURL=driver-BC0Mt5KG.d.mts.map