@vercel/factory 0.0.12 → 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-CIXB8IF8.d.mts → dispatch-B77cuPxh.d.mts} +290 -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 +119 -59
  25. package/dist/index.mjs +289 -330
  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-CFNGXVqD.mjs +0 -230
  32. package/dist/change-CFNGXVqD.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-CFNGXVqD.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) {
@@ -64,76 +98,6 @@ async function adoptPullRequest(stores, input) {
64
98
  });
65
99
  }
66
100
  //#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
101
  //#region src/metrics.ts
138
102
  const bucketTable = {
139
103
  low: {
@@ -258,9 +222,10 @@ function computeLedgerMetrics(rows, options = {}) {
258
222
  }
259
223
  const openChangeStates = /* @__PURE__ */ new Set(["verifying", "ready"]);
260
224
  /**
261
- * Green, yellow, or red from rows, never from a model. Budget and onboarding
262
- * signals are not evaluated yet: budgets do not settle and onboarding
263
- * 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.
264
229
  */
265
230
  function computeRepositoryHealth(rows, options = {}) {
266
231
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
@@ -296,31 +261,13 @@ function computeRepositoryHealth(rows, options = {}) {
296
261
  };
297
262
  }
298
263
  //#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
264
  //#region src/schema/receipt.ts
319
265
  /** States the engine writes on transitions. Any vocabulary value is valid. */
320
266
  const standardReceiptStates = [
321
267
  "approved",
322
268
  "rejected",
323
269
  "dispatched",
270
+ "stalled",
324
271
  "succeeded",
325
272
  "failed",
326
273
  "needs_human",
@@ -429,232 +376,6 @@ const signalSchema = z.strictObject({
429
376
  });
430
377
  });
431
378
  //#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
379
  //#region src/signals.ts
659
380
  /**
660
381
  * Bind normalized intake to signal creation alone. Connectors and API routes
@@ -667,6 +388,64 @@ function createSignalCreator(stores) {
667
388
  } };
668
389
  }
669
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
670
449
  //#region src/store/engine.ts
671
450
  /** An approval decision that does not apply to the task's current approval or state. */
672
451
  var ApprovalError = class extends Error {
@@ -679,6 +458,17 @@ var ApprovalError = class extends Error {
679
458
  this.reason = reason;
680
459
  }
681
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
+ };
682
472
  var ChangeAssociationError = class extends Error {
683
473
  taskId;
684
474
  repositoryId;
@@ -693,6 +483,7 @@ var ChangeAssociationError = class extends Error {
693
483
  };
694
484
  const taskReceiptStates = {
695
485
  running: "dispatched",
486
+ stalled: "stalled",
696
487
  succeeded: "succeeded",
697
488
  failed: "failed",
698
489
  needs_human: "needs_human",
@@ -714,6 +505,17 @@ function derivedId(prefix, key) {
714
505
  function bumpsAttempt(from, to) {
715
506
  return to === "queued" || from === "verifying" && to === "running";
716
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
+ }
717
519
  function createStores(options) {
718
520
  const { driver } = options;
719
521
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
@@ -749,7 +551,7 @@ function createStores(options) {
749
551
  }
750
552
  }
751
553
  async function appendReceipt(key, receipt) {
752
- await insertReceipt(derivedId("rcpt", key), receipt);
554
+ return insertReceipt(derivedId("rcpt", key), receipt);
753
555
  }
754
556
  return {
755
557
  driverName: driver.name,
@@ -898,30 +700,124 @@ function createStores(options) {
898
700
  await driver.update("tasks", id, next, existing.version);
899
701
  return next;
900
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
+ },
901
776
  async get(id) {
902
777
  const record = await read("tasks", id, taskSchema);
903
778
  return record === null ? null : record.value;
904
779
  },
905
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");
906
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));
907
785
  assertTransition(taskTransitions, existing.value.state, to);
908
- 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;
909
792
  const next = taskSchema.parse({
910
- ...existing.value,
793
+ ...cleared,
794
+ ...bumps ? { execution: void 0 } : {},
911
795
  ...transitionOptions.patch,
796
+ ...to === "stalled" ? {
797
+ stalledAt: timestamp,
798
+ stalledReason: transitionOptions.reason
799
+ } : {},
800
+ ...to === "running" ? { lastProgressAt: timestamp } : {},
912
801
  state: to,
913
- attempt,
914
- updatedAt: now()
802
+ attempt: bumps ? existing.value.attempt + 1 : existing.value.attempt,
803
+ updatedAt: timestamp
915
804
  });
916
805
  const receiptState = taskReceiptStates[to];
917
- 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
+ ]), {
918
814
  repositoryIds: next.repositoryIds,
919
815
  taskId: id,
920
816
  changeId: next.changeId,
921
817
  kind: next.kind,
922
818
  state: receiptState,
923
819
  reason: transitionOptions.reason,
924
- attempt
820
+ attempt: next.attempt
925
821
  });
926
822
  await driver.update("tasks", id, next, existing.version);
927
823
  return next;
@@ -1005,6 +901,66 @@ function createStores(options) {
1005
901
  list() {
1006
902
  return listAll("receipts", receiptSchema);
1007
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
+ }
1008
964
  }
1009
965
  };
1010
966
  }
@@ -1112,7 +1068,10 @@ async function sweepQueued(stores, options) {
1112
1068
  continue;
1113
1069
  }
1114
1070
  try {
1115
- 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
+ });
1116
1075
  result.dispatched.push(task.id);
1117
1076
  } catch (error) {
1118
1077
  if (error instanceof DispatchRefusedError) {
@@ -1160,6 +1119,6 @@ async function ensureVerificationTasks(stores) {
1160
1119
  return result;
1161
1120
  }
1162
1121
  //#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, taskApprovalSchema, taskDevboxSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, terminalStates, usdSchema, verificationBrief, vocabularySchema };
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 };
1164
1123
 
1165
1124
  //# sourceMappingURL=index.mjs.map