@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.
- package/README.md +1 -1
- package/dist/api.d.mts +225 -0
- package/dist/api.mjs +312 -0
- package/dist/api.mjs.map +1 -0
- package/dist/{approval-DdvEKLiE.mjs → approval-JX2Px9IC.mjs} +2 -2
- package/dist/{approval-DdvEKLiE.mjs.map → approval-JX2Px9IC.mjs.map} +1 -1
- package/dist/blob/index.d.mts +4 -3
- package/dist/blob/index.mjs +21 -15
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget-DzSiIA4t.mjs +42 -0
- package/dist/budget-DzSiIA4t.mjs.map +1 -0
- package/dist/build-factory.d.mts +1 -1
- package/dist/build-factory.mjs +1 -1
- package/dist/change-D2U6v7Ek.mjs +554 -0
- package/dist/change-D2U6v7Ek.mjs.map +1 -0
- package/dist/communication-Kd9nU5RP.mjs +30 -0
- package/dist/communication-Kd9nU5RP.mjs.map +1 -0
- package/dist/connectors/index.d.mts +3 -2
- package/dist/connectors/index.mjs +53 -26
- package/dist/connectors/index.mjs.map +1 -1
- package/dist/connectors/slack.d.mts +17 -6
- package/dist/connectors/slack.mjs +99 -32
- package/dist/connectors/slack.mjs.map +1 -1
- package/dist/coordination-Cq08hb1l.mjs +76 -0
- package/dist/coordination-Cq08hb1l.mjs.map +1 -0
- package/dist/dispatch-BaZS3PT8.d.mts +76 -0
- package/dist/{driver-CrZsy7sm.d.mts → driver-BC0Mt5KG.d.mts} +2 -2
- package/dist/driver-Bbd25rNM.mjs.map +1 -1
- package/dist/engine-DFHU2Pe-.d.mts +781 -0
- package/dist/engine-WaUM2cGA.mjs +1068 -0
- package/dist/engine-WaUM2cGA.mjs.map +1 -0
- package/dist/eve/index.d.mts +289 -153
- package/dist/eve/index.mjs +378 -89
- package/dist/eve/index.mjs.map +1 -1
- package/dist/{factory-config-ifd3JxiU.mjs → factory-config-BoRXOw43.mjs} +23 -22
- package/dist/factory-config-BoRXOw43.mjs.map +1 -0
- package/dist/{factory-config-Bu4mklm9.d.mts → factory-config-D2yU3G0c.d.mts} +21 -2
- package/dist/{github-wThBfbm-.d.mts → github-C5HNBN6p.d.mts} +34 -3
- package/dist/github.d.mts +2 -2
- package/dist/github.mjs +61 -1
- package/dist/github.mjs.map +1 -1
- package/dist/{id-DwCihQr4.mjs → id-BLIkCVue.mjs} +4 -2
- package/dist/{id-DwCihQr4.mjs.map → id-BLIkCVue.mjs.map} +1 -1
- package/dist/index-Bw6hggoH.d.mts +40 -0
- package/dist/index.d.mts +291 -61
- package/dist/index.mjs +140 -783
- package/dist/index.mjs.map +1 -1
- package/dist/postgres/index.d.mts +1 -1
- package/dist/task-CcOyOJ3k.mjs +154 -0
- package/dist/task-CcOyOJ3k.mjs.map +1 -0
- package/package.json +5 -1
- package/dist/approval-CIXB8IF8.d.mts +0 -522
- package/dist/change-CFNGXVqD.mjs +0 -230
- package/dist/change-CFNGXVqD.mjs.map +0 -1
- package/dist/factory-config-ifd3JxiU.mjs.map +0 -1
- package/dist/index-Cy4jfXh5.d.mts +0 -67
- package/dist/signal-CHVEmzvI.d.mts +0 -59
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
76
|
-
* when a Slack surface fronts
|
|
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 "
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
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
|
|
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() !== "" ? [
|
|
94
|
+
return value.flatMap((item) => typeof item === "string" && item.trim() !== "" ? [`• ${escapeSlackText(item.trim())}`] : []);
|
|
56
95
|
});
|
|
57
|
-
const text = [
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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: [
|
|
192
|
+
repositorySlugs: [mentions.repositories[0]]
|
|
119
193
|
};
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
131
|
-
* when a Slack surface fronts
|
|
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
|
|
135
|
-
if (
|
|
205
|
+
const mentions = resolveSlackRepositoryMentions(text, repositories);
|
|
206
|
+
if (mentions.repositories.length === 1) return {
|
|
136
207
|
status: "selected",
|
|
137
|
-
repositorySlug:
|
|
208
|
+
repositorySlug: mentions.repositories[0]
|
|
138
209
|
};
|
|
139
|
-
|
|
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:
|
|
212
|
+
repositorySlug: mentions.candidates[0]
|
|
146
213
|
};
|
|
147
|
-
if (
|
|
214
|
+
if (mentions.candidates.length > 1) return {
|
|
148
215
|
status: "ambiguous",
|
|
149
|
-
repositorySlugs:
|
|
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(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\");\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-
|
|
34
|
+
//# sourceMappingURL=driver-BC0Mt5KG.d.mts.map
|