@mastra/factory 0.24.0-alpha.9 → 0.24.0

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 (296) hide show
  1. package/README.md +4 -4
  2. package/dist/environment/build-runner.js +2 -1
  3. package/dist/environment/build-runner.js.map +1 -1
  4. package/dist/environment/build-workflow.js +1 -1
  5. package/dist/environment/build-workflow.js.map +1 -1
  6. package/dist/environment/build.js +16 -14
  7. package/dist/environment/build.js.map +1 -1
  8. package/dist/factory.d.ts.map +1 -1
  9. package/dist/factory.js +111 -19
  10. package/dist/factory.js.map +1 -1
  11. package/dist/integrations/base.d.ts +1 -1
  12. package/dist/integrations/base.d.ts.map +1 -1
  13. package/dist/integrations/github/app-identity.js.map +1 -1
  14. package/dist/integrations/github/integration.js +1 -1
  15. package/dist/integrations/github/integration.js.map +1 -1
  16. package/dist/integrations/github/reconcile-worker.js +1 -1
  17. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  18. package/dist/integrations/github/routes.d.ts +1 -1
  19. package/dist/integrations/github/routes.d.ts.map +1 -1
  20. package/dist/integrations/github/routes.js +24 -13
  21. package/dist/integrations/github/routes.js.map +1 -1
  22. package/dist/integrations/github/rules.d.ts.map +1 -1
  23. package/dist/integrations/github/rules.js +53 -9
  24. package/dist/integrations/github/rules.js.map +1 -1
  25. package/dist/integrations/github/sandbox.js +2 -2
  26. package/dist/integrations/github/sandbox.js.map +1 -1
  27. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  28. package/dist/integrations/github/session-subscriptions.js +1 -2
  29. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  30. package/dist/integrations/github/subscriptions.d.ts +7 -4
  31. package/dist/integrations/github/subscriptions.d.ts.map +1 -1
  32. package/dist/integrations/github/subscriptions.js +5 -5
  33. package/dist/integrations/github/subscriptions.js.map +1 -1
  34. package/dist/integrations/gitlab/api.js.map +1 -1
  35. package/dist/integrations/gitlab/integration.js.map +1 -1
  36. package/dist/integrations/gitlab/reconciliation-config.js +2 -1
  37. package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
  38. package/dist/integrations/gitlab/routes.d.ts.map +1 -1
  39. package/dist/integrations/gitlab/routes.js +10 -6
  40. package/dist/integrations/gitlab/routes.js.map +1 -1
  41. package/dist/integrations/gitlab/rules.js.map +1 -1
  42. package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
  43. package/dist/integrations/gitlab/session-subscriptions.js +1 -2
  44. package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
  45. package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
  46. package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
  47. package/dist/integrations/gitlab/subscriptions.js +5 -5
  48. package/dist/integrations/gitlab/subscriptions.js.map +1 -1
  49. package/dist/integrations/incidentio/api.js.map +1 -1
  50. package/dist/integrations/incidentio/integration.js.map +1 -1
  51. package/dist/integrations/incidentio/rules.js.map +1 -1
  52. package/dist/integrations/issue-reconcile-worker.js +1 -1
  53. package/dist/integrations/issue-reconcile-worker.js.map +1 -1
  54. package/dist/integrations/issue-reconciler.js +1 -1
  55. package/dist/integrations/issue-reconciler.js.map +1 -1
  56. package/dist/integrations/jira/api.js.map +1 -1
  57. package/dist/integrations/jira/integration.js.map +1 -1
  58. package/dist/integrations/jira/rules.js.map +1 -1
  59. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  60. package/dist/integrations/linear/agent-tools.js.map +1 -1
  61. package/dist/integrations/linear/integration.js.map +1 -1
  62. package/dist/integrations/linear/rules.js.map +1 -1
  63. package/dist/integrations/platform/api-client.js.map +1 -1
  64. package/dist/integrations/platform/github/event-worker.js +1 -1
  65. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  66. package/dist/integrations/platform/github/integration.js +2 -2
  67. package/dist/integrations/platform/github/integration.js.map +1 -1
  68. package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
  69. package/dist/integrations/platform/gitlab/integration.js.map +1 -1
  70. package/dist/integrations/platform/incidentio/integration.js +2 -1
  71. package/dist/integrations/platform/incidentio/integration.js.map +1 -1
  72. package/dist/integrations/platform/jira/integration.js.map +1 -1
  73. package/dist/integrations/platform/linear/event-worker.js +1 -1
  74. package/dist/integrations/platform/linear/event-worker.js.map +1 -1
  75. package/dist/integrations/platform/linear/integration.js.map +1 -1
  76. package/dist/integrations/slack/connect-route.js +1 -1
  77. package/dist/integrations/slack/connect-route.js.map +1 -1
  78. package/dist/integrations/slack/feed-publisher.js +1 -1
  79. package/dist/integrations/slack/feed-publisher.js.map +1 -1
  80. package/dist/integrations/slack/integration.js +1 -1
  81. package/dist/integrations/slack/integration.js.map +1 -1
  82. package/dist/integrations/slack/slack.d.ts +16 -5
  83. package/dist/integrations/slack/slack.d.ts.map +1 -1
  84. package/dist/integrations/slack/slack.js +87 -34
  85. package/dist/integrations/slack/slack.js.map +1 -1
  86. package/dist/integrations/subscription-session.d.ts +0 -1
  87. package/dist/integrations/subscription-session.d.ts.map +1 -1
  88. package/dist/integrations/subscription-session.js +1 -4
  89. package/dist/integrations/subscription-session.js.map +1 -1
  90. package/dist/integrations/workos/integration.js.map +1 -1
  91. package/dist/mcp/routes.d.ts.map +1 -1
  92. package/dist/mcp/routes.js +1 -1
  93. package/dist/mcp/routes.js.map +1 -1
  94. package/dist/packages/_internals/workspace/dist/index.js +18 -1
  95. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  96. package/dist/routes/attention-activity.js.map +1 -1
  97. package/dist/routes/attention-parked.js.map +1 -1
  98. package/dist/routes/attention-providers.js.map +1 -1
  99. package/dist/routes/automation-runs.js.map +1 -1
  100. package/dist/routes/config.d.ts +6 -7
  101. package/dist/routes/config.d.ts.map +1 -1
  102. package/dist/routes/config.js +20 -19
  103. package/dist/routes/config.js.map +1 -1
  104. package/dist/routes/contracts.js +1 -1
  105. package/dist/routes/contracts.js.map +1 -1
  106. package/dist/routes/custom-provider-source.js.map +1 -1
  107. package/dist/routes/fs.d.ts +31 -23
  108. package/dist/routes/fs.d.ts.map +1 -1
  109. package/dist/routes/fs.js +218 -64
  110. package/dist/routes/fs.js.map +1 -1
  111. package/dist/routes/intake.js +2 -1
  112. package/dist/routes/intake.js.map +1 -1
  113. package/dist/routes/knowledge.js +1 -1
  114. package/dist/routes/knowledge.js.map +1 -1
  115. package/dist/routes/oauth.js +3 -2
  116. package/dist/routes/oauth.js.map +1 -1
  117. package/dist/routes/projects.d.ts +1 -1
  118. package/dist/routes/projects.d.ts.map +1 -1
  119. package/dist/routes/projects.js +33 -31
  120. package/dist/routes/projects.js.map +1 -1
  121. package/dist/routes/provider-credentials.d.ts.map +1 -1
  122. package/dist/routes/provider-credentials.js +6 -1
  123. package/dist/routes/provider-credentials.js.map +1 -1
  124. package/dist/routes/session-access.d.ts +55 -0
  125. package/dist/routes/session-access.d.ts.map +1 -0
  126. package/dist/routes/session-access.js +165 -0
  127. package/dist/routes/session-access.js.map +1 -0
  128. package/dist/routes/skills.d.ts +2 -4
  129. package/dist/routes/skills.d.ts.map +1 -1
  130. package/dist/routes/skills.js +19 -65
  131. package/dist/routes/skills.js.map +1 -1
  132. package/dist/routes/source-control-sessions.d.ts +6 -1
  133. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  134. package/dist/routes/source-control-sessions.js +95 -12
  135. package/dist/routes/source-control-sessions.js.map +1 -1
  136. package/dist/routes/surface.d.ts +5 -0
  137. package/dist/routes/surface.d.ts.map +1 -1
  138. package/dist/routes/surface.js +10 -4
  139. package/dist/routes/surface.js.map +1 -1
  140. package/dist/routes/tenant-credentials.js.map +1 -1
  141. package/dist/routes/work-items.js.map +1 -1
  142. package/dist/rules/binding-context.js +11 -9
  143. package/dist/rules/binding-context.js.map +1 -1
  144. package/dist/rules/dispatcher.d.ts +1 -1
  145. package/dist/rules/dispatcher.d.ts.map +1 -1
  146. package/dist/rules/dispatcher.js +10 -10
  147. package/dist/rules/dispatcher.js.map +1 -1
  148. package/dist/rules/start-coordinator.d.ts +4 -0
  149. package/dist/rules/start-coordinator.d.ts.map +1 -1
  150. package/dist/rules/start-coordinator.js +36 -4
  151. package/dist/rules/start-coordinator.js.map +1 -1
  152. package/dist/rules/tools.js +2 -2
  153. package/dist/rules/tools.js.map +1 -1
  154. package/dist/rules/transition-service.js.map +1 -1
  155. package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
  156. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  157. package/dist/sandbox/session-retirement.js.map +1 -1
  158. package/dist/secret-encryption.d.ts.map +1 -1
  159. package/dist/secret-encryption.js +65 -25
  160. package/dist/secret-encryption.js.map +1 -1
  161. package/dist/session/channel-reply-processor.d.ts +7 -6
  162. package/dist/session/channel-reply-processor.d.ts.map +1 -1
  163. package/dist/session/channel-reply-processor.js +9 -9
  164. package/dist/session/channel-reply-processor.js.map +1 -1
  165. package/dist/session/channel-session-link-processor.js +1 -0
  166. package/dist/session/channel-session-link-processor.js.map +1 -1
  167. package/dist/session/credential-policy.d.ts.map +1 -1
  168. package/dist/session/credential-policy.js +7 -6
  169. package/dist/session/credential-policy.js.map +1 -1
  170. package/dist/session/default-model-hydration.d.ts +1 -0
  171. package/dist/session/default-model-hydration.d.ts.map +1 -1
  172. package/dist/session/default-model-hydration.js +3 -2
  173. package/dist/session/default-model-hydration.js.map +1 -1
  174. package/dist/session/environment-state-processor.js +1 -1
  175. package/dist/session/environment-state-processor.js.map +1 -1
  176. package/dist/session/filesystem-capture.d.ts +3 -3
  177. package/dist/session/filesystem-capture.d.ts.map +1 -1
  178. package/dist/session/filesystem-capture.js +16 -17
  179. package/dist/session/filesystem-capture.js.map +1 -1
  180. package/dist/session/first-exec-capture.d.ts +1 -1
  181. package/dist/session/first-exec-capture.d.ts.map +1 -1
  182. package/dist/session/first-exec-capture.js +1 -1
  183. package/dist/session/first-exec-capture.js.map +1 -1
  184. package/dist/session/first-message-capture.d.ts +1 -1
  185. package/dist/session/first-message-capture.d.ts.map +1 -1
  186. package/dist/session/first-message-capture.js +1 -1
  187. package/dist/session/first-message-capture.js.map +1 -1
  188. package/dist/session/live-sessions.d.ts +1 -1
  189. package/dist/session/live-sessions.js +1 -1
  190. package/dist/session/live-sessions.js.map +1 -1
  191. package/dist/session/memory-settings-hydration.d.ts +1 -1
  192. package/dist/session/memory-settings-hydration.d.ts.map +1 -1
  193. package/dist/session/memory-settings-hydration.js +2 -1
  194. package/dist/session/memory-settings-hydration.js.map +1 -1
  195. package/dist/session/resource-migration.d.ts +105 -0
  196. package/dist/session/resource-migration.d.ts.map +1 -0
  197. package/dist/session/resource-migration.js +195 -0
  198. package/dist/session/resource-migration.js.map +1 -0
  199. package/dist/session/session-threads.d.ts +58 -0
  200. package/dist/session/session-threads.d.ts.map +1 -0
  201. package/dist/session/session-threads.js +90 -0
  202. package/dist/session/session-threads.js.map +1 -0
  203. package/dist/session/source-control-tools.js +8 -8
  204. package/dist/session/source-control-tools.js.map +1 -1
  205. package/dist/session/thinking-default.d.ts.map +1 -1
  206. package/dist/session/thinking-default.js +3 -4
  207. package/dist/session/thinking-default.js.map +1 -1
  208. package/dist/session/thread-session-stamp.d.ts +21 -0
  209. package/dist/session/thread-session-stamp.d.ts.map +1 -0
  210. package/dist/session/thread-session-stamp.js +20 -0
  211. package/dist/session/thread-session-stamp.js.map +1 -0
  212. package/dist/session/thread-title-mirror.d.ts +1 -1
  213. package/dist/session/thread-title-mirror.d.ts.map +1 -1
  214. package/dist/session/thread-title-mirror.js +1 -1
  215. package/dist/session/thread-title-mirror.js.map +1 -1
  216. package/dist/skills/service.d.ts +6 -8
  217. package/dist/skills/service.d.ts.map +1 -1
  218. package/dist/skills/service.js +3 -3
  219. package/dist/skills/service.js.map +1 -1
  220. package/dist/spa-static.js.map +1 -1
  221. package/dist/state-signing.js +1 -1
  222. package/dist/state-signing.js.map +1 -1
  223. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
  224. package/dist/storage/domains/audit/agent-audit.js +2 -2
  225. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  226. package/dist/storage/domains/audit/base.js.map +1 -1
  227. package/dist/storage/domains/audit/domain.js.map +1 -1
  228. package/dist/storage/domains/channel-identity/base.js.map +1 -1
  229. package/dist/storage/domains/comments/base.js.map +1 -1
  230. package/dist/storage/domains/comments/domain.js.map +1 -1
  231. package/dist/storage/domains/comments/feed-context.js.map +1 -1
  232. package/dist/storage/domains/credentials/base.js.map +1 -1
  233. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  234. package/dist/storage/domains/intake/base.js.map +1 -1
  235. package/dist/storage/domains/integrations/base.d.ts +8 -0
  236. package/dist/storage/domains/integrations/base.d.ts.map +1 -1
  237. package/dist/storage/domains/integrations/base.js +7 -0
  238. package/dist/storage/domains/integrations/base.js.map +1 -1
  239. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  240. package/dist/storage/domains/model-defaults/base.js.map +1 -1
  241. package/dist/storage/domains/projects/base.js.map +1 -1
  242. package/dist/storage/domains/queue-health/base.js.map +1 -1
  243. package/dist/storage/domains/source-control/base.d.ts +19 -0
  244. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  245. package/dist/storage/domains/source-control/base.js +42 -0
  246. package/dist/storage/domains/source-control/base.js.map +1 -1
  247. package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
  248. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  249. package/dist/storage/domains/source-control/inmemory.js +7 -0
  250. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  251. package/dist/storage/domains/work-items/base.d.ts +14 -0
  252. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  253. package/dist/storage/domains/work-items/base.js +25 -1
  254. package/dist/storage/domains/work-items/base.js.map +1 -1
  255. package/dist/storage/domains/work-items/board-version.js.map +1 -1
  256. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  257. package/dist/tools/approval-tools.js +1 -1
  258. package/dist/tools/approval-tools.js.map +1 -1
  259. package/dist/tools/deliver.d.ts +8 -5
  260. package/dist/tools/deliver.d.ts.map +1 -1
  261. package/dist/tools/deliver.js +22 -8
  262. package/dist/tools/deliver.js.map +1 -1
  263. package/dist/tools/messaging-tools.d.ts +1 -1
  264. package/dist/tools/messaging-tools.d.ts.map +1 -1
  265. package/dist/tools/messaging-tools.js +203 -72
  266. package/dist/tools/messaging-tools.js.map +1 -1
  267. package/dist/tools/permissions.d.ts +1 -1
  268. package/dist/tools/permissions.d.ts.map +1 -1
  269. package/dist/tools/permissions.js +1 -1
  270. package/dist/tools/permissions.js.map +1 -1
  271. package/dist/tools/reports.d.ts.map +1 -1
  272. package/dist/tools/reports.js +10 -3
  273. package/dist/tools/reports.js.map +1 -1
  274. package/dist/tools/run-watch.d.ts +2 -0
  275. package/dist/tools/run-watch.d.ts.map +1 -1
  276. package/dist/tools/run-watch.js +42 -2
  277. package/dist/tools/run-watch.js.map +1 -1
  278. package/dist/tools/scope.d.ts +2 -1
  279. package/dist/tools/scope.d.ts.map +1 -1
  280. package/dist/tools/scope.js +3 -2
  281. package/dist/tools/scope.js.map +1 -1
  282. package/dist/tools/session-tools.d.ts +1 -1
  283. package/dist/tools/session-tools.d.ts.map +1 -1
  284. package/dist/tools/session-tools.js +33 -16
  285. package/dist/tools/session-tools.js.map +1 -1
  286. package/dist/tools/sessions-state-processor.js +2 -2
  287. package/dist/tools/sessions-state-processor.js.map +1 -1
  288. package/dist/tools/side-sessions.d.ts +1 -1
  289. package/dist/tools/side-sessions.d.ts.map +1 -1
  290. package/dist/tools/side-sessions.js +1 -1
  291. package/dist/tools/side-sessions.js.map +1 -1
  292. package/dist/workspace.d.ts +5 -2
  293. package/dist/workspace.d.ts.map +1 -1
  294. package/dist/workspace.js +28 -15
  295. package/dist/workspace.js.map +1 -1
  296. package/package.json +13 -13
