@vercel/factory 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/approval-CwhTP1XB.mjs +269 -0
  2. package/dist/approval-CwhTP1XB.mjs.map +1 -0
  3. package/dist/blob/index.d.mts +1 -1
  4. package/dist/build-factory.d.mts +1 -1
  5. package/dist/build-factory.mjs +1 -1
  6. package/dist/change-CzEAoFVE.mjs +620 -0
  7. package/dist/change-CzEAoFVE.mjs.map +1 -0
  8. package/dist/connectors/slack.d.mts +4 -4
  9. package/dist/connectors/slack.mjs +81 -30
  10. package/dist/connectors/slack.mjs.map +1 -1
  11. package/dist/{approval-CDiBco2V.d.mts → dispatch-B77cuPxh.d.mts} +293 -99
  12. package/dist/{driver-CrZsy7sm.d.mts → driver-BJutB0PE.d.mts} +2 -2
  13. package/dist/driver-Bbd25rNM.mjs.map +1 -1
  14. package/dist/eve/index.d.mts +546 -120
  15. package/dist/eve/index.mjs +358 -82
  16. package/dist/eve/index.mjs.map +1 -1
  17. package/dist/{factory-config-ifd3JxiU.mjs → factory-config-BJcjsPKl.mjs} +42 -5
  18. package/dist/factory-config-BJcjsPKl.mjs.map +1 -0
  19. package/dist/{factory-config-Bu4mklm9.d.mts → factory-config-D2yU3G0c.d.mts} +21 -2
  20. package/dist/{github-wThBfbm-.d.mts → github-DXsxboUL.d.mts} +25 -2
  21. package/dist/github.d.mts +2 -2
  22. package/dist/github.mjs +47 -1
  23. package/dist/github.mjs.map +1 -1
  24. package/dist/index.d.mts +129 -59
  25. package/dist/index.mjs +336 -333
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/postgres/index.d.mts +1 -1
  28. package/package.json +1 -1
  29. package/dist/approval-DdvEKLiE.mjs +0 -132
  30. package/dist/approval-DdvEKLiE.mjs.map +0 -1
  31. package/dist/change-BPYhWKuL.mjs +0 -230
  32. package/dist/change-BPYhWKuL.mjs.map +0 -1
  33. package/dist/factory-config-ifd3JxiU.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,10 +1,44 @@
1
1
  import { a as repositoryIdsSchema, c as SCHEMA_VERSION, d as schemaVersionSchema, f as usdSchema, i as repositoryIdSchema, l as isoDateTimeSchema, n as factoryIdSchema, o as signalIdSchema, p as vocabularySchema, r as receiptIdSchema, s as taskIdSchema, t as changeIdSchema, u as replyToSchema } from "./id-DwCihQr4.mjs";
2
- 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-DdvEKLiE.mjs";
2
+ import { C as taskUsageSchema, S as taskStateSchema, _ as taskExecutionRepositoryIdsSchema, a as findingCategorySchema, b as taskQuestionSchema, c as findingSchema, d as findingVerificationSchema, f as coordinationRepositoryIdsSchema, g as taskApprovalSchema, h as standardTaskKinds, i as requiresApproval, l as findingVerificationLevelSchema, m as harnessSchema, n as approvalRuleSchema, o as findingFingerprint, p as executionReferenceSchema, r as defaultApprovalPolicy, s as findingPrioritySchema, t as approvalPolicySchema, u as findingVerificationMethodSchema, v as taskKindSchema, x as taskSchema, y as taskOriginSchema } from "./approval-CwhTP1XB.mjs";
3
+ import { C as budgetStatus, D as sweepBudget, E as deriveFactoryId, O as sweepSettlements, S as budgetMonthOf, T as computeMonthSpendUsd, _ as taskInitialState, a as BudgetExhaustedError, b as createCoordination, 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 canReserve, x as reconcileCoordination, y as terminalStates } from "./change-CzEAoFVE.mjs";
3
4
  import { n as RecordNotFoundError, r as VersionConflictError, t as RecordExistsError } from "./driver-Bbd25rNM.mjs";
4
- import { _ as taskSchema, a as DEFAULT_MAX_LAUNCH_ATTEMPTS, c as coordinationRepositoryIdsSchema, d as taskApprovalSchema, f as taskDevboxSchema, g as taskQuestionSchema, h as taskOriginSchema, i as levelSchema, l as harnessSchema, m as taskKindSchema, n as changeSchema, o as DispatchRefusedError, p as taskExecutionRepositoryIdsSchema, r as changeStateSchema, s as dispatchTask, t as changeEvidenceSchema, u as standardTaskKinds, v as taskStateSchema } from "./change-BPYhWKuL.mjs";
5
- import { a as repositorySlugSchema, i as repositorySchema, n as factoryConfigRepositorySchema, r as factoryConfigSchema, t as defineFactory } from "./factory-config-ifd3JxiU.mjs";
5
+ import { a as factoryConfigSchema, c as budgetSchema, i as factoryConfigRepositorySchema, l as repositorySchema, n as defineFactory, o as budgetLimitsSchema, r as executionPolicySchema, s as budgetMonthSchema, t as defaultExecutionPolicy, u as repositorySlugSchema } from "./factory-config-BJcjsPKl.mjs";
6
6
  import { z } from "zod";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
