@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
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,48 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as
|
|
1
|
+
import { a as receiptIdSchema, c as signalIdSchema, d as isoDateTimeSchema, f as replyToSchema, h as vocabularySchema, i as outboxIdSchema, l as taskIdSchema, m as usdSchema, n as changeIdSchema, o as repositoryIdSchema, p as schemaVersionSchema, r as factoryIdSchema, s as repositoryIdsSchema, t as answerIdSchema, u as SCHEMA_VERSION } from "./id-BLIkCVue.mjs";
|
|
2
|
+
import { a as taskApprovalSchema, c as taskOriginSchema, d as taskStateSchema, f as taskUsageSchema, i as standardTaskKinds, l as taskQuestionSchema, n as executionReferenceSchema, o as taskExecutionRepositoryIdsSchema, r as harnessSchema, s as taskKindSchema, t as coordinationRepositoryIdsSchema, u as taskSchema } from "./task-CcOyOJ3k.mjs";
|
|
3
|
+
import { a as findingCategorySchema, c as findingSchema, d as findingVerificationSchema, i as requiresApproval, l as findingVerificationLevelSchema, n as approvalRuleSchema, o as findingFingerprint, r as defaultApprovalPolicy, s as findingPrioritySchema, t as approvalPolicySchema, u as findingVerificationMethodSchema } from "./approval-JX2Px9IC.mjs";
|
|
4
|
+
import { C as computeMonthSpendUsd, E as sweepSettlements, S as canReserve, T as sweepBudget, _ as taskInitialState, a as BudgetExhaustedError, b as budgetMonthOf, c as dispatchTask, d as changeInitialState, f as changeTransitions, g as signalOutcomeTransitions, h as signalInitialOutcome, i as levelSchema, l as InvalidTransitionError, m as legalNextStates, n as changeSchema, o as DEFAULT_MAX_LAUNCH_ATTEMPTS, p as isTransitionAllowed, r as changeStateSchema, s as DispatchRefusedError, t as changeEvidenceSchema, u as assertTransition, v as taskTransitions, w as deriveFactoryId, x as budgetStatus, y as terminalStates } from "./change-D2U6v7Ek.mjs";
|
|
3
5
|
import { n as RecordNotFoundError, r as VersionConflictError, t as RecordExistsError } from "./driver-Bbd25rNM.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { a as repositorySlugSchema, i as repositorySchema, n as
|
|
6
|
+
import { n as reconcileCoordination, t as createCoordination } from "./coordination-Cq08hb1l.mjs";
|
|
7
|
+
import { a as repositorySlugSchema, i as repositorySchema, n as budgetMonthSchema, r as budgetSchema, t as budgetLimitsSchema } from "./budget-DzSiIA4t.mjs";
|
|
8
|
+
import { C as standardReceiptPhases, S as standardReceiptKinds, T as answerEventSchema, _ as costSchema, a as SessionConflictError, b as receiptSchema, c as agentSessionIdSchema, d as signalSchema, f as signalSourceSchema, g as outboxStateSchema, h as outboxQueueSchema, i as OutboxEventConflictError, l as agentSessionSchema, m as outboxMessageSchema, n as ApprovalError, o as SettlementError, p as standardSignalSources, r as ChangeAssociationError, s as createStores, t as AnswerEventConflictError, u as signalOutcomeSchema, v as receiptKindSchema, w as standardReceiptStates, x as receiptStateSchema, y as receiptPhaseSchema } from "./engine-WaUM2cGA.mjs";
|
|
9
|
+
import { n as communicationOutcomeSchema, r as communicationOutcomes, t as communicationEventSchema } from "./communication-Kd9nU5RP.mjs";
|
|
10
|
+
import { a as factoryConfigSchema, i as factoryConfigRepositorySchema, n as defineFactory, r as executionPolicySchema, t as defaultExecutionPolicy } from "./factory-config-BoRXOw43.mjs";
|
|
6
11
|
import { z } from "zod";
|
|
7
|
-
|
|
12
|
+
//#region src/agent-routes.ts
|
|
13
|
+
const agentRouteSchema = z.strictObject({
|
|
14
|
+
id: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u).describe("Stable route selected by a conversational agent or workflow."),
|
|
15
|
+
agent: z.string().regex(/^[a-z][a-z0-9-]{0,31}$/u).describe("Internally addressable Eve agent that executes this route."),
|
|
16
|
+
taskKind: taskKindSchema.describe("Factory task kind created for this route."),
|
|
17
|
+
description: z.string().min(1).max(500).describe("Model-facing guidance for when this route is appropriate."),
|
|
18
|
+
permissions: z.array(z.string().min(1)).default([]).describe("Inspectable capabilities granted to the target agent.")
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Define the application-owned routes an agent or workflow may select. Route
|
|
22
|
+
* descriptions inform judgment; this validated registry remains the authority.
|
|
23
|
+
*/
|
|
24
|
+
function defineAgentRoutes(inputs) {
|
|
25
|
+
const routes = inputs.map((input) => agentRouteSchema.parse(input));
|
|
26
|
+
const byId = /* @__PURE__ */ new Map();
|
|
27
|
+
const byTaskKind = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const route of routes) {
|
|
29
|
+
if (byId.has(route.id)) throw new Error(`Agent route ${route.id} is registered more than once.`);
|
|
30
|
+
if (byTaskKind.has(route.taskKind)) throw new Error(`Task kind ${route.taskKind} is handled by more than one agent route.`);
|
|
31
|
+
byId.set(route.id, route);
|
|
32
|
+
byTaskKind.set(route.taskKind, route);
|
|
33
|
+
}
|
|
34
|
+
const frozen = Object.freeze(routes.map((route) => Object.freeze(route)));
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
routes: frozen,
|
|
37
|
+
get(id) {
|
|
38
|
+
return byId.get(id);
|
|
39
|
+
},
|
|
40
|
+
forTaskKind(kind) {
|
|
41
|
+
return byTaskKind.get(kind);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
8
46
|
//#region src/briefs.ts
|
|
9
47
|
/** The finding behind a task, when its signal carries one. */
|
|
10
48
|
async function findingForTask(stores, task) {
|
|
@@ -48,8 +86,6 @@ function verificationBrief(task, repository, change) {
|
|
|
48
86
|
//#endregion
|
|
49
87
|
//#region src/changes.ts
|
|
50
88
|
async function adoptPullRequest(stores, input) {
|
|
51
|
-
const existing = (await stores.changes.list()).find((change) => change.repositoryId === input.repositoryId && change.prNumber === input.prNumber && (change.state === "verifying" || change.state === "ready"));
|
|
52
|
-
if (existing !== void 0) return existing;
|
|
53
89
|
return stores.changes.create({
|
|
54
90
|
...input,
|
|
55
91
|
risk: "low",
|
|
@@ -64,76 +100,6 @@ async function adoptPullRequest(stores, input) {
|
|
|
64
100
|
});
|
|
65
101
|
}
|
|
66
102
|
//#endregion
|
|
67
|
-
//#region src/coordination.ts
|
|
68
|
-
/** Create one non-executable parent and one independently executable child per repository. */
|
|
69
|
-
async function createCoordination(stores, input) {
|
|
70
|
-
const repositoryIds = [...new Set(input.repositoryIds)];
|
|
71
|
-
if (repositoryIds.length < 2) throw new Error("coordination requires at least two distinct repositories");
|
|
72
|
-
const parent = await stores.tasks.create({
|
|
73
|
-
repositoryIds,
|
|
74
|
-
kind: "coordination",
|
|
75
|
-
origin: { signalId: input.signalId },
|
|
76
|
-
replyTo: input.replyTo
|
|
77
|
-
});
|
|
78
|
-
return {
|
|
79
|
-
parent: await stores.tasks.transition(parent.id, "running", { reason: "repository-scoped child tasks created" }),
|
|
80
|
-
children: await Promise.all(repositoryIds.map((repositoryId) => stores.tasks.create({
|
|
81
|
-
repositoryIds: [repositoryId],
|
|
82
|
-
kind: "code_change",
|
|
83
|
-
origin: { parentTaskId: parent.id },
|
|
84
|
-
replyTo: input.replyTo
|
|
85
|
-
})))
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/** Reconcile a coordination parent from its direct children without ever dispatching the parent. */
|
|
89
|
-
async function reconcileCoordination(stores, parentTaskId, options) {
|
|
90
|
-
const parent = await stores.tasks.get(parentTaskId);
|
|
91
|
-
if (parent === null) throw new RecordNotFoundError("tasks", parentTaskId);
|
|
92
|
-
if (parent.kind !== "coordination") throw new Error(`task ${parentTaskId} is ${parent.kind}, not coordination`);
|
|
93
|
-
const children = (await stores.tasks.list()).filter((task) => task.origin.parentTaskId === parentTaskId);
|
|
94
|
-
if (children.length !== parent.repositoryIds.length) return {
|
|
95
|
-
status: "pending",
|
|
96
|
-
parent,
|
|
97
|
-
children
|
|
98
|
-
};
|
|
99
|
-
if (parent.state === "succeeded" || parent.state === "failed") return {
|
|
100
|
-
status: parent.state,
|
|
101
|
-
transitioned: false,
|
|
102
|
-
parent,
|
|
103
|
-
children
|
|
104
|
-
};
|
|
105
|
-
if (children.some((child) => child.state === "failed" || child.state === "cancelled")) {
|
|
106
|
-
await options.present({
|
|
107
|
-
status: "failed",
|
|
108
|
-
parent,
|
|
109
|
-
children
|
|
110
|
-
});
|
|
111
|
-
return {
|
|
112
|
-
status: "failed",
|
|
113
|
-
transitioned: true,
|
|
114
|
-
parent: await stores.tasks.transition(parent.id, "failed", { reason: "one or more repository tasks failed" }),
|
|
115
|
-
children
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
if (!children.every((child) => child.state === "succeeded")) return {
|
|
119
|
-
status: "pending",
|
|
120
|
-
parent,
|
|
121
|
-
children
|
|
122
|
-
};
|
|
123
|
-
await options.present({
|
|
124
|
-
status: "succeeded",
|
|
125
|
-
parent,
|
|
126
|
-
children
|
|
127
|
-
});
|
|
128
|
-
await stores.tasks.transition(parent.id, "verifying", { reason: "all repository tasks succeeded" });
|
|
129
|
-
return {
|
|
130
|
-
status: "succeeded",
|
|
131
|
-
transitioned: true,
|
|
132
|
-
parent: await stores.tasks.transition(parent.id, "succeeded", { reason: "aggregate multi-repository handoff is ready" }),
|
|
133
|
-
children
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
//#endregion
|
|
137
103
|
//#region src/metrics.ts
|
|
138
104
|
const bucketTable = {
|
|
139
105
|
low: {
|
|
@@ -258,15 +224,17 @@ function computeLedgerMetrics(rows, options = {}) {
|
|
|
258
224
|
}
|
|
259
225
|
const openChangeStates = /* @__PURE__ */ new Set(["verifying", "ready"]);
|
|
260
226
|
/**
|
|
261
|
-
* Green, yellow, or red from rows, never from a model. Budget and
|
|
262
|
-
*
|
|
263
|
-
*
|
|
227
|
+
* Green, yellow, or red from rows, never from a model. Budget threshold and
|
|
228
|
+
* onboarding rules are not evaluated here yet: budget notices live on the
|
|
229
|
+
* budgets collection (not in these rows), and onboarding investigations are
|
|
230
|
+
* not recorded.
|
|
264
231
|
*/
|
|
265
232
|
function computeRepositoryHealth(rows, options = {}) {
|
|
266
233
|
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
267
234
|
const dayMs = 864e5;
|
|
268
235
|
const red = [];
|
|
269
236
|
const yellow = [];
|
|
237
|
+
const tasksById = new Map(rows.tasks.map((task) => [task.id, task]));
|
|
270
238
|
for (const receipt of rows.receipts) {
|
|
271
239
|
if (now - Date.parse(receipt.recordedAt) >= dayMs) continue;
|
|
272
240
|
if (receipt.state === "rolled_back") red.push(`rollback in the last 24h (${receipt.receiptId})`);
|
|
@@ -276,6 +244,12 @@ function computeRepositoryHealth(rows, options = {}) {
|
|
|
276
244
|
if (openQuestions > 0) yellow.push(`${openQuestions} open question(s)`);
|
|
277
245
|
const needsHuman = rows.tasks.filter((task) => task.kind !== "question" && task.state === "needs_human").length;
|
|
278
246
|
if (needsHuman > 0) yellow.push(`${needsHuman} task(s) waiting on a human`);
|
|
247
|
+
const maxQueuedMinutes = options.maxQueuedMinutes ?? 5;
|
|
248
|
+
const staleQueued = rows.tasks.filter((task) => {
|
|
249
|
+
if (task.state !== "queued" || task.kind === "question" || task.kind === "coordination" || task.approval === "required") return false;
|
|
250
|
+
return (task.dependsOn ?? []).every((dependencyId) => tasksById.get(dependencyId)?.changeId !== void 0) && now - Date.parse(task.updatedAt) >= maxQueuedMinutes * 60 * 1e3;
|
|
251
|
+
}).length;
|
|
252
|
+
if (staleQueued > 0) yellow.push(`${staleQueued} eligible task(s) queued for at least ${maxQueuedMinutes} minutes`);
|
|
279
253
|
const blocked = rows.changes.filter((change) => openChangeStates.has(change.state) && reviewBucket(change.risk, change.confidence) === "blocked").length;
|
|
280
254
|
if (blocked > 0) yellow.push(`${blocked} change(s) in the blocked bucket`);
|
|
281
255
|
if (options.maxOpenChanges !== void 0) {
|
|
@@ -296,365 +270,6 @@ function computeRepositoryHealth(rows, options = {}) {
|
|
|
296
270
|
};
|
|
297
271
|
}
|
|
298
272
|
//#endregion
|
|
299
|
-
//#region src/schema/budget.ts
|
|
300
|
-
const budgetMonthSchema = z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/);
|
|
301
|
-
const budgetSchema = z.strictObject({
|
|
302
|
-
schemaVersion: schemaVersionSchema,
|
|
303
|
-
factoryId: factoryIdSchema,
|
|
304
|
-
month: budgetMonthSchema,
|
|
305
|
-
limits: z.strictObject({
|
|
306
|
-
totalUsd: usdSchema,
|
|
307
|
-
investigationUsd: usdSchema,
|
|
308
|
-
perTaskReservationUsd: usdSchema,
|
|
309
|
-
warningPercent: z.number().int().min(1).max(100),
|
|
310
|
-
perRepositoryUsd: z.record(repositorySlugSchema, usdSchema).optional()
|
|
311
|
-
}),
|
|
312
|
-
notices: z.strictObject({
|
|
313
|
-
warningAt: isoDateTimeSchema.optional(),
|
|
314
|
-
exhaustedAt: isoDateTimeSchema.optional()
|
|
315
|
-
})
|
|
316
|
-
});
|
|
317
|
-
//#endregion
|
|
318
|
-
//#region src/schema/receipt.ts
|
|
319
|
-
/** States the engine writes on transitions. Any vocabulary value is valid. */
|
|
320
|
-
const standardReceiptStates = [
|
|
321
|
-
"approved",
|
|
322
|
-
"rejected",
|
|
323
|
-
"dispatched",
|
|
324
|
-
"succeeded",
|
|
325
|
-
"failed",
|
|
326
|
-
"needs_human",
|
|
327
|
-
"cancelled",
|
|
328
|
-
"settled",
|
|
329
|
-
"ready",
|
|
330
|
-
"merged",
|
|
331
|
-
"deployed",
|
|
332
|
-
"rolled_out",
|
|
333
|
-
"rolled_back"
|
|
334
|
-
];
|
|
335
|
-
/** Phases the standard loop uses. Any vocabulary value is valid. */
|
|
336
|
-
const standardReceiptPhases = [
|
|
337
|
-
"approval",
|
|
338
|
-
"budget",
|
|
339
|
-
"dispatch",
|
|
340
|
-
"implementation",
|
|
341
|
-
"verification",
|
|
342
|
-
"pull_request",
|
|
343
|
-
"cleanup"
|
|
344
|
-
];
|
|
345
|
-
/** Kinds the standard loop writes: the standard task kinds plus "change". */
|
|
346
|
-
const standardReceiptKinds = [...standardTaskKinds, "change"];
|
|
347
|
-
const receiptStateSchema = vocabularySchema.describe("What happened, e.g. dispatched, settled, merged, or a custom event name.");
|
|
348
|
-
const receiptPhaseSchema = vocabularySchema.describe("Where in the process it happened, e.g. dispatch or verification.");
|
|
349
|
-
const receiptKindSchema = vocabularySchema.describe("What the receipt is about: a task kind, change, or a custom kind.");
|
|
350
|
-
const costSchema = z.strictObject({ totalUsd: usdSchema });
|
|
351
|
-
const receiptSchema = z.strictObject({
|
|
352
|
-
schemaVersion: schemaVersionSchema,
|
|
353
|
-
receiptId: receiptIdSchema,
|
|
354
|
-
repositoryIds: repositoryIdsSchema.describe("Repositories the recorded event concerns."),
|
|
355
|
-
taskId: taskIdSchema.optional(),
|
|
356
|
-
changeId: changeIdSchema.optional(),
|
|
357
|
-
kind: receiptKindSchema,
|
|
358
|
-
state: receiptStateSchema,
|
|
359
|
-
phase: receiptPhaseSchema.optional(),
|
|
360
|
-
reason: z.string().min(1).max(2e3).optional().describe("Why it happened. Scrubbed and truncated; never secrets."),
|
|
361
|
-
attempt: z.number().int().min(1).describe("Attempt the event belongs to."),
|
|
362
|
-
cost: costSchema.optional().describe("Measured cost. Settled receipts only."),
|
|
363
|
-
decidedBy: z.string().min(1).optional().describe("Human who made the decision. Change and approval receipts only."),
|
|
364
|
-
recordedAt: isoDateTimeSchema
|
|
365
|
-
}).superRefine((receipt, ctx) => {
|
|
366
|
-
if (receipt.state === "settled" && receipt.cost === void 0) ctx.addIssue({
|
|
367
|
-
code: "custom",
|
|
368
|
-
path: ["cost"],
|
|
369
|
-
message: "settled receipts require cost"
|
|
370
|
-
});
|
|
371
|
-
if (receipt.state !== "settled" && receipt.cost !== void 0) ctx.addIssue({
|
|
372
|
-
code: "custom",
|
|
373
|
-
path: ["cost"],
|
|
374
|
-
message: "cost is only valid on settled receipts"
|
|
375
|
-
});
|
|
376
|
-
if (receipt.decidedBy !== void 0 && receipt.kind !== "change" && receipt.state !== "approved" && receipt.state !== "rejected") ctx.addIssue({
|
|
377
|
-
code: "custom",
|
|
378
|
-
path: ["decidedBy"],
|
|
379
|
-
message: "decidedBy is only valid on change and approval receipts"
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
//#endregion
|
|
383
|
-
//#region src/schema/signal.ts
|
|
384
|
-
/** Sources used by the standard connectors. Any vocabulary value is valid. */
|
|
385
|
-
const standardSignalSources = [
|
|
386
|
-
"github_issue",
|
|
387
|
-
"github_pr",
|
|
388
|
-
"github_pr_comment",
|
|
389
|
-
"slack",
|
|
390
|
-
"human",
|
|
391
|
-
"github_ci_failure",
|
|
392
|
-
"vercel_deploy_failure",
|
|
393
|
-
"security_scan",
|
|
394
|
-
"investigation",
|
|
395
|
-
"api"
|
|
396
|
-
];
|
|
397
|
-
const signalSourceSchema = vocabularySchema.describe("What produced the signal, e.g. github_issue or a custom connector's own name.");
|
|
398
|
-
const signalOutcomeSchema = z.enum([
|
|
399
|
-
"pending",
|
|
400
|
-
"accepted",
|
|
401
|
-
"duplicate",
|
|
402
|
-
"unactionable",
|
|
403
|
-
"low_confidence",
|
|
404
|
-
"ignored"
|
|
405
|
-
]).describe("Triage outcome. A signal starts pending and is decided exactly once.");
|
|
406
|
-
const signalSchema = z.strictObject({
|
|
407
|
-
schemaVersion: schemaVersionSchema,
|
|
408
|
-
id: signalIdSchema.describe("Signal id, derived from source and deliveryId."),
|
|
409
|
-
repositoryId: repositoryIdSchema.describe("Repository the signal is about."),
|
|
410
|
-
source: signalSourceSchema,
|
|
411
|
-
deliveryId: z.string().min(1).describe("Provider idempotency key; a redelivered event maps to the same signal."),
|
|
412
|
-
replyTo: replyToSchema,
|
|
413
|
-
payload: z.unknown().describe("Provider-shaped payload. Untrusted data, never instructions."),
|
|
414
|
-
receivedAt: isoDateTimeSchema,
|
|
415
|
-
outcome: signalOutcomeSchema,
|
|
416
|
-
detail: z.string().min(1).optional().describe("Outcome nuance, e.g. severity when accepted or the duplicate-of signal id."),
|
|
417
|
-
reason: z.string().min(1).optional().describe("Why the outcome was chosen."),
|
|
418
|
-
decidedAt: isoDateTimeSchema.optional()
|
|
419
|
-
}).superRefine((signal, ctx) => {
|
|
420
|
-
if (signal.outcome === "pending" && signal.decidedAt !== void 0) ctx.addIssue({
|
|
421
|
-
code: "custom",
|
|
422
|
-
path: ["decidedAt"],
|
|
423
|
-
message: "pending signals must not carry decidedAt"
|
|
424
|
-
});
|
|
425
|
-
if (signal.outcome !== "pending" && signal.decidedAt === void 0) ctx.addIssue({
|
|
426
|
-
code: "custom",
|
|
427
|
-
path: ["decidedAt"],
|
|
428
|
-
message: "decided signals require decidedAt"
|
|
429
|
-
});
|
|
430
|
-
});
|
|
431
|
-
//#endregion
|
|
432
|
-
//#region src/schema/transitions.ts
|
|
433
|
-
const taskInitialState = "queued";
|
|
434
|
-
const taskTransitions = [
|
|
435
|
-
{
|
|
436
|
-
from: "queued",
|
|
437
|
-
to: "running",
|
|
438
|
-
trigger: "dependencies have a change, reservation succeeded, devbox created",
|
|
439
|
-
actor: "reconciler"
|
|
440
|
-
},
|
|
441
|
-
{
|
|
442
|
-
from: "queued",
|
|
443
|
-
to: "failed",
|
|
444
|
-
trigger: "reservation refused",
|
|
445
|
-
actor: "reconciler"
|
|
446
|
-
},
|
|
447
|
-
{
|
|
448
|
-
from: "queued",
|
|
449
|
-
to: "needs_human",
|
|
450
|
-
trigger: "kind is question",
|
|
451
|
-
actor: "system"
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
from: "queued",
|
|
455
|
-
to: "cancelled",
|
|
456
|
-
trigger: "cancel or kill",
|
|
457
|
-
actor: "operator"
|
|
458
|
-
},
|
|
459
|
-
{
|
|
460
|
-
from: "running",
|
|
461
|
-
to: "verifying",
|
|
462
|
-
trigger: "devbox task reported complete",
|
|
463
|
-
actor: "reconciler"
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
from: "running",
|
|
467
|
-
to: "queued",
|
|
468
|
-
trigger: "launch failed below attempt limit",
|
|
469
|
-
actor: "reconciler"
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
from: "running",
|
|
473
|
-
to: "failed",
|
|
474
|
-
trigger: "devbox task errored, deadline exceeded, or launch failed at attempt limit",
|
|
475
|
-
actor: "reconciler"
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
from: "running",
|
|
479
|
-
to: "needs_human",
|
|
480
|
-
trigger: "devbox task requires attention",
|
|
481
|
-
actor: "reconciler"
|
|
482
|
-
},
|
|
483
|
-
{
|
|
484
|
-
from: "running",
|
|
485
|
-
to: "cancelled",
|
|
486
|
-
trigger: "cancel or kill",
|
|
487
|
-
actor: "operator"
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
from: "verifying",
|
|
491
|
-
to: "succeeded",
|
|
492
|
-
trigger: "kind criterion true",
|
|
493
|
-
actor: "reconciler"
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
from: "verifying",
|
|
497
|
-
to: "running",
|
|
498
|
-
trigger: "verification failed below attempt limit",
|
|
499
|
-
actor: "reconciler"
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
from: "verifying",
|
|
503
|
-
to: "failed",
|
|
504
|
-
trigger: "verification failed at attempt limit",
|
|
505
|
-
actor: "reconciler"
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
from: "verifying",
|
|
509
|
-
to: "cancelled",
|
|
510
|
-
trigger: "cancel or kill",
|
|
511
|
-
actor: "operator"
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
from: "needs_human",
|
|
515
|
-
to: "succeeded",
|
|
516
|
-
trigger: "question answered",
|
|
517
|
-
actor: "operator"
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
from: "needs_human",
|
|
521
|
-
to: "queued",
|
|
522
|
-
trigger: "operator retry",
|
|
523
|
-
actor: "operator"
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
from: "needs_human",
|
|
527
|
-
to: "cancelled",
|
|
528
|
-
trigger: "cancel or kill",
|
|
529
|
-
actor: "operator"
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
from: "failed",
|
|
533
|
-
to: "queued",
|
|
534
|
-
trigger: "operator retry",
|
|
535
|
-
actor: "operator"
|
|
536
|
-
}
|
|
537
|
-
];
|
|
538
|
-
const changeInitialState = "verifying";
|
|
539
|
-
const changeTransitions = [
|
|
540
|
-
{
|
|
541
|
-
from: "verifying",
|
|
542
|
-
to: "ready",
|
|
543
|
-
trigger: "verification task succeeded, risk and confidence written",
|
|
544
|
-
actor: "reconciler"
|
|
545
|
-
},
|
|
546
|
-
{
|
|
547
|
-
from: "verifying",
|
|
548
|
-
to: "closed",
|
|
549
|
-
trigger: "verification failed at attempt limit or pr closed upstream",
|
|
550
|
-
actor: "reconciler"
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
from: "ready",
|
|
554
|
-
to: "merged",
|
|
555
|
-
trigger: "pr merged on github",
|
|
556
|
-
actor: "webhook"
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
from: "ready",
|
|
560
|
-
to: "closed",
|
|
561
|
-
trigger: "pr closed without merge",
|
|
562
|
-
actor: "webhook"
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
from: "merged",
|
|
566
|
-
to: "deployed",
|
|
567
|
-
trigger: "production deployment ready containing the merge sha",
|
|
568
|
-
actor: "webhook"
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
from: "deployed",
|
|
572
|
-
to: "rolled_out",
|
|
573
|
-
trigger: "no flag, or flag ramped to full and held",
|
|
574
|
-
actor: "foreman"
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
from: "deployed",
|
|
578
|
-
to: "rolled_back",
|
|
579
|
-
trigger: "rollback rule crossed",
|
|
580
|
-
actor: "foreman"
|
|
581
|
-
},
|
|
582
|
-
{
|
|
583
|
-
from: "rolled_out",
|
|
584
|
-
to: "rolled_back",
|
|
585
|
-
trigger: "rollback rule crossed or flag flipped off",
|
|
586
|
-
actor: "foreman"
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
from: "rolled_back",
|
|
590
|
-
to: "deployed",
|
|
591
|
-
trigger: "human retried the ramp",
|
|
592
|
-
actor: "human"
|
|
593
|
-
}
|
|
594
|
-
];
|
|
595
|
-
const signalInitialOutcome = "pending";
|
|
596
|
-
const signalOutcomeTransitions = [
|
|
597
|
-
{
|
|
598
|
-
from: "pending",
|
|
599
|
-
to: "accepted",
|
|
600
|
-
trigger: "classified as actionable",
|
|
601
|
-
actor: "reconciler"
|
|
602
|
-
},
|
|
603
|
-
{
|
|
604
|
-
from: "pending",
|
|
605
|
-
to: "duplicate",
|
|
606
|
-
trigger: "classified as duplicate",
|
|
607
|
-
actor: "reconciler"
|
|
608
|
-
},
|
|
609
|
-
{
|
|
610
|
-
from: "pending",
|
|
611
|
-
to: "unactionable",
|
|
612
|
-
trigger: "classified as unactionable",
|
|
613
|
-
actor: "reconciler"
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
from: "pending",
|
|
617
|
-
to: "low_confidence",
|
|
618
|
-
trigger: "classified below confidence threshold",
|
|
619
|
-
actor: "reconciler"
|
|
620
|
-
},
|
|
621
|
-
{
|
|
622
|
-
from: "pending",
|
|
623
|
-
to: "ignored",
|
|
624
|
-
trigger: "classified as ignorable",
|
|
625
|
-
actor: "reconciler"
|
|
626
|
-
}
|
|
627
|
-
];
|
|
628
|
-
function isTransitionAllowed(transitions, from, to) {
|
|
629
|
-
return transitions.some((transition) => transition.from === from && transition.to === to);
|
|
630
|
-
}
|
|
631
|
-
function legalNextStates(transitions, from) {
|
|
632
|
-
return [...new Set(transitions.filter((transition) => transition.from === from).map((t) => t.to))];
|
|
633
|
-
}
|
|
634
|
-
var InvalidTransitionError = class extends Error {
|
|
635
|
-
from;
|
|
636
|
-
to;
|
|
637
|
-
legalNextStates;
|
|
638
|
-
constructor(from, to, legal) {
|
|
639
|
-
super(legal.length > 0 ? `invalid transition from "${from}" to "${to}"; legal next states: ${legal.map((state) => `"${state}"`).join(", ")}` : `invalid transition from "${from}" to "${to}"; "${from}" is terminal`);
|
|
640
|
-
this.name = "InvalidTransitionError";
|
|
641
|
-
this.from = from;
|
|
642
|
-
this.to = to;
|
|
643
|
-
this.legalNextStates = legal;
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
function assertTransition(transitions, from, to) {
|
|
647
|
-
if (!isTransitionAllowed(transitions, from, to)) throw new InvalidTransitionError(from, to, legalNextStates(transitions, from));
|
|
648
|
-
}
|
|
649
|
-
function terminalStates(transitions) {
|
|
650
|
-
const states = /* @__PURE__ */ new Set();
|
|
651
|
-
for (const transition of transitions) {
|
|
652
|
-
states.add(transition.from);
|
|
653
|
-
states.add(transition.to);
|
|
654
|
-
}
|
|
655
|
-
return [...states].filter((state) => !transitions.some((transition) => transition.from === state));
|
|
656
|
-
}
|
|
657
|
-
//#endregion
|
|
658
273
|
//#region src/signals.ts
|
|
659
274
|
/**
|
|
660
275
|
* Bind normalized intake to signal creation alone. Connectors and API routes
|
|
@@ -667,346 +282,62 @@ function createSignalCreator(stores) {
|
|
|
667
282
|
} };
|
|
668
283
|
}
|
|
669
284
|
//#endregion
|
|
670
|
-
//#region src/
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
taskId;
|
|
674
|
-
reason;
|
|
675
|
-
constructor(taskId, reason) {
|
|
676
|
-
super(`approval decision on "${taskId}" refused: ${reason}`);
|
|
677
|
-
this.name = "ApprovalError";
|
|
678
|
-
this.taskId = taskId;
|
|
679
|
-
this.reason = reason;
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
var ChangeAssociationError = class extends Error {
|
|
683
|
-
taskId;
|
|
684
|
-
repositoryId;
|
|
685
|
-
reason;
|
|
686
|
-
constructor(taskId, repositoryId, reason) {
|
|
687
|
-
super(`change for task "${taskId}" refused: ${reason}`);
|
|
688
|
-
this.name = "ChangeAssociationError";
|
|
689
|
-
this.taskId = taskId;
|
|
690
|
-
this.repositoryId = repositoryId;
|
|
691
|
-
this.reason = reason;
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
const taskReceiptStates = {
|
|
695
|
-
running: "dispatched",
|
|
696
|
-
succeeded: "succeeded",
|
|
697
|
-
failed: "failed",
|
|
698
|
-
needs_human: "needs_human",
|
|
699
|
-
cancelled: "cancelled"
|
|
700
|
-
};
|
|
701
|
-
const changeReceiptStates = {
|
|
702
|
-
ready: "ready",
|
|
703
|
-
merged: "merged",
|
|
704
|
-
deployed: "deployed",
|
|
705
|
-
rolled_out: "rolled_out",
|
|
706
|
-
rolled_back: "rolled_back"
|
|
707
|
-
};
|
|
708
|
-
function randomId(prefix) {
|
|
709
|
-
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
710
|
-
}
|
|
711
|
-
function derivedId(prefix, key) {
|
|
712
|
-
return `${prefix}_${createHash("sha256").update(key).digest("hex").slice(0, 32)}`;
|
|
285
|
+
//#region src/stall.ts
|
|
286
|
+
function stallWindowMinutes(policy, kind) {
|
|
287
|
+
return policy.stallMinutesByKind[kind] ?? policy.stallMinutes;
|
|
713
288
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
async function insertReceipt(receiptId, receipt) {
|
|
737
|
-
const full = receiptSchema.parse({
|
|
738
|
-
...receipt,
|
|
739
|
-
schemaVersion: 1,
|
|
740
|
-
receiptId,
|
|
741
|
-
recordedAt: now()
|
|
742
|
-
});
|
|
289
|
+
/**
|
|
290
|
+
* The reconciler's stall pass. A running task whose execution has not
|
|
291
|
+
* progressed within its stall window is marked stalled; a stalled task is
|
|
292
|
+
* re-queued for a replacement attempt while attempts remain, and failed once
|
|
293
|
+
* they are exhausted. Only `running` tasks can stall: a task whose result has
|
|
294
|
+
* already been recorded (verifying, or terminal) is never retried by this
|
|
295
|
+
* pass. Coordination parents run only through their children and are skipped.
|
|
296
|
+
*/
|
|
297
|
+
async function sweepStalled(stores, options) {
|
|
298
|
+
const nowMs = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
299
|
+
const result = {
|
|
300
|
+
stalled: [],
|
|
301
|
+
requeued: [],
|
|
302
|
+
failed: []
|
|
303
|
+
};
|
|
304
|
+
const tasks = await stores.tasks.list();
|
|
305
|
+
for (const task of tasks) {
|
|
306
|
+
if (task.state !== "running" || task.kind === "coordination") continue;
|
|
307
|
+
const minutes = stallWindowMinutes(options.policy, task.kind);
|
|
308
|
+
const sinceProgress = nowMs - Date.parse(task.lastProgressAt ?? task.updatedAt);
|
|
309
|
+
if (sinceProgress < minutes * 6e4) continue;
|
|
743
310
|
try {
|
|
744
|
-
await
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
311
|
+
await stores.tasks.transition(task.id, "stalled", {
|
|
312
|
+
reason: `no progress for ${Math.floor(sinceProgress / 6e4)} minutes (window ${minutes})`,
|
|
313
|
+
expectFrom: "running"
|
|
314
|
+
});
|
|
315
|
+
result.stalled.push(task.id);
|
|
316
|
+
} catch {
|
|
317
|
+
continue;
|
|
749
318
|
}
|
|
750
319
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const parsed = repositorySchema.parse(repository);
|
|
759
|
-
const existing = await read("repositories", parsed.id, repositorySchema);
|
|
760
|
-
if (existing === null) await driver.insert("repositories", parsed.id, parsed);
|
|
761
|
-
else await driver.update("repositories", parsed.id, parsed, existing.version);
|
|
762
|
-
return parsed;
|
|
763
|
-
},
|
|
764
|
-
async get(id) {
|
|
765
|
-
const record = await read("repositories", id, repositorySchema);
|
|
766
|
-
return record === null ? null : record.value;
|
|
767
|
-
},
|
|
768
|
-
list() {
|
|
769
|
-
return listAll("repositories", repositorySchema);
|
|
770
|
-
}
|
|
771
|
-
},
|
|
772
|
-
signals: {
|
|
773
|
-
async create(input) {
|
|
774
|
-
const id = derivedId("sig", `${input.source}:${input.deliveryId}`);
|
|
775
|
-
const signal = signalSchema.parse({
|
|
776
|
-
schemaVersion: 1,
|
|
777
|
-
id,
|
|
778
|
-
repositoryId: input.repositoryId,
|
|
779
|
-
source: input.source,
|
|
780
|
-
deliveryId: input.deliveryId,
|
|
781
|
-
replyTo: input.replyTo,
|
|
782
|
-
payload: input.payload,
|
|
783
|
-
receivedAt: now(),
|
|
784
|
-
outcome: "pending"
|
|
785
|
-
});
|
|
786
|
-
try {
|
|
787
|
-
await driver.insert("signals", id, signal);
|
|
788
|
-
return signal;
|
|
789
|
-
} catch (error) {
|
|
790
|
-
if (error instanceof RecordExistsError) return (await mustRead("signals", id, signalSchema)).value;
|
|
791
|
-
throw error;
|
|
792
|
-
}
|
|
793
|
-
},
|
|
794
|
-
async get(id) {
|
|
795
|
-
const record = await read("signals", id, signalSchema);
|
|
796
|
-
return record === null ? null : record.value;
|
|
797
|
-
},
|
|
798
|
-
async decide(id, outcome, decideOptions = {}) {
|
|
799
|
-
const existing = await mustRead("signals", id, signalSchema);
|
|
800
|
-
assertTransition(signalOutcomeTransitions, existing.value.outcome, outcome);
|
|
801
|
-
const next = signalSchema.parse({
|
|
802
|
-
...existing.value,
|
|
803
|
-
outcome,
|
|
804
|
-
detail: decideOptions.detail,
|
|
805
|
-
reason: decideOptions.reason,
|
|
806
|
-
decidedAt: now()
|
|
807
|
-
});
|
|
808
|
-
await driver.update("signals", id, next, existing.version);
|
|
809
|
-
return next;
|
|
810
|
-
},
|
|
811
|
-
list() {
|
|
812
|
-
return listAll("signals", signalSchema);
|
|
813
|
-
}
|
|
814
|
-
},
|
|
815
|
-
tasks: {
|
|
816
|
-
async create(input) {
|
|
817
|
-
const timestamp = now();
|
|
818
|
-
const id = input.dedupeKey === void 0 ? randomId("task") : derivedId("task", input.dedupeKey);
|
|
819
|
-
const task = taskSchema.parse({
|
|
820
|
-
schemaVersion: 1,
|
|
821
|
-
id,
|
|
822
|
-
repositoryIds: input.repositoryIds,
|
|
823
|
-
kind: input.kind,
|
|
824
|
-
area: input.area,
|
|
825
|
-
state: taskInitialState,
|
|
826
|
-
approval: input.approval,
|
|
827
|
-
origin: input.origin,
|
|
828
|
-
dependsOn: input.dependsOn,
|
|
829
|
-
replyTo: input.replyTo,
|
|
830
|
-
attempt: 1,
|
|
831
|
-
question: input.question,
|
|
832
|
-
changeId: input.changeId,
|
|
833
|
-
createdAt: timestamp,
|
|
834
|
-
updatedAt: timestamp
|
|
835
|
-
});
|
|
836
|
-
try {
|
|
837
|
-
await driver.insert("tasks", task.id, task);
|
|
838
|
-
return task;
|
|
839
|
-
} catch (error) {
|
|
840
|
-
if (input.dedupeKey !== void 0 && error instanceof RecordExistsError) return (await mustRead("tasks", task.id, taskSchema)).value;
|
|
841
|
-
throw error;
|
|
842
|
-
}
|
|
843
|
-
},
|
|
844
|
-
async approve(id, decision) {
|
|
845
|
-
const existing = await mustRead("tasks", id, taskSchema);
|
|
846
|
-
const task = existing.value;
|
|
847
|
-
if (task.approval !== "required") throw new ApprovalError(id, `approval is ${task.approval ?? "not required"}`);
|
|
848
|
-
if (task.state !== "queued") throw new ApprovalError(id, `task is ${task.state}, not queued`);
|
|
849
|
-
const next = taskSchema.parse({
|
|
850
|
-
...task,
|
|
851
|
-
approval: "granted",
|
|
852
|
-
updatedAt: now()
|
|
853
|
-
});
|
|
854
|
-
if (next.repositoryIds !== void 0) await appendReceipt(`task:${id}:approved:${next.attempt}`, {
|
|
855
|
-
repositoryIds: next.repositoryIds,
|
|
856
|
-
taskId: id,
|
|
857
|
-
kind: next.kind,
|
|
858
|
-
state: "approved",
|
|
859
|
-
phase: "approval",
|
|
860
|
-
reason: decision.reason,
|
|
861
|
-
attempt: next.attempt,
|
|
862
|
-
decidedBy: decision.by
|
|
863
|
-
});
|
|
864
|
-
await driver.update("tasks", id, next, existing.version);
|
|
865
|
-
return next;
|
|
866
|
-
},
|
|
867
|
-
async reject(id, decision) {
|
|
868
|
-
const existing = await mustRead("tasks", id, taskSchema);
|
|
869
|
-
const task = existing.value;
|
|
870
|
-
if (task.approval !== "required") throw new ApprovalError(id, `approval is ${task.approval ?? "not required"}`);
|
|
871
|
-
if (task.state !== "queued") throw new ApprovalError(id, `task is ${task.state}, not queued`);
|
|
872
|
-
const next = taskSchema.parse({
|
|
873
|
-
...task,
|
|
874
|
-
approval: "denied",
|
|
875
|
-
state: "cancelled",
|
|
876
|
-
updatedAt: now()
|
|
877
|
-
});
|
|
878
|
-
if (next.repositoryIds !== void 0) {
|
|
879
|
-
await appendReceipt(`task:${id}:rejected:${next.attempt}`, {
|
|
880
|
-
repositoryIds: next.repositoryIds,
|
|
881
|
-
taskId: id,
|
|
882
|
-
kind: next.kind,
|
|
883
|
-
state: "rejected",
|
|
884
|
-
phase: "approval",
|
|
885
|
-
reason: decision.reason,
|
|
886
|
-
attempt: next.attempt,
|
|
887
|
-
decidedBy: decision.by
|
|
888
|
-
});
|
|
889
|
-
await appendReceipt(`task:${id}:cancelled:${next.attempt}`, {
|
|
890
|
-
repositoryIds: next.repositoryIds,
|
|
891
|
-
taskId: id,
|
|
892
|
-
kind: next.kind,
|
|
893
|
-
state: "cancelled",
|
|
894
|
-
reason: decision.reason ?? `rejected by ${decision.by}`,
|
|
895
|
-
attempt: next.attempt
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
await driver.update("tasks", id, next, existing.version);
|
|
899
|
-
return next;
|
|
900
|
-
},
|
|
901
|
-
async get(id) {
|
|
902
|
-
const record = await read("tasks", id, taskSchema);
|
|
903
|
-
return record === null ? null : record.value;
|
|
904
|
-
},
|
|
905
|
-
async transition(id, to, transitionOptions = {}) {
|
|
906
|
-
const existing = await mustRead("tasks", id, taskSchema);
|
|
907
|
-
assertTransition(taskTransitions, existing.value.state, to);
|
|
908
|
-
const attempt = bumpsAttempt(existing.value.state, to) ? existing.value.attempt + 1 : existing.value.attempt;
|
|
909
|
-
const next = taskSchema.parse({
|
|
910
|
-
...existing.value,
|
|
911
|
-
...transitionOptions.patch,
|
|
912
|
-
state: to,
|
|
913
|
-
attempt,
|
|
914
|
-
updatedAt: now()
|
|
915
|
-
});
|
|
916
|
-
const receiptState = taskReceiptStates[to];
|
|
917
|
-
if (receiptState !== void 0 && next.repositoryIds !== void 0) await appendReceipt(`task:${id}:${receiptState}:${attempt}`, {
|
|
918
|
-
repositoryIds: next.repositoryIds,
|
|
919
|
-
taskId: id,
|
|
920
|
-
changeId: next.changeId,
|
|
921
|
-
kind: next.kind,
|
|
922
|
-
state: receiptState,
|
|
923
|
-
reason: transitionOptions.reason,
|
|
924
|
-
attempt
|
|
925
|
-
});
|
|
926
|
-
await driver.update("tasks", id, next, existing.version);
|
|
927
|
-
return next;
|
|
928
|
-
},
|
|
929
|
-
list() {
|
|
930
|
-
return listAll("tasks", taskSchema);
|
|
931
|
-
}
|
|
932
|
-
},
|
|
933
|
-
changes: {
|
|
934
|
-
async create(input) {
|
|
935
|
-
const timestamp = now();
|
|
936
|
-
const change = changeSchema.parse({
|
|
937
|
-
schemaVersion: 1,
|
|
938
|
-
id: randomId("chg"),
|
|
939
|
-
repositoryId: input.repositoryId,
|
|
940
|
-
taskId: input.taskId,
|
|
941
|
-
groupId: input.groupId,
|
|
942
|
-
prNumber: input.prNumber,
|
|
943
|
-
prUrl: input.prUrl,
|
|
944
|
-
author: input.author,
|
|
945
|
-
risk: input.risk,
|
|
946
|
-
confidence: input.confidence,
|
|
947
|
-
evidence: input.evidence,
|
|
948
|
-
state: changeInitialState,
|
|
949
|
-
createdAt: timestamp,
|
|
950
|
-
updatedAt: timestamp
|
|
951
|
-
});
|
|
952
|
-
await mustRead("repositories", change.repositoryId, repositorySchema);
|
|
953
|
-
if (change.taskId !== void 0) {
|
|
954
|
-
const task = (await mustRead("tasks", change.taskId, taskSchema)).value;
|
|
955
|
-
if (task.kind !== "code_change") throw new ChangeAssociationError(change.taskId, change.repositoryId, "changes can only be produced by code_change tasks");
|
|
956
|
-
const taskRepositoryId = task.repositoryIds[0];
|
|
957
|
-
if (taskRepositoryId !== change.repositoryId) throw new ChangeAssociationError(change.taskId, change.repositoryId, `repository "${change.repositoryId}" does not match task repository "${taskRepositoryId}"`);
|
|
958
|
-
}
|
|
959
|
-
await driver.insert("changes", change.id, change);
|
|
960
|
-
return change;
|
|
961
|
-
},
|
|
962
|
-
async get(id) {
|
|
963
|
-
const record = await read("changes", id, changeSchema);
|
|
964
|
-
return record === null ? null : record.value;
|
|
965
|
-
},
|
|
966
|
-
async transition(id, to, transitionOptions = {}) {
|
|
967
|
-
const existing = await mustRead("changes", id, changeSchema);
|
|
968
|
-
assertTransition(changeTransitions, existing.value.state, to);
|
|
969
|
-
const next = changeSchema.parse({
|
|
970
|
-
...existing.value,
|
|
971
|
-
...transitionOptions.patch,
|
|
972
|
-
state: to,
|
|
973
|
-
updatedAt: now()
|
|
974
|
-
});
|
|
975
|
-
const receiptState = changeReceiptStates[to];
|
|
976
|
-
if (receiptState !== void 0) await appendReceipt(`change:${id}:${receiptState}`, {
|
|
977
|
-
repositoryIds: [next.repositoryId],
|
|
978
|
-
taskId: next.taskId,
|
|
979
|
-
changeId: id,
|
|
980
|
-
kind: "change",
|
|
981
|
-
state: receiptState,
|
|
982
|
-
reason: transitionOptions.reason,
|
|
983
|
-
attempt: 1,
|
|
984
|
-
decidedBy: next.decidedBy
|
|
320
|
+
for (const task of await stores.tasks.list()) {
|
|
321
|
+
if (task.state !== "stalled") continue;
|
|
322
|
+
try {
|
|
323
|
+
if (task.attempt < options.policy.maxAttempts) {
|
|
324
|
+
await stores.tasks.transition(task.id, "queued", {
|
|
325
|
+
reason: `stalled on attempt ${task.attempt} of ${options.policy.maxAttempts}; replacing`,
|
|
326
|
+
expectFrom: "stalled"
|
|
985
327
|
});
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
},
|
|
993
|
-
receipts: {
|
|
994
|
-
append(input) {
|
|
995
|
-
const { dedupeKey, attempt, ...fields } = input;
|
|
996
|
-
return insertReceipt(dedupeKey === void 0 ? randomId("rcpt") : derivedId("rcpt", `custom:${dedupeKey}`), {
|
|
997
|
-
...fields,
|
|
998
|
-
attempt: attempt ?? 1
|
|
328
|
+
result.requeued.push(task.id);
|
|
329
|
+
} else {
|
|
330
|
+
await stores.tasks.transition(task.id, "failed", {
|
|
331
|
+
reason: `stalled on attempt ${task.attempt} of ${options.policy.maxAttempts}; attempts exhausted`,
|
|
332
|
+
expectFrom: "stalled"
|
|
999
333
|
});
|
|
1000
|
-
|
|
1001
|
-
async get(id) {
|
|
1002
|
-
const record = await read("receipts", id, receiptSchema);
|
|
1003
|
-
return record === null ? null : record.value;
|
|
1004
|
-
},
|
|
1005
|
-
list() {
|
|
1006
|
-
return listAll("receipts", receiptSchema);
|
|
334
|
+
result.failed.push(task.id);
|
|
1007
335
|
}
|
|
336
|
+
} catch {
|
|
337
|
+
continue;
|
|
1008
338
|
}
|
|
1009
|
-
}
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
1010
341
|
}
|
|
1011
342
|
//#endregion
|
|
1012
343
|
//#region src/store/memory.ts
|
|
@@ -1057,6 +388,27 @@ function createInMemoryDriver() {
|
|
|
1057
388
|
}
|
|
1058
389
|
//#endregion
|
|
1059
390
|
//#region src/sweep.ts
|
|
391
|
+
async function failUnrouteable(stores, task, reason, result) {
|
|
392
|
+
try {
|
|
393
|
+
await stores.tasks.transition(task.id, "failed", {
|
|
394
|
+
reason,
|
|
395
|
+
expectFrom: "queued"
|
|
396
|
+
});
|
|
397
|
+
result.failed.push({
|
|
398
|
+
taskId: task.id,
|
|
399
|
+
reason
|
|
400
|
+
});
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {
|
|
403
|
+
result.skipped.push({
|
|
404
|
+
taskId: task.id,
|
|
405
|
+
reason: "task changed during routing"
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
1060
412
|
/**
|
|
1061
413
|
* One pass of the factory's dispatcher, meant to run on a schedule inside the
|
|
1062
414
|
* deployment: every queued task that is allowed to run is started, in
|
|
@@ -1071,7 +423,10 @@ async function sweepQueued(stores, options) {
|
|
|
1071
423
|
skipped: [],
|
|
1072
424
|
failed: []
|
|
1073
425
|
};
|
|
1074
|
-
const
|
|
426
|
+
const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;
|
|
427
|
+
if (!(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) || maxDispatches < 1) throw new Error("maxDispatches must be a positive integer");
|
|
428
|
+
let attempted = 0;
|
|
429
|
+
const queued = (await stores.tasks.list()).filter((task) => task.state === "queued" && task.kind !== "question").sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));
|
|
1075
430
|
for (const task of queued) {
|
|
1076
431
|
if (task.kind === "coordination") {
|
|
1077
432
|
result.skipped.push({
|
|
@@ -1082,10 +437,7 @@ async function sweepQueued(stores, options) {
|
|
|
1082
437
|
}
|
|
1083
438
|
const executionRepositoryIds = taskExecutionRepositoryIdsSchema.safeParse(task.repositoryIds);
|
|
1084
439
|
if (!executionRepositoryIds.success) {
|
|
1085
|
-
result
|
|
1086
|
-
taskId: task.id,
|
|
1087
|
-
reason: "task execution requires exactly one repository"
|
|
1088
|
-
});
|
|
440
|
+
await failUnrouteable(stores, task, "task execution requires exactly one repository", result);
|
|
1089
441
|
continue;
|
|
1090
442
|
}
|
|
1091
443
|
if (task.approval === "required") {
|
|
@@ -1097,22 +449,27 @@ async function sweepQueued(stores, options) {
|
|
|
1097
449
|
}
|
|
1098
450
|
const launcher = options.launchers[task.kind];
|
|
1099
451
|
if (launcher === void 0) {
|
|
1100
|
-
|
|
1101
|
-
taskId: task.id,
|
|
1102
|
-
reason: `no agent handles ${task.kind} tasks`
|
|
1103
|
-
});
|
|
452
|
+
await failUnrouteable(stores, task, `no registered launcher handles ${task.kind} tasks`, result);
|
|
1104
453
|
continue;
|
|
1105
454
|
}
|
|
1106
455
|
const repository = await stores.repositories.get(executionRepositoryIds.data[0]);
|
|
1107
456
|
if (repository === null) {
|
|
457
|
+
await failUnrouteable(stores, task, "task has no repository", result);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (attempted >= maxDispatches) {
|
|
1108
461
|
result.skipped.push({
|
|
1109
462
|
taskId: task.id,
|
|
1110
|
-
reason: "
|
|
463
|
+
reason: "per-sweep dispatch limit reached"
|
|
1111
464
|
});
|
|
1112
465
|
continue;
|
|
1113
466
|
}
|
|
467
|
+
attempted += 1;
|
|
1114
468
|
try {
|
|
1115
|
-
await dispatchTask(stores, task.id, {
|
|
469
|
+
await dispatchTask(stores, task.id, {
|
|
470
|
+
launch: (dispatched) => launcher(dispatched, repository),
|
|
471
|
+
budget: options.budget
|
|
472
|
+
});
|
|
1116
473
|
result.dispatched.push(task.id);
|
|
1117
474
|
} catch (error) {
|
|
1118
475
|
if (error instanceof DispatchRefusedError) {
|
|
@@ -1160,6 +517,6 @@ async function ensureVerificationTasks(stores) {
|
|
|
1160
517
|
return result;
|
|
1161
518
|
}
|
|
1162
519
|
//#endregion
|
|
1163
|
-
export { ApprovalError, ChangeAssociationError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, VersionConflictError, adoptPullRequest, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetMonthSchema, budgetSchema, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, computeLedgerMetrics, computeRepositoryHealth, coordinationRepositoryIdsSchema, costSchema, createCoordination, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defineFactory, dispatchTask, ensureVerificationTasks, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, reconcileCoordination, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, reviewBucket, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepQueued,
|
|
520
|
+
export { AnswerEventConflictError, ApprovalError, BudgetExhaustedError, ChangeAssociationError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, OutboxEventConflictError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, SessionConflictError, SettlementError, VersionConflictError, adoptPullRequest, agentRouteSchema, agentSessionIdSchema, agentSessionSchema, answerEventSchema, answerIdSchema, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetLimitsSchema, budgetMonthOf, budgetMonthSchema, budgetSchema, budgetStatus, canReserve, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, communicationEventSchema, communicationOutcomeSchema, communicationOutcomes, computeLedgerMetrics, computeMonthSpendUsd, computeRepositoryHealth, coordinationRepositoryIdsSchema, costSchema, createCoordination, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defaultExecutionPolicy, defineAgentRoutes, defineFactory, deriveFactoryId, dispatchTask, ensureVerificationTasks, executionPolicySchema, executionReferenceSchema, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, outboxIdSchema, outboxMessageSchema, outboxQueueSchema, outboxStateSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, reconcileCoordination, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, reviewBucket, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, stallWindowMinutes, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepBudget, sweepQueued, sweepSettlements, sweepStalled, taskApprovalSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, taskUsageSchema, terminalStates, usdSchema, verificationBrief, vocabularySchema };
|
|
1164
521
|
|
|
1165
522
|
//# sourceMappingURL=index.mjs.map
|