@@ -1,3 +1,4 @@
1
+ import { externallyAuthoredWorkItem } from "./types.js";
1
2
  import { boardForWorkItem } from "../boards/semantics.js";
2
3
  import "../boards/index.js";
3
4
  import { primeTenantCredentialsForTenant } from "../routes/tenant-credentials.js";
@@ -5,6 +6,29 @@ import { hydrateFactorySession, resolveFactorySessionModel } from "../session/fa
5
6
  import { resolveWorkItemRepository } from "../session/work-item-repository.js";
6
7
  import { RequestContext } from "@mastra/core/request-context";
7
8
  //#region src/rules/start-coordinator.ts
9
+ var UntrustedSharedSessionError = class extends Error {
10
+ sessionId;
11
+ constructor(sessionId) {
12
+ super(`Session ${sessionId} already shares memory with this Factory, so it cannot run untrusted content. Start the run in a new session.`);
13
+ this.sessionId = sessionId;
14
+ this.name = "UntrustedSharedSessionError";
15
+ }
16
+ };
17
+ /**
18
+ * The memory resource a run works on. Trusted runs share the Factory's resource.
19
+ * A run that reads attacker-controllable content (a pull request checkout, or a
20
+ * card written outside the write-access circle) keeps its own resource, so it can
21
+ * neither plant observations that trusted runs read nor read what they observed.
22
+ * A session that was isolated stays isolated; one already sharing memory refuses
23
+ * untrusted work instead of carrying that memory into it.
24
+ */
25
+ async function runMemoryResource(controller, input) {
26
+ const existing = await controller.queryThreadById({ threadId: input.sessionId });
27
+ if (existing?.resourceId === input.sessionId) return input.sessionId;
28
+ if (!input.untrusted) return existing?.resourceId ?? input.factoryProjectId;
29
+ if (existing) throw new UntrustedSharedSessionError(input.sessionId);
30
+ return input.sessionId;
31
+ }
8
32
  var WorkItemRepositoryError = class extends Error {
9
33
  code;
10
34
  candidates;
@@ -135,6 +159,14 @@ var FactoryStartCoordinator = class {
135
159
  const untrustedCheckout = request.workItem.input.externalSource?.type === "pull-request";
136
160
  const metadataBaseBranch = request.workItem.input.metadata?.baseBranch;
137
161
  const baseRef = (sourceSession.baseBranch || void 0) ?? (typeof metadataBaseBranch === "string" && metadataBaseBranch ? metadataBaseBranch : void 0);
162
+ const memoryResourceId = await runMemoryResource(this.#controller, {
163
+ sessionId: sourceSession.sessionId,
164
+ factoryProjectId: request.factoryProjectId,
165
+ untrusted: untrustedCheckout || externallyAuthoredWorkItem(storedItem ?? {
166
+ externalSource: request.workItem.input.externalSource ?? null,
167
+ metadata: request.workItem.input.metadata ?? null
168
+ })
169
+ });
138
170
  const sessionTags = {
139
171
  factoryProjectId: request.factoryProjectId,
140
172
  projectRepositoryId: sourceSession.projectRepositoryId
@@ -142,7 +174,7 @@ var FactoryStartCoordinator = class {
142
174
  const session = await this.#controller.createSession({
143
175
  id: sourceSession.sessionId,
144
176
  ownerId: request.userId,
145
- resourceId: sourceSession.sessionId,
177
+ resourceId: memoryResourceId,
146
178
  threadId: sourceSession.sessionId,
147
179
  requestContext,
148
180
  tags: sessionTags
@@ -190,7 +222,7 @@ var FactoryStartCoordinator = class {
190
222
  branch: sourceSession.branch,
191
223
  threadId
192
224
  },
193
- resourceId: sourceSession.sessionId,
225
+ resourceId: memoryResourceId,
194
226
  kickoffKey: request.kickoffKey,
195
227
  kickoffMessage: null
196
228
  });
@@ -231,7 +263,7 @@ var FactoryStartCoordinator = class {
231
263
  workItemId: prepared.item.id,
232
264
  bindingId: prepared.binding.id,
233
265
  threadId,
234
- resourceId: sourceSession.sessionId,
266
+ resourceId: memoryResourceId,
235
267
  sessionId: sourceSession.sessionId,
236
268
  branch: sourceSession.branch,
237
269
  revision,
@@ -241,6 +273,6 @@ var FactoryStartCoordinator = class {
241
273
  }
242
274
  };
243
275
  //#endregion
244
- export { FactoryStartCoordinator, FactoryStartTransitionError, WorkItemRepositoryError };
276
+ export { FactoryStartCoordinator, FactoryStartTransitionError, UntrustedSharedSessionError, WorkItemRepositoryError };
245
277
 
246
278
  //# sourceMappingURL=start-coordinator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"start-coordinator.js","names":["#controller","#storage","#transitionService","#sourceControl","#intake","#projects","#modelDefaults"],"sources":["../../src/rules/start-coordinator.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { hydrateFactorySession, resolveFactorySessionModel } from '../session/factory-session.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport type { EffectiveModelScope, FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlSession, SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { CreateWorkItemInput, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport type { FactoryRuleStage, FactoryTransitionResult } from './types.js';\n\nexport interface FactoryStartRequest {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n sessionId: string;\n threadTitle: string;\n kickoffKey: string;\n /** Where the arrival path lands the card; a session opened on an existing card names none. */\n destinationStage?: FactoryRuleStage;\n /** The run's effective scope; a hybrid Factory's runs use the Factory's model and credentials. */\n modelScope: EffectiveModelScope;\n defaultModelId?: string;\n workItem: {\n id?: string;\n role: string;\n input: CreateWorkItemInput;\n };\n requestContext?: RequestContext;\n}\n\nexport class WorkItemRepositoryError extends Error {\n constructor(\n readonly code: 'repository_mismatch' | 'repository_ambiguous' | 'repository_unlinked',\n message: string,\n readonly candidates: string[] = [],\n ) {\n super(message);\n this.name = 'WorkItemRepositoryError';\n }\n}\n\nexport class FactoryStartTransitionError extends Error {\n readonly result: Extract<FactoryTransitionResult, { status: 'rejected' }>;\n\n constructor(result: Extract<FactoryTransitionResult, { status: 'rejected' }>) {\n super(result.reason);\n this.name = 'FactoryStartTransitionError';\n this.result = result;\n }\n}\n\nexport interface FactoryStartPreparedResult {\n workItemId: string;\n bindingId: string;\n threadId: string;\n resourceId: string;\n sessionId: string;\n branch: string;\n revision: number;\n kickoffStatus: 'pending' | 'leased' | 'retry' | 'sent' | 'failed';\n replayed: boolean;\n}\n\ntype FactoryController = AgentController<MastraCodeState>;\ntype FactorySession = Awaited<ReturnType<FactoryController['createSession']>>;\n\nasync function resolveSourceSession(\n storage: SourceControlStorageHandle,\n request: FactoryStartRequest,\n): Promise<SourceControlSession & { projectRepositoryId: string }> {\n const session = await storage.sessions.getBySessionId(request.sessionId);\n if (!session || session.orgId !== request.orgId || session.userId !== request.userId) {\n throw new Error('Factory session not found');\n }\n const projectRepositoryId = session.projectRepositoryId;\n if (!projectRepositoryId) throw new Error('Factory session repository not found');\n const projectRepository = await storage.projectRepositories.get({ orgId: request.orgId, id: projectRepositoryId });\n if (!projectRepository) throw new Error('Factory session repository not found');\n const connection = await storage.connections.get({ orgId: request.orgId, id: projectRepository.connectionId });\n if (!connection || connection.factoryProjectId !== request.factoryProjectId) {\n throw new Error('Factory session does not belong to this project');\n }\n return { ...session, projectRepositoryId };\n}\n\nasync function configureThread(session: FactorySession, request: FactoryStartRequest): Promise<string> {\n const threadId = session.thread.requireId();\n // Derived from the work item, not a user rename, so leave the title\n // unpinned and let auto-naming keep refining it.\n await session.thread.rename({ title: request.threadTitle, pin: false });\n await session.thread.setSetting({ key: 'factorySessionId', value: request.sessionId });\n return threadId;\n}\n\nexport class FactoryStartCoordinator {\n readonly #controller: FactoryController;\n readonly #storage: WorkItemsStorage;\n readonly #transitionService?: Pick<FactoryTransitionService, 'transition'>;\n readonly #sourceControl?:\n | SourceControlStorageHandle\n | ((\n request: FactoryStartRequest,\n ) => SourceControlStorageHandle | undefined | Promise<SourceControlStorageHandle | undefined>);\n readonly #intake?: IntakeStorage;\n readonly #projects?: FactoryProjectsStorage;\n readonly #modelDefaults?: ModelDefaultsStorage;\n\n constructor(\n controller: FactoryController,\n storage: WorkItemsStorage,\n transitionService?: Pick<FactoryTransitionService, 'transition'>,\n sourceControl?:\n | SourceControlStorageHandle\n | ((\n request: FactoryStartRequest,\n ) => SourceControlStorageHandle | undefined | Promise<SourceControlStorageHandle | undefined>),\n intake?: IntakeStorage,\n sessionModel?: { projects: FactoryProjectsStorage; modelDefaults: ModelDefaultsStorage },\n ) {\n this.#controller = controller;\n this.#storage = storage;\n this.#transitionService = transitionService;\n this.#sourceControl = sourceControl;\n this.#intake = intake;\n this.#projects = sessionModel?.projects;\n this.#modelDefaults = sessionModel?.modelDefaults;\n }\n\n async prepare(request: FactoryStartRequest): Promise<FactoryStartPreparedResult> {\n const storage = this.#storage;\n const sourceControl =\n typeof this.#sourceControl === 'function' ? await this.#sourceControl(request) : this.#sourceControl;\n if (!sourceControl) throw new Error('Factory source control storage is unavailable');\n const sourceSession = await resolveSourceSession(sourceControl, request);\n const sessionModel =\n this.#projects && this.#modelDefaults\n ? await resolveFactorySessionModel({\n projects: this.#projects,\n modelDefaults: this.#modelDefaults,\n factoryProjectId: request.factoryProjectId,\n orgId: request.orgId,\n ownerUserId: sourceSession.userId,\n context: 'run',\n })\n : { modelScope: request.modelScope, modelId: request.defaultModelId };\n const storedItem = request.workItem.id\n ? await storage.get({ orgId: request.orgId, id: request.workItem.id })\n : null;\n const intakeConfig = await this.#intake?.getConfig({ orgId: request.orgId, integrationIds: ['linear'] });\n let repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n item: storedItem ?? { metadata: request.workItem.input.metadata ?? null },\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n // A retry of this role's existing session keeps its already-chosen\n // repository; a newly minted session must not choose for the card.\n if (storedItem?.sessions[request.workItem.role]?.sessionId !== sourceSession.sessionId) {\n throw new WorkItemRepositoryError(\n 'repository_ambiguous',\n `Choose a repository for this work item: ${repository.candidates.join(', ')}.`,\n repository.candidates,\n );\n }\n const link = await sourceControl.projectRepositories.get({\n orgId: request.orgId,\n id: sourceSession.projectRepositoryId,\n });\n const linked = link && (await sourceControl.repositories.get({ orgId: request.orgId, id: link.repositoryId }));\n const currentLinks = link\n ? await sourceControl.projectRepositories.list({ orgId: request.orgId, connectionId: link.connectionId })\n : [];\n if (\n !linked ||\n !repository.candidates.includes(linked.slug) ||\n !currentLinks.some(candidate => candidate.id === sourceSession.projectRepositoryId)\n ) {\n throw new WorkItemRepositoryError(\n 'repository_unlinked',\n 'The existing session repository is not linked to this Factory.',\n );\n }\n repository = { status: 'resolved', slug: linked.slug, projectRepositoryId: sourceSession.projectRepositoryId };\n }\n if (repository.status === 'unlinked') {\n throw new WorkItemRepositoryError('repository_unlinked', repository.hint);\n }\n if (repository.projectRepositoryId !== sourceSession.projectRepositoryId) {\n throw new WorkItemRepositoryError(\n 'repository_mismatch',\n `This run session is bound to a different repository than ${repository.slug}.`,\n );\n }\n const requestContext = request.requestContext ?? new RequestContext();\n // Preserve an authenticated caller when present. Autonomous starts still\n // need a principal so deployed credential resolution can authenticate the\n // request before selecting the stamped session owner's exclusive scope.\n const existingUser = requestContext.get('user');\n if (!existingUser || typeof existingUser !== 'object') {\n requestContext.set('user', {\n workosId: request.userId,\n organizationId: request.orgId,\n });\n }\n // Sessions kicked off against third-party content (a PR under review, or\n // any pull-request-sourced work item) get `untrustedCheckout` so the SDK\n // never ingests the checkout's AGENTS.md/CLAUDE.md into the system prompt\n // or reminders — those files are attacker-writable in a PR branch.\n const untrustedCheckout = request.workItem.input.externalSource?.type === 'pull-request';\n // The trusted ref the SDK may serve project instruction files from on an\n // untrusted checkout (the PR's base branch). Prefer the session record's\n // base branch; fall back to the intake metadata captured from the PR.\n const metadataBaseBranch = request.workItem.input.metadata?.baseBranch;\n const baseRef =\n (sourceSession.baseBranch || undefined) ??\n (typeof metadataBaseBranch === 'string' && metadataBaseBranch ? metadataBaseBranch : undefined);\n const sessionTags = {\n factoryProjectId: request.factoryProjectId,\n projectRepositoryId: sourceSession.projectRepositoryId,\n };\n const session = await this.#controller.createSession({\n id: sourceSession.sessionId,\n ownerId: request.userId,\n resourceId: sourceSession.sessionId,\n threadId: sourceSession.sessionId,\n requestContext,\n tags: sessionTags,\n });\n // Bound-agent authority gates (the transition tool, the factory-phase\n // processor, workspace token selection) resolve the session address from\n // controller state. Seed it server-side — `tags` covers fresh creation,\n // the explicit setState covers get-or-create returning a session another\n // caller created without them — so autonomous runs never depend on a\n // browser connecting to populate the state. `untrustedCheckout` is a\n // boolean so it rides only on state (tags are string-valued).\n const targetRepositoryInstruction = `Target repository: ${repository.slug}. Before editing files or creating a pull request, verify this session's checkout matches the target repository.`;\n const existingPluginInstructions = session.state.get()?.pluginInstructions ?? [];\n await session.state.set({\n ...sessionTags,\n // The authoritative org id for every downstream identity read (the\n // memory seam's organizationId): the session owner is a USER id, not an\n // org, so it must never be improvised from ownerId.\n factoryOrgId: request.orgId,\n factoryModelScope: sessionModel.modelScope,\n factoryOwnerUserId: sourceSession.userId,\n pluginInstructions: [\n ...existingPluginInstructions.filter(instruction => instruction !== targetRepositoryInstruction),\n targetRepositoryInstruction,\n ],\n ...(untrustedCheckout ? { untrustedCheckout: true, ...(baseRef ? { baseRef } : {}) } : {}),\n });\n await primeTenantCredentialsForTenant({\n orgId: request.orgId,\n userId: sourceSession.userId,\n credentialScope: sessionModel.modelScope,\n });\n // Apply the factory's main model before any model-dependent run work.\n // Observational-memory settings are loaded per invocation by the input processor.\n await hydrateFactorySession(session, {\n orgId: request.orgId,\n modelScope: sessionModel.modelScope,\n ownerUserId: sourceSession.userId,\n defaultModelId: sessionModel.modelId,\n });\n // The tool is `requireApproval`, and that prompt parks the run whether or not\n // someone pressed Start — a person is reading the plan, not an approval queue.\n // Starting the run was already the say-so; the rules engine still governs.\n await session.permissions.setForTool({ toolName: 'factory_transition_work_item', policy: 'allow' });\n const threadId = await configureThread(session, request);\n const prepared = await storage.prepareRunStart({\n orgId: request.orgId,\n userId: request.userId,\n factoryProjectId: request.factoryProjectId,\n workItem: { id: request.workItem.id, input: request.workItem.input },\n role: request.workItem.role,\n session: { sessionId: sourceSession.sessionId, branch: sourceSession.branch, threadId },\n resourceId: sourceSession.sessionId,\n kickoffKey: request.kickoffKey,\n kickoffMessage: null,\n });\n await session.thread.setSetting({ key: 'factoryWorkItemId', value: prepared.item.id });\n\n let revision = prepared.item.revision;\n const destinationStage = request.destinationStage;\n if (destinationStage && (prepared.item.stages.length !== 1 || prepared.item.stages[0] !== destinationStage)) {\n if (!this.#transitionService) throw new Error('Factory transition service is unavailable.');\n const transition = await this.#transitionService.transition({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: prepared.item.id,\n board: boardForWorkItem(prepared.item),\n stage: destinationStage,\n expectedRevision: prepared.item.revision,\n actor: { type: 'human', id: request.userId },\n ingress: { type: 'human', identity: `start:${request.kickoffKey}:transition` },\n cause: 'run_start',\n });\n if (transition.status === 'rejected') {\n await storage.markPendingStart(prepared.binding.id, 'failed', transition.reason);\n throw new FactoryStartTransitionError(transition);\n }\n revision = transition.revision;\n }\n\n await storage.markPendingStart(prepared.binding.id, 'sent');\n prepared.pendingStart.status = 'sent';\n\n return {\n workItemId: prepared.item.id,\n bindingId: prepared.binding.id,\n threadId,\n resourceId: sourceSession.sessionId,\n sessionId: sourceSession.sessionId,\n branch: sourceSession.branch,\n revision,\n kickoffStatus: prepared.pendingStart.status,\n replayed: prepared.replayed,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"start-coordinator.js","names":[],"sources":["../../src/rules/start-coordinator.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { hydrateFactorySession, resolveFactorySessionModel } from '../session/factory-session.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport type { EffectiveModelScope, FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlSession, SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { CreateWorkItemInput, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport { externallyAuthoredWorkItem } from './types.js';\nimport type { FactoryRuleStage, FactoryTransitionResult } from './types.js';\n\nexport interface FactoryStartRequest {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n sessionId: string;\n threadTitle: string;\n kickoffKey: string;\n /** Where the arrival path lands the card; a session opened on an existing card names none. */\n destinationStage?: FactoryRuleStage;\n /** The run's effective scope; a hybrid Factory's runs use the Factory's model and credentials. */\n modelScope: EffectiveModelScope;\n defaultModelId?: string;\n workItem: {\n id?: string;\n role: string;\n input: CreateWorkItemInput;\n };\n requestContext?: RequestContext;\n}\n\nexport class UntrustedSharedSessionError extends Error {\n constructor(readonly sessionId: string) {\n super(\n `Session ${sessionId} already shares memory with this Factory, so it cannot run untrusted content. Start the run in a new session.`,\n );\n this.name = 'UntrustedSharedSessionError';\n }\n}\n\n/**\n * The memory resource a run works on. Trusted runs share the Factory's resource.\n * A run that reads attacker-controllable content (a pull request checkout, or a\n * card written outside the write-access circle) keeps its own resource, so it can\n * neither plant observations that trusted runs read nor read what they observed.\n * A session that was isolated stays isolated; one already sharing memory refuses\n * untrusted work instead of carrying that memory into it.\n */\nasync function runMemoryResource(\n controller: Pick<FactoryController, 'queryThreadById'>,\n input: { sessionId: string; factoryProjectId: string; untrusted: boolean },\n): Promise<string> {\n const existing = await controller.queryThreadById({ threadId: input.sessionId });\n if (existing?.resourceId === input.sessionId) return input.sessionId;\n if (!input.untrusted) return existing?.resourceId ?? input.factoryProjectId;\n if (existing) throw new UntrustedSharedSessionError(input.sessionId);\n return input.sessionId;\n}\n\nexport class WorkItemRepositoryError extends Error {\n constructor(\n readonly code: 'repository_mismatch' | 'repository_ambiguous' | 'repository_unlinked',\n message: string,\n readonly candidates: string[] = [],\n ) {\n super(message);\n this.name = 'WorkItemRepositoryError';\n }\n}\n\nexport class FactoryStartTransitionError extends Error {\n readonly result: Extract<FactoryTransitionResult, { status: 'rejected' }>;\n\n constructor(result: Extract<FactoryTransitionResult, { status: 'rejected' }>) {\n super(result.reason);\n this.name = 'FactoryStartTransitionError';\n this.result = result;\n }\n}\n\nexport interface FactoryStartPreparedResult {\n workItemId: string;\n bindingId: string;\n threadId: string;\n resourceId: string;\n sessionId: string;\n branch: string;\n revision: number;\n kickoffStatus: 'pending' | 'leased' | 'retry' | 'sent' | 'failed';\n replayed: boolean;\n}\n\ntype FactoryController = AgentController<MastraCodeState>;\ntype FactorySession = Awaited<ReturnType<FactoryController['createSession']>>;\n\nasync function resolveSourceSession(\n storage: SourceControlStorageHandle,\n request: FactoryStartRequest,\n): Promise<SourceControlSession & { projectRepositoryId: string }> {\n const session = await storage.sessions.getBySessionId(request.sessionId);\n if (!session || session.orgId !== request.orgId || session.userId !== request.userId) {\n throw new Error('Factory session not found');\n }\n const projectRepositoryId = session.projectRepositoryId;\n if (!projectRepositoryId) throw new Error('Factory session repository not found');\n const projectRepository = await storage.projectRepositories.get({ orgId: request.orgId, id: projectRepositoryId });\n if (!projectRepository) throw new Error('Factory session repository not found');\n const connection = await storage.connections.get({ orgId: request.orgId, id: projectRepository.connectionId });\n if (!connection || connection.factoryProjectId !== request.factoryProjectId) {\n throw new Error('Factory session does not belong to this project');\n }\n return { ...session, projectRepositoryId };\n}\n\nasync function configureThread(session: FactorySession, request: FactoryStartRequest): Promise<string> {\n const threadId = session.thread.requireId();\n // Derived from the work item, not a user rename, so leave the title\n // unpinned and let auto-naming keep refining it.\n await session.thread.rename({ title: request.threadTitle, pin: false });\n await session.thread.setSetting({ key: 'factorySessionId', value: request.sessionId });\n return threadId;\n}\n\nexport class FactoryStartCoordinator {\n readonly #controller: FactoryController;\n readonly #storage: WorkItemsStorage;\n readonly #transitionService?: Pick<FactoryTransitionService, 'transition'>;\n readonly #sourceControl?:\n | SourceControlStorageHandle\n | ((\n request: FactoryStartRequest,\n ) => SourceControlStorageHandle | undefined | Promise<SourceControlStorageHandle | undefined>);\n readonly #intake?: IntakeStorage;\n readonly #projects?: FactoryProjectsStorage;\n readonly #modelDefaults?: ModelDefaultsStorage;\n\n constructor(\n controller: FactoryController,\n storage: WorkItemsStorage,\n transitionService?: Pick<FactoryTransitionService, 'transition'>,\n sourceControl?:\n | SourceControlStorageHandle\n | ((\n request: FactoryStartRequest,\n ) => SourceControlStorageHandle | undefined | Promise<SourceControlStorageHandle | undefined>),\n intake?: IntakeStorage,\n sessionModel?: { projects: FactoryProjectsStorage; modelDefaults: ModelDefaultsStorage },\n ) {\n this.#controller = controller;\n this.#storage = storage;\n this.#transitionService = transitionService;\n this.#sourceControl = sourceControl;\n this.#intake = intake;\n this.#projects = sessionModel?.projects;\n this.#modelDefaults = sessionModel?.modelDefaults;\n }\n\n async prepare(request: FactoryStartRequest): Promise<FactoryStartPreparedResult> {\n const storage = this.#storage;\n const sourceControl =\n typeof this.#sourceControl === 'function' ? await this.#sourceControl(request) : this.#sourceControl;\n if (!sourceControl) throw new Error('Factory source control storage is unavailable');\n const sourceSession = await resolveSourceSession(sourceControl, request);\n const sessionModel =\n this.#projects && this.#modelDefaults\n ? await resolveFactorySessionModel({\n projects: this.#projects,\n modelDefaults: this.#modelDefaults,\n factoryProjectId: request.factoryProjectId,\n orgId: request.orgId,\n ownerUserId: sourceSession.userId,\n context: 'run',\n })\n : { modelScope: request.modelScope, modelId: request.defaultModelId };\n const storedItem = request.workItem.id\n ? await storage.get({ orgId: request.orgId, id: request.workItem.id })\n : null;\n const intakeConfig = await this.#intake?.getConfig({ orgId: request.orgId, integrationIds: ['linear'] });\n let repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n item: storedItem ?? { metadata: request.workItem.input.metadata ?? null },\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n // A retry of this role's existing session keeps its already-chosen\n // repository; a newly minted session must not choose for the card.\n if (storedItem?.sessions[request.workItem.role]?.sessionId !== sourceSession.sessionId) {\n throw new WorkItemRepositoryError(\n 'repository_ambiguous',\n `Choose a repository for this work item: ${repository.candidates.join(', ')}.`,\n repository.candidates,\n );\n }\n const link = await sourceControl.projectRepositories.get({\n orgId: request.orgId,\n id: sourceSession.projectRepositoryId,\n });\n const linked = link && (await sourceControl.repositories.get({ orgId: request.orgId, id: link.repositoryId }));\n const currentLinks = link\n ? await sourceControl.projectRepositories.list({ orgId: request.orgId, connectionId: link.connectionId })\n : [];\n if (\n !linked ||\n !repository.candidates.includes(linked.slug) ||\n !currentLinks.some(candidate => candidate.id === sourceSession.projectRepositoryId)\n ) {\n throw new WorkItemRepositoryError(\n 'repository_unlinked',\n 'The existing session repository is not linked to this Factory.',\n );\n }\n repository = { status: 'resolved', slug: linked.slug, projectRepositoryId: sourceSession.projectRepositoryId };\n }\n if (repository.status === 'unlinked') {\n throw new WorkItemRepositoryError('repository_unlinked', repository.hint);\n }\n if (repository.projectRepositoryId !== sourceSession.projectRepositoryId) {\n throw new WorkItemRepositoryError(\n 'repository_mismatch',\n `This run session is bound to a different repository than ${repository.slug}.`,\n );\n }\n const requestContext = request.requestContext ?? new RequestContext();\n // Preserve an authenticated caller when present. Autonomous starts still\n // need a principal so deployed credential resolution can authenticate the\n // request before selecting the stamped session owner's exclusive scope.\n const existingUser = requestContext.get('user');\n if (!existingUser || typeof existingUser !== 'object') {\n requestContext.set('user', {\n workosId: request.userId,\n organizationId: request.orgId,\n });\n }\n // Sessions kicked off against third-party content (a PR under review, or\n // any pull-request-sourced work item) get `untrustedCheckout` so the SDK\n // never ingests the checkout's AGENTS.md/CLAUDE.md into the system prompt\n // or reminders — those files are attacker-writable in a PR branch.\n const untrustedCheckout = request.workItem.input.externalSource?.type === 'pull-request';\n // The trusted ref the SDK may serve project instruction files from on an\n // untrusted checkout (the PR's base branch). Prefer the session record's\n // base branch; fall back to the intake metadata captured from the PR.\n const metadataBaseBranch = request.workItem.input.metadata?.baseBranch;\n const baseRef =\n (sourceSession.baseBranch || undefined) ??\n (typeof metadataBaseBranch === 'string' && metadataBaseBranch ? metadataBaseBranch : undefined);\n const memoryResourceId = await runMemoryResource(this.#controller, {\n sessionId: sourceSession.sessionId,\n factoryProjectId: request.factoryProjectId,\n untrusted:\n untrustedCheckout ||\n externallyAuthoredWorkItem(\n storedItem ?? {\n externalSource: request.workItem.input.externalSource ?? null,\n metadata: request.workItem.input.metadata ?? null,\n },\n ),\n });\n const sessionTags = {\n factoryProjectId: request.factoryProjectId,\n projectRepositoryId: sourceSession.projectRepositoryId,\n };\n // The run is addressed by its session id; its threads and memory belong to\n // the Factory, so concurrent runs share one memory resource.\n const session = await this.#controller.createSession({\n id: sourceSession.sessionId,\n ownerId: request.userId,\n resourceId: memoryResourceId,\n threadId: sourceSession.sessionId,\n requestContext,\n tags: sessionTags,\n });\n // Bound-agent authority gates (the transition tool, the factory-phase\n // processor, workspace token selection) resolve the session address from\n // controller state. Seed it server-side — `tags` covers fresh creation,\n // the explicit setState covers get-or-create returning a session another\n // caller created without them — so autonomous runs never depend on a\n // browser connecting to populate the state. `untrustedCheckout` is a\n // boolean so it rides only on state (tags are string-valued).\n const targetRepositoryInstruction = `Target repository: ${repository.slug}. Before editing files or creating a pull request, verify this session's checkout matches the target repository.`;\n const existingPluginInstructions = session.state.get()?.pluginInstructions ?? [];\n await session.state.set({\n ...sessionTags,\n // The authoritative org id for every downstream identity read (the\n // memory seam's organizationId): the session owner is a USER id, not an\n // org, so it must never be improvised from ownerId.\n factoryOrgId: request.orgId,\n factoryModelScope: sessionModel.modelScope,\n factoryOwnerUserId: sourceSession.userId,\n pluginInstructions: [\n ...existingPluginInstructions.filter(instruction => instruction !== targetRepositoryInstruction),\n targetRepositoryInstruction,\n ],\n ...(untrustedCheckout ? { untrustedCheckout: true, ...(baseRef ? { baseRef } : {}) } : {}),\n });\n await primeTenantCredentialsForTenant({\n orgId: request.orgId,\n userId: sourceSession.userId,\n credentialScope: sessionModel.modelScope,\n });\n // Apply the factory's main model before any model-dependent run work.\n // Observational-memory settings are loaded per invocation by the input processor.\n await hydrateFactorySession(session, {\n orgId: request.orgId,\n modelScope: sessionModel.modelScope,\n ownerUserId: sourceSession.userId,\n defaultModelId: sessionModel.modelId,\n });\n // The tool is `requireApproval`, and that prompt parks the run whether or not\n // someone pressed Start — a person is reading the plan, not an approval queue.\n // Starting the run was already the say-so; the rules engine still governs.\n await session.permissions.setForTool({ toolName: 'factory_transition_work_item', policy: 'allow' });\n const threadId = await configureThread(session, request);\n const prepared = await storage.prepareRunStart({\n orgId: request.orgId,\n userId: request.userId,\n factoryProjectId: request.factoryProjectId,\n workItem: { id: request.workItem.id, input: request.workItem.input },\n role: request.workItem.role,\n session: { sessionId: sourceSession.sessionId, branch: sourceSession.branch, threadId },\n resourceId: memoryResourceId,\n kickoffKey: request.kickoffKey,\n kickoffMessage: null,\n });\n await session.thread.setSetting({ key: 'factoryWorkItemId', value: prepared.item.id });\n\n let revision = prepared.item.revision;\n const destinationStage = request.destinationStage;\n if (destinationStage && (prepared.item.stages.length !== 1 || prepared.item.stages[0] !== destinationStage)) {\n if (!this.#transitionService) throw new Error('Factory transition service is unavailable.');\n const transition = await this.#transitionService.transition({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: prepared.item.id,\n board: boardForWorkItem(prepared.item),\n stage: destinationStage,\n expectedRevision: prepared.item.revision,\n actor: { type: 'human', id: request.userId },\n ingress: { type: 'human', identity: `start:${request.kickoffKey}:transition` },\n cause: 'run_start',\n });\n if (transition.status === 'rejected') {\n await storage.markPendingStart(prepared.binding.id, 'failed', transition.reason);\n throw new FactoryStartTransitionError(transition);\n }\n revision = transition.revision;\n }\n\n await storage.markPendingStart(prepared.binding.id, 'sent');\n prepared.pendingStart.status = 'sent';\n\n return {\n workItemId: prepared.item.id,\n bindingId: prepared.binding.id,\n threadId,\n resourceId: memoryResourceId,\n sessionId: sourceSession.sessionId,\n branch: sourceSession.branch,\n revision,\n kickoffStatus: prepared.pendingStart.status,\n replayed: prepared.replayed,\n };\n }\n}\n"],"mappings":"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"}
@@ -12,7 +12,7 @@ const MAX_RATIONALE_LENGTH = 1e3;
12
12
  const transitionInputSchema = z.object({
13
13
  stage: z.string().max(128).regex(BOARD_IDENTIFIER_RE),
14
14
  expectedRevision: z.number().int().positive(),
15
- rationale: z.string().trim().min(1).transform((value) => value.length <= MAX_RATIONALE_LENGTH ? value : `${value.slice(0, MAX_RATIONALE_LENGTH - 1)}…`),
15
+ rationale: z.string().trim().min(1).transform((value) => value.length <= MAX_RATIONALE_LENGTH ? value : `${value.slice(0, 999)}…`),
16
16
  triageType: z.enum(FACTORY_TRIAGE_TYPES).optional()
17
17
  }).strict();
18
18
  const triageTransitionInputSchema = transitionInputSchema.extend({ triageType: z.enum(FACTORY_TRIAGE_TYPES) });
@@ -96,7 +96,7 @@ const MAX_SELF_REVIEW_FINDINGS_LENGTH = 2e4;
96
96
  const reviewVerdictInputSchema = z.object({
97
97
  verdict: z.enum(REVIEW_VERDICTS),
98
98
  reviewedHeadSha: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i, "Use the full commit SHA of the reviewed head."),
99
- findings: z.string().trim().min(1).transform((value) => value.length <= MAX_SELF_REVIEW_FINDINGS_LENGTH ? value : `${value.slice(0, MAX_SELF_REVIEW_FINDINGS_LENGTH - 1)}…`).optional().describe("Self-review only: the findings and test report, in Markdown. On request changes the builder receives them; on approve they go in the change request body.")
99
+ findings: z.string().trim().min(1).transform((value) => value.length <= MAX_SELF_REVIEW_FINDINGS_LENGTH ? value : `${value.slice(0, 19999)}…`).optional().describe("Self-review only: the findings and test report, in Markdown. On request changes the builder receives them; on approve they go in the change request body.")
100
100
  });