+ //#region src/agent-routes.ts
9
+ const agentRouteSchema = z.strictObject({
10
+ id: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u).describe("Stable route selected by a conversational agent or workflow."),
11
+ agent: z.string().regex(/^[a-z][a-z0-9-]{0,31}$/u).describe("Internally addressable Eve agent that executes this route."),
12
+ taskKind: taskKindSchema.describe("Factory task kind created for this route."),
13
+ description: z.string().min(1).max(500).describe("Model-facing guidance for when this route is appropriate."),
14
+ permissions: z.array(z.string().min(1)).default([]).describe("Inspectable capabilities granted to the target agent.")
15
+ });
16
+ /**
17
+ * Define the application-owned routes an agent or workflow may select. Route
18
+ * descriptions inform judgment; this validated registry remains the authority.
19
+ */
20
+ function defineAgentRoutes(inputs) {
21
+ const routes = inputs.map((input) => agentRouteSchema.parse(input));
22
+ const byId = /* @__PURE__ */ new Map();
23
+ const byTaskKind = /* @__PURE__ */ new Map();
24
+ for (const route of routes) {
25
+ if (byId.has(route.id)) throw new Error(`Agent route ${route.id} is registered more than once.`);
26
+ if (byTaskKind.has(route.taskKind)) throw new Error(`Task kind ${route.taskKind} is handled by more than one agent route.`);
27
+ byId.set(route.id, route);
28
+ byTaskKind.set(route.taskKind, route);
29
+ }
30
+ const frozen = Object.freeze(routes.map((route) => Object.freeze(route)));
31
+ return Object.freeze({
32
+ routes: frozen,
33
+ get(id) {
34
+ return byId.get(id);
35
+ },
36
+ forTaskKind(kind) {
37
+ return byTaskKind.get(kind);
38
+ }
39
+ });
40
+ }
41
+ //#endregion
8
42
  //#region src/briefs.ts
9
43
  /** The finding behind a task, when its signal carries one. */
10
44
  async function findingForTask(stores, task) {
@@ -37,6 +71,14 @@ function scanBrief(task, repository) {
37
71
  "You never fix anything. When you are done, summarize what deepsec found and what you added, by priority, and what you could not scan."
38
72
  ].join("\n");
39
73
  }
74
+ /** The verifier's starting message for a recorded pull request. */
75
+ function verificationBrief(task, repository, change) {
76
+ return [
77
+ `You are assigned factory verification task ${task.id} in repository ${repository.slug}.`,
78
+ `Change under review: ${change.id} (${change.prUrl}).`,
79
+ "Read the task with get_task, independently inspect and test the change, then call record_verification with every evidence field."
80
+ ].join("\n");
81
+ }
40
82
  //#endregion
41
83
  //#region src/changes.ts
42
84
  async function adoptPullRequest(stores, input) {
@@ -56,76 +98,6 @@ async function adoptPullRequest(stores, input) {
56
98
  });
57
99
  }
58
100
  //#endregion
