@mastra/factory 0.24.0-alpha.1 → 0.24.0-alpha.2

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 (175) hide show
  1. package/README.md +80 -4
  2. package/dist/auth.d.ts +8 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +15 -1
  5. package/dist/auth.js.map +1 -1
  6. package/dist/boards/work-transition-policy.d.ts.map +1 -1
  7. package/dist/boards/work-transition-policy.js +10 -0
  8. package/dist/boards/work-transition-policy.js.map +1 -1
  9. package/dist/boards/work.d.ts +14 -0
  10. package/dist/boards/work.d.ts.map +1 -1
  11. package/dist/boards/work.js +70 -7
  12. package/dist/boards/work.js.map +1 -1
  13. package/dist/capabilities/version-control.d.ts +26 -0
  14. package/dist/capabilities/version-control.d.ts.map +1 -1
  15. package/dist/factory.d.ts.map +1 -1
  16. package/dist/factory.js +173 -11
  17. package/dist/factory.js.map +1 -1
  18. package/dist/integrations/github/integration.d.ts.map +1 -1
  19. package/dist/integrations/github/integration.js +37 -1
  20. package/dist/integrations/github/integration.js.map +1 -1
  21. package/dist/integrations/gitlab/api.d.ts +15 -0
  22. package/dist/integrations/gitlab/api.d.ts.map +1 -1
  23. package/dist/integrations/gitlab/api.js +9 -0
  24. package/dist/integrations/gitlab/api.js.map +1 -1
  25. package/dist/integrations/gitlab/version-control.d.ts.map +1 -1
  26. package/dist/integrations/gitlab/version-control.js +37 -3
  27. package/dist/integrations/gitlab/version-control.js.map +1 -1
  28. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  29. package/dist/integrations/platform/github/integration.js +27 -1
  30. package/dist/integrations/platform/github/integration.js.map +1 -1
  31. package/dist/integrations/subscription-session.d.ts +15 -0
  32. package/dist/integrations/subscription-session.d.ts.map +1 -1
  33. package/dist/integrations/subscription-session.js +30 -13
  34. package/dist/integrations/subscription-session.js.map +1 -1
  35. package/dist/provider-display-name.d.ts +2 -0
  36. package/dist/provider-display-name.d.ts.map +1 -0
  37. package/dist/provider-display-name.js +16 -0
  38. package/dist/provider-display-name.js.map +1 -0
  39. package/dist/routes/config.js +2 -2
  40. package/dist/routes/config.js.map +1 -1
  41. package/dist/routes/contracts.d.ts +4 -0
  42. package/dist/routes/contracts.d.ts.map +1 -1
  43. package/dist/routes/contracts.js +3 -1
  44. package/dist/routes/contracts.js.map +1 -1
  45. package/dist/routes/oauth.js +1 -1
  46. package/dist/routes/oauth.js.map +1 -1
  47. package/dist/routes/provider-credentials.d.ts +6 -2
  48. package/dist/routes/provider-credentials.d.ts.map +1 -1
  49. package/dist/routes/provider-credentials.js +20 -7
  50. package/dist/routes/provider-credentials.js.map +1 -1
  51. package/dist/routes/source-control-sessions.d.ts +1 -0
  52. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  53. package/dist/routes/source-control-sessions.js +1 -1
  54. package/dist/routes/source-control-sessions.js.map +1 -1
  55. package/dist/routes/surface.d.ts +1 -0
  56. package/dist/routes/surface.d.ts.map +1 -1
  57. package/dist/routes/surface.js +1 -0
  58. package/dist/routes/surface.js.map +1 -1
  59. package/dist/routes/work-items.d.ts.map +1 -1
  60. package/dist/routes/work-items.js +57 -117
  61. package/dist/routes/work-items.js.map +1 -1
  62. package/dist/rules/dispatcher.d.ts +7 -0
  63. package/dist/rules/dispatcher.d.ts.map +1 -1
  64. package/dist/rules/dispatcher.js +60 -3
  65. package/dist/rules/dispatcher.js.map +1 -1
  66. package/dist/rules/processor.d.ts.map +1 -1
  67. package/dist/rules/processor.js +2 -1
  68. package/dist/rules/processor.js.map +1 -1
  69. package/dist/rules/tools.d.ts.map +1 -1
  70. package/dist/rules/tools.js +82 -3
  71. package/dist/rules/tools.js.map +1 -1
  72. package/dist/rules/transition-service.d.ts +17 -0
  73. package/dist/rules/transition-service.d.ts.map +1 -1
  74. package/dist/rules/transition-service.js +25 -0
  75. package/dist/rules/transition-service.js.map +1 -1
  76. package/dist/rules/types.d.ts +2 -0
  77. package/dist/rules/types.d.ts.map +1 -1
  78. package/dist/rules/types.js.map +1 -1
  79. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
  80. package/dist/sandbox/local-factory-sandbox.js +1 -0
  81. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  82. package/dist/session/factory-session.d.ts +15 -1
  83. package/dist/session/factory-session.d.ts.map +1 -1
  84. package/dist/session/factory-session.js +4 -2
  85. package/dist/session/factory-session.js.map +1 -1
  86. package/dist/session/reply-credential-processor.d.ts +15 -0
  87. package/dist/session/reply-credential-processor.d.ts.map +1 -0
  88. package/dist/session/reply-credential-processor.js +40 -0
  89. package/dist/session/reply-credential-processor.js.map +1 -0
  90. package/dist/session/source-control-tools.d.ts +19 -1
  91. package/dist/session/source-control-tools.d.ts.map +1 -1
  92. package/dist/session/source-control-tools.js +70 -3
  93. package/dist/session/source-control-tools.js.map +1 -1
  94. package/dist/storage/domains/projects/base.d.ts +6 -0
  95. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  96. package/dist/storage/domains/projects/base.js +12 -0
  97. package/dist/storage/domains/projects/base.js.map +1 -1
  98. package/dist/storage/domains/source-control/base.d.ts +17 -0
  99. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  100. package/dist/storage/domains/source-control/base.js +23 -0
  101. package/dist/storage/domains/source-control/base.js.map +1 -1
  102. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  103. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  104. package/dist/storage/domains/source-control/inmemory.js +5 -1
  105. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  106. package/dist/storage/domains/work-items/base.d.ts +56 -0
  107. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  108. package/dist/storage/domains/work-items/base.js +139 -25
  109. package/dist/storage/domains/work-items/base.js.map +1 -1
  110. package/dist/tools/approval-tools.d.ts +15 -0
  111. package/dist/tools/approval-tools.d.ts.map +1 -0
  112. package/dist/tools/approval-tools.js +77 -0
  113. package/dist/tools/approval-tools.js.map +1 -0
  114. package/dist/tools/deliver.d.ts +54 -0
  115. package/dist/tools/deliver.d.ts.map +1 -0
  116. package/dist/tools/deliver.js +105 -0
  117. package/dist/tools/deliver.js.map +1 -0
  118. package/dist/tools/index.d.ts +29 -0
  119. package/dist/tools/index.d.ts.map +1 -0
  120. package/dist/tools/index.js +39 -0
  121. package/dist/tools/index.js.map +1 -0
  122. package/dist/tools/messaging-tools.d.ts +28 -0
  123. package/dist/tools/messaging-tools.d.ts.map +1 -0
  124. package/dist/tools/messaging-tools.js +312 -0
  125. package/dist/tools/messaging-tools.js.map +1 -0
  126. package/dist/tools/permissions.d.ts +31 -0
  127. package/dist/tools/permissions.d.ts.map +1 -0
  128. package/dist/tools/permissions.js +44 -0
  129. package/dist/tools/permissions.js.map +1 -0
  130. package/dist/tools/read-tools.d.ts +27 -0
  131. package/dist/tools/read-tools.d.ts.map +1 -0
  132. package/dist/tools/read-tools.js +285 -0
  133. package/dist/tools/read-tools.js.map +1 -0
  134. package/dist/tools/reports.d.ts +163 -0
  135. package/dist/tools/reports.d.ts.map +1 -0
  136. package/dist/tools/reports.js +352 -0
  137. package/dist/tools/reports.js.map +1 -0
  138. package/dist/tools/run-watch.d.ts +46 -0
  139. package/dist/tools/run-watch.d.ts.map +1 -0
  140. package/dist/tools/run-watch.js +102 -0
  141. package/dist/tools/run-watch.js.map +1 -0
  142. package/dist/tools/scope.d.ts +45 -0
  143. package/dist/tools/scope.d.ts.map +1 -0
  144. package/dist/tools/scope.js +73 -0
  145. package/dist/tools/scope.js.map +1 -0
  146. package/dist/tools/session-tools.d.ts +31 -0
  147. package/dist/tools/session-tools.d.ts.map +1 -0
  148. package/dist/tools/session-tools.js +312 -0
  149. package/dist/tools/session-tools.js.map +1 -0
  150. package/dist/tools/sessions-state-processor.d.ts +37 -0
  151. package/dist/tools/sessions-state-processor.d.ts.map +1 -0
  152. package/dist/tools/sessions-state-processor.js +206 -0
  153. package/dist/tools/sessions-state-processor.js.map +1 -0
  154. package/dist/tools/work-item-tools.d.ts +28 -0
  155. package/dist/tools/work-item-tools.d.ts.map +1 -0
  156. package/dist/tools/work-item-tools.js +322 -0
  157. package/dist/tools/work-item-tools.js.map +1 -0
  158. package/dist/work-items/create-work-item.d.ts +55 -0
  159. package/dist/work-items/create-work-item.d.ts.map +1 -0
  160. package/dist/work-items/create-work-item.js +101 -0
  161. package/dist/work-items/create-work-item.js.map +1 -0
  162. package/dist/work-items/decision-actions.d.ts +42 -0
  163. package/dist/work-items/decision-actions.d.ts.map +1 -0
  164. package/dist/work-items/decision-actions.js +70 -0
  165. package/dist/work-items/decision-actions.js.map +1 -0
  166. package/dist/workspace.d.ts.map +1 -1
  167. package/dist/workspace.js +3 -1
  168. package/dist/workspace.js.map +1 -1
  169. package/factory-skills/factory-gitlab-rereview/SKILL.md +1 -1
  170. package/factory-skills/factory-gitlab-review/SKILL.md +1 -1
  171. package/factory-skills/factory-plan/SKILL.md +4 -4
  172. package/factory-skills/factory-rereview/SKILL.md +3 -1
  173. package/factory-skills/factory-review/SKILL.md +3 -1
  174. package/factory-skills/factory-self-review/SKILL.md +46 -0
  175. package/package.json +9 -9
