@vercel/factory 0.0.15 → 0.0.16

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 (214) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +49 -261
  3. package/dist/agent-routes.d.mts +47 -3
  4. package/dist/agent-routes.mjs +28 -1
  5. package/dist/agent-routes.mjs.map +1 -1
  6. package/dist/api-contracts.d.mts +20 -1
  7. package/dist/api-contracts.mjs +2 -1
  8. package/dist/api-contracts.mjs.map +1 -1
  9. package/dist/api.d.mts +45 -2
  10. package/dist/api.mjs +199 -10
  11. package/dist/api.mjs.map +1 -1
  12. package/dist/approval-contracts.d.mts +6 -0
  13. package/dist/blob/index.d.mts +51 -14
  14. package/dist/blob/index.mjs +26 -10
  15. package/dist/blob/index.mjs.map +1 -1
  16. package/dist/budget.d.mts +7 -0
  17. package/dist/budget.mjs +6 -0
  18. package/dist/budget.mjs.map +1 -1
  19. package/dist/build-factory.d.mts +1 -0
  20. package/dist/change-verification/dispatch.d.mts +16 -3
  21. package/dist/change-verification/dispatch.mjs +45 -7
  22. package/dist/change-verification/dispatch.mjs.map +1 -1
  23. package/dist/change-verification/eve-tool.d.mts +2 -1
  24. package/dist/change-verification/eve-tool.mjs +35 -47
  25. package/dist/change-verification/eve-tool.mjs.map +1 -1
  26. package/dist/change-verification/result.mjs +130 -0
  27. package/dist/change-verification/result.mjs.map +1 -0
  28. package/dist/changes/eve-record-change.d.mts +2 -2
  29. package/dist/changes/eve-record-change.mjs +43 -9
  30. package/dist/changes/eve-record-change.mjs.map +1 -1
  31. package/dist/changes.d.mts +4 -3
  32. package/dist/changes.mjs +2 -2
  33. package/dist/changes.mjs.map +1 -1
  34. package/dist/client-events.d.mts +10 -3
  35. package/dist/client-events.mjs +6 -2
  36. package/dist/client-events.mjs.map +1 -1
  37. package/dist/client-stream.mjs +8 -2
  38. package/dist/client-stream.mjs.map +1 -1
  39. package/dist/client-transcript.mjs +5 -1
  40. package/dist/client-transcript.mjs.map +1 -1
  41. package/dist/client.d.mts +121 -12
  42. package/dist/client.mjs +117 -9
  43. package/dist/client.mjs.map +1 -1
  44. package/dist/code-review/contracts.d.mts +1 -0
  45. package/dist/code-review/eve-post-review.d.mts +4 -4
  46. package/dist/code-review/eve-post-review.mjs +29 -17
  47. package/dist/code-review/eve-post-review.mjs.map +1 -1
  48. package/dist/code-review/eve-review-comments.d.mts +13 -2
  49. package/dist/code-review/eve-review-comments.mjs +45 -11
  50. package/dist/code-review/eve-review-comments.mjs.map +1 -1
  51. package/dist/code-review/github-reporter.d.mts +2 -0
  52. package/dist/code-review/github-reporter.mjs +7 -5
  53. package/dist/code-review/github-reporter.mjs.map +1 -1
  54. package/dist/code-review.d.mts +3 -2
  55. package/dist/deepsec/eve-tool.mjs +3 -1
  56. package/dist/deepsec/eve-tool.mjs.map +1 -1
  57. package/dist/dispatch.d.mts +79 -8
  58. package/dist/dispatch.mjs +68 -9
  59. package/dist/dispatch.mjs.map +1 -1
  60. package/dist/eve/index.d.mts +70 -10
  61. package/dist/eve/index.mjs +93 -22
  62. package/dist/eve/index.mjs.map +1 -1
  63. package/dist/eve/invoke.mjs +24 -11
  64. package/dist/eve/invoke.mjs.map +1 -1
  65. package/dist/eve/session-client.d.mts +122 -4
  66. package/dist/eve/session-client.mjs +127 -13
  67. package/dist/eve/session-client.mjs.map +1 -1
  68. package/dist/eve/task-execution.d.mts +380 -0
  69. package/dist/eve/task-execution.mjs +57 -2
  70. package/dist/eve/task-execution.mjs.map +1 -1
  71. package/dist/eve/task-session.d.mts +44 -2
  72. package/dist/eve/task-session.mjs +44 -2
  73. package/dist/eve/task-session.mjs.map +1 -1
  74. package/dist/eve/transcript.mjs +5 -1
  75. package/dist/eve/transcript.mjs.map +1 -1
  76. package/dist/execution.d.mts +117 -9
  77. package/dist/execution.mjs +76 -6
  78. package/dist/execution.mjs.map +1 -1
  79. package/dist/finding-remediation/admission.d.mts +2 -0
  80. package/dist/finding-remediation/admission.mjs +4 -1
  81. package/dist/finding-remediation/admission.mjs.map +1 -1
  82. package/dist/findings.d.mts +1 -0
  83. package/dist/github-publication.d.mts +1 -0
  84. package/dist/github-publication.mjs +97 -84
  85. package/dist/github-publication.mjs.map +1 -1
  86. package/dist/github-transfer.d.mts +15 -6
  87. package/dist/github-transfer.mjs +214 -66
  88. package/dist/github-transfer.mjs.map +1 -1
  89. package/dist/github.d.mts +51 -11
  90. package/dist/github.mjs +126 -24
  91. package/dist/github.mjs.map +1 -1
  92. package/dist/inbox-activity.d.mts +53 -0
  93. package/dist/inbox-activity.mjs +41 -0
  94. package/dist/inbox-activity.mjs.map +1 -0
  95. package/dist/index.d.mts +3 -1
  96. package/dist/index.mjs +3 -2
  97. package/dist/intake-contracts.d.mts +0 -1
  98. package/dist/integrations/deepsec.d.mts +1 -0
  99. package/dist/integrations/github.d.mts +2 -2
  100. package/dist/integrations/github.mjs +2 -2
  101. package/dist/integrations/slack.d.mts +3 -1
  102. package/dist/integrations/slack.mjs +3 -1
  103. package/dist/integrations/vercel.d.mts +4 -2
  104. package/dist/integrations/vercel.mjs +3 -2
  105. package/dist/merge-resolution/eve-tools.d.mts +1 -0
  106. package/dist/merge-resolution/eve-tools.mjs +7 -2
  107. package/dist/merge-resolution/eve-tools.mjs.map +1 -1
  108. package/dist/model-settings.d.mts +41 -0
  109. package/dist/model-settings.mjs +35 -0
  110. package/dist/model-settings.mjs.map +1 -0
  111. package/dist/planning/reconcile.mjs +6 -0
  112. package/dist/planning/reconcile.mjs.map +1 -1
  113. package/dist/postgres/index.d.mts +43 -2
  114. package/dist/postgres/index.mjs +40 -2
  115. package/dist/postgres/index.mjs.map +1 -1
  116. package/dist/presets/software-development/dispatch.d.mts +4 -1
  117. package/dist/presets/software-development/dispatch.mjs +2 -1
  118. package/dist/presets/software-development/dispatch.mjs.map +1 -1
  119. package/dist/presets/software-development/task-communication.d.mts +1 -0
  120. package/dist/presets/software-development/task-communication.mjs +48 -11
  121. package/dist/presets/software-development/task-communication.mjs.map +1 -1
  122. package/dist/presets/software-development.d.mts +1 -0
  123. package/dist/pull-requests/github-publisher.d.mts +15 -1
  124. package/dist/pull-requests/github-publisher.mjs +61 -1
  125. package/dist/pull-requests/github-publisher.mjs.map +1 -1
  126. package/dist/pull-requests.d.mts +1 -0
  127. package/dist/sandbox/index.d.mts +1 -0
  128. package/dist/schema/agent-route.d.mts +19 -1
  129. package/dist/schema/agent-route.mjs +19 -1
  130. package/dist/schema/agent-route.mjs.map +1 -1
  131. package/dist/schema/factory-config.d.mts +27 -0
  132. package/dist/schema/factory-config.mjs +33 -3
  133. package/dist/schema/factory-config.mjs.map +1 -1
  134. package/dist/schema/repository.d.mts +4 -0
  135. package/dist/schema/repository.mjs +5 -1
  136. package/dist/schema/repository.mjs.map +1 -1
  137. package/dist/schema/session.d.mts +1 -0
  138. package/dist/schema/session.mjs +1 -0
  139. package/dist/schema/session.mjs.map +1 -1
  140. package/dist/schema/slack-pr-notifications.d.mts +12 -0
  141. package/dist/schema/slack-pr-notifications.mjs +11 -0
  142. package/dist/schema/slack-pr-notifications.mjs.map +1 -0
  143. package/dist/schema/task-graph.d.mts +39 -0
  144. package/dist/schema/task.d.mts +1 -0
  145. package/dist/schema/task.mjs +2 -1
  146. package/dist/schema/task.mjs.map +1 -1
  147. package/dist/schema/transcript.d.mts +6 -0
  148. package/dist/schema/transcript.mjs +2 -1
  149. package/dist/schema/transcript.mjs.map +1 -1
  150. package/dist/schema/work.d.mts +52 -3
  151. package/dist/schema/work.mjs.map +1 -1
  152. package/dist/session-previews.d.mts +76 -0
  153. package/dist/session-previews.mjs +55 -0
  154. package/dist/session-previews.mjs.map +1 -0
  155. package/dist/session-review.d.mts +120 -0
  156. package/dist/session-review.mjs +79 -0
  157. package/dist/session-review.mjs.map +1 -0
  158. package/dist/signal-triage.mjs +1 -1
  159. package/dist/signals.d.mts +1 -0
  160. package/dist/stall.d.mts +4 -1
  161. package/dist/stall.mjs +6 -2
  162. package/dist/stall.mjs.map +1 -1
  163. package/dist/store/driver.d.mts +1 -1
  164. package/dist/store/driver.mjs.map +1 -1
  165. package/dist/store/engine.d.mts +206 -8
  166. package/dist/store/engine.mjs +147 -13
  167. package/dist/store/engine.mjs.map +1 -1
  168. package/dist/store/memory.d.mts +18 -1
  169. package/dist/store/memory.mjs +18 -1
  170. package/dist/store/memory.mjs.map +1 -1
  171. package/dist/store/slack-pr-notifications.d.mts +44 -0
  172. package/dist/store/slack-pr-notifications.mjs +121 -0
  173. package/dist/store/slack-pr-notifications.mjs.map +1 -0
  174. package/dist/store/task-work.d.mts +121 -6
  175. package/dist/store/task-work.mjs +7 -4
  176. package/dist/store/task-work.mjs.map +1 -1
  177. package/dist/sweep.d.mts +28 -6
  178. package/dist/sweep.mjs +34 -6
  179. package/dist/sweep.mjs.map +1 -1
  180. package/dist/task-graph-view.d.mts +3 -0
  181. package/dist/tasks.d.mts +3 -3
  182. package/dist/tasks.mjs +3 -3
  183. package/dist/vercel-git.d.mts +35 -3
  184. package/dist/vercel-git.mjs +265 -33
  185. package/dist/vercel-git.mjs.map +1 -1
  186. package/dist/vercel-github-api.d.mts +103 -0
  187. package/dist/vercel-github-api.mjs +363 -0
  188. package/dist/vercel-github-api.mjs.map +1 -0
  189. package/dist/vercel.d.mts +3 -2
  190. package/dist/vercel.mjs +3 -2
  191. package/dist/vercel.mjs.map +1 -1
  192. package/dist/work-triage.d.mts +1 -0
  193. package/dist/workflows.d.mts +102 -4
  194. package/dist/workflows.mjs +55 -2
  195. package/dist/workflows.mjs.map +1 -1
  196. package/dist/workspace-files-git.d.mts +15 -0
  197. package/dist/workspace-files-git.mjs +61 -0
  198. package/dist/workspace-files-git.mjs.map +1 -0
  199. package/dist/workspace-files.d.mts +107 -0
  200. package/dist/workspace-files.mjs +74 -0
  201. package/dist/workspace-files.mjs.map +1 -0
  202. package/docs/getting-started.md +104 -0
  203. package/docs/index.md +100 -0
  204. package/docs/recipes/cancellation.md +215 -0
  205. package/docs/recipes/custom-workflow.md +153 -0
  206. package/docs/recipes/dependent-tasks.md +207 -0
  207. package/docs/recipes/eve-agent.md +277 -0
  208. package/docs/recipes/human-input.md +204 -0
  209. package/docs/recipes/persistence-recovery.md +268 -0
  210. package/docs/recipes/retry-recovery.md +241 -0
  211. package/docs/recipes/task-messaging.md +215 -0
  212. package/docs/recipes/typed-eve-result.md +161 -0
  213. package/docs/runtime-integration.md +137 -0
  214. package/package.json +17 -6