101
101
  /**
102
102
  * A review pass ends by recording its verdict on the card, which stays in
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","names":[],"sources":["../../src/rules/tools.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem, workItemPhaseSemantics } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { SELF_REVIEW_ROUND_LIMIT } from '../boards/work.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { WorkItemUpdateConflictError } from '../storage/domains/work-items/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactorySessionSourceLookup } from './binding-context.js';\nimport { resolveFactorySessionAddress } from './binding-context.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport { FACTORY_TRIAGE_TYPES } from './types.js';\nimport { BOARD_IDENTIFIER_RE, MAX_BOARD_IDENTIFIER_LENGTH } from './validation.js';\n\nconst MAX_RATIONALE_LENGTH = 1_000;\n\nconst transitionInputSchema = z\n .object({\n stage: z.string().max(MAX_BOARD_IDENTIFIER_LENGTH).regex(BOARD_IDENTIFIER_RE),\n expectedRevision: z.number().int().positive(),\n // Providers strip maxLength from the JSON schema and models can't count characters, so a\n // hard cap invites overshoot-retry loops at the end of every run. Accept and clamp instead.\n rationale: z\n .string()\n .trim()\n .min(1)\n .transform(value =>\n value.length <= MAX_RATIONALE_LENGTH ? value : `${value.slice(0, MAX_RATIONALE_LENGTH - 1)}…`,\n ),\n // Sessions are shared across role rotations, so a non-triage agent sees the triage\n // agent's earlier call (with triageType) in its history and copies the shape. Accept\n // the key here and drop it in execute rather than fail the whole call on it.\n triageType: z.enum(FACTORY_TRIAGE_TYPES).optional(),\n })\n .strict();\n\nconst triageTransitionInputSchema = transitionInputSchema.extend({\n triageType: z.enum(FACTORY_TRIAGE_TYPES),\n});\n\nexport async function createFactoryTransitionTools(options: {\n requestContext: RequestContext;\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n sessions?: FactorySessionSourceLookup;\n boards?: BoardRegistry;\n}): Promise<IntegrationTools> {\n const resolution = await resolveFactorySessionAddress({\n requestContext: options.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n if (!resolution) return {};\n const availableBinding = resolution.binding ?? (await options.storage.findActiveRunBinding(resolution.address));\n if (!availableBinding) {\n // No live binding for this session. A resumed session whose binding was\n // revoked when its work item reached a terminal stage was retired out from\n // under the model: rather than silently drop the tool — which leaves the\n // resumed run to rationalize a false blocked state — surface a tool that\n // fails with the concrete reason. Threads that were never bound (or whose\n // binding was rotated to a peer role, leaving the item still in flight) get\n // no tool as before.\n const retired = await options.storage.findRunBindingBySession(resolution.address);\n if (retired && retired.status === 'revoked' && options.boards) {\n const item = await options.storage.get({ orgId: retired.orgId, id: retired.workItemId });\n if (item && workItemPhaseSemantics(options.boards, item)?.kind === 'terminal') {\n return { factory_transition_work_item: createRetiredSessionTool(retired.workItemId, item.stages[0]) };\n }\n }\n return {};\n }\n let isTriage = false;\n if (availableBinding.role === 'triage') {\n const item = await options.storage.get({ orgId: availableBinding.orgId, id: availableBinding.workItemId });\n if (!item) return {};\n isTriage = boardForWorkItem(item) === 'work';\n }\n\n return {\n ...(availableBinding.role === 'review' ? createReviewVerdictTool(options, availableBinding.workItemId) : {}),\n factory_transition_work_item: createTool({\n id: 'factory_transition_work_item',\n description: isTriage\n ? 'Report the triage classification and request a governed stage transition for the Factory work item exactly bound to this thread. Only bugs may request Planning autonomously; closure outcomes may request a terminal stage. Feature requests and other non-bug classifications that remain open must stay in their current Intake or Triage stage for maintainer approval.'\n : 'Request a governed stage transition for the Factory work item exactly bound to this thread. Use the current revision from the factory-phase signal and explain why the transition is appropriate.',\n inputSchema: isTriage ? triageTransitionInputSchema : transitionInputSchema,\n requireApproval: true,\n execute: async ({ stage, expectedRevision, rationale, triageType: requestedTriageType }, execution) => {\n const currentResolution = await resolveFactorySessionAddress({\n requestContext: execution.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n const currentAddress = currentResolution?.address ?? null;\n const toolCallId = execution.agent?.toolCallId;\n if (!currentAddress || !toolCallId) {\n throw new Error('Factory transitions require an authenticated bound agent tool call.');\n }\n const binding = await options.storage.findActiveRunBinding(currentAddress);\n // Authority is the work item this session is bound to, not the individual\n // binding row. Handing the next role its turn in an existing session\n // rotates the binding, and tools built for the previous role stay live\n // across that rotation; keying on row identity would strand the run that\n // the rotation exists to start. Re-pointing a session at a different item\n // is the hijack this guards against.\n if (!binding || binding.workItemId !== availableBinding.workItemId) {\n throw new Error('Factory agent binding is unavailable, revoked, or no longer matches this session.');\n }\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) throw new Error('Bound Factory work item not found.');\n const board = boardForWorkItem(item);\n // Only a Work triage binding may classify; other roles can echo the key from history.\n const triageType = board === 'work' && binding.role === 'triage' ? requestedTriageType : undefined;\n\n const result = await options.transitionService.transition({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n board,\n stage,\n expectedRevision,\n actor: { type: 'agent', bindingId: binding.id, role: binding.role },\n ingress: { type: 'agent', identity: `${binding.id}:${toolCallId}` },\n cause: rationale,\n ...(triageType ? { triageType } : {}),\n });\n\n return result;\n },\n }),\n };\n}\n\nexport const REVIEW_VERDICTS = ['approve', 'request changes'] as const;\n\nconst MAX_SELF_REVIEW_FINDINGS_LENGTH = 20_000;\n\nconst reviewVerdictInputSchema = z.object({\n verdict: z.enum(REVIEW_VERDICTS),\n reviewedHeadSha: z\n .string()\n .regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i, 'Use the full commit SHA of the reviewed head.'),\n findings: z\n .string()\n .trim()\n .min(1)\n .transform(value =>\n value.length <= MAX_SELF_REVIEW_FINDINGS_LENGTH\n ? value\n : `${value.slice(0, MAX_SELF_REVIEW_FINDINGS_LENGTH - 1)}…`,\n )\n .optional()\n .describe(\n 'Self-review only: the findings and test report, in Markdown. On request changes the builder receives them; on approve they go in the change request body.',\n ),\n});\n\n/**\n * A review pass ends by recording its verdict on the card, which stays in\n * Reviewing: Done is reserved for the merge, and a verdict is not a lane. The\n * next push re-reviews from the recorded verdict.\n */\nfunction createReviewVerdictTool(\n options: {\n requestContext: RequestContext;\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n sessions?: FactorySessionSourceLookup;\n },\n boundWorkItemId: string,\n): IntegrationTools {\n return {\n factory_record_review_verdict: createTool({\n id: 'factory_record_review_verdict',\n description:\n 'Record the review verdict and the head SHA it covers on the card bound to this thread, ending the review pass. On a Review card it records the published PR verdict and the card stays in Reviewing. On a Work card in Review with no change request open yet (self-review), approve keeps the card in Review and wakes the builder to open the change request; request changes sends it back to Building with your findings.',\n inputSchema: reviewVerdictInputSchema,\n requireApproval: false,\n execute: async ({ verdict, reviewedHeadSha, findings }, execution) => {\n const resolution = await resolveFactorySessionAddress({\n requestContext: execution.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n const binding = resolution ? await options.storage.findActiveRunBinding(resolution.address) : null;\n if (!binding || binding.workItemId !== boundWorkItemId || binding.role !== 'review') {\n throw new Error('Factory review binding is unavailable, revoked, or no longer matches this session.');\n }\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) throw new Error('Bound Factory work item not found.');\n if (\n boardForWorkItem(item) === 'work' &&\n item.stages[0] === 'review' &&\n typeof item.metadata?.openPullRequestNumber !== 'number'\n ) {\n return recordSelfReviewVerdict({\n storage: options.storage,\n transitionService: options.transitionService,\n binding,\n item,\n verdict,\n reviewedHeadSha: reviewedHeadSha.toLowerCase(),\n findings,\n toolCallId: execution.agent?.toolCallId,\n });\n }\n if (boardForWorkItem(item) !== 'review' || item.stages[0] !== 'review') {\n throw new Error(`Only a card in Reviewing records a verdict; this one is in ${item.stages.join(', ')}.`);\n }\n const reviewedAt = new Date().toISOString();\n // Revision-checked, so a merge that moves the card between the stage\n // check and this write fails the call instead of stamping a Done card.\n try {\n await options.storage.update({\n orgId: binding.orgId,\n id: item.id,\n userId: `agent:${binding.id}`,\n expectedRevision: item.revision,\n patch: {\n metadata: {\n ...(item.metadata ?? {}),\n reviewVerdict: verdict,\n reviewedHeadSha: reviewedHeadSha.toLowerCase(),\n reviewedAt,\n },\n },\n });\n } catch (error) {\n if (!(error instanceof WorkItemUpdateConflictError)) throw error;\n throw new Error(\n 'The Review card changed while recording the verdict; call again to record it on the current card.',\n );\n }\n // Mirror the verdict onto the Work item that authored the PR, so its\n // builder cannot close the work while changes are still requested.\n const parent = item.parentWorkItemId\n ? await options.storage.get({ orgId: binding.orgId, id: item.parentWorkItemId })\n : null;\n if (parent && boardForWorkItem(parent) === 'work') {\n await options.storage.update({\n orgId: binding.orgId,\n id: parent.id,\n userId: `agent:${binding.id}`,\n patch: {\n metadata: { reviewVerdict: verdict, reviewedHeadSha: reviewedHeadSha.toLowerCase(), reviewedAt },\n },\n });\n }\n return { status: 'recorded', verdict, reviewedHeadSha, reviewedAt };\n },\n }),\n };\n}\n\n/**\n * Self-review ends the same way a PR review does, but on the Work card before\n * any change request exists, and it moves the card itself: the verdict is the\n * only thing that decides where the work goes next.\n */\nasync function recordSelfReviewVerdict(input: {\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n binding: { id: string; orgId: string; factoryProjectId: string; workItemId: string; role: string };\n item: WorkItemRow;\n verdict: (typeof REVIEW_VERDICTS)[number];\n reviewedHeadSha: string;\n findings: string | undefined;\n toolCallId: string | undefined;\n}) {\n const { storage, binding, item, verdict, reviewedHeadSha, findings } = input;\n if (!input.toolCallId) throw new Error('Self-review verdicts require an authenticated bound agent tool call.');\n const reviewedAt = new Date().toISOString();\n const previousRounds = typeof item.metadata?.selfReviewRounds === 'number' ? item.metadata.selfReviewRounds : 0;\n const rounds = verdict === 'request changes' ? previousRounds + 1 : previousRounds;\n // Every SELF_REVIEW_ROUND_LIMIT rounds the card rests in Intake for a person instead of looping;\n // moving it back to Building grants another full set of rounds.\n const parked = verdict === 'request changes' && rounds % SELF_REVIEW_ROUND_LIMIT === 0;\n let updated: Awaited<ReturnType<WorkItemsStorage['update']>>;\n try {\n updated = await storage.update({\n orgId: binding.orgId,\n id: item.id,\n userId: `agent:${binding.id}`,\n expectedRevision: item.revision,\n patch: {\n metadata: {\n selfReviewVerdict: verdict,\n selfReviewedHeadSha: reviewedHeadSha,\n selfReviewedAt: reviewedAt,\n selfReviewFindings: findings ?? null,\n selfReviewRounds: rounds,\n selfReviewNeedsHuman: parked,\n },\n },\n });\n } catch (error) {\n if (!(error instanceof WorkItemUpdateConflictError)) throw error;\n throw new Error('The Work card changed while recording the verdict; call again to record it on the current card.');\n }\n if (!updated) throw new Error('Bound Factory work item not found.');\n if (verdict === 'approve')\n return { status: 'recorded', verdict, reviewedHeadSha, reviewedAt, selfReviewRounds: rounds };\n const transition = await input.transitionService.transition({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n board: 'work',\n stage: parked ? 'intake' : 'execute',\n expectedRevision: updated.item.revision,\n actor: { type: 'agent', bindingId: binding.id, role: binding.role },\n ingress: { type: 'agent', identity: `${binding.id}:${input.toolCallId}` },\n cause: parked\n ? `Self-review requested changes ${rounds} times; parked for a person to decide how to proceed.`\n : `Self-review requested changes on ${reviewedHeadSha} (round ${rounds}).`,\n });\n return { status: 'recorded', verdict, reviewedHeadSha, reviewedAt, selfReviewRounds: rounds, transition };\n}\n\n/**\n * A retired session's transition tool: it exists only to fail loudly. The run's\n * binding was revoked when its work item settled at a terminal stage, so there\n * is nothing to transition. Registering it (instead of dropping the tool) stops\n * a resumed model from inventing a false \"blocked\" state when the affordance it\n * expects has silently vanished.\n */\nfunction createRetiredSessionTool(workItemId: string, stage: string | undefined): IntegrationTools[string] {\n return createTool({\n id: 'factory_transition_work_item',\n description:\n 'This Factory session has been retired: its work item already reached a terminal stage and the run binding was revoked. No further transitions can be requested from this session.',\n inputSchema: transitionInputSchema,\n requireApproval: false,\n execute: async () => {\n throw new Error(\n `This Factory session has been retired: work item ${workItemId} reached a terminal stage${\n stage ? ` (${stage})` : ''\n } and its run binding was revoked. No transition can be requested from this session.`,\n );\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tools.js","names":[],"sources":["../../src/rules/tools.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem, workItemPhaseSemantics } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { SELF_REVIEW_ROUND_LIMIT } from '../boards/work.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { WorkItemUpdateConflictError } from '../storage/domains/work-items/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactorySessionSourceLookup } from './binding-context.js';\nimport { resolveFactorySessionAddress } from './binding-context.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport { FACTORY_TRIAGE_TYPES } from './types.js';\nimport { BOARD_IDENTIFIER_RE, MAX_BOARD_IDENTIFIER_LENGTH } from './validation.js';\n\nconst MAX_RATIONALE_LENGTH = 1_000;\n\nconst transitionInputSchema = z\n .object({\n stage: z.string().max(MAX_BOARD_IDENTIFIER_LENGTH).regex(BOARD_IDENTIFIER_RE),\n expectedRevision: z.number().int().positive(),\n // Providers strip maxLength from the JSON schema and models can't count characters, so a\n // hard cap invites overshoot-retry loops at the end of every run. Accept and clamp instead.\n rationale: z\n .string()\n .trim()\n .min(1)\n .transform(value =>\n value.length <= MAX_RATIONALE_LENGTH ? value : `${value.slice(0, MAX_RATIONALE_LENGTH - 1)}…`,\n ),\n // Sessions are shared across role rotations, so a non-triage agent sees the triage\n // agent's earlier call (with triageType) in its history and copies the shape. Accept\n // the key here and drop it in execute rather than fail the whole call on it.\n triageType: z.enum(FACTORY_TRIAGE_TYPES).optional(),\n })\n .strict();\n\nconst triageTransitionInputSchema = transitionInputSchema.extend({\n triageType: z.enum(FACTORY_TRIAGE_TYPES),\n});\n\nexport async function createFactoryTransitionTools(options: {\n requestContext: RequestContext;\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n sessions?: FactorySessionSourceLookup;\n boards?: BoardRegistry;\n}): Promise<IntegrationTools> {\n const resolution = await resolveFactorySessionAddress({\n requestContext: options.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n if (!resolution) return {};\n const availableBinding = resolution.binding ?? (await options.storage.findActiveRunBinding(resolution.address));\n if (!availableBinding) {\n // No live binding for this session. A resumed session whose binding was\n // revoked when its work item reached a terminal stage was retired out from\n // under the model: rather than silently drop the tool — which leaves the\n // resumed run to rationalize a false blocked state — surface a tool that\n // fails with the concrete reason. Threads that were never bound (or whose\n // binding was rotated to a peer role, leaving the item still in flight) get\n // no tool as before.\n const retired = await options.storage.findRunBindingBySession(resolution.address);\n if (retired && retired.status === 'revoked' && options.boards) {\n const item = await options.storage.get({ orgId: retired.orgId, id: retired.workItemId });\n if (item && workItemPhaseSemantics(options.boards, item)?.kind === 'terminal') {\n return { factory_transition_work_item: createRetiredSessionTool(retired.workItemId, item.stages[0]) };\n }\n }\n return {};\n }\n let isTriage = false;\n if (availableBinding.role === 'triage') {\n const item = await options.storage.get({ orgId: availableBinding.orgId, id: availableBinding.workItemId });\n if (!item) return {};\n isTriage = boardForWorkItem(item) === 'work';\n }\n\n return {\n ...(availableBinding.role === 'review' ? createReviewVerdictTool(options, availableBinding.workItemId) : {}),\n factory_transition_work_item: createTool({\n id: 'factory_transition_work_item',\n description: isTriage\n ? 'Report the triage classification and request a governed stage transition for the Factory work item exactly bound to this thread. Only bugs may request Planning autonomously; closure outcomes may request a terminal stage. Feature requests and other non-bug classifications that remain open must stay in their current Intake or Triage stage for maintainer approval.'\n : 'Request a governed stage transition for the Factory work item exactly bound to this thread. Use the current revision from the factory-phase signal and explain why the transition is appropriate.',\n inputSchema: isTriage ? triageTransitionInputSchema : transitionInputSchema,\n requireApproval: true,\n execute: async ({ stage, expectedRevision, rationale, triageType: requestedTriageType }, execution) => {\n const currentResolution = await resolveFactorySessionAddress({\n requestContext: execution.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n const currentAddress = currentResolution?.address ?? null;\n const toolCallId = execution.agent?.toolCallId;\n if (!currentAddress || !toolCallId) {\n throw new Error('Factory transitions require an authenticated bound agent tool call.');\n }\n const binding = await options.storage.findActiveRunBinding(currentAddress);\n // Authority is the work item this session is bound to, not the individual\n // binding row. Handing the next role its turn in an existing session\n // rotates the binding, and tools built for the previous role stay live\n // across that rotation; keying on row identity would strand the run that\n // the rotation exists to start. Re-pointing a session at a different item\n // is the hijack this guards against.\n if (!binding || binding.workItemId !== availableBinding.workItemId) {\n throw new Error('Factory agent binding is unavailable, revoked, or no longer matches this session.');\n }\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) throw new Error('Bound Factory work item not found.');\n const board = boardForWorkItem(item);\n // Only a Work triage binding may classify; other roles can echo the key from history.\n const triageType = board === 'work' && binding.role === 'triage' ? requestedTriageType : undefined;\n\n const result = await options.transitionService.transition({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n board,\n stage,\n expectedRevision,\n actor: { type: 'agent', bindingId: binding.id, role: binding.role },\n ingress: { type: 'agent', identity: `${binding.id}:${toolCallId}` },\n cause: rationale,\n ...(triageType ? { triageType } : {}),\n });\n\n return result;\n },\n }),\n };\n}\n\nexport const REVIEW_VERDICTS = ['approve', 'request changes'] as const;\n\nconst MAX_SELF_REVIEW_FINDINGS_LENGTH = 20_000;\n\nconst reviewVerdictInputSchema = z.object({\n verdict: z.enum(REVIEW_VERDICTS),\n reviewedHeadSha: z\n .string()\n .regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i, 'Use the full commit SHA of the reviewed head.'),\n findings: z\n .string()\n .trim()\n .min(1)\n .transform(value =>\n value.length <= MAX_SELF_REVIEW_FINDINGS_LENGTH\n ? value\n : `${value.slice(0, MAX_SELF_REVIEW_FINDINGS_LENGTH - 1)}…`,\n )\n .optional()\n .describe(\n 'Self-review only: the findings and test report, in Markdown. On request changes the builder receives them; on approve they go in the change request body.',\n ),\n});\n\n/**\n * A review pass ends by recording its verdict on the card, which stays in\n * Reviewing: Done is reserved for the merge, and a verdict is not a lane. The\n * next push re-reviews from the recorded verdict.\n */\nfunction createReviewVerdictTool(\n options: {\n requestContext: RequestContext;\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n sessions?: FactorySessionSourceLookup;\n },\n boundWorkItemId: string,\n): IntegrationTools {\n return {\n factory_record_review_verdict: createTool({\n id: 'factory_record_review_verdict',\n description:\n 'Record the review verdict and the head SHA it covers on the card bound to this thread, ending the review pass. On a Review card it records the published PR verdict and the card stays in Reviewing. On a Work card in Review with no change request open yet (self-review), approve keeps the card in Review and wakes the builder to open the change request; request changes sends it back to Building with your findings.',\n inputSchema: reviewVerdictInputSchema,\n requireApproval: false,\n execute: async ({ verdict, reviewedHeadSha, findings }, execution) => {\n const resolution = await resolveFactorySessionAddress({\n requestContext: execution.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n const binding = resolution ? await options.storage.findActiveRunBinding(resolution.address) : null;\n if (!binding || binding.workItemId !== boundWorkItemId || binding.role !== 'review') {\n throw new Error('Factory review binding is unavailable, revoked, or no longer matches this session.');\n }\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) throw new Error('Bound Factory work item not found.');\n if (\n boardForWorkItem(item) === 'work' &&\n item.stages[0] === 'review' &&\n typeof item.metadata?.openPullRequestNumber !== 'number'\n ) {\n return recordSelfReviewVerdict({\n storage: options.storage,\n transitionService: options.transitionService,\n binding,\n item,\n verdict,\n reviewedHeadSha: reviewedHeadSha.toLowerCase(),\n findings,\n toolCallId: execution.agent?.toolCallId,\n });\n }\n if (boardForWorkItem(item) !== 'review' || item.stages[0] !== 'review') {\n throw new Error(`Only a card in Reviewing records a verdict; this one is in ${item.stages.join(', ')}.`);\n }\n const reviewedAt = new Date().toISOString();\n // Revision-checked, so a merge that moves the card between the stage\n // check and this write fails the call instead of stamping a Done card.\n try {\n await options.storage.update({\n orgId: binding.orgId,\n id: item.id,\n userId: `agent:${binding.id}`,\n expectedRevision: item.revision,\n patch: {\n metadata: {\n ...(item.metadata ?? {}),\n reviewVerdict: verdict,\n reviewedHeadSha: reviewedHeadSha.toLowerCase(),\n reviewedAt,\n },\n },\n });\n } catch (error) {\n if (!(error instanceof WorkItemUpdateConflictError)) throw error;\n throw new Error(\n 'The Review card changed while recording the verdict; call again to record it on the current card.',\n );\n }\n // Mirror the verdict onto the Work item that authored the PR, so its\n // builder cannot close the work while changes are still requested.\n const parent = item.parentWorkItemId\n ? await options.storage.get({ orgId: binding.orgId, id: item.parentWorkItemId })\n : null;\n if (parent && boardForWorkItem(parent) === 'work') {\n await options.storage.update({\n orgId: binding.orgId,\n id: parent.id,\n userId: `agent:${binding.id}`,\n patch: {\n metadata: { reviewVerdict: verdict, reviewedHeadSha: reviewedHeadSha.toLowerCase(), reviewedAt },\n },\n });\n }\n return { status: 'recorded', verdict, reviewedHeadSha, reviewedAt };\n },\n }),\n };\n}\n\n/**\n * Self-review ends the same way a PR review does, but on the Work card before\n * any change request exists, and it moves the card itself: the verdict is the\n * only thing that decides where the work goes next.\n */\nasync function recordSelfReviewVerdict(input: {\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n binding: { id: string; orgId: string; factoryProjectId: string; workItemId: string; role: string };\n item: WorkItemRow;\n verdict: (typeof REVIEW_VERDICTS)[number];\n reviewedHeadSha: string;\n findings: string | undefined;\n toolCallId: string | undefined;\n}) {\n const { storage, binding, item, verdict, reviewedHeadSha, findings } = input;\n if (!input.toolCallId) throw new Error('Self-review verdicts require an authenticated bound agent tool call.');\n const reviewedAt = new Date().toISOString();\n const previousRounds = typeof item.metadata?.selfReviewRounds === 'number' ? item.metadata.selfReviewRounds : 0;\n const rounds = verdict === 'request changes' ? previousRounds + 1 : previousRounds;\n // Every SELF_REVIEW_ROUND_LIMIT rounds the card rests in Intake for a person instead of looping;\n // moving it back to Building grants another full set of rounds.\n const parked = verdict === 'request changes' && rounds % SELF_REVIEW_ROUND_LIMIT === 0;\n let updated: Awaited<ReturnType<WorkItemsStorage['update']>>;\n try {\n updated = await storage.update({\n orgId: binding.orgId,\n id: item.id,\n userId: `agent:${binding.id}`,\n expectedRevision: item.revision,\n patch: {\n metadata: {\n selfReviewVerdict: verdict,\n selfReviewedHeadSha: reviewedHeadSha,\n selfReviewedAt: reviewedAt,\n selfReviewFindings: findings ?? null,\n selfReviewRounds: rounds,\n selfReviewNeedsHuman: parked,\n },\n },\n });\n } catch (error) {\n if (!(error instanceof WorkItemUpdateConflictError)) throw error;\n throw new Error('The Work card changed while recording the verdict; call again to record it on the current card.');\n }\n if (!updated) throw new Error('Bound Factory work item not found.');\n if (verdict === 'approve')\n return { status: 'recorded', verdict, reviewedHeadSha, reviewedAt, selfReviewRounds: rounds };\n const transition = await input.transitionService.transition({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n board: 'work',\n stage: parked ? 'intake' : 'execute',\n expectedRevision: updated.item.revision,\n actor: { type: 'agent', bindingId: binding.id, role: binding.role },\n ingress: { type: 'agent', identity: `${binding.id}:${input.toolCallId}` },\n cause: parked\n ? `Self-review requested changes ${rounds} times; parked for a person to decide how to proceed.`\n : `Self-review requested changes on ${reviewedHeadSha} (round ${rounds}).`,\n });\n return { status: 'recorded', verdict, reviewedHeadSha, reviewedAt, selfReviewRounds: rounds, transition };\n}\n\n/**\n * A retired session's transition tool: it exists only to fail loudly. The run's\n * binding was revoked when its work item settled at a terminal stage, so there\n * is nothing to transition. Registering it (instead of dropping the tool) stops\n * a resumed model from inventing a false \"blocked\" state when the affordance it\n * expects has silently vanished.\n */\nfunction createRetiredSessionTool(workItemId: string, stage: string | undefined): IntegrationTools[string] {\n return createTool({\n id: 'factory_transition_work_item',\n description:\n 'This Factory session has been retired: its work item already reached a terminal stage and the run binding was revoked. No further transitions can be requested from this session.',\n inputSchema: transitionInputSchema,\n requireApproval: false,\n execute: async () => {\n throw new Error(\n `This Factory session has been retired: work item ${workItemId} reached a terminal stage${\n stage ? ` (${stage})` : ''\n } and its run binding was revoked. No transition can be requested from this session.`,\n );\n },\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"transition-service.js","names":["#configVersion","#boards","#storage","#timeoutMs","#onTerminalStage","#terminalCleanupTimeoutMs","#onAccepted","#onTransitionCommitted","#autoApprovePlans","#selfReviewEnabled","#audit","#commitRejection","#recordTransition","#evaluateAndCommit","#notifyCommitted","#commit"],"sources":["../../src/rules/transition-service.ts"],"sourcesContent":["import { createBoardRegistry } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { boardTransitionPolicyResultSchema, immutablePolicySnapshot } from '../boards/transition-policy.js';\nimport type { AuditActorProfileInput, AuditActorType, AuditContext } from '../storage/domains/audit/base.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport { isAgentActor } from '../storage/domains/work-items/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { resolveFactoryStageRules } from './resolve.js';\nimport type {\n FactoryCommitDecision,\n FactoryRuleActor,\n FactoryRuleBoard,\n FactoryRuleCausalEntry,\n FactoryRuleRejectionCode,\n FactoryRuleStage,\n FactoryTriageType,\n FactoryStageRuleContext,\n FactoryTransitionResult,\n} from './types.js';\nimport {\n externallyAuthoredWorkItem,\n factoryRuleSourceForWorkItem,\n isFactoryRuleStage,\n workItemSource,\n} from './types.js';\nimport {\n MAX_FACTORY_RULE_CAUSAL_DEPTH,\n assertFactoryDecisionTarget,\n validateFactoryRuleDecision,\n validateFactoryRuleDecisions,\n} from './validation.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\nconst MAX_REJECTION_REASON = 512;\n/** Longest a committed transition waits for terminal resource cleanup. Cleanup\n * reattaches remote sandboxes, so a hung provider call must not leave the\n * already-committed transition request pending; past this bound the cleanup\n * keeps running in the background as pure best-effort. */\nconst TERMINAL_CLEANUP_TIMEOUT_MS = 30_000;\n\nexport interface FactoryTransitionRequest {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n /** Installed board id; the service rejects boards that are not installed. */\n board: string;\n stage: FactoryRuleStage;\n expectedRevision: number;\n actor: FactoryRuleActor;\n actorProfile?: AuditActorProfileInput;\n /** Where a browser request came from; rules and agents carry none. */\n context?: AuditContext;\n ingress: { type: 'human' | 'agent' | 'toolResult' | 'github' | 'rule'; identity: string; transitionId?: string };\n cause: string;\n causalChain?: readonly FactoryRuleCausalEntry[];\n /** Internal materialization path: evaluate only the destination onEnter leaf even when already at that stage. */\n initialEntry?: boolean;\n /** Re-runs the stage's entry rules when the item already holds that stage, to restart work the entry invalidated. */\n reenter?: boolean;\n /** Structured verdict required from a bound triage-agent terminal request. */\n triageType?: FactoryTriageType;\n /** Internal proof that this move consumes an approved `submit_plan` result. */\n planApproved?: true;\n}\n\nexport interface FactoryTransitionServiceOptions {\n configVersion: string;\n storage: WorkItemsStorage;\n boards?: BoardRegistry;\n /** Every commit, accepted or rejected, lands here as `stage_moved` / `transition_rejected` under the request's actor. */\n audit?: AuditRecorder;\n timeoutMs?: number;\n /**\n * Called after a transition commits into a phase the board declares\n * terminal — the point where the item's sessions stop receiving runs, so\n * resources they hold (e.g. sandboxes) can be released for reuse. Awaited,\n * but failures are swallowed: releasing resources must never break or roll\n * back the committed transition.\n */\n onTerminalStage?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n stage: FactoryRuleStage;\n revision: number;\n /** The actor that committed this terminal transition. Lets cleanup leave\n * the seat that drove its own transition (an agent tool call) untouched. */\n actor: FactoryRuleActor;\n }) => Promise<void> | void;\n /**\n * Called after a transition commits a stage change, e.g. to tell the item's\n * followers. Not awaited, and failures are logged: a notification must never\n * fail or delay the committed transition.\n */\n onTransitionCommitted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n }) => Promise<void> | void;\n /** Upper bound on how long a committed transition waits for\n * `onTerminalStage` before returning (default 30s). The cleanup continues\n * in the background past the bound. */\n terminalCleanupTimeoutMs?: number;\n /**\n * Called after a transition first records a person's acceptance of a\n * non-bug item (see `WorkItemRow.acceptedAt`). Fire-and-forget: failures\n * are swallowed, the committed transition never depends on it.\n */\n onAccepted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n item: WorkItemRow;\n }) => Promise<void> | void;\n /**\n * Resolves whether a project auto-approves produced plans. Unset means off:\n * a plan without explicit approval is a plan a person must review.\n */\n autoApprovePlans?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n /** Resolves whether a project self-reviews Work items before the change request opens. Unset means off. */\n selfReviewEnabled?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n}\n\nfunction rejection(\n transitionId: string,\n itemId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n): FactoryTransitionResult {\n return { status: 'rejected', transitionId, itemId, code, reason: reason.slice(0, MAX_REJECTION_REASON) };\n}\n\nfunction actorId(actor: FactoryRuleActor): string {\n switch (actor.type) {\n case 'human':\n case 'system':\n return actor.id;\n case 'agent':\n return `agent:${actor.bindingId}`;\n case 'github':\n return `github:${actor.login}`;\n case 'gitlab':\n return `gitlab:${actor.username}`;\n }\n}\n\n// The dispatcher executes an agent-approved decision as a human actor so its consent carries; the trail still names the agent.\nexport function auditActorOf(actor: FactoryRuleActor): { actorId: string; actorType: AuditActorType } {\n const id = actorId(actor);\n switch (actor.type) {\n case 'github':\n case 'gitlab':\n return { actorId: id, actorType: 'human' };\n case 'human':\n return { actorId: id, actorType: isAgentActor(id) ? 'agent' : 'human' };\n default:\n return { actorId: id, actorType: actor.type };\n }\n}\n\nexport function currentStage(stages: readonly string[]): FactoryRuleStage | undefined {\n if (stages.length !== 1) return undefined;\n const stage = stages[0];\n return isFactoryRuleStage(stage) ? stage : undefined;\n}\n\ninterface TransitionConsentOptions {\n autonomy?: 'arm' | 'disarm';\n consentedBy?: string;\n accept?: boolean;\n triageType?: FactoryTriageType;\n}\n\n// Entering a resting lane disarms whoever rests it; only a person's move into a working lane arms.\nfunction transitionConsent(working: boolean, humanMove: boolean): 'arm' | 'disarm' | undefined {\n if (!working) return 'disarm';\n return humanMove ? 'arm' : undefined;\n}\n\n// An event arriving as data (GitHub, sweeps) never pre-approves the runs its transition queues.\nfunction bearsConsent(actor: FactoryRuleActor): boolean {\n return actor.type === 'human' || actor.type === 'agent';\n}\n\n// Rides the transition's own revision-checked commit, so a stale or rejected commit flips nothing.\nfunction consentEffect(\n request: FactoryTransitionRequest,\n working: boolean,\n humanMove: boolean,\n): TransitionConsentOptions {\n const autonomy = transitionConsent(working, humanMove);\n return bearsConsent(request.actor) ? { autonomy, consentedBy: actorId(request.actor) } : { autonomy };\n}\n\ntype RunStartDecision = Extract<FactoryCommitDecision, { type: 'invokeSkill' | 'sendMessage' }>;\n\nfunction startsRun(decision: FactoryCommitDecision): decision is RunStartDecision {\n return decision.type === 'invokeSkill' || (decision.type === 'sendMessage' && decision.prepareBinding === true);\n}\n\n// Answering a recorded run start, or the role's own mid-run agent, with a run would start a second one.\nfunction runAlreadyUnderway(request: FactoryTransitionRequest, decision: RunStartDecision): boolean {\n if (request.cause === 'run_start') return true;\n return request.actor.type === 'agent' && request.actor.role === decision.role;\n}\n\nfunction stageTransitionMessage(fromStage: FactoryRuleStage, toStage: FactoryRuleStage): string {\n return `This work was moved from the ${fromStage} stage to the ${toStage} stage.`;\n}\n\nfunction isTriageAgent(actor: FactoryRuleActor): actor is Extract<FactoryRuleActor, { type: 'agent' }> {\n return actor.type === 'agent' && actor.role === 'triage';\n}\n\nfunction isHumanTransition(request: FactoryTransitionRequest): boolean {\n return request.actor.type === 'human' && request.ingress.type === 'human';\n}\n\nfunction ruleFailure(error: unknown): { code: FactoryRuleRejectionCode; reason: string } {\n return {\n code: 'rule_error',\n reason: error instanceof Error ? `Factory rule failed: ${error.message}` : 'Factory rule failed.',\n };\n}\n\nasync function withRuleTimeout<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), timeoutMs);\n });\n try {\n return await Promise.race([operation, timeout]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nexport class FactoryTransitionService {\n readonly #configVersion: string;\n readonly #boards: BoardRegistry;\n readonly #storage: WorkItemsStorage;\n readonly #timeoutMs: number;\n readonly #onTerminalStage: FactoryTransitionServiceOptions['onTerminalStage'];\n readonly #terminalCleanupTimeoutMs: number;\n readonly #onAccepted: FactoryTransitionServiceOptions['onAccepted'];\n readonly #onTransitionCommitted: FactoryTransitionServiceOptions['onTransitionCommitted'];\n readonly #autoApprovePlans: FactoryTransitionServiceOptions['autoApprovePlans'];\n readonly #selfReviewEnabled: FactoryTransitionServiceOptions['selfReviewEnabled'];\n readonly #audit: AuditRecorder | undefined;\n\n constructor(options: FactoryTransitionServiceOptions) {\n this.#configVersion = options.configVersion;\n this.#boards = options.boards ?? createBoardRegistry();\n this.#storage = options.storage;\n this.#audit = options.audit;\n this.#timeoutMs = options.timeoutMs ?? RULE_TIMEOUT_MS;\n this.#onTerminalStage = options.onTerminalStage;\n this.#onAccepted = options.onAccepted;\n this.#onTransitionCommitted = options.onTransitionCommitted;\n this.#autoApprovePlans = options.autoApprovePlans;\n this.#selfReviewEnabled = options.selfReviewEnabled;\n this.#terminalCleanupTimeoutMs = options.terminalCleanupTimeoutMs ?? TERMINAL_CLEANUP_TIMEOUT_MS;\n }\n\n get configVersion(): string {\n return this.#configVersion;\n }\n\n async transition(request: FactoryTransitionRequest): Promise<FactoryTransitionResult> {\n const replay = await this.#storage.getTransitionResultByIngress(\n request.orgId,\n request.factoryProjectId,\n request.ingress.identity,\n );\n if (replay) return replay as unknown as FactoryTransitionResult;\n\n const transitionId = request.ingress.transitionId ?? globalThis.crypto.randomUUID();\n const item = await this.#storage.get({ orgId: request.orgId, id: request.workItemId });\n if (!item) {\n const rejection = await this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'Work item not found.',\n );\n await this.#recordTransition(request, undefined, rejection);\n return rejection;\n }\n const result = await this.#evaluateAndCommit(request, transitionId, item);\n await this.#recordTransition(request, item, result);\n this.#notifyCommitted(request, item, result);\n return result;\n }\n\n #notifyCommitted(request: FactoryTransitionRequest, item: WorkItemRow, result: FactoryTransitionResult): void {\n const onTransitionCommitted = this.#onTransitionCommitted;\n if (!onTransitionCommitted || result.status !== 'accepted') return;\n // Board-neutral: `currentStage` only knows the built-in rule stages.\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (result.stage === fromStage) return;\n // Inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onTransitionCommitted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: item.id,\n fromStage,\n toStage: result.stage,\n }),\n )\n .catch(error => {\n console.warn(`[factory] stage notification failed for work item ${item.id}:`, error);\n });\n }\n\n /** A rejection can outlive its work item: the row still names the id the caller asked for. */\n async #recordTransition(\n request: FactoryTransitionRequest,\n item: WorkItemRow | undefined,\n result: FactoryTransitionResult,\n ): Promise<void> {\n if (!this.#audit) return;\n const from = item ? currentStage(item.stages) : undefined;\n if (result.status === 'accepted' && result.stage === from && !request.reenter) return;\n const outcome =\n result.status === 'accepted'\n ? { action: 'factory.work_item.stage_moved' as const, to: result.stage, revision: result.revision }\n : {\n action: 'factory.work_item.transition_rejected' as const,\n to: request.stage,\n code: result.code,\n reason: result.reason,\n };\n const { action, ...detail } = outcome;\n await this.#audit\n .record({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n ...auditActorOf(request.actor),\n actorProfile: request.actorProfile,\n ...(request.context ? { context: request.context } : {}),\n action,\n idempotencyKey: result.transitionId,\n targets: [{ type: 'work_item', id: item?.id ?? request.workItemId, ...(item ? { name: item.title } : {}) }],\n metadata: {\n transitionId: result.transitionId,\n ingressType: request.ingress.type,\n cause: request.cause,\n configVersion: this.#configVersion,\n ...(from ? { from } : {}),\n ...(request.reenter ? { reenter: true } : {}),\n ...detail,\n },\n })\n .catch(error => {\n console.warn(`[factory] audit failed for transition ${result.transitionId}:`, error);\n });\n }\n\n async #evaluateAndCommit(\n request: FactoryTransitionRequest,\n transitionId: string,\n item: WorkItemRow,\n ): Promise<FactoryTransitionResult> {\n if (request.causalChain && request.causalChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n return this.#commitRejection(\n request,\n transitionId,\n 'causal_depth_exceeded',\n 'Factory rule causal depth exceeded.',\n );\n }\n const itemSource = workItemSource(item.externalSource);\n const source = factoryRuleSourceForWorkItem(itemSource);\n const isPullRequest = source === 'pullRequest' || source === 'gitlabPullRequest';\n const legacyBoard = isPullRequest ? 'review' : 'work';\n if (item.board === null && !this.#boards.has(legacyBoard)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'This legacy work item has no assigned board. Assign an installed board and phase through the work-item PATCH endpoint before transitioning it.',\n );\n }\n const itemBoard = item.board ?? legacyBoard;\n if (request.board !== itemBoard) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The work item belongs to board \"${itemBoard}\", not \"${request.board}\".`,\n );\n }\n if ((itemBoard === 'review' && !isPullRequest) || (itemBoard === 'work' && isPullRequest)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not belong to the requested board.',\n );\n }\n const board = this.#boards.get(request.board);\n if (!board) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `Board \"${request.board}\" is not installed.`,\n );\n }\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (!fromStage || !Object.prototype.hasOwnProperty.call(board.phases, fromStage)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not have one canonical phase on the requested board.',\n );\n }\n if (\n !Object.prototype.hasOwnProperty.call(board.phases, request.stage) ||\n !board.allowsTransition(fromStage, request.stage)\n ) {\n const nextStages = [...new Set((board.transitions[fromStage] ?? []).map(transition => transition.to))];\n const nextStagesReason =\n nextStages.length > 0\n ? `Next stages declared from ${fromStage}: ${nextStages.join(', ')}.`\n : `No next stage is declared from ${fromStage}.`;\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The ${board.title} board does not allow moving from ${fromStage} to ${request.stage}. ${nextStagesReason}`,\n );\n }\n\n // The coordinator's own self-move at run start would otherwise inject a second run's kickoff.\n const humanMove = request.actor.type === 'human' && fromStage !== request.stage && request.cause !== 'run_start';\n // The board, not the phase name, says whether a seat is engaged on either side of this move.\n const entersWorking = board.isWorking(request.stage);\n const seatRole = board.roleForPhase(request.stage);\n\n const contextBase = {\n tenant: { orgId: request.orgId, projectId: request.factoryProjectId },\n actor: request.actor,\n ingress: { type: request.ingress.type, id: request.ingress.identity },\n cause: request.cause,\n causalChain: request.causalChain ?? [],\n configVersion: this.#configVersion,\n item: {\n id: item.id,\n source: itemSource,\n sourceKey: item.externalSource\n ? `${item.externalSource.integrationId}:${item.externalSource.type}:${item.externalSource.externalId}`\n : null,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n url: item.externalSource?.url ?? null,\n stages: [...item.stages],\n acceptedAt: item.acceptedAt,\n metadata: item.metadata,\n },\n board: request.board,\n itemRevision: item.revision,\n source,\n fromStage,\n toStage: request.stage,\n } satisfies Omit<FactoryStageRuleContext, 'stage'>;\n\n let evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[]; intents: TransitionConsentOptions }\n | { outcome: 'rejected'; code: string; reason: string };\n try {\n evaluation = await withRuleTimeout(\n (async () => {\n // Resolve the project switch inside the timed block so a resolver rejection\n // surfaces as a committed rule_error and a slow lookup is bounded.\n const plansAutoApproved = this.#autoApprovePlans\n ? await this.#autoApprovePlans({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const policy = boardTransitionPolicyResultSchema.parse(\n await board.transitionPolicy?.(\n immutablePolicySnapshot({\n ...contextBase,\n item: { ...contextBase.item, triageType: item.triageType },\n initialEntry: request.initialEntry ?? false,\n reenter: request.reenter ?? false,\n isHumanTransition: isHumanTransition(request),\n plansAutoApproved,\n planApproved: request.planApproved === true,\n requestedTriageType: request.triageType,\n }),\n ),\n );\n if (policy?.type === 'reject') {\n return { outcome: 'rejected' as const, code: policy.code, reason: policy.reason };\n }\n if (\n policy?.triageType !== undefined &&\n (!isTriageAgent(request.actor) ||\n policy.triageType !== request.triageType ||\n (item.triageType !== null && item.triageType !== policy.triageType))\n ) {\n throw new Error('Board policy requested an unauthorized classification.');\n }\n if (policy?.accept && !isHumanTransition(request)) {\n throw new Error('Board policy requested unauthorized acceptance.');\n }\n // External content can steer a bound agent; board allowance cannot bypass this guard.\n if (\n request.actor.type === 'agent' &&\n !board.isWorking(fromStage) &&\n entersWorking &&\n externallyAuthoredWorkItem(item)\n ) {\n return {\n outcome: 'rejected' as const,\n code: 'approval_required',\n reason:\n 'This card comes from outside the write-access circle; a person must resume it from the Factory board.',\n };\n }\n const selfReviewEnabled =\n request.board === 'work' && this.#selfReviewEnabled\n ? await this.#selfReviewEnabled({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const decisions: FactoryCommitDecision[] = [];\n for (const rule of resolveFactoryStageRules(this.#boards, {\n board: request.board,\n source,\n fromStage,\n toStage: request.stage,\n initialEntry: request.initialEntry,\n reenter: request.reenter,\n })) {\n const context: FactoryStageRuleContext = Object.freeze({\n ...contextBase,\n selfReviewEnabled,\n stage: rule.phase === 'exit' ? fromStage : request.stage,\n });\n const raw = await rule.handler(context);\n if (raw === undefined) continue;\n const decision = validateFactoryRuleDecision(raw, context.causalChain.length);\n if (decision.type === 'reject') {\n return { outcome: 'rejected' as const, code: decision.code, reason: decision.reason };\n }\n assertFactoryDecisionTarget(decision, this.#boards, itemBoard);\n if (startsRun(decision) && runAlreadyUnderway(request, decision)) continue;\n decisions.push(decision);\n }\n const validated = validateFactoryRuleDecisions(decisions);\n if (humanMove) {\n const message = stageTransitionMessage(fromStage, request.stage);\n const skill = validated.find(decision => decision.type === 'invokeSkill');\n if (skill) {\n skill.precedingMessage = message;\n } else {\n validated.unshift({\n type: 'sendMessage',\n idempotencyKey: `factory-stage:${transitionId}`,\n message,\n priority: 'urgent',\n idleBehavior: 'wake',\n // Parking a card says stop: no seat is right by construction, so\n // the notice goes to whichever session is live — or nobody.\n ...(entersWorking && seatRole !== undefined ? { role: seatRole, prepareBinding: true } : {}),\n });\n }\n }\n return {\n outcome: 'accepted' as const,\n intents: { triageType: policy?.triageType, accept: policy?.accept === true && !item.acceptedAt },\n decisions: validateFactoryRuleDecisions(validated) as unknown as Record<string, unknown>[],\n };\n })(),\n this.#timeoutMs,\n );\n } catch (error) {\n const failed =\n error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT'\n ? { code: 'timeout' as const, reason: 'Factory rule evaluation timed out.' }\n : ruleFailure(error);\n evaluation = { outcome: 'rejected', ...failed };\n }\n return this.#commit(\n request,\n transitionId,\n evaluation,\n evaluation.outcome === 'accepted'\n ? { ...consentEffect(request, entersWorking, humanMove), ...evaluation.intents }\n : {},\n );\n }\n\n async #commitRejection(\n request: FactoryTransitionRequest,\n transitionId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n ): Promise<FactoryTransitionResult> {\n return this.#commit(request, transitionId, {\n outcome: 'rejected',\n code,\n reason: reason.slice(0, MAX_REJECTION_REASON),\n });\n }\n\n async #commit(\n request: FactoryTransitionRequest,\n transitionId: string,\n evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[] }\n | { outcome: 'rejected'; code: string; reason: string },\n options: TransitionConsentOptions = {},\n ): Promise<FactoryTransitionResult> {\n const committed = await this.#storage.commitTransition({\n autonomy: options.autonomy,\n consentedBy: options.consentedBy,\n ...(options.accept ? { accept: true } : {}),\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n expectedRevision: request.expectedRevision,\n destinationStage: request.stage,\n actorId: actorId(request.actor),\n ingress: { identity: request.ingress.identity, triggerType: request.ingress.type, transitionId },\n configVersion: this.#configVersion,\n causalChain: [...(request.causalChain ?? [])],\n evaluation,\n ...(options.triageType ? { triageType: options.triageType } : {}),\n });\n if (committed.status === 'missing') {\n return rejection(transitionId, request.workItemId, 'invalid_transition', 'Work item not found.');\n }\n const result = committed.result as unknown as FactoryTransitionResult;\n if (\n this.#onAccepted &&\n options.accept &&\n committed.status === 'committed' &&\n result.status === 'accepted' &&\n committed.item?.acceptedAt\n ) {\n const item = committed.item;\n const onAccepted = this.#onAccepted;\n // Invoke inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onAccepted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n item,\n }),\n )\n .catch(error => {\n console.warn(`[factory] acceptance hook failed for work item ${request.workItemId}:`, error);\n });\n }\n // Only an installed board's declaration releases resources; an unknown board or phase never does.\n if (\n this.#onTerminalStage &&\n result.status === 'accepted' &&\n this.#boards.get(request.board)?.isTerminal(result.stage) === true\n ) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const cleanup = Promise.resolve(\n this.#onTerminalStage({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n stage: result.stage,\n revision: result.revision,\n actor: request.actor,\n }),\n );\n // A late rejection after the timeout wins the race must not surface\n // as an unhandled rejection.\n cleanup.catch(() => {});\n await Promise.race([\n cleanup,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, this.#terminalCleanupTimeoutMs);\n }),\n ]);\n } catch {\n // Resource release is best-effort — never fail a committed transition.\n } finally {\n clearTimeout(timer);\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transition-service.js","names":[],"sources":["../../src/rules/transition-service.ts"],"sourcesContent":["import { createBoardRegistry } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { boardTransitionPolicyResultSchema, immutablePolicySnapshot } from '../boards/transition-policy.js';\nimport type { AuditActorProfileInput, AuditActorType, AuditContext } from '../storage/domains/audit/base.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport { isAgentActor } from '../storage/domains/work-items/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { resolveFactoryStageRules } from './resolve.js';\nimport type {\n FactoryCommitDecision,\n FactoryRuleActor,\n FactoryRuleBoard,\n FactoryRuleCausalEntry,\n FactoryRuleRejectionCode,\n FactoryRuleStage,\n FactoryTriageType,\n FactoryStageRuleContext,\n FactoryTransitionResult,\n} from './types.js';\nimport {\n externallyAuthoredWorkItem,\n factoryRuleSourceForWorkItem,\n isFactoryRuleStage,\n workItemSource,\n} from './types.js';\nimport {\n MAX_FACTORY_RULE_CAUSAL_DEPTH,\n assertFactoryDecisionTarget,\n validateFactoryRuleDecision,\n validateFactoryRuleDecisions,\n} from './validation.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\nconst MAX_REJECTION_REASON = 512;\n/** Longest a committed transition waits for terminal resource cleanup. Cleanup\n * reattaches remote sandboxes, so a hung provider call must not leave the\n * already-committed transition request pending; past this bound the cleanup\n * keeps running in the background as pure best-effort. */\nconst TERMINAL_CLEANUP_TIMEOUT_MS = 30_000;\n\nexport interface FactoryTransitionRequest {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n /** Installed board id; the service rejects boards that are not installed. */\n board: string;\n stage: FactoryRuleStage;\n expectedRevision: number;\n actor: FactoryRuleActor;\n actorProfile?: AuditActorProfileInput;\n /** Where a browser request came from; rules and agents carry none. */\n context?: AuditContext;\n ingress: { type: 'human' | 'agent' | 'toolResult' | 'github' | 'rule'; identity: string; transitionId?: string };\n cause: string;\n causalChain?: readonly FactoryRuleCausalEntry[];\n /** Internal materialization path: evaluate only the destination onEnter leaf even when already at that stage. */\n initialEntry?: boolean;\n /** Re-runs the stage's entry rules when the item already holds that stage, to restart work the entry invalidated. */\n reenter?: boolean;\n /** Structured verdict required from a bound triage-agent terminal request. */\n triageType?: FactoryTriageType;\n /** Internal proof that this move consumes an approved `submit_plan` result. */\n planApproved?: true;\n}\n\nexport interface FactoryTransitionServiceOptions {\n configVersion: string;\n storage: WorkItemsStorage;\n boards?: BoardRegistry;\n /** Every commit, accepted or rejected, lands here as `stage_moved` / `transition_rejected` under the request's actor. */\n audit?: AuditRecorder;\n timeoutMs?: number;\n /**\n * Called after a transition commits into a phase the board declares\n * terminal — the point where the item's sessions stop receiving runs, so\n * resources they hold (e.g. sandboxes) can be released for reuse. Awaited,\n * but failures are swallowed: releasing resources must never break or roll\n * back the committed transition.\n */\n onTerminalStage?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n stage: FactoryRuleStage;\n revision: number;\n /** The actor that committed this terminal transition. Lets cleanup leave\n * the seat that drove its own transition (an agent tool call) untouched. */\n actor: FactoryRuleActor;\n }) => Promise<void> | void;\n /**\n * Called after a transition commits a stage change, e.g. to tell the item's\n * followers. Not awaited, and failures are logged: a notification must never\n * fail or delay the committed transition.\n */\n onTransitionCommitted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n }) => Promise<void> | void;\n /** Upper bound on how long a committed transition waits for\n * `onTerminalStage` before returning (default 30s). The cleanup continues\n * in the background past the bound. */\n terminalCleanupTimeoutMs?: number;\n /**\n * Called after a transition first records a person's acceptance of a\n * non-bug item (see `WorkItemRow.acceptedAt`). Fire-and-forget: failures\n * are swallowed, the committed transition never depends on it.\n */\n onAccepted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n item: WorkItemRow;\n }) => Promise<void> | void;\n /**\n * Resolves whether a project auto-approves produced plans. Unset means off:\n * a plan without explicit approval is a plan a person must review.\n */\n autoApprovePlans?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n /** Resolves whether a project self-reviews Work items before the change request opens. Unset means off. */\n selfReviewEnabled?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n}\n\nfunction rejection(\n transitionId: string,\n itemId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n): FactoryTransitionResult {\n return { status: 'rejected', transitionId, itemId, code, reason: reason.slice(0, MAX_REJECTION_REASON) };\n}\n\nfunction actorId(actor: FactoryRuleActor): string {\n switch (actor.type) {\n case 'human':\n case 'system':\n return actor.id;\n case 'agent':\n return `agent:${actor.bindingId}`;\n case 'github':\n return `github:${actor.login}`;\n case 'gitlab':\n return `gitlab:${actor.username}`;\n }\n}\n\n// The dispatcher executes an agent-approved decision as a human actor so its consent carries; the trail still names the agent.\nexport function auditActorOf(actor: FactoryRuleActor): { actorId: string; actorType: AuditActorType } {\n const id = actorId(actor);\n switch (actor.type) {\n case 'github':\n case 'gitlab':\n return { actorId: id, actorType: 'human' };\n case 'human':\n return { actorId: id, actorType: isAgentActor(id) ? 'agent' : 'human' };\n default:\n return { actorId: id, actorType: actor.type };\n }\n}\n\nexport function currentStage(stages: readonly string[]): FactoryRuleStage | undefined {\n if (stages.length !== 1) return undefined;\n const stage = stages[0];\n return isFactoryRuleStage(stage) ? stage : undefined;\n}\n\ninterface TransitionConsentOptions {\n autonomy?: 'arm' | 'disarm';\n consentedBy?: string;\n accept?: boolean;\n triageType?: FactoryTriageType;\n}\n\n// Entering a resting lane disarms whoever rests it; only a person's move into a working lane arms.\nfunction transitionConsent(working: boolean, humanMove: boolean): 'arm' | 'disarm' | undefined {\n if (!working) return 'disarm';\n return humanMove ? 'arm' : undefined;\n}\n\n// An event arriving as data (GitHub, sweeps) never pre-approves the runs its transition queues.\nfunction bearsConsent(actor: FactoryRuleActor): boolean {\n return actor.type === 'human' || actor.type === 'agent';\n}\n\n// Rides the transition's own revision-checked commit, so a stale or rejected commit flips nothing.\nfunction consentEffect(\n request: FactoryTransitionRequest,\n working: boolean,\n humanMove: boolean,\n): TransitionConsentOptions {\n const autonomy = transitionConsent(working, humanMove);\n return bearsConsent(request.actor) ? { autonomy, consentedBy: actorId(request.actor) } : { autonomy };\n}\n\ntype RunStartDecision = Extract<FactoryCommitDecision, { type: 'invokeSkill' | 'sendMessage' }>;\n\nfunction startsRun(decision: FactoryCommitDecision): decision is RunStartDecision {\n return decision.type === 'invokeSkill' || (decision.type === 'sendMessage' && decision.prepareBinding === true);\n}\n\n// Answering a recorded run start, or the role's own mid-run agent, with a run would start a second one.\nfunction runAlreadyUnderway(request: FactoryTransitionRequest, decision: RunStartDecision): boolean {\n if (request.cause === 'run_start') return true;\n return request.actor.type === 'agent' && request.actor.role === decision.role;\n}\n\nfunction stageTransitionMessage(fromStage: FactoryRuleStage, toStage: FactoryRuleStage): string {\n return `This work was moved from the ${fromStage} stage to the ${toStage} stage.`;\n}\n\nfunction isTriageAgent(actor: FactoryRuleActor): actor is Extract<FactoryRuleActor, { type: 'agent' }> {\n return actor.type === 'agent' && actor.role === 'triage';\n}\n\nfunction isHumanTransition(request: FactoryTransitionRequest): boolean {\n return request.actor.type === 'human' && request.ingress.type === 'human';\n}\n\nfunction ruleFailure(error: unknown): { code: FactoryRuleRejectionCode; reason: string } {\n return {\n code: 'rule_error',\n reason: error instanceof Error ? `Factory rule failed: ${error.message}` : 'Factory rule failed.',\n };\n}\n\nasync function withRuleTimeout<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), timeoutMs);\n });\n try {\n return await Promise.race([operation, timeout]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nexport class FactoryTransitionService {\n readonly #configVersion: string;\n readonly #boards: BoardRegistry;\n readonly #storage: WorkItemsStorage;\n readonly #timeoutMs: number;\n readonly #onTerminalStage: FactoryTransitionServiceOptions['onTerminalStage'];\n readonly #terminalCleanupTimeoutMs: number;\n readonly #onAccepted: FactoryTransitionServiceOptions['onAccepted'];\n readonly #onTransitionCommitted: FactoryTransitionServiceOptions['onTransitionCommitted'];\n readonly #autoApprovePlans: FactoryTransitionServiceOptions['autoApprovePlans'];\n readonly #selfReviewEnabled: FactoryTransitionServiceOptions['selfReviewEnabled'];\n readonly #audit: AuditRecorder | undefined;\n\n constructor(options: FactoryTransitionServiceOptions) {\n this.#configVersion = options.configVersion;\n this.#boards = options.boards ?? createBoardRegistry();\n this.#storage = options.storage;\n this.#audit = options.audit;\n this.#timeoutMs = options.timeoutMs ?? RULE_TIMEOUT_MS;\n this.#onTerminalStage = options.onTerminalStage;\n this.#onAccepted = options.onAccepted;\n this.#onTransitionCommitted = options.onTransitionCommitted;\n this.#autoApprovePlans = options.autoApprovePlans;\n this.#selfReviewEnabled = options.selfReviewEnabled;\n this.#terminalCleanupTimeoutMs = options.terminalCleanupTimeoutMs ?? TERMINAL_CLEANUP_TIMEOUT_MS;\n }\n\n get configVersion(): string {\n return this.#configVersion;\n }\n\n async transition(request: FactoryTransitionRequest): Promise<FactoryTransitionResult> {\n const replay = await this.#storage.getTransitionResultByIngress(\n request.orgId,\n request.factoryProjectId,\n request.ingress.identity,\n );\n if (replay) return replay as unknown as FactoryTransitionResult;\n\n const transitionId = request.ingress.transitionId ?? globalThis.crypto.randomUUID();\n const item = await this.#storage.get({ orgId: request.orgId, id: request.workItemId });\n if (!item) {\n const rejection = await this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'Work item not found.',\n );\n await this.#recordTransition(request, undefined, rejection);\n return rejection;\n }\n const result = await this.#evaluateAndCommit(request, transitionId, item);\n await this.#recordTransition(request, item, result);\n this.#notifyCommitted(request, item, result);\n return result;\n }\n\n #notifyCommitted(request: FactoryTransitionRequest, item: WorkItemRow, result: FactoryTransitionResult): void {\n const onTransitionCommitted = this.#onTransitionCommitted;\n if (!onTransitionCommitted || result.status !== 'accepted') return;\n // Board-neutral: `currentStage` only knows the built-in rule stages.\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (result.stage === fromStage) return;\n // Inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onTransitionCommitted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: item.id,\n fromStage,\n toStage: result.stage,\n }),\n )\n .catch(error => {\n console.warn(`[factory] stage notification failed for work item ${item.id}:`, error);\n });\n }\n\n /** A rejection can outlive its work item: the row still names the id the caller asked for. */\n async #recordTransition(\n request: FactoryTransitionRequest,\n item: WorkItemRow | undefined,\n result: FactoryTransitionResult,\n ): Promise<void> {\n if (!this.#audit) return;\n const from = item ? currentStage(item.stages) : undefined;\n if (result.status === 'accepted' && result.stage === from && !request.reenter) return;\n const outcome =\n result.status === 'accepted'\n ? { action: 'factory.work_item.stage_moved' as const, to: result.stage, revision: result.revision }\n : {\n action: 'factory.work_item.transition_rejected' as const,\n to: request.stage,\n code: result.code,\n reason: result.reason,\n };\n const { action, ...detail } = outcome;\n await this.#audit\n .record({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n ...auditActorOf(request.actor),\n actorProfile: request.actorProfile,\n ...(request.context ? { context: request.context } : {}),\n action,\n idempotencyKey: result.transitionId,\n targets: [{ type: 'work_item', id: item?.id ?? request.workItemId, ...(item ? { name: item.title } : {}) }],\n metadata: {\n transitionId: result.transitionId,\n ingressType: request.ingress.type,\n cause: request.cause,\n configVersion: this.#configVersion,\n ...(from ? { from } : {}),\n ...(request.reenter ? { reenter: true } : {}),\n ...detail,\n },\n })\n .catch(error => {\n console.warn(`[factory] audit failed for transition ${result.transitionId}:`, error);\n });\n }\n\n async #evaluateAndCommit(\n request: FactoryTransitionRequest,\n transitionId: string,\n item: WorkItemRow,\n ): Promise<FactoryTransitionResult> {\n if (request.causalChain && request.causalChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n return this.#commitRejection(\n request,\n transitionId,\n 'causal_depth_exceeded',\n 'Factory rule causal depth exceeded.',\n );\n }\n const itemSource = workItemSource(item.externalSource);\n const source = factoryRuleSourceForWorkItem(itemSource);\n const isPullRequest = source === 'pullRequest' || source === 'gitlabPullRequest';\n const legacyBoard = isPullRequest ? 'review' : 'work';\n if (item.board === null && !this.#boards.has(legacyBoard)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'This legacy work item has no assigned board. Assign an installed board and phase through the work-item PATCH endpoint before transitioning it.',\n );\n }\n const itemBoard = item.board ?? legacyBoard;\n if (request.board !== itemBoard) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The work item belongs to board \"${itemBoard}\", not \"${request.board}\".`,\n );\n }\n if ((itemBoard === 'review' && !isPullRequest) || (itemBoard === 'work' && isPullRequest)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not belong to the requested board.',\n );\n }\n const board = this.#boards.get(request.board);\n if (!board) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `Board \"${request.board}\" is not installed.`,\n );\n }\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (!fromStage || !Object.prototype.hasOwnProperty.call(board.phases, fromStage)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not have one canonical phase on the requested board.',\n );\n }\n if (\n !Object.prototype.hasOwnProperty.call(board.phases, request.stage) ||\n !board.allowsTransition(fromStage, request.stage)\n ) {\n const nextStages = [...new Set((board.transitions[fromStage] ?? []).map(transition => transition.to))];\n const nextStagesReason =\n nextStages.length > 0\n ? `Next stages declared from ${fromStage}: ${nextStages.join(', ')}.`\n : `No next stage is declared from ${fromStage}.`;\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The ${board.title} board does not allow moving from ${fromStage} to ${request.stage}. ${nextStagesReason}`,\n );\n }\n\n // The coordinator's own self-move at run start would otherwise inject a second run's kickoff.\n const humanMove = request.actor.type === 'human' && fromStage !== request.stage && request.cause !== 'run_start';\n // The board, not the phase name, says whether a seat is engaged on either side of this move.\n const entersWorking = board.isWorking(request.stage);\n const seatRole = board.roleForPhase(request.stage);\n\n const contextBase = {\n tenant: { orgId: request.orgId, projectId: request.factoryProjectId },\n actor: request.actor,\n ingress: { type: request.ingress.type, id: request.ingress.identity },\n cause: request.cause,\n causalChain: request.causalChain ?? [],\n configVersion: this.#configVersion,\n item: {\n id: item.id,\n source: itemSource,\n sourceKey: item.externalSource\n ? `${item.externalSource.integrationId}:${item.externalSource.type}:${item.externalSource.externalId}`\n : null,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n url: item.externalSource?.url ?? null,\n stages: [...item.stages],\n acceptedAt: item.acceptedAt,\n metadata: item.metadata,\n },\n board: request.board,\n itemRevision: item.revision,\n source,\n fromStage,\n toStage: request.stage,\n } satisfies Omit<FactoryStageRuleContext, 'stage'>;\n\n let evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[]; intents: TransitionConsentOptions }\n | { outcome: 'rejected'; code: string; reason: string };\n try {\n evaluation = await withRuleTimeout(\n (async () => {\n // Resolve the project switch inside the timed block so a resolver rejection\n // surfaces as a committed rule_error and a slow lookup is bounded.\n const plansAutoApproved = this.#autoApprovePlans\n ? await this.#autoApprovePlans({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const policy = boardTransitionPolicyResultSchema.parse(\n await board.transitionPolicy?.(\n immutablePolicySnapshot({\n ...contextBase,\n item: { ...contextBase.item, triageType: item.triageType },\n initialEntry: request.initialEntry ?? false,\n reenter: request.reenter ?? false,\n isHumanTransition: isHumanTransition(request),\n plansAutoApproved,\n planApproved: request.planApproved === true,\n requestedTriageType: request.triageType,\n }),\n ),\n );\n if (policy?.type === 'reject') {\n return { outcome: 'rejected' as const, code: policy.code, reason: policy.reason };\n }\n if (\n policy?.triageType !== undefined &&\n (!isTriageAgent(request.actor) ||\n policy.triageType !== request.triageType ||\n (item.triageType !== null && item.triageType !== policy.triageType))\n ) {\n throw new Error('Board policy requested an unauthorized classification.');\n }\n if (policy?.accept && !isHumanTransition(request)) {\n throw new Error('Board policy requested unauthorized acceptance.');\n }\n // External content can steer a bound agent; board allowance cannot bypass this guard.\n if (\n request.actor.type === 'agent' &&\n !board.isWorking(fromStage) &&\n entersWorking &&\n externallyAuthoredWorkItem(item)\n ) {\n return {\n outcome: 'rejected' as const,\n code: 'approval_required',\n reason:\n 'This card comes from outside the write-access circle; a person must resume it from the Factory board.',\n };\n }\n const selfReviewEnabled =\n request.board === 'work' && this.#selfReviewEnabled\n ? await this.#selfReviewEnabled({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const decisions: FactoryCommitDecision[] = [];\n for (const rule of resolveFactoryStageRules(this.#boards, {\n board: request.board,\n source,\n fromStage,\n toStage: request.stage,\n initialEntry: request.initialEntry,\n reenter: request.reenter,\n })) {\n const context: FactoryStageRuleContext = Object.freeze({\n ...contextBase,\n selfReviewEnabled,\n stage: rule.phase === 'exit' ? fromStage : request.stage,\n });\n const raw = await rule.handler(context);\n if (raw === undefined) continue;\n const decision = validateFactoryRuleDecision(raw, context.causalChain.length);\n if (decision.type === 'reject') {\n return { outcome: 'rejected' as const, code: decision.code, reason: decision.reason };\n }\n assertFactoryDecisionTarget(decision, this.#boards, itemBoard);\n if (startsRun(decision) && runAlreadyUnderway(request, decision)) continue;\n decisions.push(decision);\n }\n const validated = validateFactoryRuleDecisions(decisions);\n if (humanMove) {\n const message = stageTransitionMessage(fromStage, request.stage);\n const skill = validated.find(decision => decision.type === 'invokeSkill');\n if (skill) {\n skill.precedingMessage = message;\n } else {\n validated.unshift({\n type: 'sendMessage',\n idempotencyKey: `factory-stage:${transitionId}`,\n message,\n priority: 'urgent',\n idleBehavior: 'wake',\n // Parking a card says stop: no seat is right by construction, so\n // the notice goes to whichever session is live — or nobody.\n ...(entersWorking && seatRole !== undefined ? { role: seatRole, prepareBinding: true } : {}),\n });\n }\n }\n return {\n outcome: 'accepted' as const,\n intents: { triageType: policy?.triageType, accept: policy?.accept === true && !item.acceptedAt },\n decisions: validateFactoryRuleDecisions(validated) as unknown as Record<string, unknown>[],\n };\n })(),\n this.#timeoutMs,\n );\n } catch (error) {\n const failed =\n error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT'\n ? { code: 'timeout' as const, reason: 'Factory rule evaluation timed out.' }\n : ruleFailure(error);\n evaluation = { outcome: 'rejected', ...failed };\n }\n return this.#commit(\n request,\n transitionId,\n evaluation,\n evaluation.outcome === 'accepted'\n ? { ...consentEffect(request, entersWorking, humanMove), ...evaluation.intents }\n : {},\n );\n }\n\n async #commitRejection(\n request: FactoryTransitionRequest,\n transitionId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n ): Promise<FactoryTransitionResult> {\n return this.#commit(request, transitionId, {\n outcome: 'rejected',\n code,\n reason: reason.slice(0, MAX_REJECTION_REASON),\n });\n }\n\n async #commit(\n request: FactoryTransitionRequest,\n transitionId: string,\n evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[] }\n | { outcome: 'rejected'; code: string; reason: string },\n options: TransitionConsentOptions = {},\n ): Promise<FactoryTransitionResult> {\n const committed = await this.#storage.commitTransition({\n autonomy: options.autonomy,\n consentedBy: options.consentedBy,\n ...(options.accept ? { accept: true } : {}),\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n expectedRevision: request.expectedRevision,\n destinationStage: request.stage,\n actorId: actorId(request.actor),\n ingress: { identity: request.ingress.identity, triggerType: request.ingress.type, transitionId },\n configVersion: this.#configVersion,\n causalChain: [...(request.causalChain ?? [])],\n evaluation,\n ...(options.triageType ? { triageType: options.triageType } : {}),\n });\n if (committed.status === 'missing') {\n return rejection(transitionId, request.workItemId, 'invalid_transition', 'Work item not found.');\n }\n const result = committed.result as unknown as FactoryTransitionResult;\n if (\n this.#onAccepted &&\n options.accept &&\n committed.status === 'committed' &&\n result.status === 'accepted' &&\n committed.item?.acceptedAt\n ) {\n const item = committed.item;\n const onAccepted = this.#onAccepted;\n // Invoke inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onAccepted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n item,\n }),\n )\n .catch(error => {\n console.warn(`[factory] acceptance hook failed for work item ${request.workItemId}:`, error);\n });\n }\n // Only an installed board's declaration releases resources; an unknown board or phase never does.\n if (\n this.#onTerminalStage &&\n result.status === 'accepted' &&\n this.#boards.get(request.board)?.isTerminal(result.stage) === true\n ) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const cleanup = Promise.resolve(\n this.#onTerminalStage({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n stage: result.stage,\n revision: result.revision,\n actor: request.actor,\n }),\n );\n // A late rejection after the timeout wins the race must not surface\n // as an unhandled rejection.\n cleanup.catch(() => {});\n await Promise.race([\n cleanup,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, this.#terminalCleanupTimeoutMs);\n }),\n ]);\n } catch {\n // Resource release is best-effort — never fail a committed transition.\n } finally {\n clearTimeout(timer);\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"callback-factory-sandbox.js","names":["#callback"],"sources":["../../src/sandbox/callback-factory-sandbox.ts"],"sourcesContent":["import { FactorySandbox } from '@mastra/core/workspace';\nimport type { FactorySandboxContext, MastraSandbox } from '@mastra/core/workspace';\n\nimport type { MastraFactorySandboxConfig } from './session-sandbox.js';\n\n/**\n * Wraps the `sandbox: ctx => new SomeSandbox(...)` host callback\n * as a FactorySandbox. The host owns everything inside the callback, so the\n * factory knows nothing about it: no settings, no template, no builds.\n */\nexport class CallbackFactorySandbox extends FactorySandbox<Record<string, never>> {\n readonly provider = 'custom';\n\n readonly #callback: MastraFactorySandboxConfig;\n\n constructor(callback: MastraFactorySandboxConfig) {\n super();\n this.#callback = callback;\n }\n\n create(ctx: FactorySandboxContext): MastraSandbox {\n return this.#callback(ctx);\n }\n}\n"],"mappings":";;;;;;;AAUA,IAAa,yBAAb,cAA4C,eAAsC;CAChF,WAAoB;CAEpB;CAEA,YAAY,UAAsC;EAChD,MAAM;EACN,KAAKA,YAAY;CACnB;CAEA,OAAO,KAA2C;EAChD,OAAO,KAAKA,UAAU,GAAG;CAC3B;AACF"}
1
+ {"version":3,"file":"callback-factory-sandbox.js","names":[],"sources":["../../src/sandbox/callback-factory-sandbox.ts"],"sourcesContent":["import { FactorySandbox } from '@mastra/core/workspace';\nimport type { FactorySandboxContext, MastraSandbox } from '@mastra/core/workspace';\n\nimport type { MastraFactorySandboxConfig } from './session-sandbox.js';\n\n/**\n * Wraps the `sandbox: ctx => new SomeSandbox(...)` host callback\n * as a FactorySandbox. The host owns everything inside the callback, so the\n * factory knows nothing about it: no settings, no template, no builds.\n */\nexport class CallbackFactorySandbox extends FactorySandbox<Record<string, never>> {\n readonly provider = 'custom';\n\n readonly #callback: MastraFactorySandboxConfig;\n\n constructor(callback: MastraFactorySandboxConfig) {\n super();\n this.#callback = callback;\n }\n\n create(ctx: FactorySandboxContext): MastraSandbox {\n return this.#callback(ctx);\n }\n}\n"],"mappings":";;;;;;;AAUA,IAAa,yBAAb,cAA4C,eAAsC;CAChF,WAAoB;CAEpB;CAEA,YAAY,UAAsC;EAChD,MAAM;EACN,KAAK,YAAY;CACnB;CAEA,OAAO,KAA2C;EAChD,OAAO,KAAK,UAAU,GAAG;CAC3B;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"local-factory-sandbox.js","names":["#options"],"sources":["../../src/sandbox/local-factory-sandbox.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { FactorySandbox, LocalSandbox } from '@mastra/core/workspace';\nimport type { FactorySandboxContext } from '@mastra/core/workspace';\n\nexport interface LocalFactorySandboxOptions {\n /** Directory under which every session gets its own working directory. */\n root: string;\n /** Environment for commands run in the sandbox. */\n env?: Record<string, string>;\n}\n\n/**\n * Sessions run on the host machine: each session's sandbox is a LocalSandbox\n * rooted at `join(root, sessionId)`, where the repository checks out as a\n * subdirectory. No template and no user-tunable settings.\n */\nexport class LocalFactorySandbox extends FactorySandbox<Record<string, never>> {\n readonly provider = 'local';\n\n readonly #options: LocalFactorySandboxOptions;\n\n constructor(options: LocalFactorySandboxOptions) {\n super();\n this.#options = options;\n }\n\n create(ctx: FactorySandboxContext): LocalSandbox {\n return new LocalSandbox({\n id: ctx.sessionId,\n timeout: 30_000,\n workingDirectory: join(this.#options.root, ctx.sessionId),\n ...(this.#options.env ? { env: this.#options.env } : {}),\n });\n }\n}\n"],"mappings":";;;;;;;;AAgBA,IAAa,sBAAb,cAAyC,eAAsC;CAC7E,WAAoB;CAEpB;CAEA,YAAY,SAAqC;EAC/C,MAAM;EACN,KAAKA,WAAW;CAClB;CAEA,OAAO,KAA0C;EAC/C,OAAO,IAAI,aAAa;GACtB,IAAI,IAAI;GACR,SAAS;GACT,kBAAkB,KAAK,KAAKA,SAAS,MAAM,IAAI,SAAS;GACxD,GAAI,KAAKA,SAAS,MAAM,EAAE,KAAK,KAAKA,SAAS,IAAI,IAAI,CAAC;EACxD,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"local-factory-sandbox.js","names":[],"sources":["../../src/sandbox/local-factory-sandbox.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { FactorySandbox, LocalSandbox } from '@mastra/core/workspace';\nimport type { FactorySandboxContext } from '@mastra/core/workspace';\n\nexport interface LocalFactorySandboxOptions {\n /** Directory under which every session gets its own working directory. */\n root: string;\n /** Environment for commands run in the sandbox. */\n env?: Record<string, string>;\n}\n\n/**\n * Sessions run on the host machine: each session's sandbox is a LocalSandbox\n * rooted at `join(root, sessionId)`, where the repository checks out as a\n * subdirectory. No template and no user-tunable settings.\n */\nexport class LocalFactorySandbox extends FactorySandbox<Record<string, never>> {\n readonly provider = 'local';\n\n readonly #options: LocalFactorySandboxOptions;\n\n constructor(options: LocalFactorySandboxOptions) {\n super();\n this.#options = options;\n }\n\n create(ctx: FactorySandboxContext): LocalSandbox {\n return new LocalSandbox({\n id: ctx.sessionId,\n timeout: 30_000,\n workingDirectory: join(this.#options.root, ctx.sessionId),\n ...(this.#options.env ? { env: this.#options.env } : {}),\n });\n }\n}\n"],"mappings":";;;;;;;;AAgBA,IAAa,sBAAb,cAAyC,eAAsC;CAC7E,WAAoB;CAEpB;CAEA,YAAY,SAAqC;EAC/C,MAAM;EACN,KAAK,WAAW;CAClB;CAEA,OAAO,KAA0C;EAC/C,OAAO,IAAI,aAAa;GACtB,IAAI,IAAI;GACR,SAAS;GACT,kBAAkB,KAAK,KAAK,SAAS,MAAM,IAAI,SAAS;GACxD,GAAI,KAAK,SAAS,MAAM,EAAE,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC;EACxD,CAAC;CACH;AACF"}