59
- //#region src/coordination.ts
60
- /** Create one non-executable parent and one independently executable child per repository. */
61
- async function createCoordination(stores, input) {
62
- const repositoryIds = [...new Set(input.repositoryIds)];
63
- if (repositoryIds.length < 2) throw new Error("coordination requires at least two distinct repositories");
64
- const parent = await stores.tasks.create({
65
- repositoryIds,
66
- kind: "coordination",
67
- origin: { signalId: input.signalId },
68
- replyTo: input.replyTo
69
- });
70
- return {
71
- parent: await stores.tasks.transition(parent.id, "running", { reason: "repository-scoped child tasks created" }),
72
- children: await Promise.all(repositoryIds.map((repositoryId) => stores.tasks.create({
73
- repositoryIds: [repositoryId],
74
- kind: "code_change",
75
- origin: { parentTaskId: parent.id },
76
- replyTo: input.replyTo
77
- })))
78
- };
79
- }
80
- /** Reconcile a coordination parent from its direct children without ever dispatching the parent. */
81
- async function reconcileCoordination(stores, parentTaskId, options) {
82
- const parent = await stores.tasks.get(parentTaskId);
83
- if (parent === null) throw new RecordNotFoundError("tasks", parentTaskId);
84
- if (parent.kind !== "coordination") throw new Error(`task ${parentTaskId} is ${parent.kind}, not coordination`);
85
- const children = (await stores.tasks.list()).filter((task) => task.origin.parentTaskId === parentTaskId);
86
- if (children.length !== parent.repositoryIds.length) return {
87
- status: "pending",
88
- parent,
89
- children
90
- };
91
- if (parent.state === "succeeded" || parent.state === "failed") return {
92
- status: parent.state,
93
- transitioned: false,
94
- parent,
95
- children
96
- };
97
- if (children.some((child) => child.state === "failed" || child.state === "cancelled")) {
98
- await options.present({
99
- status: "failed",
100
- parent,
101
- children
102
- });
103
- return {
104
- status: "failed",
105
- transitioned: true,
106
- parent: await stores.tasks.transition(parent.id, "failed", { reason: "one or more repository tasks failed" }),
107
- children
108
- };
109
- }
110
- if (!children.every((child) => child.state === "succeeded")) return {
111
- status: "pending",
112
- parent,
113
- children
114
- };
115
- await options.present({
116
- status: "succeeded",
117
- parent,
118
- children
119
- });
120
- await stores.tasks.transition(parent.id, "verifying", { reason: "all repository tasks succeeded" });
121
- return {
122
- status: "succeeded",
123
- transitioned: true,
124
- parent: await stores.tasks.transition(parent.id, "succeeded", { reason: "aggregate multi-repository handoff is ready" }),
125
- children
126
- };
127
- }
128
- //#endregion
129
101
  //#region src/metrics.ts
130
102
  const bucketTable = {
131
103
  low: {
@@ -250,9 +222,10 @@ function computeLedgerMetrics(rows, options = {}) {
250
222
  }
251
223
  const openChangeStates = /* @__PURE__ */ new Set(["verifying", "ready"]);
252
224
  /**
253
- * Green, yellow, or red from rows, never from a model. Budget and onboarding
254
- * signals are not evaluated yet: budgets do not settle and onboarding
255
- * investigations are not recorded, so those spec rules have no rows to read.
225
+ * Green, yellow, or red from rows, never from a model. Budget threshold and
226
+ * onboarding rules are not evaluated here yet: budget notices live on the
227
+ * budgets collection (not in these rows), and onboarding investigations are
228
+ * not recorded.
256
229
  */
257
230
  function computeRepositoryHealth(rows, options = {}) {
258
231
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
@@ -288,31 +261,13 @@ function computeRepositoryHealth(rows, options = {}) {
288
261
  };
289
262
  }
290
263
  //#endregion
291
- //#region src/schema/budget.ts
292
- const budgetMonthSchema = z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/);
293
- const budgetSchema = z.strictObject({
294
- schemaVersion: schemaVersionSchema,
295
- factoryId: factoryIdSchema,
296
- month: budgetMonthSchema,
297
- limits: z.strictObject({
298
- totalUsd: usdSchema,
299
- investigationUsd: usdSchema,
300
- perTaskReservationUsd: usdSchema,
301
- warningPercent: z.number().int().min(1).max(100),
302
- perRepositoryUsd: z.record(repositorySlugSchema, usdSchema).optional()
303
- }),
304
- notices: z.strictObject({
305
- warningAt: isoDateTimeSchema.optional(),
306
- exhaustedAt: isoDateTimeSchema.optional()
307
- })
308
- });
309
- //#endregion
310
264
  //#region src/schema/receipt.ts
311
265
  /** States the engine writes on transitions. Any vocabulary value is valid. */
312
266
  const standardReceiptStates = [
313
267
  "approved",
314
268
  "rejected",
315
269
  "dispatched",
270
+ "stalled",
316
271
  "succeeded",
317
272
  "failed",
318
273
  "needs_human",
@@ -421,232 +376,6 @@ const signalSchema = z.strictObject({
421
376
  });
422
377
  });
423
378
  //#endregion