package/dist/sweep.mjs CHANGED
@@ -2,6 +2,7 @@ import { InvalidTransitionError } from "./schema/transitions.mjs";
2
2
  import { routeKey } from "./agent-routes.mjs";
3
3
  import { VersionConflictError } from "./store/driver.mjs";
4
4
  import { DispatchRefusedError, dispatchTask } from "./dispatch.mjs";
5
+ import { TaskAttemptConflictError } from "./store/engine.mjs";
5
6
  //#region src/sweep.ts
6
7
  async function failUnrouteable(stores, task, reasonCode, reason, result) {
7
8
  try {
@@ -27,12 +28,29 @@ async function failUnrouteable(stores, task, reasonCode, reason, result) {
27
28
  }
28
29
  }
29
30
  /**
30
- * One pass of the factory's dispatcher, meant to run on a schedule inside the
31
- * deployment: every queued task that is allowed to run is started, in
32
- * creation order. Nothing outside the factory ever starts an agent; the CLI
33
- * only writes tasks and approvals, and this picks them up.
34
- * Idempotent by construction: dispatchTask's single-winner transition means
35
- * two overlapping sweeps cannot start the same task twice.
31
+ * Runs one bounded pass over queued Tasks and dispatches each eligible exact route.
32
+ *
33
+ * @remarks
34
+ * Candidates are ordered by newest attempt first and then creation time. Route-less work remains
35
+ * queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in
36
+ * `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures
37
+ * appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the
38
+ * race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:
39
+ * an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays
40
+ * running for recovery. Callers should surface every failure, reread those Tasks before deciding
41
+ * remediation, and schedule another pass for skipped work.
42
+ *
43
+ * Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a
44
+ * launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can
45
+ * become stale, and corresponding races are returned as `task_changed` rather than hidden.
46
+ * `maxDispatches` counts provider launches started during the pass, including launches that later
47
+ * fail, so `dispatched.length` may be smaller than the limit.
48
+ *
49
+ * @param stores - Graph, receipt, repository, and Task stores used during the pass.
50
+ * @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.
51
+ * @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.
52
+ * @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.
53
+ * @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.
36
54
  */
37
55
  async function sweepQueued(stores, options) {
38
56
  const result = {
@@ -100,12 +118,14 @@ async function sweepQueued(stores, options) {
100
118
  await failUnrouteable(stores, task, "repository_not_found", "task has no repository", result);
101
119
  continue;
102
120
  }
121
+ let launchStarted = false;
103
122
  try {
104
123
  const dispatched = await dispatchTask(stores, {
105
124
  taskId: task.id,
106
125
  route,
107
126
  launch: (dispatched) => {
108
127
  attempted += 1;
128
+ launchStarted = true;
109
129
  return launcher(dispatched, repository);
110
130
  },
111
131
  budget: options.budget === void 0 ? void 0 : {
@@ -128,6 +148,14 @@ async function sweepQueued(stores, options) {
128
148
  });
129
149
  continue;
130
150
  }
151
+ if (!launchStarted && (error instanceof InvalidTransitionError || error instanceof TaskAttemptConflictError || error instanceof VersionConflictError)) {
152
+ result.skipped.push({
153
+ taskId: task.id,
154
+ reasonCode: "task_changed",
155
+ reason: "task changed before dispatch"
156
+ });
157
+ continue;
158
+ }
131
159
  result.failed.push({
132
160
  taskId: task.id,
133
161
  reasonCode: "launch_failed",
@@ -1 +1 @@
1
- {"version":3,"file":"sweep.mjs","names":[],"sources":["../src/sweep.ts"],"sourcesContent":["import { routeKey, type AgentRouteKey } from \"./agent-routes\";\nimport { dispatchTask, DispatchRefusedError, type DispatchBudget } from \"./dispatch\";\nimport type { DispatchRefusalCode } from \"./dispatch\";\nimport type { TaskId } from \"./schema/id\";\nimport type { Repository } from \"./schema/repository\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { VersionConflictError } from \"./store/driver\";\nimport type { FactoryStores } from \"./store/engine\";\n\n/** Graph, receipt, repository, and Task capabilities required by queued-work dispatch. */\nexport type SweepQueuedStores = Pick<\n FactoryStores,\n \"graphs\" | \"receipts\" | \"repositories\" | \"tasks\"\n>;\n\n/**\n * Single-repository adapter for an exact agent route. Supplied by the app, which knows its\n * agents' addresses. Returns the provider's execution reference when it has one.\n */\nexport type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;\n\n/** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */\nexport interface SweepQueuedOptions {\n /** Single-repository agent adapters keyed by routeKey(binding), e.g. code-change@1. */\n launchers: Readonly<Partial<Record<AgentRouteKey, TaskLauncher>>>;\n /** Reserve-before-execution limits, passed through to every dispatch. */\n budget?: DispatchBudget;\n /** Maximum task launches attempted in one pass; omitted means no limit. */\n maxDispatches?: number;\n /** A dispatcher-wide snapshot avoids another complete task read. */\n tasks?: readonly Task[];\n}\n\n/** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */\nexport interface SweepQueuedResult {\n readonly dispatched: readonly TaskId[];\n /** Tasks left in the queue because a declared gate may become eligible later. */\n readonly skipped: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepSkipReasonCode;\n readonly reason: string;\n }[];\n /** Tasks whose routing is invalid or whose launch threw. */\n readonly failed: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepFailureReasonCode;\n readonly reason: string;\n }[];\n}\n\n/** Stable reason a queued Task remained eligible for a later dispatch pass. */\nexport type SweepSkipReasonCode =\n | DispatchRefusalCode\n | \"workflow_owned\"\n | \"awaiting_approval\"\n | \"dispatch_limit\"\n | \"task_changed\";\n\n/** Stable reason a queued Task could not be routed or launched. */\nexport type SweepFailureReasonCode =\n | \"invalid_repository_scope\"\n | \"repository_not_found\"\n | \"launcher_not_registered\"\n | \"launch_failed\";\n\ntype MutableSweepQueuedResult = {\n dispatched: TaskId[];\n skipped: { taskId: TaskId; reasonCode: SweepSkipReasonCode; reason: string }[];\n failed: { taskId: TaskId; reasonCode: SweepFailureReasonCode; reason: string }[];\n};\n\nasync function failUnrouteable(\n stores: SweepQueuedStores,\n task: Task,\n reasonCode: SweepFailureReasonCode,\n reason: string,\n result: MutableSweepQueuedResult,\n): Promise<void> {\n try {\n await stores.tasks.transition(task.id, \"failed\", { reason, expectFrom: \"queued\" });\n result.failed.push({ taskId: task.id, reasonCode, reason });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed during routing\",\n });\n return;\n }\n throw error;\n }\n}\n\n/**\n * One pass of the factory's dispatcher, meant to run on a schedule inside the\n * deployment: every queued task that is allowed to run is started, in\n * creation order. Nothing outside the factory ever starts an agent; the CLI\n * only writes tasks and approvals, and this picks them up.\n * Idempotent by construction: dispatchTask's single-winner transition means\n * two overlapping sweeps cannot start the same task twice.\n */\nexport async function sweepQueued(\n stores: SweepQueuedStores,\n options: SweepQueuedOptions,\n): Promise<SweepQueuedResult> {\n const result: MutableSweepQueuedResult = { dispatched: [], skipped: [], failed: [] };\n const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;\n if (\n !(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) ||\n maxDispatches < 1\n ) {\n throw new Error(\"maxDispatches must be a positive integer\");\n }\n let attempted = 0;\n const taskSnapshot = options.tasks ?? (await stores.tasks.list());\n const budgetSnapshot =\n options.budget?.tasks ??\n (options.budget && options.tasks ? await stores.tasks.list() : taskSnapshot);\n const budgetTasks = new Map(budgetSnapshot.map((task) => [task.id, task]));\n const queued = taskSnapshot\n .filter((task) => task.state === \"queued\")\n .sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));\n\n for (const task of queued) {\n const route = task.work.route;\n if (route === undefined) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"workflow_owned\",\n reason: \"no agent route; workflow owns execution\",\n });\n continue;\n }\n const [repositoryId] = task.repositoryIds;\n if (task.repositoryIds.length !== 1 || repositoryId === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"invalid_repository_scope\",\n \"sweep launcher adapter requires exactly one repository\",\n result,\n );\n continue;\n }\n if (task.approval === \"required\") {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"awaiting_approval\",\n reason: \"awaiting human approval\",\n });\n continue;\n }\n const key = routeKey(route);\n const launcher = Object.hasOwn(options.launchers, key) ? options.launchers[key] : undefined;\n if (launcher === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"launcher_not_registered\",\n `no registered launcher handles agent route ${key}`,\n result,\n );\n continue;\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dependencies_unsatisfied\",\n reason: \"graph dependencies must all be succeeded before dispatch\",\n });\n continue;\n }\n if (attempted >= maxDispatches) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dispatch_limit\",\n reason: \"per-sweep dispatch limit reached\",\n });\n continue;\n }\n const repository = await stores.repositories.get(repositoryId);\n if (repository === null) {\n await failUnrouteable(stores, task, \"repository_not_found\", \"task has no repository\", result);\n continue;\n }\n try {\n const dispatched = await dispatchTask(stores, {\n taskId: task.id,\n route,\n launch: (dispatched) => {\n attempted += 1;\n return launcher(dispatched, repository);\n },\n budget:\n options.budget === undefined\n ? undefined\n : { ...options.budget, tasks: [...budgetTasks.values()] },\n });\n budgetTasks.set(dispatched.id, dispatched);\n result.dispatched.push(task.id);\n } catch (error) {\n if (options.budget !== undefined) {\n const current = await stores.tasks.get(task.id).catch(() => null);\n if (current !== null) budgetTasks.set(current.id, current);\n }\n if (error instanceof DispatchRefusedError) {\n // Dependencies not ready, or another sweep won the race. Leave it.\n result.skipped.push({ taskId: task.id, reasonCode: error.code, reason: error.reason });\n continue;\n }\n result.failed.push({\n taskId: task.id,\n reasonCode: \"launch_failed\",\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sweep.mjs","names":[],"sources":["../src/sweep.ts"],"sourcesContent":["import { routeKey, type AgentRouteKey } from \"./agent-routes\";\nimport { dispatchTask, DispatchRefusedError, type DispatchBudget } from \"./dispatch\";\nimport type { DispatchRefusalCode } from \"./dispatch\";\nimport type { TaskId } from \"./schema/id\";\nimport type { Repository } from \"./schema/repository\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { VersionConflictError } from \"./store/driver\";\nimport { TaskAttemptConflictError, type FactoryStores } from \"./store/engine\";\n\n/** Graph, receipt, repository, and Task capabilities required by queued-work dispatch. */\nexport type SweepQueuedStores = Pick<\n FactoryStores,\n \"graphs\" | \"receipts\" | \"repositories\" | \"tasks\"\n>;\n\n/**\n * Single-repository adapter for an exact agent route. Supplied by the app, which knows its\n * agents' addresses. Returns the provider's execution reference when it has one.\n *\n * @param task - Running Task already admitted by the dispatch gate.\n * @param repository - Exact enabled repository selected from the Task scope.\n * @returns Provider execution reference that dispatch can persist for recovery.\n */\nexport type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;\n\n/** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */\nexport interface SweepQueuedOptions {\n /** Single-repository agent adapters keyed by routeKey(binding), e.g. code-change@1. */\n launchers: Readonly<Partial<Record<AgentRouteKey, TaskLauncher>>>;\n /** Reserve-before-execution limits, passed through to every dispatch. */\n budget?: DispatchBudget;\n /** Maximum task launches attempted in one pass; omitted means no limit. */\n maxDispatches?: number;\n /** A dispatcher-wide snapshot avoids another complete task read. */\n tasks?: readonly Task[];\n}\n\n/** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */\nexport interface SweepQueuedResult {\n /** Task IDs whose provider execution reference was durably recorded. */\n readonly dispatched: readonly TaskId[];\n /** Tasks left in the queue because a declared gate may become eligible later. */\n readonly skipped: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepSkipReasonCode;\n readonly reason: string;\n }[];\n /** Tasks whose routing is invalid or whose launch threw. */\n readonly failed: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepFailureReasonCode;\n readonly reason: string;\n }[];\n}\n\n/** Stable reason a queued Task remained eligible for a later dispatch pass. */\nexport type SweepSkipReasonCode =\n | DispatchRefusalCode\n | \"workflow_owned\"\n | \"awaiting_approval\"\n | \"dispatch_limit\"\n | \"task_changed\";\n\n/** Stable reason a queued Task could not be routed or launched. */\nexport type SweepFailureReasonCode =\n | \"invalid_repository_scope\"\n | \"repository_not_found\"\n | \"launcher_not_registered\"\n | \"launch_failed\";\n\ntype MutableSweepQueuedResult = {\n dispatched: TaskId[];\n skipped: { taskId: TaskId; reasonCode: SweepSkipReasonCode; reason: string }[];\n failed: { taskId: TaskId; reasonCode: SweepFailureReasonCode; reason: string }[];\n};\n\nasync function failUnrouteable(\n stores: SweepQueuedStores,\n task: Task,\n reasonCode: SweepFailureReasonCode,\n reason: string,\n result: MutableSweepQueuedResult,\n): Promise<void> {\n try {\n await stores.tasks.transition(task.id, \"failed\", { reason, expectFrom: \"queued\" });\n result.failed.push({ taskId: task.id, reasonCode, reason });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed during routing\",\n });\n return;\n }\n throw error;\n }\n}\n\n/**\n * Runs one bounded pass over queued Tasks and dispatches each eligible exact route.\n *\n * @remarks\n * Candidates are ordered by newest attempt first and then creation time. Route-less work remains\n * queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in\n * `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures\n * appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the\n * race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:\n * an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays\n * running for recovery. Callers should surface every failure, reread those Tasks before deciding\n * remediation, and schedule another pass for skipped work.\n *\n * Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a\n * launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can\n * become stale, and corresponding races are returned as `task_changed` rather than hidden.\n * `maxDispatches` counts provider launches started during the pass, including launches that later\n * fail, so `dispatched.length` may be smaller than the limit.\n *\n * @param stores - Graph, receipt, repository, and Task stores used during the pass.\n * @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.\n * @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.\n * @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.\n * @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.\n */\nexport async function sweepQueued(\n stores: SweepQueuedStores,\n options: SweepQueuedOptions,\n): Promise<SweepQueuedResult> {\n const result: MutableSweepQueuedResult = { dispatched: [], skipped: [], failed: [] };\n const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;\n if (\n !(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) ||\n maxDispatches < 1\n ) {\n throw new Error(\"maxDispatches must be a positive integer\");\n }\n let attempted = 0;\n const taskSnapshot = options.tasks ?? (await stores.tasks.list());\n const budgetSnapshot =\n options.budget?.tasks ??\n (options.budget && options.tasks ? await stores.tasks.list() : taskSnapshot);\n const budgetTasks = new Map(budgetSnapshot.map((task) => [task.id, task]));\n const queued = taskSnapshot\n .filter((task) => task.state === \"queued\")\n .sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));\n\n for (const task of queued) {\n const route = task.work.route;\n if (route === undefined) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"workflow_owned\",\n reason: \"no agent route; workflow owns execution\",\n });\n continue;\n }\n const [repositoryId] = task.repositoryIds;\n if (task.repositoryIds.length !== 1 || repositoryId === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"invalid_repository_scope\",\n \"sweep launcher adapter requires exactly one repository\",\n result,\n );\n continue;\n }\n if (task.approval === \"required\") {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"awaiting_approval\",\n reason: \"awaiting human approval\",\n });\n continue;\n }\n const key = routeKey(route);\n const launcher = Object.hasOwn(options.launchers, key) ? options.launchers[key] : undefined;\n if (launcher === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"launcher_not_registered\",\n `no registered launcher handles agent route ${key}`,\n result,\n );\n continue;\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dependencies_unsatisfied\",\n reason: \"graph dependencies must all be succeeded before dispatch\",\n });\n continue;\n }\n if (attempted >= maxDispatches) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dispatch_limit\",\n reason: \"per-sweep dispatch limit reached\",\n });\n continue;\n }\n const repository = await stores.repositories.get(repositoryId);\n if (repository === null) {\n await failUnrouteable(stores, task, \"repository_not_found\", \"task has no repository\", result);\n continue;\n }\n let launchStarted = false;\n try {\n const dispatched = await dispatchTask(stores, {\n taskId: task.id,\n route,\n launch: (dispatched) => {\n attempted += 1;\n launchStarted = true;\n return launcher(dispatched, repository);\n },\n budget:\n options.budget === undefined\n ? undefined\n : { ...options.budget, tasks: [...budgetTasks.values()] },\n });\n budgetTasks.set(dispatched.id, dispatched);\n result.dispatched.push(task.id);\n } catch (error) {\n if (options.budget !== undefined) {\n const current = await stores.tasks.get(task.id).catch(() => null);\n if (current !== null) budgetTasks.set(current.id, current);\n }\n if (error instanceof DispatchRefusedError) {\n // Dependencies not ready, or another sweep won the race. Leave it.\n result.skipped.push({ taskId: task.id, reasonCode: error.code, reason: error.reason });\n continue;\n }\n if (\n !launchStarted &&\n (error instanceof InvalidTransitionError ||\n error instanceof TaskAttemptConflictError ||\n error instanceof VersionConflictError)\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed before dispatch\",\n });\n continue;\n }\n result.failed.push({\n taskId: task.id,\n reasonCode: \"launch_failed\",\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return result;\n}\n"],"mappings":"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"}
@@ -13,6 +13,7 @@ declare const taskGraphViewSchema: z.ZodObject<{
13
13
  id: z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
14
14
  repositoryIds: z.ZodReadonly<z.ZodTuple<[z.ZodType<`repo_${string}`, string, z.core.$ZodTypeInternals<`repo_${string}`, string>>], z.ZodType<`repo_${string}`, string, z.core.$ZodTypeInternals<`repo_${string}`, string>>>>;
15
15
  kind: z.ZodType<TaskKind, unknown, z.core.$ZodTypeInternals<TaskKind, unknown>>;
16
+ model: z.ZodOptional<z.ZodString>;
16
17
  area: z.ZodOptional<z.ZodString>;
17
18
  state: z.ZodEnum<{
18
19
  queued: "queued";
@@ -129,6 +130,7 @@ declare const taskGraphViewSchema: z.ZodObject<{
129
130
  attempt: number;
130
131
  createdAt: string;
131
132
  updatedAt: string;
133
+ model?: string | undefined;
132
134
  area?: string | undefined;
133
135
  approval?: "required" | "granted" | "denied" | undefined;
134
136
  workResult?: {
@@ -206,6 +208,7 @@ declare const taskGraphViewSchema: z.ZodObject<{
206
208
  attempt: number;
207
209
  createdAt: string;
208
210
  updatedAt: string;
211
+ model?: string | undefined;
209
212
  area?: string | undefined;
210
213
  approval?: "required" | "granted" | "denied" | undefined;
211
214
  workResult?: {
package/dist/tasks.d.mts CHANGED
@@ -7,11 +7,11 @@ import { MAX_TASK_GRAPH_DEPENDENCIES, MAX_TASK_GRAPH_NODES, TaskGraph, TaskGraph
7
7
  import { AdmitTaskGraphInput, AssertTaskAdmissionInput, CreateTaskGraphStoreOptions, SetTaskGraphDependenciesInput, TaskGraphConflictError, TaskGraphNodeRef, TaskGraphStore, assertTaskAdmission, createTaskGraphStore } from "./store/task-graphs.mjs";
8
8
  import { TaskEffect, TaskEffectKey, TaskEffectReference, taskEffectKeySchema, taskEffectReferenceSchema, taskEffectSchema } from "./schema/task-effect.mjs";
9
9
  import { CreateTaskEffectStoreOptions, RecordTaskEffectInput, ResolveTaskEffectInput, TaskEffectLookupInput, TaskEffectStore, createTaskEffectStore } from "./store/task-effects.mjs";
10
- import { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore } from "./store/task-work.mjs";
10
+ import { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkCompletionFence, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore } from "./store/task-work.mjs";
11
11
  import { AnswerEvent, answerEventSchema } from "./schema/answer.mjs";
12
- import { AgentSessionBinding, AnswerEventConflictError, AnswerQuestionInput, AnswerQuestionResult, ApprovalDecision, ApprovalError, CreateTaskInput, RejectionDecision, RootTaskOrigin, TaskPatch, TaskTransitionArguments, TaskTransitionOptions } from "./store/engine.mjs";
12
+ import { AgentSessionBinding, AnswerEventConflictError, AnswerQuestionInput, AnswerQuestionResult, ApprovalDecision, ApprovalError, CreateTaskInput, RejectionDecision, RootTaskOrigin, TaskAttemptConflictError, TaskPatch, TaskTransitionArguments, TaskTransitionOptions } from "./store/engine.mjs";
13
13
  import { ApprovalAccess, ApprovalAccessDecisionInput, ApprovalAccessError, ApprovalAccessOptions, ApprovalAccessStores, createApprovalAccess } from "./approval-access.mjs";
14
14
  import { taskInitialState, taskTransitions } from "./schema/transitions.mjs";
15
15
  import { ProcessTaskMessageOptions, ProcessTaskMessageResult, TaskMessageProcessingStores, processTaskMessage, readTaskMessageDecision } from "./task-message-processing.mjs";
16
16
  import { AssignmentPayload, DefaultTaskMessageProtocols, HandoffPayload, StandardTaskMessageProtocol, assignmentPayloadSchema, assignmentTaskMessageProtocol, createDefaultTaskMessageProtocols, handoffPayloadSchema, handoffTaskMessageProtocol, standardTaskMessageProtocols } from "./task-protocols.mjs";
17
- export { type AdmitTaskGraphInput, type AgentSessionBinding, type AnswerEvent, AnswerEventConflictError, type AnswerId, type AnswerQuestionInput, type AnswerQuestionResult, type ApprovalAccess, type ApprovalAccessDecisionInput, ApprovalAccessError, type ApprovalAccessOptions, type ApprovalAccessStores, type ApprovalDecision, ApprovalError, type ApprovalPolicy, type ApprovalPriority, type ApprovalRule, type ApprovalSubject, type AssertTaskAdmissionInput, type AssertTaskMessageClaimInput, type AssignmentPayload, type ClaimTaskMessageInput, type ClaimTaskMessageResult, type CompleteTaskMessageInput, type CompleteTaskWorkInput, type CompleteWorkflowTaskWorkInput, type CreateTaskEffectStoreOptions, type CreateTaskGraphStoreOptions, type CreateTaskInput, type CreateTaskMessageStoreOptions, type CreateTaskWorkStoreOptions, type DefaultTaskMessageProtocols, type DiscardTaskMessageInput, type ExecutionReference, type HandoffPayload, MAX_TASK_GRAPH_DEPENDENCIES, MAX_TASK_GRAPH_NODES, MAX_TASK_INBOX_PENDING, type MessageWaitingTask, type NeedsHumanTask, type ProcessTaskMessageOptions, type ProcessTaskMessageResult, type RecordTaskEffectInput, type RejectionDecision, type ResolveTaskEffectInput, type ResumeTaskWorkInput, type RootTaskOrigin, type SendTaskMessageInput, type SetTaskGraphDependenciesInput, type StandardTaskMessageProtocol, type SucceededTask, type Task, type TaskApproval, type TaskEffect, type TaskEffectKey, type TaskEffectLookupInput, type TaskEffectReference, type TaskEffectStore, type TaskGraph, TaskGraphConflictError, type TaskGraphNode, type TaskGraphNodeRef, type TaskGraphStore, type TaskId, type TaskInbox, type TaskKind, type TaskMessage, type TaskMessageAction, TaskMessageAuthorizationError, type TaskMessageAuthorizationInput, type TaskMessageAuthorize, type TaskMessageClaim, TaskMessageConflictError, type TaskMessageDiscard, type TaskMessageEffectKeyInput, TaskMessageError, type TaskMessageForProtocol, type TaskMessageId, TaskMessageLeaseError, type TaskMessageLookupInput, type TaskMessageOperationId, type TaskMessagePrincipal, type TaskMessageProcessing, type TaskMessageProcessingStores, type TaskMessageProtocol, type TaskMessageProtocolBinding, type TaskMessageProtocolBindingInput, type TaskMessageProtocolInput, type TaskMessageProtocolInputPayload, type TaskMessageProtocolStoredPayload, type TaskMessageProtocols, type TaskMessageRecipientInput, type TaskMessageSendInput, TaskMessageSourceFenceError, type TaskMessageStore, type TaskOrigin, type TaskPatch, type TaskQuestion, type TaskState, type TaskTransitionArguments, type TaskTransitionOptions, type TaskUsage, type TaskWorkStore, type TaskWorkStores, type TaskWorkUsage, type WaitForTaskMessagesInput, allowByDefaultApprovalPolicy, answerEventSchema, answerIdSchema, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, assertTaskAdmission, assignmentPayloadSchema, assignmentTaskMessageProtocol, createApprovalAccess, createDefaultTaskMessageProtocols, createTaskEffectStore, createTaskGraphStore, createTaskMessageStore, createTaskWorkStore, defineTaskMessageProtocol, defineTaskMessageProtocols, executionReferenceSchema, handoffPayloadSchema, handoffTaskMessageProtocol, harnessSchema, isMessageWaitingTask, isNeedsHumanTask, isSucceededTask, parseTaskMessageProtocolBinding, processTaskMessage, readTaskMessageDecision, requiresApproval, sendTaskMessageInputSchema, standardTaskKinds, standardTaskMessageProtocols, taskApprovalSchema, taskBoundMessagePolicy, taskEffectKeySchema, taskEffectReferenceSchema, taskEffectSchema, taskExecutionRepositoryIdsSchema, taskGraphAdmissionSchema, taskGraphDependenciesSchema, taskGraphNodeSchema, taskGraphSchema, taskIdSchema, taskInboxSchema, taskInitialState, taskKindSchema, taskMessageClaimSchema, taskMessageDiscardSchema, taskMessageIdSchema, taskMessageInput, taskMessageOperationIdSchema, taskMessagePrincipalSchema, taskMessageProcessingSchema, taskMessageProtocolBindingSchema, taskMessageProtocolSchema, taskMessageReservationSchema, taskMessageSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, taskUsageSchema };
17
+ export { type AdmitTaskGraphInput, type AgentSessionBinding, type AnswerEvent, AnswerEventConflictError, type AnswerId, type AnswerQuestionInput, type AnswerQuestionResult, type ApprovalAccess, type ApprovalAccessDecisionInput, ApprovalAccessError, type ApprovalAccessOptions, type ApprovalAccessStores, type ApprovalDecision, ApprovalError, type ApprovalPolicy, type ApprovalPriority, type ApprovalRule, type ApprovalSubject, type AssertTaskAdmissionInput, type AssertTaskMessageClaimInput, type AssignmentPayload, type ClaimTaskMessageInput, type ClaimTaskMessageResult, type CompleteTaskMessageInput, type CompleteTaskWorkInput, type CompleteWorkflowTaskWorkInput, type CreateTaskEffectStoreOptions, type CreateTaskGraphStoreOptions, type CreateTaskInput, type CreateTaskMessageStoreOptions, type CreateTaskWorkStoreOptions, type DefaultTaskMessageProtocols, type DiscardTaskMessageInput, type ExecutionReference, type HandoffPayload, MAX_TASK_GRAPH_DEPENDENCIES, MAX_TASK_GRAPH_NODES, MAX_TASK_INBOX_PENDING, type MessageWaitingTask, type NeedsHumanTask, type ProcessTaskMessageOptions, type ProcessTaskMessageResult, type RecordTaskEffectInput, type RejectionDecision, type ResolveTaskEffectInput, type ResumeTaskWorkInput, type RootTaskOrigin, type SendTaskMessageInput, type SetTaskGraphDependenciesInput, type StandardTaskMessageProtocol, type SucceededTask, type Task, type TaskApproval, TaskAttemptConflictError, type TaskEffect, type TaskEffectKey, type TaskEffectLookupInput, type TaskEffectReference, type TaskEffectStore, type TaskGraph, TaskGraphConflictError, type TaskGraphNode, type TaskGraphNodeRef, type TaskGraphStore, type TaskId, type TaskInbox, type TaskKind, type TaskMessage, type TaskMessageAction, TaskMessageAuthorizationError, type TaskMessageAuthorizationInput, type TaskMessageAuthorize, type TaskMessageClaim, TaskMessageConflictError, type TaskMessageDiscard, type TaskMessageEffectKeyInput, TaskMessageError, type TaskMessageForProtocol, type TaskMessageId, TaskMessageLeaseError, type TaskMessageLookupInput, type TaskMessageOperationId, type TaskMessagePrincipal, type TaskMessageProcessing, type TaskMessageProcessingStores, type TaskMessageProtocol, type TaskMessageProtocolBinding, type TaskMessageProtocolBindingInput, type TaskMessageProtocolInput, type TaskMessageProtocolInputPayload, type TaskMessageProtocolStoredPayload, type TaskMessageProtocols, type TaskMessageRecipientInput, type TaskMessageSendInput, TaskMessageSourceFenceError, type TaskMessageStore, type TaskOrigin, type TaskPatch, type TaskQuestion, type TaskState, type TaskTransitionArguments, type TaskTransitionOptions, type TaskUsage, type TaskWorkCompletionFence, type TaskWorkStore, type TaskWorkStores, type TaskWorkUsage, type WaitForTaskMessagesInput, allowByDefaultApprovalPolicy, answerEventSchema, answerIdSchema, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, assertTaskAdmission, assignmentPayloadSchema, assignmentTaskMessageProtocol, createApprovalAccess, createDefaultTaskMessageProtocols, createTaskEffectStore, createTaskGraphStore, createTaskMessageStore, createTaskWorkStore, defineTaskMessageProtocol, defineTaskMessageProtocols, executionReferenceSchema, handoffPayloadSchema, handoffTaskMessageProtocol, harnessSchema, isMessageWaitingTask, isNeedsHumanTask, isSucceededTask, parseTaskMessageProtocolBinding, processTaskMessage, readTaskMessageDecision, requiresApproval, sendTaskMessageInputSchema, standardTaskKinds, standardTaskMessageProtocols, taskApprovalSchema, taskBoundMessagePolicy, taskEffectKeySchema, taskEffectReferenceSchema, taskEffectSchema, taskExecutionRepositoryIdsSchema, taskGraphAdmissionSchema, taskGraphDependenciesSchema, taskGraphNodeSchema, taskGraphSchema, taskIdSchema, taskInboxSchema, taskInitialState, taskKindSchema, taskMessageClaimSchema, taskMessageDiscardSchema, taskMessageIdSchema, taskMessageInput, taskMessageOperationIdSchema, taskMessagePrincipalSchema, taskMessageProcessingSchema, taskMessageProtocolBindingSchema, taskMessageProtocolSchema, taskMessageReservationSchema, taskMessageSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, taskUsageSchema };
package/dist/tasks.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { answerIdSchema, taskIdSchema } from "./schema/id.mjs";
2
- import { allowByDefaultApprovalPolicy, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, requiresApproval } from "./approval.mjs";
3
2
  import { executionReferenceSchema, harnessSchema, isMessageWaitingTask, isNeedsHumanTask, isSucceededTask, standardTaskKinds, taskApprovalSchema, taskExecutionRepositoryIdsSchema, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskUsageSchema } from "./schema/task.mjs";
3
+ import { allowByDefaultApprovalPolicy, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, requiresApproval } from "./approval.mjs";
4
4
  import { taskInitialState, taskTransitions } from "./schema/transitions.mjs";
5
5
  import { ApprovalAccessError, createApprovalAccess } from "./approval-access.mjs";
6
6
  import { answerEventSchema } from "./schema/answer.mjs";
@@ -13,5 +13,5 @@ import { createTaskWorkStore } from "./store/task-work.mjs";
13
13
  import { createTaskEffectStore } from "./store/task-effects.mjs";
14
14
  import { processTaskMessage, readTaskMessageDecision } from "./task-message-processing.mjs";
15
15
  import { assignmentPayloadSchema, assignmentTaskMessageProtocol, createDefaultTaskMessageProtocols, handoffPayloadSchema, handoffTaskMessageProtocol, standardTaskMessageProtocols } from "./task-protocols.mjs";
16
- import { AnswerEventConflictError, ApprovalError } from "./store/engine.mjs";
17
- export { AnswerEventConflictError, ApprovalAccessError, ApprovalError, MAX_TASK_GRAPH_DEPENDENCIES, MAX_TASK_GRAPH_NODES, MAX_TASK_INBOX_PENDING, TaskGraphConflictError, TaskMessageAuthorizationError, TaskMessageConflictError, TaskMessageError, TaskMessageLeaseError, TaskMessageSourceFenceError, allowByDefaultApprovalPolicy, answerEventSchema, answerIdSchema, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, assertTaskAdmission, assignmentPayloadSchema, assignmentTaskMessageProtocol, createApprovalAccess, createDefaultTaskMessageProtocols, createTaskEffectStore, createTaskGraphStore, createTaskMessageStore, createTaskWorkStore, defineTaskMessageProtocol, defineTaskMessageProtocols, executionReferenceSchema, handoffPayloadSchema, handoffTaskMessageProtocol, harnessSchema, isMessageWaitingTask, isNeedsHumanTask, isSucceededTask, parseTaskMessageProtocolBinding, processTaskMessage, readTaskMessageDecision, requiresApproval, sendTaskMessageInputSchema, standardTaskKinds, standardTaskMessageProtocols, taskApprovalSchema, taskBoundMessagePolicy, taskEffectKeySchema, taskEffectReferenceSchema, taskEffectSchema, taskExecutionRepositoryIdsSchema, taskGraphAdmissionSchema, taskGraphDependenciesSchema, taskGraphNodeSchema, taskGraphSchema, taskIdSchema, taskInboxSchema, taskInitialState, taskKindSchema, taskMessageClaimSchema, taskMessageDiscardSchema, taskMessageIdSchema, taskMessageInput, taskMessageOperationIdSchema, taskMessagePrincipalSchema, taskMessageProcessingSchema, taskMessageProtocolBindingSchema, taskMessageProtocolSchema, taskMessageReservationSchema, taskMessageSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, taskUsageSchema };
16
+ import { AnswerEventConflictError, ApprovalError, TaskAttemptConflictError } from "./store/engine.mjs";
17
+ export { AnswerEventConflictError, ApprovalAccessError, ApprovalError, MAX_TASK_GRAPH_DEPENDENCIES, MAX_TASK_GRAPH_NODES, MAX_TASK_INBOX_PENDING, TaskAttemptConflictError, TaskGraphConflictError, TaskMessageAuthorizationError, TaskMessageConflictError, TaskMessageError, TaskMessageLeaseError, TaskMessageSourceFenceError, allowByDefaultApprovalPolicy, answerEventSchema, answerIdSchema, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, assertTaskAdmission, assignmentPayloadSchema, assignmentTaskMessageProtocol, createApprovalAccess, createDefaultTaskMessageProtocols, createTaskEffectStore, createTaskGraphStore, createTaskMessageStore, createTaskWorkStore, defineTaskMessageProtocol, defineTaskMessageProtocols, executionReferenceSchema, handoffPayloadSchema, handoffTaskMessageProtocol, harnessSchema, isMessageWaitingTask, isNeedsHumanTask, isSucceededTask, parseTaskMessageProtocolBinding, processTaskMessage, readTaskMessageDecision, requiresApproval, sendTaskMessageInputSchema, standardTaskKinds, standardTaskMessageProtocols, taskApprovalSchema, taskBoundMessagePolicy, taskEffectKeySchema, taskEffectReferenceSchema, taskEffectSchema, taskExecutionRepositoryIdsSchema, taskGraphAdmissionSchema, taskGraphDependenciesSchema, taskGraphNodeSchema, taskGraphSchema, taskIdSchema, taskInboxSchema, taskInitialState, taskKindSchema, taskMessageClaimSchema, taskMessageDiscardSchema, taskMessageIdSchema, taskMessageInput, taskMessageOperationIdSchema, taskMessagePrincipalSchema, taskMessageProcessingSchema, taskMessageProtocolBindingSchema, taskMessageProtocolSchema, taskMessageReservationSchema, taskMessageSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, taskUsageSchema };
@@ -1,7 +1,27 @@
1
1
  import { RepositorySlug } from "./schema/repository.mjs";
2
2
  import { GitHubGitTransport } from "./github-transfer.mjs";
3
- import { Sandbox } from "@vercel/sandbox";
3
+ import { NetworkPolicy, Sandbox } from "@vercel/sandbox";
4
4
  //#region src/vercel-git.d.ts
5
+ type TrustedGitSandbox = Pick<Sandbox, "cwd" | "runCommand" | "writeFiles" | "readFile" | "updateNetworkPolicy" | "stop">;
6
+ /** Exact creation request for a fresh Vercel-managed trusted Git publication Sandbox. */
7
+ interface VercelManagedGitHubSandboxCreateInput {
8
+ source: {
9
+ type: "git";
10
+ url: string;
11
+ };
12
+ __source_credentials: true;
13
+ __commitAs: {
14
+ name: string;
15
+ email: string;
16
+ };
17
+ networkPolicy: NetworkPolicy;
18
+ persistent: false;
19
+ resources: {
20
+ vcpus: 6;
21
+ };
22
+ runtime: "node24";
23
+ timeout: number;
24
+ }
5
25
  /** Repository and access level used to mint one trusted Git transport token. */
6
26
  interface VercelGitHubTokenInput {
7
27
  repository: RepositorySlug;
@@ -12,10 +32,22 @@ interface VercelGitHubGitTransportOptions {
12
32
  /** Reads must mint contents:read credentials; writes are confined to the supplied repository. */
13
33
  token(input: VercelGitHubTokenInput): Promise<string>;
14
34
  /** Creates a fresh sandbox, without a snapshot, repository, or running agent. */
15
- createSandbox?: () => Promise<Pick<Sandbox, "runCommand" | "writeFiles" | "readFileToBuffer" | "updateNetworkPolicy" | "stop">>;
35
+ createSandbox?: () => Promise<TrustedGitSandbox>;
36
+ }
37
+ /** Configures Vercel-managed Git reads and signed publication. */
38
+ interface VercelManagedGitHubGitTransportOptions {
39
+ /** Attribution metadata supplied for managed signed commits. */
40
+ commitIdentity: {
41
+ name: string;
42
+ email: string;
43
+ };
44
+ /** Creates one unnamed Sandbox from the exact managed credential request. */
45
+ createSandbox?: (input: VercelManagedGitHubSandboxCreateInput) => Promise<TrustedGitSandbox>;
16
46
  }
17
47
  /** Transfer Git objects through fresh trusted sandboxes; credentials never reach an agent sandbox. */
18
48
  declare function createVercelGitHubGitTransport(options: VercelGitHubGitTransportOptions): GitHubGitTransport;
49
+ /** Use fresh Vercel-managed trusted Sandboxes for Git reads and signed publication. */
50
+ declare function createVercelManagedGitHubGitTransport(options: VercelManagedGitHubGitTransportOptions): GitHubGitTransport;
19
51
  //#endregion
20
- export { VercelGitHubGitTransportOptions, VercelGitHubTokenInput, createVercelGitHubGitTransport };
52
+ export { VercelGitHubGitTransportOptions, VercelGitHubTokenInput, VercelManagedGitHubGitTransportOptions, VercelManagedGitHubSandboxCreateInput, createVercelGitHubGitTransport, createVercelManagedGitHubGitTransport };
21
53
  //# sourceMappingURL=vercel-git.d.mts.map
@@ -1,7 +1,80 @@
1
1
  import { repositorySlugSchema } from "./schema/repository.mjs";
2
- import { validateGitBundle } from "./github-transfer.mjs";
2
+ import { withSandboxGitBundle } from "./github-transfer.mjs";
3
3
  import { isSafeGitRef } from "./github-publication.mjs";
4
+ import { randomUUID } from "node:crypto";
5
+ import { Readable } from "node:stream";
4
6
  //#region src/vercel-git.ts
7
+ const gitOidPattern = /^[a-f0-9]{40}$/u;
8
+ const absentGitOid = "0".repeat(40);
9
+ const managedGitTimeoutMs = 18e4;
10
+ async function createManagedSandbox(input) {
11
+ const { Sandbox } = await import("@vercel/sandbox");
12
+ return Sandbox.create({ ...input });
13
+ }
14
+ function publicationBase(input) {
15
+ if (!isSafeGitRef(input.branch) || !gitOidPattern.test(input.head) || !gitOidPattern.test(input.expectedHead) || (input.expectedHead === absentGitOid ? input.baseHead === void 0 || !gitOidPattern.test(input.baseHead) : input.baseHead !== void 0)) throw new Error("Invalid Git publication target");
16
+ return input.expectedHead === absentGitOid ? input.baseHead : input.expectedHead;
17
+ }
18
+ async function trustedCommand(sandbox, input) {
19
+ const result = await sandbox.runCommand({
20
+ ...input,
21
+ timeoutMs: 12e4
22
+ });
23
+ if (result.exitCode !== 0) throw new Error(`Trusted Git operation failed (${result.exitCode})`);
24
+ return (await result.stdout()).trim();
25
+ }
26
+ function bundleTransferSandbox(sandbox) {
27
+ return {
28
+ async run({ command }) {
29
+ const result = await sandbox.runCommand({
30
+ cmd: "sh",
31
+ args: ["-c", command]
32
+ });
33
+ return {
34
+ exitCode: result.exitCode,
35
+ stdout: await result.stdout()
36
+ };
37
+ },
38
+ async writeBinaryFile({ path, content }) {
39
+ await sandbox.writeFiles([{
40
+ path,
41
+ content: Buffer.from(content.buffer, content.byteOffset, content.byteLength)
42
+ }]);
43
+ }
44
+ };
45
+ }
46
+ async function consumeSandboxBundle(sandbox, path, use) {
47
+ const size = await sandbox.runCommand({
48
+ cmd: "stat",
49
+ args: [
50
+ "-c",
51
+ "%s",
52
+ path
53
+ ]
54
+ });
55
+ const byteLength = Number(await size.stdout());
56
+ if (size.exitCode !== 0 || !Number.isSafeInteger(byteLength) || byteLength < 1 || byteLength > 314572800) throw new Error("Git bundle exceeds transfer limit");
57
+ const source = await sandbox.readFile({ path });
58
+ if (!source) throw new Error("Git bundle was not created");
59
+ const readable = source instanceof Readable ? source : new Readable({ highWaterMark: 65536 }).wrap(source);
60
+ const iterator = readable[Symbol.asyncIterator]();
61
+ const stream = new ReadableStream({
62
+ async pull(controller) {
63
+ const { done, value } = await iterator.next();
64
+ if (done) controller.close();
65
+ else controller.enqueue(value);
66
+ },
67
+ async cancel() {
68
+ await iterator.return?.();
69
+ }
70
+ }, { highWaterMark: 0 });
71
+ try {
72
+ await use({ stream });
73
+ } finally {
74
+ readable.destroy();
75
+ if (!stream.locked) await stream.cancel();
76
+ }
77
+ }
5
78
  function credentialPolicy(repository, token) {
6
79
  return { allow: { "github.com": [{
7
80
  match: { path: { startsWith: `/${repository}.git/` } },
@@ -57,7 +130,7 @@ function createVercelGitHubGitTransport(options) {
57
130
  }
58
131
  }
59
132
  return {
60
- async fetch(input) {
133
+ async fetch(input, use) {
61
134
  if (input.refs.some((ref) => !ref.startsWith("refs/") || !isSafeGitRef(ref))) throw new Error("Invalid Git fetch ref");
62
135
  return isolated(input.repository, async (sandbox, git, slug) => {
63
136
  await sandbox.updateNetworkPolicy(credentialPolicy(slug, await options.token({
@@ -68,45 +141,204 @@ function createVercelGitHubGitTransport(options) {
68
141
  if (input.refs.length) await git("-C", "/tmp/transfer.git", "fetch", "--no-tags", "origin", ...input.refs.map((ref) => `+${ref}:${ref}`));
69
142
  await sandbox.updateNetworkPolicy("deny-all");
70
143
  await git("-C", "/tmp/transfer.git", "bundle", "create", "/tmp/transfer.bundle", "--all");
71
- const size = await sandbox.runCommand({
72
- cmd: "stat",
144
+ await consumeSandboxBundle(sandbox, "/tmp/transfer.bundle", use);
145
+ });
146
+ },
147
+ async push(input) {
148
+ try {
149
+ const baseHead = publicationBase(input);
150
+ return await isolated(input.repository, async (sandbox, git, slug) => {
151
+ await git("init", "--bare", "/tmp/transfer.git");
152
+ return await withSandboxGitBundle({
153
+ sandbox: bundleTransferSandbox(sandbox),
154
+ bundle: input.bundle,
155
+ async use(path) {
156
+ await git("-C", "/tmp/transfer.git", "bundle", "verify", path);
157
+ await git("-C", "/tmp/transfer.git", "fetch", path, "HEAD:refs/heads/candidate");
158
+ if (await git("-C", "/tmp/transfer.git", "rev-parse", "refs/heads/candidate") !== input.head) throw new Error("Git bundle head does not match publication target");
159
+ await git("-C", "/tmp/transfer.git", "merge-base", "--is-ancestor", baseHead, "refs/heads/candidate");
160
+ await sandbox.updateNetworkPolicy(credentialPolicy(slug, await options.token({
161
+ repository: slug,
162
+ access: "write"
163
+ })));
164
+ await input.authorize();
165
+ await git("-C", "/tmp/transfer.git", "push", `--force-with-lease=refs/heads/${input.branch}:${input.expectedHead}`, `https://github.com/${slug}.git`, `${input.head}:refs/heads/${input.branch}`);
166
+ return input.head;
167
+ }
168
+ });
169
+ });
170
+ } finally {
171
+ if (!input.bundle.stream.locked) await input.bundle.stream.cancel();
172
+ }
173
+ }
174
+ };
175
+ }
176
+ /** Use fresh Vercel-managed trusted Sandboxes for Git reads and signed publication. */
177
+ function createVercelManagedGitHubGitTransport(options) {
178
+ const createSandbox = options.createSandbox ?? createManagedSandbox;
179
+ const createInput = (repository, access) => ({
180
+ source: {
181
+ type: "git",
182
+ url: `https://github.com/${repository}.git`
183
+ },
184
+ __source_credentials: true,
185
+ __commitAs: options.commitIdentity,
186
+ networkPolicy: { allow: access === "read" ? ["github.com"] : ["github.com", "api.github.com"] },
187
+ persistent: false,
188
+ resources: { vcpus: 6 },
189
+ runtime: "node24",
190
+ timeout: managedGitTimeoutMs
191
+ });
192
+ return {
193
+ async fetch(input, use) {
194
+ if (input.refs.some((ref) => !ref.startsWith("refs/") || !isSafeGitRef(ref))) throw new Error("Invalid Git fetch ref");
195
+ const repository = repositorySlugSchema.parse(input.repository.toLowerCase());
196
+ const sandbox = await createSandbox(createInput(repository, "read"));
197
+ const repositoryPath = sandbox.cwd;
198
+ try {
199
+ await trustedCommand(sandbox, {
200
+ cmd: "/usr/bin/git",
73
201
  args: [
74
- "-c",
75
- "%s",
76
- "/tmp/transfer.bundle"
202
+ "clone",
203
+ "--bare",
204
+ "--no-local",
205
+ repositoryPath,
206
+ "/tmp/transfer.git"
77
207
  ]
78
208
  });
79
- const byteLength = Number(await size.stdout());
80
- if (size.exitCode !== 0 || !Number.isSafeInteger(byteLength) || byteLength < 1 || byteLength > 52428800) throw new Error("Git bundle exceeds transfer limit");
81
- const data = await sandbox.readFileToBuffer({ path: "/tmp/transfer.bundle" });
82
- if (!data || data.length > 52428800) throw new Error("Invalid Git bundle size");
83
- const bundle = { base64: data.toString("base64") };
84
- validateGitBundle(bundle);
85
- return bundle;
86
- });
209
+ if (input.refs.length > 0) await trustedCommand(sandbox, {
210
+ cmd: "git",
211
+ args: [
212
+ "-C",
213
+ "/tmp/transfer.git",
214
+ "fetch",
215
+ "--no-tags",
216
+ `https://github.com/${repository}.git`,
217
+ ...input.refs.map((ref) => `+${ref}:${ref}`)
218
+ ]
219
+ });
220
+ await trustedCommand(sandbox, {
221
+ cmd: "/usr/bin/git",
222
+ args: [
223
+ "-C",
224
+ "/tmp/transfer.git",
225
+ "bundle",
226
+ "create",
227
+ "/tmp/transfer.bundle",
228
+ "--all"
229
+ ]
230
+ });
231
+ await consumeSandboxBundle(sandbox, "/tmp/transfer.bundle", use);
232
+ } finally {
233
+ await sandbox.stop();
234
+ }
87
235
  },
88
236
  async push(input) {
89
- if (!isSafeGitRef(input.branch) || !/^[a-f0-9]{40}$/.test(input.head) || !/^[a-f0-9]{40}$/.test(input.expectedHead)) throw new Error("Invalid Git publication target");
90
- const bundle = validateGitBundle(input.bundle);
91
- await isolated(input.repository, async (sandbox, git, slug) => {
92
- await git("init", "--bare", "/tmp/transfer.git");
93
- await sandbox.writeFiles([{
94
- path: "/tmp/transfer.bundle",
95
- content: bundle
96
- }]);
97
- await git("-C", "/tmp/transfer.git", "bundle", "verify", "/tmp/transfer.bundle");
98
- await git("-C", "/tmp/transfer.git", "fetch", "/tmp/transfer.bundle", "HEAD:refs/heads/candidate");
99
- if (await git("-C", "/tmp/transfer.git", "rev-parse", "refs/heads/candidate") !== input.head) throw new Error("Git bundle head does not match publication target");
100
- await sandbox.updateNetworkPolicy(credentialPolicy(slug, await options.token({
101
- repository: slug,
102
- access: "write"
103
- })));
104
- await git("-C", "/tmp/transfer.git", "push", `--force-with-lease=refs/heads/${input.branch}:${input.expectedHead}`, `https://github.com/${slug}.git`, `${input.head}:refs/heads/${input.branch}`);
105
- });
237
+ try {
238
+ const baseHead = publicationBase(input);
239
+ if (input.expectedHead === absentGitOid) throw new Error("Vercel-managed signed publication requires an existing remote branch");
240
+ const repository = repositorySlugSchema.parse(input.repository.toLowerCase());
241
+ const sandbox = await createSandbox(createInput(repository, "write"));
242
+ const repositoryPath = sandbox.cwd;
243
+ try {
244
+ const hooksPath = `/tmp/factory-hooks-${randomUUID()}`;
245
+ await trustedCommand(sandbox, {
246
+ cmd: "mkdir",
247
+ args: [
248
+ "-m",
249
+ "700",
250
+ hooksPath
251
+ ]
252
+ });
253
+ await trustedCommand(sandbox, {
254
+ cmd: "/usr/bin/git",
255
+ args: [
256
+ "config",
257
+ "core.hooksPath",
258
+ hooksPath
259
+ ],
260
+ cwd: repositoryPath
261
+ });
262
+ await withSandboxGitBundle({
263
+ sandbox: bundleTransferSandbox(sandbox),
264
+ bundle: input.bundle,
265
+ async use(path) {
266
+ await trustedCommand(sandbox, {
267
+ cmd: "/usr/bin/git",
268
+ args: [
269
+ "bundle",
270
+ "verify",
271
+ path
272
+ ],
273
+ cwd: repositoryPath
274
+ });
275
+ await trustedCommand(sandbox, {
276
+ cmd: "/usr/bin/git",
277
+ args: [
278
+ "fetch",
279
+ "--no-tags",
280
+ path,
281
+ `${input.head}:refs/factory/candidate`
282
+ ],
283
+ cwd: repositoryPath
284
+ });
285
+ if (await trustedCommand(sandbox, {
286
+ cmd: "/usr/bin/git",
287
+ args: ["rev-parse", "refs/factory/candidate"],
288
+ cwd: repositoryPath
289
+ }) !== input.head) throw new Error("Git bundle head does not match publication target");
290
+ await trustedCommand(sandbox, {
291
+ cmd: "/usr/bin/git",
292
+ args: [
293
+ "merge-base",
294
+ "--is-ancestor",
295
+ baseHead,
296
+ "refs/factory/candidate"
297
+ ],
298
+ cwd: repositoryPath
299
+ });
300
+ await trustedCommand(sandbox, {
301
+ cmd: "/usr/bin/git",
302
+ args: [
303
+ "checkout",
304
+ "-B",
305
+ input.branch,
306
+ "refs/factory/candidate"
307
+ ],
308
+ cwd: repositoryPath
309
+ });
310
+ await input.authorize();
311
+ await trustedCommand(sandbox, {
312
+ cmd: "/usr/bin/env",
313
+ args: [
314
+ `VERCEL_GIT_EXPECTED_REMOTE_OID=${input.expectedHead}`,
315
+ ...input.baseHead === void 0 ? [] : [`VERCEL_GIT_BASE_OID=${input.baseHead}`],
316
+ "git",
317
+ "push",
318
+ "origin",
319
+ input.branch
320
+ ],
321
+ cwd: repositoryPath
322
+ });
323
+ }
324
+ });
325
+ const signedHead = await trustedCommand(sandbox, {
326
+ cmd: "/usr/bin/git",
327
+ args: ["rev-parse", "HEAD"],
328
+ cwd: repositoryPath
329
+ });
330
+ if (!gitOidPattern.test(signedHead)) throw new Error("Managed signed push returned an invalid Git head");
331
+ return signedHead;
332
+ } finally {
333
+ await sandbox.stop();
334
+ }
335
+ } finally {
336
+ if (!input.bundle.stream.locked) await input.bundle.stream.cancel();
337
+ }
106
338
  }
107
339
  };
108
340
  }
109
341
  //#endregion
110
- export { createVercelGitHubGitTransport };
342
+ export { createVercelGitHubGitTransport, createVercelManagedGitHubGitTransport };
111
343
 
112
344
  //# sourceMappingURL=vercel-git.mjs.map