@@ -0,0 +1,312 @@
1
+ import { FACTORY_SESSION_NOTIFICATION_SOURCE, deliverFactorySessionNotification, resolveSessionThread } from "./deliver.js";
2
+ import { workItemNumber } from "./read-tools.js";
3
+ import { reportNotification, workItemReportAttributes, workItemReportRecipients } from "./reports.js";
4
+ import { z } from "zod";
5
+ import { createTool } from "@mastra/core/tools";
6
+ //#region src/tools/messaging-tools.ts
7
+ /** Messages plus replies, per sender session, per target thread, per window. */
8
+ const SIGNAL_RATE_LIMIT = 10;
9
+ const SIGNAL_RATE_WINDOW_MS = 600 * 1e3;
10
+ /** How many recent `factory-session` notifications on a thread the rate and duplicate checks read. */
11
+ const RECENT_SCAN = 100;
12
+ const RATE_LIMITED = "Too many messages to that session in the last 10 minutes; wait before sending more.";
13
+ const message = z.string().trim().min(1).max(4e3);
14
+ function senderAttributes(scope) {
15
+ return {
16
+ "from-session": scope.sessionId,
17
+ "from-thread": scope.threadId,
18
+ "from-user": scope.userId
19
+ };
20
+ }
21
+ function unavailable(result) {
22
+ return /* @__PURE__ */ new Error(`The session is not available (${result.reason}).`);
23
+ }
24
+ function createFactoryMessagingTools(scope, deps) {
25
+ const requireMessaging = async () => {
26
+ const messaging = await deps.messaging();
27
+ if (!messaging) throw new Error("Session messaging is not available on this server.");
28
+ return messaging;
29
+ };
30
+ const human = scope.kind === "user" && scope.interactive;
31
+ const audit = (input) => deps.auditRecorder.record({
32
+ orgId: scope.orgId,
33
+ actorId: human ? scope.userId : `agent:${scope.threadId}`,
34
+ actorType: human ? "human" : "agent",
35
+ action: "factory.agent.signaled",
36
+ factoryProjectId: scope.factoryProjectId,
37
+ targets: [{
38
+ type: "session",
39
+ id: input.targetSessionId
40
+ }],
41
+ metadata: {
42
+ targetSessionId: input.targetSessionId,
43
+ priority: input.priority,
44
+ kind: input.kind,
45
+ delivery: input.delivery
46
+ }
47
+ });
48
+ /** Why this session can't reach `row`, or null when it can: the rule for message targets and reply targets alike. */
49
+ const unreachable = (row) => {
50
+ if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) return "not-found";
51
+ if (row.visibility === "private" && row.userId !== scope.userId) return "private";
52
+ return null;
53
+ };
54
+ /** Messages, replies and reports (not progress) this session put on a thread inside the rate window. */
55
+ const overRateLimit = (records) => {
56
+ const since = Date.now() - SIGNAL_RATE_WINDOW_MS;
57
+ return records.filter((record) => record.kind !== "progress" && record.kind !== "stage" && record.attributes?.["from-session"] === scope.sessionId && record.createdAt.getTime() >= since).length >= 10;
58
+ };
59
+ const recent = (notifications, threadId) => notifications.listNotifications({
60
+ threadId,
61
+ source: FACTORY_SESSION_NOTIFICATION_SOURCE,
62
+ limit: RECENT_SCAN
63
+ });
64
+ /** On turns nobody signed in started, only sessions working on items this session follows. */
65
+ const followedWorkers = async () => {
66
+ const project = {
67
+ orgId: scope.orgId,
68
+ factoryProjectId: scope.factoryProjectId
69
+ };
70
+ const follows = await deps.workItems.listSessionSubscriptions({
71
+ ...project,
72
+ sessionId: scope.sessionId
73
+ });
74
+ const bindings = await Promise.all(follows.map((follow) => deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, follow.workItemId)));
75
+ const sideSessions = await deps.sessions.listByOrigin({
76
+ orgId: scope.orgId,
77
+ originSessionId: scope.sessionId
78
+ });
79
+ return /* @__PURE__ */ new Set([...bindings.flat().filter((binding) => binding.status === "active").map((binding) => binding.sessionId), ...sideSessions.map((session) => session.sessionId)]);
80
+ };
81
+ const workItemAttribute = async (sessionId, orgId, factoryProjectId) => {
82
+ const binding = await deps.workItems.findActiveRunBindingForSession({
83
+ orgId,
84
+ factoryProjectId,
85
+ sessionId
86
+ });
87
+ if (!binding) return {};
88
+ const item = await deps.workItems.getForProject(orgId, factoryProjectId, binding.workItemId);
89
+ if (!item) return {};
90
+ const number = workItemNumber(item);
91
+ return { "work-item": number === null ? item.id : `#${number}` };
92
+ };
93
+ const signal = createTool({
94
+ id: "factory_signal_session",
95
+ description: [
96
+ "Send a message to another session in this Factory: a work item role session, or a teammate session you can see.",
97
+ "Replies come back to this session as `factory-session` notifications with kind `reply` and an `in-reply-to` attribute naming your message.",
98
+ "Priority `medium` (default) wakes an idle session and waits for a busy one to finish its step; use `urgent` only to make it stop and change course.",
99
+ ...scope.interactive ? [] : ["On this turn you can only message your side sessions and sessions working on items this session follows."]
100
+ ].join(" "),
101
+ inputSchema: z.object({
102
+ sessionId: z.string().trim().min(1).max(200).describe("The session to message."),
103
+ message: message.describe("What to say. It arrives as-is, so make it self-contained."),
104
+ priority: z.enum([
105
+ "low",
106
+ "medium",
107
+ "high",
108
+ "urgent"
109
+ ]).default("medium")
110
+ }).strict(),
111
+ execute: async ({ sessionId, message: text, priority }) => {
112
+ if (sessionId === scope.sessionId) throw new Error("You can't message your own session.");
113
+ const row = await deps.sessions.getBySessionId(sessionId);
114
+ const blocked = unreachable(row);
115
+ if (blocked === "not-found" || !row) throw new Error(`No session ${sessionId} was found in this Factory.`);
116
+ if (blocked === "private") throw new Error(`Session ${sessionId} is private to its owner.`);
117
+ if (!scope.interactive && !(await followedWorkers()).has(sessionId)) throw new Error(`On this turn you can only message this session's side sessions and sessions working on items it follows; ${sessionId} isn't one.`);
118
+ const messaging = await requireMessaging();
119
+ const threadId = await resolveSessionThread({
120
+ controller: messaging.controller,
121
+ sessions: deps.sessions,
122
+ workItems: deps.workItems,
123
+ sessionId
124
+ });
125
+ if (!threadId) throw new Error("The session is not available (thread-not-found).");
126
+ if (overRateLimit(await recent(messaging.notifications, threadId))) throw new Error(RATE_LIMITED);
127
+ const result = await deliverFactorySessionNotification({
128
+ controller: messaging.controller,
129
+ sessions: deps.sessions,
130
+ target: {
131
+ sessionId,
132
+ threadId
133
+ },
134
+ notification: {
135
+ kind: "message",
136
+ priority,
137
+ summary: text,
138
+ payload: { message: text },
139
+ sourceId: scope.sessionId,
140
+ attributes: {
141
+ ...await workItemAttribute(sessionId, row.orgId, scope.factoryProjectId),
142
+ ...senderAttributes(scope)
143
+ }
144
+ }
145
+ });
146
+ if (result.delivered === "skipped") throw unavailable(result);
147
+ await audit({
148
+ targetSessionId: sessionId,
149
+ priority,
150
+ kind: "message",
151
+ delivery: result.delivered
152
+ });
153
+ return {
154
+ notificationId: result.notificationId,
155
+ delivered: result.delivered
156
+ };
157
+ }
158
+ });
159
+ const reply = createTool({
160
+ id: "factory_message_origin",
161
+ description: [
162
+ "Answer a message another Factory session sent you, or report on your work.",
163
+ "With `inReplyTo` (the id of a `factory-session` notification of kind `message`) the answer goes back to the session and thread that sent it; each message can be answered once.",
164
+ "Without it, the report goes to whoever follows your work: the session that started you, or everyone following your work item.",
165
+ "Set `status`: `progress` for an update, `blocked` when you need an answer to go on, `done` when finished, `failed` when you can't finish. Send one `done` or `failed` when you finish."
166
+ ].join(" "),
167
+ inputSchema: z.object({
168
+ message: message.describe("Your answer."),
169
+ inReplyTo: z.string().trim().min(1).max(200).optional().describe("Id of the message notification you are answering."),
170
+ status: z.enum([
171
+ "progress",
172
+ "blocked",
173
+ "done",
174
+ "failed"
175
+ ]).optional()
176
+ }).strict(),
177
+ execute: async ({ message: text, inReplyTo, status }) => {
178
+ if (!inReplyTo) return report(text, status);
179
+ const messaging = await requireMessaging();
180
+ const original = await messaging.notifications.getNotification({
181
+ threadId: scope.threadId,
182
+ id: inReplyTo
183
+ });
184
+ if (!original || original.source !== "factory-session") throw new Error(`No message ${inReplyTo} was sent to this session.`);
185
+ if (original.kind !== "message") throw new Error(`Only messages can be answered; ${inReplyTo} is a ${original.kind}.`);
186
+ const fromSession = original.attributes?.["from-session"];
187
+ const fromThread = original.attributes?.["from-thread"];
188
+ if (typeof fromSession !== "string" || typeof fromThread !== "string" || original.sourceId !== fromSession) throw new Error(`Message ${inReplyTo} doesn't say which session sent it.`);
189
+ const blocked = unreachable(await deps.sessions.getBySessionId(fromSession));
190
+ if (blocked === "not-found") throw new Error(`Message ${inReplyTo} didn't come from a session in this Factory.`);
191
+ if (blocked === "private") throw new Error(`Message ${inReplyTo} came from a private session this session can't answer.`);
192
+ const onSenderThread = await recent(messaging.notifications, fromThread);
193
+ if (onSenderThread.some((record) => record.kind === "reply" && record.attributes?.["in-reply-to"] === inReplyTo && record.attributes?.["from-session"] === scope.sessionId)) throw new Error(`You already answered ${inReplyTo}.`);
194
+ if (overRateLimit(onSenderThread)) throw new Error(RATE_LIMITED);
195
+ const result = await deliverFactorySessionNotification({
196
+ controller: messaging.controller,
197
+ sessions: deps.sessions,
198
+ target: {
199
+ sessionId: fromSession,
200
+ threadId: fromThread
201
+ },
202
+ notification: {
203
+ kind: "reply",
204
+ priority: "high",
205
+ summary: text,
206
+ payload: { message: text },
207
+ sourceId: scope.sessionId,
208
+ attributes: {
209
+ ...senderAttributes(scope),
210
+ "in-reply-to": inReplyTo,
211
+ ...status ? { status } : {}
212
+ }
213
+ }
214
+ });
215
+ if (result.delivered === "skipped") throw unavailable(result);
216
+ await audit({
217
+ targetSessionId: fromSession,
218
+ priority: "high",
219
+ kind: "reply",
220
+ delivery: result.delivered
221
+ });
222
+ return {
223
+ notificationId: result.notificationId,
224
+ delivered: result.delivered
225
+ };
226
+ }
227
+ });
228
+ /** A report with no message to answer: to the origin, else to the item's followers. */
229
+ async function report(text, status) {
230
+ const sender = {
231
+ sessionId: scope.sessionId,
232
+ threadId: scope.threadId,
233
+ userId: scope.userId
234
+ };
235
+ let recipients;
236
+ let attributes = {};
237
+ if (scope.origin) recipients = [scope.origin];
238
+ else {
239
+ const binding = await deps.workItems.findActiveRunBindingForSession({
240
+ orgId: scope.orgId,
241
+ factoryProjectId: scope.factoryProjectId,
242
+ sessionId: scope.sessionId
243
+ });
244
+ if (!binding) throw new Error("Nothing started this session; pass inReplyTo to answer a message.");
245
+ recipients = await workItemReportRecipients(deps.workItems, {
246
+ orgId: scope.orgId,
247
+ workItemId: binding.workItemId,
248
+ senderSessionId: scope.sessionId
249
+ });
250
+ ({attributes} = await workItemReportAttributes(deps.workItems, {
251
+ orgId: scope.orgId,
252
+ factoryProjectId: scope.factoryProjectId,
253
+ workItemId: binding.workItemId,
254
+ role: binding.role
255
+ }));
256
+ }
257
+ if (recipients.length === 0) return {
258
+ delivered: 0,
259
+ recipients: []
260
+ };
261
+ const messaging = await requireMessaging();
262
+ const notification = reportNotification({
263
+ sender,
264
+ status,
265
+ message: text,
266
+ attributes
267
+ });
268
+ const results = [];
269
+ for (const recipient of recipients) {
270
+ if (status !== "progress" && overRateLimit(await recent(messaging.notifications, recipient.threadId))) {
271
+ results.push({
272
+ sessionId: recipient.sessionId,
273
+ delivered: "skipped",
274
+ reason: "rate-limited"
275
+ });
276
+ continue;
277
+ }
278
+ const result = await deliverFactorySessionNotification({
279
+ controller: messaging.controller,
280
+ sessions: deps.sessions,
281
+ target: recipient,
282
+ notification
283
+ });
284
+ results.push({
285
+ sessionId: recipient.sessionId,
286
+ delivered: result.delivered
287
+ });
288
+ if (result.delivered === "skipped") continue;
289
+ await audit({
290
+ targetSessionId: recipient.sessionId,
291
+ priority: notification.priority ?? "high",
292
+ kind: notification.kind,
293
+ delivery: result.delivered
294
+ });
295
+ }
296
+ const delivered = results.filter((result) => result.delivered !== "skipped").length;
297
+ if (delivered === 0 && results.every((result) => result.reason === "rate-limited")) throw new Error(RATE_LIMITED);
298
+ return {
299
+ delivered,
300
+ recipients: results
301
+ };
302
+ }
303
+ if (scope.kind === "worker") return { factory_message_origin: reply };
304
+ return {
305
+ factory_signal_session: signal,
306
+ factory_message_origin: reply
307
+ };
308
+ }
309
+ //#endregion
310
+ export { SIGNAL_RATE_LIMIT, SIGNAL_RATE_WINDOW_MS, createFactoryMessagingTools };
311
+
312
+ //# sourceMappingURL=messaging-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messaging-tools.js","names":[],"sources":["../../src/tools/messaging-tools.ts"],"sourcesContent":["/**\n * Messages between Factory sessions. A user session can message any session in\n * its Factory it can see; the receiver answers that exact message. Both\n * directions are `factory-session` notifications on the receiver's stored thread.\n */\n\nimport type { NotificationPriority, NotificationRecord, NotificationsStorage } from '@mastra/core/notifications';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationTools } from '../integrations/base.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport {\n FACTORY_SESSION_NOTIFICATION_SOURCE,\n deliverFactorySessionNotification,\n resolveSessionThread,\n} from './deliver.js';\nimport type { FactoryDeliveryController, FactoryDeliveryResult } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\nimport { reportNotification, workItemReportAttributes, workItemReportRecipients } from './reports.js';\nimport type { FactoryToolScope } from './scope.js';\n\n/** Messages plus replies, per sender session, per target thread, per window. */\nexport const SIGNAL_RATE_LIMIT = 10;\nexport const SIGNAL_RATE_WINDOW_MS = 10 * 60 * 1000;\n/** How many recent `factory-session` notifications on a thread the rate and duplicate checks read. */\nconst RECENT_SCAN = 100;\nconst RATE_LIMITED = 'Too many messages to that session in the last 10 minutes; wait before sending more.';\n\nexport interface FactoryMessaging {\n controller: FactoryDeliveryController;\n notifications: Pick<NotificationsStorage, 'listNotifications' | 'getNotification'>;\n}\n\nexport interface FactoryMessagingToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin'>;\n workItems: Pick<\n WorkItemsStorage,\n | 'findActiveRunBindingForSession'\n | 'getForProject'\n | 'listSessionSubscriptions'\n | 'listRunBindings'\n | 'listWorkItemSubscribers'\n >;\n auditRecorder: AuditRecorder;\n /** The live controller and notifications store; resolved per call because they exist only once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n}\n\nconst message = z.string().trim().min(1).max(4000);\n\nfunction senderAttributes(scope: FactoryToolScope) {\n return {\n 'from-session': scope.sessionId,\n 'from-thread': scope.threadId,\n 'from-user': scope.userId,\n };\n}\n\nfunction unavailable(result: Extract<FactoryDeliveryResult, { delivered: 'skipped' }>) {\n return new Error(`The session is not available (${result.reason}).`);\n}\n\nexport function createFactoryMessagingTools(\n scope: FactoryToolScope,\n deps: FactoryMessagingToolDependencies,\n): IntegrationTools {\n const requireMessaging = async () => {\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session messaging is not available on this server.');\n return messaging;\n };\n // Only a user-session turn the person started is the person's act. Workers\n // (even when someone types into one) and turns nobody signed in started\n // (wakes, webhooks) act as the thread's agent.\n const human = scope.kind === 'user' && scope.interactive;\n const audit = (input: { targetSessionId: string; priority: NotificationPriority; kind: string; delivery: string }) =>\n deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: human ? scope.userId : `agent:${scope.threadId}`,\n actorType: human ? 'human' : 'agent',\n action: 'factory.agent.signaled',\n factoryProjectId: scope.factoryProjectId,\n targets: [{ type: 'session', id: input.targetSessionId }],\n metadata: {\n targetSessionId: input.targetSessionId,\n priority: input.priority,\n kind: input.kind,\n delivery: input.delivery,\n },\n });\n /** Why this session can't reach `row`, or null when it can: the rule for message targets and reply targets alike. */\n const unreachable = (row: SourceControlSession | null): 'not-found' | 'private' | null => {\n if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) return 'not-found';\n if (row.visibility === 'private' && row.userId !== scope.userId) return 'private';\n return null;\n };\n /** Messages, replies and reports (not progress) this session put on a thread inside the rate window. */\n const overRateLimit = (records: NotificationRecord[]) => {\n const since = Date.now() - SIGNAL_RATE_WINDOW_MS;\n return (\n records.filter(\n record =>\n // Everything this session sends counts except coalescing progress updates.\n record.kind !== 'progress' &&\n record.kind !== 'stage' &&\n record.attributes?.['from-session'] === scope.sessionId &&\n record.createdAt.getTime() >= since,\n ).length >= SIGNAL_RATE_LIMIT\n );\n };\n const recent = (notifications: FactoryMessaging['notifications'], threadId: string) =>\n notifications.listNotifications({ threadId, source: FACTORY_SESSION_NOTIFICATION_SOURCE, limit: RECENT_SCAN });\n\n /** On turns nobody signed in started, only sessions working on items this session follows. */\n const followedWorkers = async () => {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const follows = await deps.workItems.listSessionSubscriptions({ ...project, sessionId: scope.sessionId });\n const bindings = await Promise.all(\n follows.map(follow => deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, follow.workItemId)),\n );\n const sideSessions = await deps.sessions.listByOrigin({ orgId: scope.orgId, originSessionId: scope.sessionId });\n return new Set([\n ...bindings\n .flat()\n .filter(binding => binding.status === 'active')\n .map(binding => binding.sessionId),\n ...sideSessions.map(session => session.sessionId),\n ]);\n };\n\n const workItemAttribute = async (sessionId: string, orgId: string, factoryProjectId: string) => {\n const binding = await deps.workItems.findActiveRunBindingForSession({ orgId, factoryProjectId, sessionId });\n if (!binding) return {};\n const item = await deps.workItems.getForProject(orgId, factoryProjectId, binding.workItemId);\n if (!item) return {};\n const number = workItemNumber(item);\n return { 'work-item': number === null ? item.id : `#${number}` };\n };\n\n const signal = createTool({\n id: 'factory_signal_session',\n description: [\n 'Send a message to another session in this Factory: a work item role session, or a teammate session you can see.',\n 'Replies come back to this session as `factory-session` notifications with kind `reply` and an `in-reply-to` attribute naming your message.',\n 'Priority `medium` (default) wakes an idle session and waits for a busy one to finish its step; use `urgent` only to make it stop and change course.',\n ...(scope.interactive\n ? []\n : ['On this turn you can only message your side sessions and sessions working on items this session follows.']),\n ].join(' '),\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200).describe('The session to message.'),\n message: message.describe('What to say. It arrives as-is, so make it self-contained.'),\n priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),\n })\n .strict(),\n execute: async ({ sessionId, message: text, priority }) => {\n if (sessionId === scope.sessionId) throw new Error(\"You can't message your own session.\");\n const row = await deps.sessions.getBySessionId(sessionId);\n const blocked = unreachable(row);\n if (blocked === 'not-found' || !row) throw new Error(`No session ${sessionId} was found in this Factory.`);\n if (blocked === 'private') throw new Error(`Session ${sessionId} is private to its owner.`);\n if (!scope.interactive && !(await followedWorkers()).has(sessionId)) {\n throw new Error(\n `On this turn you can only message this session's side sessions and sessions working on items it follows; ${sessionId} isn't one.`,\n );\n }\n\n const messaging = await requireMessaging();\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('The session is not available (thread-not-found).');\n\n if (overRateLimit(await recent(messaging.notifications, threadId))) {\n throw new Error(RATE_LIMITED);\n }\n\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n target: { sessionId, threadId },\n notification: {\n kind: 'message',\n priority,\n summary: text,\n payload: { message: text },\n sourceId: scope.sessionId,\n attributes: {\n ...(await workItemAttribute(sessionId, row.orgId, scope.factoryProjectId)),\n ...senderAttributes(scope),\n },\n },\n });\n if (result.delivered === 'skipped') throw unavailable(result);\n await audit({ targetSessionId: sessionId, priority, kind: 'message', delivery: result.delivered });\n return { notificationId: result.notificationId, delivered: result.delivered };\n },\n });\n\n const reply = createTool({\n id: 'factory_message_origin',\n description: [\n 'Answer a message another Factory session sent you, or report on your work.',\n 'With `inReplyTo` (the id of a `factory-session` notification of kind `message`) the answer goes back to the session and thread that sent it; each message can be answered once.',\n 'Without it, the report goes to whoever follows your work: the session that started you, or everyone following your work item.',\n \"Set `status`: `progress` for an update, `blocked` when you need an answer to go on, `done` when finished, `failed` when you can't finish. Send one `done` or `failed` when you finish.\",\n ].join(' '),\n inputSchema: z\n .object({\n message: message.describe('Your answer.'),\n inReplyTo: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Id of the message notification you are answering.'),\n status: z.enum(['progress', 'blocked', 'done', 'failed']).optional(),\n })\n .strict(),\n execute: async ({ message: text, inReplyTo, status }) => {\n if (!inReplyTo) return report(text, status);\n const messaging = await requireMessaging();\n // Only a message that reached this session's own thread can be answered,\n // and the answer goes where that message came from: never to an address\n // the model supplies.\n const original: NotificationRecord | null = await messaging.notifications.getNotification({\n threadId: scope.threadId,\n id: inReplyTo,\n });\n if (!original || original.source !== FACTORY_SESSION_NOTIFICATION_SOURCE) {\n throw new Error(`No message ${inReplyTo} was sent to this session.`);\n }\n if (original.kind !== 'message') {\n throw new Error(`Only messages can be answered; ${inReplyTo} is a ${original.kind}.`);\n }\n const fromSession = original.attributes?.['from-session'];\n const fromThread = original.attributes?.['from-thread'];\n if (typeof fromSession !== 'string' || typeof fromThread !== 'string' || original.sourceId !== fromSession) {\n throw new Error(`Message ${inReplyTo} doesn't say which session sent it.`);\n }\n // Notification records can be written outside these tools, so the\n // sender they name gets the same checks a message to it would.\n const blocked = unreachable(await deps.sessions.getBySessionId(fromSession));\n if (blocked === 'not-found') throw new Error(`Message ${inReplyTo} didn't come from a session in this Factory.`);\n if (blocked === 'private') {\n throw new Error(`Message ${inReplyTo} came from a private session this session can't answer.`);\n }\n const onSenderThread = await recent(messaging.notifications, fromThread);\n // Replies count toward the same cap as messages, so records written to\n // this session can't turn it into an unlimited high-priority sender.\n const answered = onSenderThread.some(\n record =>\n record.kind === 'reply' &&\n record.attributes?.['in-reply-to'] === inReplyTo &&\n record.attributes?.['from-session'] === scope.sessionId,\n );\n if (answered) throw new Error(`You already answered ${inReplyTo}.`);\n if (overRateLimit(onSenderThread)) throw new Error(RATE_LIMITED);\n\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n target: { sessionId: fromSession, threadId: fromThread },\n notification: {\n kind: 'reply',\n priority: 'high',\n summary: text,\n payload: { message: text },\n sourceId: scope.sessionId,\n attributes: {\n ...senderAttributes(scope),\n 'in-reply-to': inReplyTo,\n ...(status ? { status } : {}),\n },\n },\n });\n if (result.delivered === 'skipped') throw unavailable(result);\n await audit({ targetSessionId: fromSession, priority: 'high', kind: 'reply', delivery: result.delivered });\n return { notificationId: result.notificationId, delivered: result.delivered };\n },\n });\n\n /** A report with no message to answer: to the origin, else to the item's followers. */\n async function report(text: string, status: 'progress' | 'blocked' | 'done' | 'failed' | undefined) {\n const sender = { sessionId: scope.sessionId, threadId: scope.threadId, userId: scope.userId };\n let recipients: Array<{ sessionId: string; threadId: string }>;\n let attributes: Record<string, string> = {};\n if (scope.origin) {\n recipients = [scope.origin];\n } else {\n const binding = await deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n if (!binding) throw new Error('Nothing started this session; pass inReplyTo to answer a message.');\n recipients = await workItemReportRecipients(deps.workItems, {\n orgId: scope.orgId,\n workItemId: binding.workItemId,\n senderSessionId: scope.sessionId,\n });\n ({ attributes } = await workItemReportAttributes(deps.workItems, {\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: binding.workItemId,\n role: binding.role,\n }));\n }\n if (recipients.length === 0) return { delivered: 0, recipients: [] };\n\n const messaging = await requireMessaging();\n const notification = reportNotification({ sender, status, message: text, attributes });\n const results: Array<{ sessionId: string; delivered: FactoryDeliveryResult['delivered']; reason?: string }> = [];\n for (const recipient of recipients) {\n // Progress replaces itself while unread; anything else can wake a follower, so it counts.\n if (status !== 'progress' && overRateLimit(await recent(messaging.notifications, recipient.threadId))) {\n results.push({ sessionId: recipient.sessionId, delivered: 'skipped', reason: 'rate-limited' });\n continue;\n }\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n target: recipient,\n notification,\n });\n results.push({ sessionId: recipient.sessionId, delivered: result.delivered });\n if (result.delivered === 'skipped') continue;\n await audit({\n targetSessionId: recipient.sessionId,\n priority: notification.priority ?? 'high',\n kind: notification.kind,\n delivery: result.delivered,\n });\n }\n const delivered = results.filter(result => result.delivered !== 'skipped').length;\n if (delivered === 0 && results.every(result => result.reason === 'rate-limited')) throw new Error(RATE_LIMITED);\n return { delivered, recipients: results };\n }\n\n if (scope.kind === 'worker') return { factory_message_origin: reply };\n return { factory_signal_session: signal, factory_message_origin: reply };\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { PermissionPolicy, PermissionRules } from '@mastra/core/agent-controller';
2
+ import type { SourceControlSessionLookup } from '../session/factory-session.js';
3
+ /**
4
+ * Factory tools that run without asking. Factory tools fall into the SDK's
5
+ * `mcp` category, whose default policy is `ask`, so each of these needs an
6
+ * explicit per-session `allow`.
7
+ */
8
+ export declare const FACTORY_TOOLS_WITHOUT_APPROVAL: readonly string[];
9
+ export interface FactoryToolPermissionsSession {
10
+ readonly identity: {
11
+ getResourceId(): string;
12
+ };
13
+ readonly permissions: {
14
+ getRules(): PermissionRules;
15
+ setForTool(args: {
16
+ toolName: string;
17
+ policy: PermissionPolicy;
18
+ }): Promise<void>;
19
+ };
20
+ }
21
+ export interface FactoryToolPermissionsDependencies {
22
+ sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;
23
+ }
24
+ /**
25
+ * Allow the no-approval Factory tools on a newly created Factory session.
26
+ * This only sets policy; which tools a turn actually gets is decided by the
27
+ * tool scope. A person's own choices win: a per-tool policy is never
28
+ * overwritten, and an explicit `mcp` category policy leaves everything alone.
29
+ */
30
+ export declare function applyFactoryToolPermissions(session: FactoryToolPermissionsSession, { sessions }: FactoryToolPermissionsDependencies, toolNames?: readonly string[]): Promise<void>;
31
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/tools/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEvF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAgB3D,CAAC;AAEF,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,IAAI,eAAe,CAAC;QAC5B,UAAU,CAAC,IAAI,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,gBAAgB,CAAA;SAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACjF,CAAC;CACH;AAED,MAAM,WAAW,kCAAkC;IACjD,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,6BAA6B,EACtC,EAAE,QAAQ,EAAE,EAAE,kCAAkC,EAChD,SAAS,GAAE,SAAS,MAAM,EAAmC,GAC5D,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -0,0 +1,44 @@
1
+ //#region src/tools/permissions.ts
2
+ /**
3
+ * Factory tools that run without asking. Factory tools fall into the SDK's
4
+ * `mcp` category, whose default policy is `ask`, so each of these needs an
5
+ * explicit per-session `allow`.
6
+ */
7
+ const FACTORY_TOOLS_WITHOUT_APPROVAL = [
8
+ "factory_list_work_items",
9
+ "factory_inspect_work_item",
10
+ "factory_list_attention",
11
+ "factory_create_work_item",
12
+ "factory_update_work_item",
13
+ "factory_transition_work_item",
14
+ "factory_retry_decision",
15
+ "factory_dismiss_decision",
16
+ "factory_resolve_proposal",
17
+ "factory_subscribe_work_item",
18
+ "factory_unsubscribe_work_item",
19
+ "factory_signal_session",
20
+ "factory_message_origin",
21
+ "factory_start_session",
22
+ "factory_answer_approval"
23
+ ];
24
+ /**
25
+ * Allow the no-approval Factory tools on a newly created Factory session.
26
+ * This only sets policy; which tools a turn actually gets is decided by the
27
+ * tool scope. A person's own choices win: a per-tool policy is never
28
+ * overwritten, and an explicit `mcp` category policy leaves everything alone.
29
+ */
30
+ async function applyFactoryToolPermissions(session, { sessions }, toolNames = FACTORY_TOOLS_WITHOUT_APPROVAL) {
31
+ if (!(await sessions.getBySessionId(session.identity.getResourceId()))?.factoryProjectId) return;
32
+ if (session.permissions.getRules().categories.mcp !== void 0) return;
33
+ for (const toolName of toolNames) {
34
+ if (session.permissions.getRules().tools[toolName] !== void 0) continue;
35
+ await session.permissions.setForTool({
36
+ toolName,
37
+ policy: "allow"
38
+ });
39
+ }
40
+ }
41
+ //#endregion
42
+ export { FACTORY_TOOLS_WITHOUT_APPROVAL, applyFactoryToolPermissions };
43
+
44
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","names":[],"sources":["../../src/tools/permissions.ts"],"sourcesContent":["import type { PermissionPolicy, PermissionRules } from '@mastra/core/agent-controller';\n\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\n\n/**\n * Factory tools that run without asking. Factory tools fall into the SDK's\n * `mcp` category, whose default policy is `ask`, so each of these needs an\n * explicit per-session `allow`.\n */\nexport const FACTORY_TOOLS_WITHOUT_APPROVAL: readonly string[] = [\n 'factory_list_work_items',\n 'factory_inspect_work_item',\n 'factory_list_attention',\n 'factory_create_work_item',\n 'factory_update_work_item',\n 'factory_transition_work_item',\n 'factory_retry_decision',\n 'factory_dismiss_decision',\n 'factory_resolve_proposal',\n 'factory_subscribe_work_item',\n 'factory_unsubscribe_work_item',\n 'factory_signal_session',\n 'factory_message_origin',\n 'factory_start_session',\n 'factory_answer_approval',\n];\n\nexport interface FactoryToolPermissionsSession {\n readonly identity: { getResourceId(): string };\n readonly permissions: {\n getRules(): PermissionRules;\n setForTool(args: { toolName: string; policy: PermissionPolicy }): Promise<void>;\n };\n}\n\nexport interface FactoryToolPermissionsDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n}\n\n/**\n * Allow the no-approval Factory tools on a newly created Factory session.\n * This only sets policy; which tools a turn actually gets is decided by the\n * tool scope. A person's own choices win: a per-tool policy is never\n * overwritten, and an explicit `mcp` category policy leaves everything alone.\n */\nexport async function applyFactoryToolPermissions(\n session: FactoryToolPermissionsSession,\n { sessions }: FactoryToolPermissionsDependencies,\n toolNames: readonly string[] = FACTORY_TOOLS_WITHOUT_APPROVAL,\n): Promise<void> {\n const row = await sessions.getBySessionId(session.identity.getResourceId());\n if (!row?.factoryProjectId) return;\n const rules = session.permissions.getRules();\n if (rules.categories.mcp !== undefined) return;\n for (const toolName of toolNames) {\n if (session.permissions.getRules().tools[toolName] !== undefined) continue;\n await session.permissions.setForTool({ toolName, policy: 'allow' });\n }\n}\n"],"mappings":";;;;;;AASA,MAAa,iCAAoD;CAC/D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAoBA,eAAsB,4BACpB,SACA,EAAE,YACF,YAA+B,gCAChB;CAEf,IAAI,EAAC,MADa,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC,EAAA,EAChE,kBAAkB;CAE5B,IADc,QAAQ,YAAY,SAC1B,CAAC,CAAC,WAAW,QAAQ,KAAA,GAAW;CACxC,KAAK,MAAM,YAAY,WAAW;EAChC,IAAI,QAAQ,YAAY,SAAS,CAAC,CAAC,MAAM,cAAc,KAAA,GAAW;EAClE,MAAM,QAAQ,YAAY,WAAW;GAAE;GAAU,QAAQ;EAAQ,CAAC;CACpE;AACF"}
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import type { BoardRegistry } from '../boards/index.js';
3
+ import type { IntegrationTools } from '../integrations/base.js';
4
+ import type { AuditStorage } from '../storage/domains/audit/base.js';
5
+ import type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';
6
+ import type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';
7
+ import type { FactoryToolScope } from './scope.js';
8
+ export declare const MAX_TEXT = 600;
9
+ export declare const MAX_ERROR = 400;
10
+ export declare const MAX_LIST = 50;
11
+ export interface FactoryReadToolDependencies {
12
+ workItems: Pick<WorkItemsStorage, 'list' | 'getForProject' | 'listRunBindings' | 'listDeferredDecisions' | 'listByIds' | 'listDecisionsForEvaluations' | 'listWorkItemSubscribers' | 'listSessionSubscriptions'>;
13
+ comments: Pick<WorkItemCommentsStorage, 'listRecent'>;
14
+ audit: Pick<AuditStorage, 'list'>;
15
+ boards: BoardRegistry;
16
+ }
17
+ export declare function truncate(text: string, max: number): string;
18
+ /** The number a person sees on the card: the issue or PR number the item was filed from. */
19
+ export declare function workItemNumber(item: Pick<WorkItemRow, 'metadata'>): number | null;
20
+ export declare function workItemStage(item: Pick<WorkItemRow, 'stages'>): string;
21
+ /** A work item id or its card number; anything else can't name an item. */
22
+ export declare const workItemRefSchema: z.ZodString;
23
+ /** Resolve `<uuid>` or `#123` to an item in this Factory only. Pass a ref validated by `workItemRefSchema`. */
24
+ export declare function findWorkItem(workItems: Pick<WorkItemsStorage, 'list' | 'getForProject'>, scope: Pick<FactoryToolScope, 'orgId' | 'factoryProjectId'>, ref: string): Promise<WorkItemRow | null>;
25
+ export declare function workItemNotFound(ref: string): Error;
26
+ export declare function createFactoryReadTools(scope: FactoryToolScope, deps: FactoryReadToolDependencies): IntegrationTools;
27
+ //# sourceMappingURL=read-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-tools.d.ts","sourceRoot":"","sources":["../../src/tools/read-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,WAAW,EACX,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,eAAO,MAAM,QAAQ,KAAK,CAAC;AAG3B,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,uBAAuB,GACvB,WAAW,GACX,6BAA6B,GAC7B,yBAAyB,GACzB,0BAA0B,CAC7B,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAC,CAAC;IACtD,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAClC,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAQD,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CAGjF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,MAAM,CAEvE;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,aAOyB,CAAC;AAExD,+GAA+G;AAC/G,wBAAsB,YAAY,CAChC,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,eAAe,CAAC,EAC3D,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,kBAAkB,CAAC,EAC3D,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ7B;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAEnD;AAkED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,2BAA2B,GAAG,gBAAgB,CA8MnH"}