424
- //#region src/schema/transitions.ts
425
- const taskInitialState = "queued";
426
- const taskTransitions = [
427
- {
428
- from: "queued",
429
- to: "running",
430
- trigger: "dependencies have a change, reservation succeeded, devbox created",
431
- actor: "reconciler"
432
- },
433
- {
434
- from: "queued",
435
- to: "failed",
436
- trigger: "reservation refused",
437
- actor: "reconciler"
438
- },
439
- {
440
- from: "queued",
441
- to: "needs_human",
442
- trigger: "kind is question",
443
- actor: "system"
444
- },
445
- {
446
- from: "queued",
447
- to: "cancelled",
448
- trigger: "cancel or kill",
449
- actor: "operator"
450
- },
451
- {
452
- from: "running",
453
- to: "verifying",
454
- trigger: "devbox task reported complete",
455
- actor: "reconciler"
456
- },
457
- {
458
- from: "running",
459
- to: "queued",
460
- trigger: "launch failed below attempt limit",
461
- actor: "reconciler"
462
- },
463
- {
464
- from: "running",
465
- to: "failed",
466
- trigger: "devbox task errored, deadline exceeded, or launch failed at attempt limit",
467
- actor: "reconciler"
468
- },
469
- {
470
- from: "running",
471
- to: "needs_human",
472
- trigger: "devbox task requires attention",
473
- actor: "reconciler"
474
- },
475
- {
476
- from: "running",
477
- to: "cancelled",
478
- trigger: "cancel or kill",
479
- actor: "operator"
480
- },
481
- {
482
- from: "verifying",
483
- to: "succeeded",
484
- trigger: "kind criterion true",
485
- actor: "reconciler"
486
- },
487
- {
488
- from: "verifying",
489
- to: "running",
490
- trigger: "verification failed below attempt limit",
491
- actor: "reconciler"
492
- },
493
- {
494
- from: "verifying",
495
- to: "failed",
496
- trigger: "verification failed at attempt limit",
497
- actor: "reconciler"
498
- },
499
- {
500
- from: "verifying",
501
- to: "cancelled",
502
- trigger: "cancel or kill",
503
- actor: "operator"
504
- },
505
- {
506
- from: "needs_human",
507
- to: "succeeded",
508
- trigger: "question answered",
509
- actor: "operator"
510
- },
511
- {
512
- from: "needs_human",
513
- to: "queued",
514
- trigger: "operator retry",
515
- actor: "operator"
516
- },
517
- {
518
- from: "needs_human",
519
- to: "cancelled",
520
- trigger: "cancel or kill",
521
- actor: "operator"
522
- },
523
- {
524
- from: "failed",
525
- to: "queued",
526
- trigger: "operator retry",
527
- actor: "operator"
528
- }
529
- ];
530
- const changeInitialState = "verifying";
531
- const changeTransitions = [
532
- {
533
- from: "verifying",
534
- to: "ready",
535
- trigger: "verification task succeeded, risk and confidence written",
536
- actor: "reconciler"
537
- },
538
- {
539
- from: "verifying",
540
- to: "closed",
541
- trigger: "verification failed at attempt limit or pr closed upstream",
542
- actor: "reconciler"
543
- },
544
- {
545
- from: "ready",
546
- to: "merged",
547
- trigger: "pr merged on github",
548
- actor: "webhook"
549
- },
550
- {
551
- from: "ready",
552
- to: "closed",
553
- trigger: "pr closed without merge",
554
- actor: "webhook"
555
- },
556
- {
557
- from: "merged",
558
- to: "deployed",
559
- trigger: "production deployment ready containing the merge sha",
560
- actor: "webhook"
561
- },
562
- {
563
- from: "deployed",
564
- to: "rolled_out",
565
- trigger: "no flag, or flag ramped to full and held",
566
- actor: "foreman"
567
- },
568
- {
569
- from: "deployed",
570
- to: "rolled_back",
571
- trigger: "rollback rule crossed",
572
- actor: "foreman"
573
- },
574
- {
575
- from: "rolled_out",
576
- to: "rolled_back",
577
- trigger: "rollback rule crossed or flag flipped off",
578
- actor: "foreman"
579
- },
580
- {
581
- from: "rolled_back",
582
- to: "deployed",
583
- trigger: "human retried the ramp",
584
- actor: "human"
585
- }
586
- ];
587
- const signalInitialOutcome = "pending";
588
- const signalOutcomeTransitions = [
589
- {
590
- from: "pending",
591
- to: "accepted",
592
- trigger: "classified as actionable",
593
- actor: "reconciler"
594
- },
595
- {
596
- from: "pending",
597
- to: "duplicate",
598
- trigger: "classified as duplicate",
599
- actor: "reconciler"
600
- },
601
- {
602
- from: "pending",
603
- to: "unactionable",
604
- trigger: "classified as unactionable",
605
- actor: "reconciler"
606
- },
607
- {
608
- from: "pending",
609
- to: "low_confidence",
610
- trigger: "classified below confidence threshold",
611
- actor: "reconciler"
612
- },
613
- {
614
- from: "pending",
615
- to: "ignored",
616
- trigger: "classified as ignorable",
617
- actor: "reconciler"
618
- }
619
- ];
620
- function isTransitionAllowed(transitions, from, to) {
621
- return transitions.some((transition) => transition.from === from && transition.to === to);
622
- }
623
- function legalNextStates(transitions, from) {
624
- return [...new Set(transitions.filter((transition) => transition.from === from).map((t) => t.to))];
625
- }
626
- var InvalidTransitionError = class extends Error {
627
- from;
628
- to;
629
- legalNextStates;
630
- constructor(from, to, legal) {
631
- 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`);
632
- this.name = "InvalidTransitionError";
633
- this.from = from;
634
- this.to = to;
635
- this.legalNextStates = legal;
636
- }
637
- };
638
- function assertTransition(transitions, from, to) {
639
- if (!isTransitionAllowed(transitions, from, to)) throw new InvalidTransitionError(from, to, legalNextStates(transitions, from));
640
- }
641
- function terminalStates(transitions) {
642
- const states = /* @__PURE__ */ new Set();
643
- for (const transition of transitions) {
644
- states.add(transition.from);
645
- states.add(transition.to);
646
- }
647
- return [...states].filter((state) => !transitions.some((transition) => transition.from === state));
648
- }
649
- //#endregion
650
379
  //#region src/signals.ts
651
380
  /**
652
381
  * Bind normalized intake to signal creation alone. Connectors and API routes
@@ -659,6 +388,64 @@ function createSignalCreator(stores) {
659
388
  } };
660
389
  }
661
390
  //#endregion
391
+ //#region src/stall.ts
392
+ function stallWindowMinutes(policy, kind) {
393
+ return policy.stallMinutesByKind[kind] ?? policy.stallMinutes;
394
+ }
395
+ /**
396
+ * The reconciler's stall pass. A running task whose execution has not
397
+ * progressed within its stall window is marked stalled; a stalled task is
398
+ * re-queued for a replacement attempt while attempts remain, and failed once
399
+ * they are exhausted. Only `running` tasks can stall: a task whose result has
400
+ * already been recorded (verifying, or terminal) is never retried by this
401
+ * pass. Coordination parents run only through their children and are skipped.
402
+ */
403
+ async function sweepStalled(stores, options) {
404
+ const nowMs = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
405
+ const result = {
406
+ stalled: [],
407
+ requeued: [],
408
+ failed: []
409
+ };
410
+ const tasks = await stores.tasks.list();
411
+ for (const task of tasks) {
412
+ if (task.state !== "running" || task.kind === "coordination") continue;
413
+ const minutes = stallWindowMinutes(options.policy, task.kind);
414
+ const sinceProgress = nowMs - Date.parse(task.lastProgressAt ?? task.updatedAt);
415
+ if (sinceProgress < minutes * 6e4) continue;
416
+ try {
417
+ await stores.tasks.transition(task.id, "stalled", {
418
+ reason: `no progress for ${Math.floor(sinceProgress / 6e4)} minutes (window ${minutes})`,
419
+ expectFrom: "running"
420
+ });
421
+ result.stalled.push(task.id);
422
+ } catch {
423
+ continue;
424
+ }
425
+ }
426
+ for (const task of await stores.tasks.list()) {
427
+ if (task.state !== "stalled") continue;
428
+ try {
429
+ if (task.attempt < options.policy.maxAttempts) {
430
+ await stores.tasks.transition(task.id, "queued", {
431
+ reason: `stalled on attempt ${task.attempt} of ${options.policy.maxAttempts}; replacing`,
432
+ expectFrom: "stalled"
433
+ });
434
+ result.requeued.push(task.id);
435
+ } else {
436
+ await stores.tasks.transition(task.id, "failed", {
437
+ reason: `stalled on attempt ${task.attempt} of ${options.policy.maxAttempts}; attempts exhausted`,
438
+ expectFrom: "stalled"
439
+ });
440
+ result.failed.push(task.id);
441
+ }
442
+ } catch {
443
+ continue;
444
+ }
445
+ }
446
+ return result;
447
+ }
448
+ //#endregion
662
449
  //#region src/store/engine.ts
663
450
  /** An approval decision that does not apply to the task's current approval or state. */
664
451
  var ApprovalError = class extends Error {
@@ -671,6 +458,17 @@ var ApprovalError = class extends Error {
671
458
  this.reason = reason;
672
459
  }
673
460
  };
461
+ /** A settlement that does not apply to the task's current state. */
462
+ var SettlementError = class extends Error {
463
+ taskId;
464
+ reason;
465
+ constructor(taskId, reason) {
466
+ super(`settlement on "${taskId}" refused: ${reason}`);
467
+ this.name = "SettlementError";
468
+ this.taskId = taskId;
469
+ this.reason = reason;
470
+ }
471
+ };
674
472
  var ChangeAssociationError = class extends Error {
675
473
  taskId;
676
474
  repositoryId;
@@ -685,6 +483,7 @@ var ChangeAssociationError = class extends Error {
685
483
  };
686
484
  const taskReceiptStates = {
687
485
  running: "dispatched",
486
+ stalled: "stalled",
688
487
  succeeded: "succeeded",
689
488
  failed: "failed",
690
489
  needs_human: "needs_human",
@@ -706,6 +505,17 @@ function derivedId(prefix, key) {
706
505
  function bumpsAttempt(from, to) {
707
506
  return to === "queued" || from === "verifying" && to === "running";
708
507
  }
508
+ const emptyUsage = {
509
+ costUsd: 0,
510
+ inputTokens: 0,
511
+ outputTokens: 0,
512
+ cacheReadTokens: 0,
513
+ cacheWriteTokens: 0,
514
+ steps: {}
515
+ };
516
+ function addUsd(a, b) {
517
+ return (Math.round(a * 1e6) + Math.round(b * 1e6)) / 1e6;
518
+ }
709
519
  function createStores(options) {
710
520
  const { driver } = options;
711
521
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
@@ -741,7 +551,7 @@ function createStores(options) {
741
551
  }
742
552
  }
743
553
  async function appendReceipt(key, receipt) {
744
- await insertReceipt(derivedId("rcpt", key), receipt);
554
+ return insertReceipt(derivedId("rcpt", key), receipt);
745
555
  }
746
556
  return {
747
557
  driverName: driver.name,
@@ -807,9 +617,10 @@ function createStores(options) {
807
617
  tasks: {
808
618
  async create(input) {
809
619
  const timestamp = now();
620
+ const id = input.dedupeKey === void 0 ? randomId("task") : derivedId("task", input.dedupeKey);
810
621
  const task = taskSchema.parse({
811
622
  schemaVersion: 1,
812
- id: randomId("task"),
623
+ id,
813
624
  repositoryIds: input.repositoryIds,
814
625
  kind: input.kind,
815
626
  area: input.area,
@@ -820,11 +631,17 @@ function createStores(options) {
820
631
  replyTo: input.replyTo,
821
632
  attempt: 1,
822
633
  question: input.question,
634
+ changeId: input.changeId,
823
635
  createdAt: timestamp,
824
636
  updatedAt: timestamp
825
637
  });
826
- await driver.insert("tasks", task.id, task);
827
- return task;
638
+ try {
639
+ await driver.insert("tasks", task.id, task);
640
+ return task;
641
+ } catch (error) {
642
+ if (input.dedupeKey !== void 0 && error instanceof RecordExistsError) return (await mustRead("tasks", task.id, taskSchema)).value;
643
+ throw error;
644
+ }
828
645
  },
829
646
  async approve(id, decision) {
830
647
  const existing = await mustRead("tasks", id, taskSchema);
@@ -883,30 +700,124 @@ function createStores(options) {
883
700
  await driver.update("tasks", id, next, existing.version);
884
701
  return next;
885
702
  },
703
+ async recordExecution(id, execution) {
704
+ const existing = await mustRead("tasks", id, taskSchema);
705
+ const task = existing.value;
706
+ if (task.state !== "running") throw new InvalidTransitionError(task.state, "running", legalNextStates(taskTransitions, task.state));
707
+ const timestamp = now();
708
+ const next = taskSchema.parse({
709
+ ...task,
710
+ execution,
711
+ lastProgressAt: timestamp,
712
+ updatedAt: timestamp
713
+ });
714
+ await driver.update("tasks", id, next, existing.version);
715
+ return next;
716
+ },
717
+ async recordUsage(id, step) {
718
+ for (;;) {
719
+ const existing = await mustRead("tasks", id, taskSchema);
720
+ const task = existing.value;
721
+ const usage = task.usage ?? emptyUsage;
722
+ const recordedIndex = usage.steps[step.turnId];
723
+ if (recordedIndex !== void 0 && step.stepIndex <= recordedIndex) return task;
724
+ const timestamp = now();
725
+ const next = taskSchema.parse({
726
+ ...task,
727
+ lastProgressAt: timestamp,
728
+ usage: {
729
+ costUsd: addUsd(usage.costUsd, step.costUsd ?? 0),
730
+ inputTokens: usage.inputTokens + (step.inputTokens ?? 0),
731
+ outputTokens: usage.outputTokens + (step.outputTokens ?? 0),
732
+ cacheReadTokens: usage.cacheReadTokens + (step.cacheReadTokens ?? 0),
733
+ cacheWriteTokens: usage.cacheWriteTokens + (step.cacheWriteTokens ?? 0),
734
+ steps: {
735
+ ...usage.steps,
736
+ [step.turnId]: step.stepIndex
737
+ }
738
+ },
739
+ updatedAt: timestamp
740
+ });
741
+ try {
742
+ await driver.update("tasks", id, next, existing.version);
743
+ return next;
744
+ } catch (error) {
745
+ if (!(error instanceof VersionConflictError)) throw error;
746
+ }
747
+ }
748
+ },
749
+ async settle(id, cost, settleOptions = {}) {
750
+ const existing = await mustRead("tasks", id, taskSchema);
751
+ const task = existing.value;
752
+ if (task.settledUsd !== void 0) return task;
753
+ if (task.state !== "succeeded" && task.state !== "failed" && task.state !== "cancelled") throw new SettlementError(id, `task is ${task.state}, not terminal`);
754
+ const validated = costSchema.parse(cost);
755
+ const normalized = { totalUsd: Math.round(validated.totalUsd * 1e6) / 1e6 };
756
+ let recorded = normalized;
757
+ if (task.repositoryIds !== void 0) recorded = (await appendReceipt(`task:${id}:settled:${task.attempt}`, {
758
+ repositoryIds: task.repositoryIds,
759
+ taskId: id,
760
+ changeId: task.changeId,
761
+ kind: task.kind,
762
+ state: "settled",
763
+ phase: "budget",
764
+ reason: settleOptions.reason,
765
+ attempt: task.attempt,
766
+ cost: normalized
767
+ })).cost ?? normalized;
768
+ const next = taskSchema.parse({
769
+ ...task,
770
+ settledUsd: recorded.totalUsd,
771
+ updatedAt: now()
772
+ });
773
+ await driver.update("tasks", id, next, existing.version);
774
+ return next;
775
+ },
886
776
  async get(id) {
887
777
  const record = await read("tasks", id, taskSchema);
888
778
  return record === null ? null : record.value;
889
779
  },
890
780
  async transition(id, to, transitionOptions = {}) {
781
+ if (transitionOptions.patch !== void 0 && "settledUsd" in transitionOptions.patch) throw new SettlementError(id, "settledUsd is set only by settle, not a transition");
782
+ if (transitionOptions.patch !== void 0 && "usage" in transitionOptions.patch) throw new SettlementError(id, "usage is set only by recordUsage, not a transition");
891
783
  const existing = await mustRead("tasks", id, taskSchema);
784
+ if (transitionOptions.expectFrom !== void 0 && existing.value.state !== transitionOptions.expectFrom) throw new InvalidTransitionError(existing.value.state, to, legalNextStates(taskTransitions, existing.value.state));
892
785
  assertTransition(taskTransitions, existing.value.state, to);
893
- const attempt = bumpsAttempt(existing.value.state, to) ? existing.value.attempt + 1 : existing.value.attempt;
786
+ if (to === "needs_human" && existing.value.kind !== "question") {
787
+ if (!(await listAll("tasks", taskSchema)).some((task) => task.kind === "question" && task.state === "needs_human" && task.origin.parentTaskId === id)) throw new Error(`task ${id} cannot enter needs_human without an open QuestionTask`);
788
+ }
789
+ const bumps = bumpsAttempt(existing.value.state, to);
790
+ const timestamp = now();
791
+ const { stalledAt: _stalledAt, stalledReason: _stalledReason, ...cleared } = existing.value;
894
792
  const next = taskSchema.parse({
895
- ...existing.value,
793
+ ...cleared,
794
+ ...bumps ? { execution: void 0 } : {},
896
795
  ...transitionOptions.patch,
796
+ ...to === "stalled" ? {
797
+ stalledAt: timestamp,
798
+ stalledReason: transitionOptions.reason
799
+ } : {},
800
+ ...to === "running" ? { lastProgressAt: timestamp } : {},
897
801
  state: to,
898
- attempt,
899
- updatedAt: now()
802
+ attempt: bumps ? existing.value.attempt + 1 : existing.value.attempt,
803
+ updatedAt: timestamp
900
804
  });
901
805
  const receiptState = taskReceiptStates[to];
902
- if (receiptState !== void 0 && next.repositoryIds !== void 0) await appendReceipt(`task:${id}:${receiptState}:${attempt}`, {
806
+ if (receiptState !== void 0 && next.repositoryIds !== void 0) await appendReceipt(JSON.stringify([
807
+ "task",
808
+ id,
809
+ receiptState,
810
+ next.attempt,
811
+ next.changeId ?? null,
812
+ transitionOptions.reason ?? null
813
+ ]), {
903
814
  repositoryIds: next.repositoryIds,
904
815
  taskId: id,
905
816
  changeId: next.changeId,
906
817
  kind: next.kind,
907
818
  state: receiptState,
908
819
  reason: transitionOptions.reason,
909
- attempt
820
+ attempt: next.attempt
910
821
  });
911
822
  await driver.update("tasks", id, next, existing.version);
912
823
  return next;
@@ -990,6 +901,66 @@ function createStores(options) {
990
901
  list() {
991
902
  return listAll("receipts", receiptSchema);
992
903
  }
904
+ },
905
+ budgets: {
906
+ async ensure(input) {
907
+ const budget = budgetSchema.parse({
908
+ schemaVersion: 1,
909
+ factoryId: input.factoryId,
910
+ month: input.month,
911
+ limits: input.limits,
912
+ notices: {}
913
+ });
914
+ try {
915
+ await driver.insert("budgets", input.month, budget);
916
+ return budget;
917
+ } catch (error) {
918
+ if (!(error instanceof RecordExistsError)) throw error;
919
+ }
920
+ const existing = await mustRead("budgets", input.month, budgetSchema);
921
+ const refreshed = budgetSchema.parse({
922
+ ...existing.value,
923
+ limits: input.limits
924
+ });
925
+ if (JSON.stringify(refreshed) === JSON.stringify(existing.value)) return existing.value;
926
+ try {
927
+ await driver.update("budgets", input.month, refreshed, existing.version);
928
+ return refreshed;
929
+ } catch (error) {
930
+ if (error instanceof VersionConflictError) return (await mustRead("budgets", input.month, budgetSchema)).value;
931
+ throw error;
932
+ }
933
+ },
934
+ async get(month) {
935
+ const record = await read("budgets", month, budgetSchema);
936
+ return record === null ? null : record.value;
937
+ },
938
+ async stampNotice(month, notice) {
939
+ const field = notice === "warning" ? "warningAt" : "exhaustedAt";
940
+ for (;;) {
941
+ const existing = await mustRead("budgets", month, budgetSchema);
942
+ if (existing.value.notices[field] !== void 0) return {
943
+ budget: existing.value,
944
+ stamped: false
945
+ };
946
+ const next = budgetSchema.parse({
947
+ ...existing.value,
948
+ notices: {
949
+ ...existing.value.notices,
950
+ [field]: now()
951
+ }
952
+ });
953
+ try {
954
+ await driver.update("budgets", month, next, existing.version);
955
+ return {
956
+ budget: next,
957
+ stamped: true
958
+ };
959
+ } catch (error) {
960
+ if (!(error instanceof VersionConflictError)) throw error;
961
+ }
962
+ }
963
+ }
993
964
  }
994
965
  };
995
966
  }
@@ -1097,7 +1068,10 @@ async function sweepQueued(stores, options) {
1097
1068
  continue;
1098
1069
  }
1099
1070
  try {
1100
- await dispatchTask(stores, task.id, { launch: (dispatched) => launcher(dispatched, repository) });
1071
+ await dispatchTask(stores, task.id, {
1072
+ launch: (dispatched) => launcher(dispatched, repository),
1073
+ budget: options.budget
1074
+ });
1101
1075
  result.dispatched.push(task.id);
1102
1076
  } catch (error) {
1103
1077
  if (error instanceof DispatchRefusedError) {
@@ -1116,6 +1090,35 @@ async function sweepQueued(stores, options) {
1116
1090
  return result;
1117
1091
  }
1118
1092
  //#endregion
1119
- 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, 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, taskApprovalSchema, taskDevboxSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, terminalStates, usdSchema, vocabularySchema };
1093
+ //#region src/verification-dispatch.ts
1094
+ /** Ensure every recorded code change has exactly one durable verification task. */
1095
+ async function ensureVerificationTasks(stores) {
1096
+ const tasks = await stores.tasks.list();
1097
+ const existingIds = new Set(tasks.map((task) => task.id));
1098
+ const result = {
1099
+ created: [],
1100
+ existing: []
1101
+ };
1102
+ const sources = tasks.filter((task) => task.kind === "code_change" && task.state === "verifying" && task.changeId !== void 0);
1103
+ for (const source of sources) {
1104
+ const existing = tasks.find((task) => task.kind === "verification" && task.origin.parentTaskId === source.id);
1105
+ if (existing !== void 0) {
1106
+ result.existing.push(existing.id);
1107
+ continue;
1108
+ }
1109
+ const verification = await stores.tasks.create({
1110
+ repositoryIds: source.repositoryIds,
1111
+ kind: "verification",
1112
+ origin: { parentTaskId: source.id },
1113
+ replyTo: source.replyTo,
1114
+ changeId: source.changeId,
1115
+ dedupeKey: `verification:${source.id}:${source.changeId}`
1116
+ });
1117
+ (existingIds.has(verification.id) ? result.existing : result.created).push(verification.id);
1118
+ }
1119
+ return result;
1120
+ }
1121
+ //#endregion
1122
+ export { ApprovalError, BudgetExhaustedError, ChangeAssociationError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, SettlementError, VersionConflictError, adoptPullRequest, agentRouteSchema, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetLimitsSchema, budgetMonthOf, budgetMonthSchema, budgetSchema, budgetStatus, canReserve, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, 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, 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 };
1120
1123
 
1121
1124
  //# sourceMappingURL=index.mjs.map