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

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 (190) hide show
  1. package/README.md +103 -4
  2. package/dist/auth.d.ts +14 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +45 -9
  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 +8 -0
  16. package/dist/factory.d.ts.map +1 -1
  17. package/dist/factory.js +191 -16
  18. package/dist/factory.js.map +1 -1
  19. package/dist/integrations/github/integration.d.ts.map +1 -1
  20. package/dist/integrations/github/integration.js +37 -1
  21. package/dist/integrations/github/integration.js.map +1 -1
  22. package/dist/integrations/gitlab/api.d.ts +15 -0
  23. package/dist/integrations/gitlab/api.d.ts.map +1 -1
  24. package/dist/integrations/gitlab/api.js +9 -0
  25. package/dist/integrations/gitlab/api.js.map +1 -1
  26. package/dist/integrations/gitlab/version-control.d.ts.map +1 -1
  27. package/dist/integrations/gitlab/version-control.js +37 -3
  28. package/dist/integrations/gitlab/version-control.js.map +1 -1
  29. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  30. package/dist/integrations/platform/github/integration.js +27 -1
  31. package/dist/integrations/platform/github/integration.js.map +1 -1
  32. package/dist/integrations/subscription-session.d.ts +15 -0
  33. package/dist/integrations/subscription-session.d.ts.map +1 -1
  34. package/dist/integrations/subscription-session.js +30 -13
  35. package/dist/integrations/subscription-session.js.map +1 -1
  36. package/dist/mcp/disconnect.d.ts +10 -0
  37. package/dist/mcp/disconnect.d.ts.map +1 -0
  38. package/dist/mcp/disconnect.js +44 -0
  39. package/dist/mcp/disconnect.js.map +1 -0
  40. package/dist/mcp/routes.d.ts +46 -0
  41. package/dist/mcp/routes.d.ts.map +1 -0
  42. package/dist/mcp/routes.js +243 -0
  43. package/dist/mcp/routes.js.map +1 -0
  44. package/dist/provider-display-name.d.ts +2 -0
  45. package/dist/provider-display-name.d.ts.map +1 -0
  46. package/dist/provider-display-name.js +16 -0
  47. package/dist/provider-display-name.js.map +1 -0
  48. package/dist/routes/config.js +2 -2
  49. package/dist/routes/config.js.map +1 -1
  50. package/dist/routes/contracts.d.ts +4 -0
  51. package/dist/routes/contracts.d.ts.map +1 -1
  52. package/dist/routes/contracts.js +3 -1
  53. package/dist/routes/contracts.js.map +1 -1
  54. package/dist/routes/oauth.js +1 -1
  55. package/dist/routes/oauth.js.map +1 -1
  56. package/dist/routes/provider-credentials.d.ts +6 -2
  57. package/dist/routes/provider-credentials.d.ts.map +1 -1
  58. package/dist/routes/provider-credentials.js +20 -7
  59. package/dist/routes/provider-credentials.js.map +1 -1
  60. package/dist/routes/source-control-sessions.d.ts +1 -0
  61. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  62. package/dist/routes/source-control-sessions.js +1 -1
  63. package/dist/routes/source-control-sessions.js.map +1 -1
  64. package/dist/routes/surface.d.ts +1 -0
  65. package/dist/routes/surface.d.ts.map +1 -1
  66. package/dist/routes/surface.js +4 -3
  67. package/dist/routes/surface.js.map +1 -1
  68. package/dist/routes/work-items.d.ts.map +1 -1
  69. package/dist/routes/work-items.js +61 -121
  70. package/dist/routes/work-items.js.map +1 -1
  71. package/dist/rules/dispatcher.d.ts +7 -0
  72. package/dist/rules/dispatcher.d.ts.map +1 -1
  73. package/dist/rules/dispatcher.js +62 -5
  74. package/dist/rules/dispatcher.js.map +1 -1
  75. package/dist/rules/processor.d.ts.map +1 -1
  76. package/dist/rules/processor.js +2 -1
  77. package/dist/rules/processor.js.map +1 -1
  78. package/dist/rules/tools.d.ts.map +1 -1
  79. package/dist/rules/tools.js +82 -3
  80. package/dist/rules/tools.js.map +1 -1
  81. package/dist/rules/transition-service.d.ts +17 -0
  82. package/dist/rules/transition-service.d.ts.map +1 -1
  83. package/dist/rules/transition-service.js +25 -0
  84. package/dist/rules/transition-service.js.map +1 -1
  85. package/dist/rules/types.d.ts +2 -4
  86. package/dist/rules/types.d.ts.map +1 -1
  87. package/dist/rules/types.js +1 -4
  88. package/dist/rules/types.js.map +1 -1
  89. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
  90. package/dist/sandbox/local-factory-sandbox.js +1 -0
  91. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  92. package/dist/session/factory-session.d.ts +15 -1
  93. package/dist/session/factory-session.d.ts.map +1 -1
  94. package/dist/session/factory-session.js +4 -2
  95. package/dist/session/factory-session.js.map +1 -1
  96. package/dist/session/reply-credential-processor.d.ts +15 -0
  97. package/dist/session/reply-credential-processor.d.ts.map +1 -0
  98. package/dist/session/reply-credential-processor.js +40 -0
  99. package/dist/session/reply-credential-processor.js.map +1 -0
  100. package/dist/session/source-control-tools.d.ts +19 -1
  101. package/dist/session/source-control-tools.d.ts.map +1 -1
  102. package/dist/session/source-control-tools.js +70 -3
  103. package/dist/session/source-control-tools.js.map +1 -1
  104. package/dist/storage/domains/audit/domain.js +1 -1
  105. package/dist/storage/domains/projects/base.d.ts +6 -0
  106. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  107. package/dist/storage/domains/projects/base.js +12 -0
  108. package/dist/storage/domains/projects/base.js.map +1 -1
  109. package/dist/storage/domains/source-control/base.d.ts +17 -0
  110. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  111. package/dist/storage/domains/source-control/base.js +23 -0
  112. package/dist/storage/domains/source-control/base.js.map +1 -1
  113. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  114. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  115. package/dist/storage/domains/source-control/inmemory.js +5 -1
  116. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  117. package/dist/storage/domains/work-items/base.d.ts +56 -0
  118. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  119. package/dist/storage/domains/work-items/base.js +139 -25
  120. package/dist/storage/domains/work-items/base.js.map +1 -1
  121. package/dist/tools/annotations.d.ts +7 -0
  122. package/dist/tools/annotations.d.ts.map +1 -0
  123. package/dist/tools/annotations.js +102 -0
  124. package/dist/tools/annotations.js.map +1 -0
  125. package/dist/tools/approval-tools.d.ts +15 -0
  126. package/dist/tools/approval-tools.d.ts.map +1 -0
  127. package/dist/tools/approval-tools.js +77 -0
  128. package/dist/tools/approval-tools.js.map +1 -0
  129. package/dist/tools/deliver.d.ts +54 -0
  130. package/dist/tools/deliver.d.ts.map +1 -0
  131. package/dist/tools/deliver.js +105 -0
  132. package/dist/tools/deliver.js.map +1 -0
  133. package/dist/tools/index.d.ts +29 -0
  134. package/dist/tools/index.d.ts.map +1 -0
  135. package/dist/tools/index.js +39 -0
  136. package/dist/tools/index.js.map +1 -0
  137. package/dist/tools/messaging-tools.d.ts +28 -0
  138. package/dist/tools/messaging-tools.d.ts.map +1 -0
  139. package/dist/tools/messaging-tools.js +312 -0
  140. package/dist/tools/messaging-tools.js.map +1 -0
  141. package/dist/tools/permissions.d.ts +31 -0
  142. package/dist/tools/permissions.d.ts.map +1 -0
  143. package/dist/tools/permissions.js +44 -0
  144. package/dist/tools/permissions.js.map +1 -0
  145. package/dist/tools/read-tools.d.ts +27 -0
  146. package/dist/tools/read-tools.d.ts.map +1 -0
  147. package/dist/tools/read-tools.js +285 -0
  148. package/dist/tools/read-tools.js.map +1 -0
  149. package/dist/tools/reports.d.ts +163 -0
  150. package/dist/tools/reports.d.ts.map +1 -0
  151. package/dist/tools/reports.js +352 -0
  152. package/dist/tools/reports.js.map +1 -0
  153. package/dist/tools/run-watch.d.ts +46 -0
  154. package/dist/tools/run-watch.d.ts.map +1 -0
  155. package/dist/tools/run-watch.js +102 -0
  156. package/dist/tools/run-watch.js.map +1 -0
  157. package/dist/tools/scope.d.ts +45 -0
  158. package/dist/tools/scope.d.ts.map +1 -0
  159. package/dist/tools/scope.js +73 -0
  160. package/dist/tools/scope.js.map +1 -0
  161. package/dist/tools/session-tools.d.ts +31 -0
  162. package/dist/tools/session-tools.d.ts.map +1 -0
  163. package/dist/tools/session-tools.js +312 -0
  164. package/dist/tools/session-tools.js.map +1 -0
  165. package/dist/tools/sessions-state-processor.d.ts +37 -0
  166. package/dist/tools/sessions-state-processor.d.ts.map +1 -0
  167. package/dist/tools/sessions-state-processor.js +206 -0
  168. package/dist/tools/sessions-state-processor.js.map +1 -0
  169. package/dist/tools/work-item-tools.d.ts +28 -0
  170. package/dist/tools/work-item-tools.d.ts.map +1 -0
  171. package/dist/tools/work-item-tools.js +322 -0
  172. package/dist/tools/work-item-tools.js.map +1 -0
  173. package/dist/work-items/create-work-item.d.ts +55 -0
  174. package/dist/work-items/create-work-item.d.ts.map +1 -0
  175. package/dist/work-items/create-work-item.js +101 -0
  176. package/dist/work-items/create-work-item.js.map +1 -0
  177. package/dist/work-items/decision-actions.d.ts +42 -0
  178. package/dist/work-items/decision-actions.d.ts.map +1 -0
  179. package/dist/work-items/decision-actions.js +70 -0
  180. package/dist/work-items/decision-actions.js.map +1 -0
  181. package/dist/workspace.d.ts.map +1 -1
  182. package/dist/workspace.js +3 -1
  183. package/dist/workspace.js.map +1 -1
  184. package/factory-skills/factory-gitlab-rereview/SKILL.md +1 -1
  185. package/factory-skills/factory-gitlab-review/SKILL.md +1 -1
  186. package/factory-skills/factory-plan/SKILL.md +4 -4
  187. package/factory-skills/factory-rereview/SKILL.md +3 -1
  188. package/factory-skills/factory-review/SKILL.md +3 -1
  189. package/factory-skills/factory-self-review/SKILL.md +46 -0
  190. package/package.json +13 -11
@@ -0,0 +1,206 @@
1
+ import { boardForWorkItem } from "../boards/semantics.js";
2
+ import "../boards/index.js";
3
+ import { FACTORY_SESSION_NOTIFICATION_SOURCE } from "./deliver.js";
4
+ import { workItemNumber } from "./read-tools.js";
5
+ import { resolveFactoryToolScope } from "./scope.js";
6
+ import { createHash } from "crypto";
7
+ //#region src/tools/sessions-state-processor.ts
8
+ const STATE_ID = "factory-sessions";
9
+ /** Finished side sessions and items that reached a terminal stage drop out after this long. */
10
+ const FACTORY_SESSIONS_MAX_AGE_MS = 1440 * 60 * 1e3;
11
+ /** Rows shown before the rest collapse into "+N more". */
12
+ const FACTORY_SESSIONS_MAX_ROWS = 20;
13
+ const NOTIFICATION_SCAN = 100;
14
+ const EMPTY_CACHE_KEY = "empty";
15
+ function escapeText(value) {
16
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17
+ }
18
+ function cacheKeyFor(value) {
19
+ return `sessions:${createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
20
+ }
21
+ function report(record) {
22
+ return record ? {
23
+ kind: record.kind,
24
+ at: record.createdAt.toISOString()
25
+ } : null;
26
+ }
27
+ function newest(records, match) {
28
+ let found;
29
+ for (const record of records) if (match(record) && (!found || record.createdAt > found.createdAt)) found = record;
30
+ return found;
31
+ }
32
+ function itemLabel(item) {
33
+ const number = workItemNumber(item);
34
+ return number === null ? item.id : `#${number}`;
35
+ }
36
+ /**
37
+ * The `<factory-sessions>` state signal. A user session sees the side sessions
38
+ * it started and the work items it follows; a side session or item worker sees
39
+ * who it reports to. Sessions it only messaged never appear. Silent until there
40
+ * is something to show; when the last entry ages out it sends one final empty
41
+ * snapshot, then stays silent.
42
+ */
43
+ var FactorySessionsStateProcessor = class {
44
+ id = STATE_ID;
45
+ stateId = STATE_ID;
46
+ #deps;
47
+ constructor(deps) {
48
+ this.#deps = deps;
49
+ }
50
+ async computeStateSignal(args) {
51
+ const scope = await resolveFactoryToolScope({
52
+ requestContext: args.requestContext,
53
+ authEnabled: this.#deps.authEnabled,
54
+ sessions: this.#deps.sessions,
55
+ projects: this.#deps.projects,
56
+ workItems: this.#deps.workItems
57
+ });
58
+ if (!scope) return;
59
+ const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;
60
+ const previousKey = hasBase ? args.tracking?.currentCacheKey ?? (args.lastSnapshot?.metadata?.state)?.cacheKey : void 0;
61
+ const unchanged = (cacheKey) => previousKey === cacheKey;
62
+ const cleared = () => hasBase && previousKey !== void 0 && previousKey !== EMPTY_CACHE_KEY;
63
+ if (scope.kind === "worker") {
64
+ const contents = await this.#workerLine(scope);
65
+ if (!contents) return cleared() ? this.#signal(EMPTY_CACHE_KEY, "This session is not reporting on any work now.", {}, { entries: 0 }) : void 0;
66
+ const cacheKey = cacheKeyFor(contents);
67
+ if (unchanged(cacheKey)) return;
68
+ return this.#signal(cacheKey, escapeText(contents), { worker: contents }, { entries: 1 });
69
+ }
70
+ const state = await this.#userState(scope);
71
+ if (state.sideSessions.length === 0 && state.following.length === 0) {
72
+ if (!cleared()) return;
73
+ return this.#signal(EMPTY_CACHE_KEY, "No sub-sessions or followed work items.", state, { entries: 0 });
74
+ }
75
+ const cacheKey = cacheKeyFor(state);
76
+ if (unchanged(cacheKey)) return;
77
+ return this.#signal(cacheKey, this.#render(state), state, {
78
+ sideSessions: state.sideSessions.length,
79
+ following: state.following.length
80
+ });
81
+ }
82
+ #signal(cacheKey, contents, value, attributes) {
83
+ return {
84
+ id: STATE_ID,
85
+ cacheKey,
86
+ mode: "snapshot",
87
+ tagName: "factory-sessions",
88
+ contents,
89
+ value: { sessions: value },
90
+ attributes,
91
+ metadata: { value: { sessions: value } }
92
+ };
93
+ }
94
+ async #workerLine(scope) {
95
+ if (scope.origin) return `Started by session ${scope.origin.sessionId} (thread ${scope.origin.threadId}). Report back with factory_message_origin.`;
96
+ const binding = await this.#deps.workItems.findActiveRunBindingForSession({
97
+ orgId: scope.orgId,
98
+ factoryProjectId: scope.factoryProjectId,
99
+ sessionId: scope.sessionId
100
+ });
101
+ if (!binding) return null;
102
+ const item = await this.#deps.workItems.getForProject(scope.orgId, scope.factoryProjectId, binding.workItemId);
103
+ const following = (await this.#deps.workItems.listWorkItemSubscribers({
104
+ orgId: scope.orgId,
105
+ workItemId: binding.workItemId
106
+ })).filter((subscriber) => subscriber.sessionId !== scope.sessionId).length;
107
+ if (following === 0) return null;
108
+ return `Working on ${item ? itemLabel(item) : binding.workItemId} as ${binding.role}. Reports go to ${following} following session${following === 1 ? "" : "s"}.`;
109
+ }
110
+ async #userState(scope) {
111
+ const now = this.#deps.now?.() ?? Date.now();
112
+ const notifications = await this.#deps.notifications();
113
+ const records = notifications ? await notifications.listNotifications({
114
+ threadId: scope.threadId,
115
+ source: FACTORY_SESSION_NOTIFICATION_SOURCE,
116
+ limit: NOTIFICATION_SCAN
117
+ }) : [];
118
+ const sideRows = await this.#deps.sessions.listByOrigin({
119
+ orgId: scope.orgId,
120
+ originSessionId: scope.sessionId
121
+ });
122
+ const sideSessions = [];
123
+ for (const row of sideRows) {
124
+ if (row.factoryProjectId !== scope.factoryProjectId) continue;
125
+ const last = newest(records, (record) => record.attributes?.["from-session"] === row.sessionId);
126
+ const running = await this.#deps.isRunning(row.sessionId);
127
+ const lastActivity = last?.createdAt.getTime() ?? row.updatedAt.getTime();
128
+ if (!running && now - lastActivity > 864e5) continue;
129
+ const state = running ? "running" : last?.kind === "blocked" ? "parked" : "idle";
130
+ sideSessions.push({
131
+ sessionId: row.sessionId,
132
+ title: row.title ?? row.sessionId,
133
+ state,
134
+ lastReport: report(last),
135
+ active: state !== "idle"
136
+ });
137
+ }
138
+ const subscriptions = await this.#deps.workItems.listSessionSubscriptions({
139
+ orgId: scope.orgId,
140
+ factoryProjectId: scope.factoryProjectId,
141
+ sessionId: scope.sessionId
142
+ });
143
+ const following = [];
144
+ for (const subscription of subscriptions) {
145
+ const item = await this.#deps.workItems.getForProject(scope.orgId, scope.factoryProjectId, subscription.workItemId);
146
+ if (!item) continue;
147
+ const stage = item.stages[0] ?? "unknown";
148
+ const terminal = this.#deps.boards.get(boardForWorkItem(item))?.isTerminal(stage) ?? false;
149
+ if (terminal && now - item.updatedAt.getTime() > 864e5) continue;
150
+ const active = (await this.#deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id)).find((binding) => binding.status === "active");
151
+ const label = itemLabel(item);
152
+ const last = newest(records, (record) => record.attributes?.["work-item"] === label || record.kind === "stage" && record.sourceId === item.id);
153
+ const running = active ? await this.#deps.isRunning(active.sessionId) : false;
154
+ const lastFromWorker = active ? newest(records, (record) => record.attributes?.["from-session"] === active.sessionId) : void 0;
155
+ following.push({
156
+ workItemId: item.id,
157
+ label,
158
+ title: item.title,
159
+ stage,
160
+ worker: active ? {
161
+ role: active.role,
162
+ sessionId: active.sessionId,
163
+ state: running ? "running" : lastFromWorker?.kind === "blocked" ? "parked" : "idle"
164
+ } : null,
165
+ lastReport: report(last),
166
+ active: running && !terminal
167
+ });
168
+ }
169
+ const ordered = [...sideSessions.map((row) => ({
170
+ section: "side",
171
+ row
172
+ })), ...following.map((row) => ({
173
+ section: "following",
174
+ row
175
+ }))].sort((a, b) => Number(b.row.active) - Number(a.row.active));
176
+ const shown = ordered.slice(0, 20);
177
+ const strip = ({ active: _active, ...row }) => row;
178
+ return {
179
+ sideSessions: shown.flatMap((entry) => entry.section === "side" ? [strip(entry.row)] : []),
180
+ following: shown.flatMap((entry) => entry.section === "following" ? [strip(entry.row)] : []),
181
+ more: ordered.length - shown.length
182
+ };
183
+ }
184
+ #render(state) {
185
+ const lastReport = (last) => last ? `; last report ${last.kind} at ${last.at}` : "";
186
+ const lines = [];
187
+ if (state.sideSessions.length > 0) {
188
+ lines.push("Side sessions you started:");
189
+ for (const row of state.sideSessions) lines.push(`- "${escapeText(row.title)}" (session ${row.sessionId}): ${row.state}${lastReport(row.lastReport)}`);
190
+ }
191
+ if (state.following.length > 0) {
192
+ lines.push("Work items you follow:");
193
+ for (const row of state.following) {
194
+ const worker = row.worker ? `; ${escapeText(row.worker.role)} session ${row.worker.sessionId} ${row.worker.state}` : "";
195
+ lines.push(`- ${escapeText(row.label)} "${escapeText(row.title)}": ${escapeText(row.stage)}${worker}${lastReport(row.lastReport)}`);
196
+ }
197
+ }
198
+ if (state.more > 0) lines.push(`+${state.more} more`);
199
+ lines.push("Message any of these sessions with factory_signal_session; read full reports with notification_inbox.");
200
+ return lines.join("\n");
201
+ }
202
+ };
203
+ //#endregion
204
+ export { FACTORY_SESSIONS_MAX_AGE_MS, FACTORY_SESSIONS_MAX_ROWS, FactorySessionsStateProcessor };
205
+
206
+ //# sourceMappingURL=sessions-state-processor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions-state-processor.js","names":["#deps","#workerLine","#signal","#userState","#render"],"sources":["../../src/tools/sessions-state-processor.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { NotificationRecord, NotificationsStorage } from '@mastra/core/notifications';\nimport type { ComputeStateSignalArgs, ComputeStateSignalResult, Processor } from '@mastra/core/processors';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { FACTORY_SESSION_NOTIFICATION_SOURCE } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\nimport { resolveFactoryToolScope } from './scope.js';\nimport type { FactoryToolScope, ResolveFactoryToolScopeOptions } from './scope.js';\n\nconst STATE_ID = 'factory-sessions';\n/** Finished side sessions and items that reached a terminal stage drop out after this long. */\nexport const FACTORY_SESSIONS_MAX_AGE_MS = 24 * 60 * 60 * 1000;\n/** Rows shown before the rest collapse into \"+N more\". */\nexport const FACTORY_SESSIONS_MAX_ROWS = 20;\nconst NOTIFICATION_SCAN = 100;\nconst EMPTY_CACHE_KEY = 'empty';\n\nexport type FactorySessionRunState = 'running' | 'idle' | 'parked';\n\nexport interface FactorySessionsStateDependencies {\n authEnabled: boolean;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin'>;\n projects: ResolveFactoryToolScopeOptions['projects'];\n workItems: Pick<\n WorkItemsStorage,\n | 'hasRunBindingForSession'\n | 'listSessionSubscriptions'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'listWorkItemSubscribers'\n >;\n boards: BoardRegistry;\n /** Whether the session's agent is mid-run in this process; `idle` when it isn't live here. */\n isRunning: (sessionId: string) => Promise<boolean>;\n notifications: () => Promise<Pick<NotificationsStorage, 'listNotifications'> | null>;\n now?: () => number;\n}\n\ninterface LastReport {\n kind: string;\n at: string;\n}\n\ninterface SideSessionRow {\n sessionId: string;\n title: string;\n state: FactorySessionRunState;\n lastReport: LastReport | null;\n}\n\ninterface FollowedRow {\n workItemId: string;\n label: string;\n title: string;\n stage: string;\n worker: { role: string; sessionId: string; state: FactorySessionRunState } | null;\n lastReport: LastReport | null;\n}\n\ninterface SessionsState {\n sideSessions: SideSessionRow[];\n following: FollowedRow[];\n more: number;\n}\n\nfunction escapeText(value: string): string {\n return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n}\n\nfunction cacheKeyFor(value: unknown): string {\n return `sessions:${createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16)}`;\n}\n\nfunction report(record: NotificationRecord | undefined): LastReport | null {\n return record ? { kind: record.kind, at: record.createdAt.toISOString() } : null;\n}\n\nfunction newest(records: NotificationRecord[], match: (record: NotificationRecord) => boolean) {\n let found: NotificationRecord | undefined;\n for (const record of records) {\n if (match(record) && (!found || record.createdAt > found.createdAt)) found = record;\n }\n return found;\n}\n\nfunction itemLabel(item: WorkItemRow): string {\n const number = workItemNumber(item);\n return number === null ? item.id : `#${number}`;\n}\n\n/**\n * The `<factory-sessions>` state signal. A user session sees the side sessions\n * it started and the work items it follows; a side session or item worker sees\n * who it reports to. Sessions it only messaged never appear. Silent until there\n * is something to show; when the last entry ages out it sends one final empty\n * snapshot, then stays silent.\n */\nexport class FactorySessionsStateProcessor implements Processor<'factory-sessions'> {\n readonly id = STATE_ID;\n readonly stateId = STATE_ID;\n readonly #deps: FactorySessionsStateDependencies;\n\n constructor(deps: FactorySessionsStateDependencies) {\n this.#deps = deps;\n }\n\n async computeStateSignal(args: ComputeStateSignalArgs): Promise<ComputeStateSignalResult> {\n const scope = await resolveFactoryToolScope({\n requestContext: args.requestContext!,\n authEnabled: this.#deps.authEnabled,\n sessions: this.#deps.sessions,\n projects: this.#deps.projects,\n workItems: this.#deps.workItems,\n });\n if (!scope) return;\n\n const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;\n const previousKey = hasBase\n ? (args.tracking?.currentCacheKey ?? (args.lastSnapshot?.metadata?.state as { cacheKey?: string })?.cacheKey)\n : undefined;\n const unchanged = (cacheKey: string) => previousKey === cacheKey;\n // Silence rule: one final empty snapshot replaces a non-empty one, then nothing.\n const cleared = () => hasBase && previousKey !== undefined && previousKey !== EMPTY_CACHE_KEY;\n\n if (scope.kind === 'worker') {\n const contents = await this.#workerLine(scope);\n if (!contents) {\n return cleared()\n ? this.#signal(EMPTY_CACHE_KEY, 'This session is not reporting on any work now.', {}, { entries: 0 })\n : undefined;\n }\n const cacheKey = cacheKeyFor(contents);\n if (unchanged(cacheKey)) return;\n return this.#signal(cacheKey, escapeText(contents), { worker: contents }, { entries: 1 });\n }\n\n const state = await this.#userState(scope);\n if (state.sideSessions.length === 0 && state.following.length === 0) {\n if (!cleared()) return;\n return this.#signal(EMPTY_CACHE_KEY, 'No sub-sessions or followed work items.', state, { entries: 0 });\n }\n const cacheKey = cacheKeyFor(state);\n if (unchanged(cacheKey)) return;\n return this.#signal(cacheKey, this.#render(state), state, {\n sideSessions: state.sideSessions.length,\n following: state.following.length,\n });\n }\n\n #signal(cacheKey: string, contents: string, value: unknown, attributes: Record<string, number>) {\n return {\n id: STATE_ID,\n cacheKey,\n mode: 'snapshot' as const,\n tagName: 'factory-sessions',\n contents,\n value: { sessions: value },\n attributes,\n metadata: { value: { sessions: value } },\n };\n }\n\n async #workerLine(scope: FactoryToolScope): Promise<string | null> {\n if (scope.origin) {\n return `Started by session ${scope.origin.sessionId} (thread ${scope.origin.threadId}). Report back with factory_message_origin.`;\n }\n const binding = await this.#deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n if (!binding) return null;\n const item = await this.#deps.workItems.getForProject(scope.orgId, scope.factoryProjectId, binding.workItemId);\n const subscribers = await this.#deps.workItems.listWorkItemSubscribers({\n orgId: scope.orgId,\n workItemId: binding.workItemId,\n });\n const following = subscribers.filter(subscriber => subscriber.sessionId !== scope.sessionId).length;\n // Nobody to report to: nothing worth showing (most board-started workers).\n if (following === 0) return null;\n const label = item ? itemLabel(item) : binding.workItemId;\n return `Working on ${label} as ${binding.role}. Reports go to ${following} following session${following === 1 ? '' : 's'}.`;\n }\n\n async #userState(scope: FactoryToolScope): Promise<SessionsState> {\n const now = this.#deps.now?.() ?? Date.now();\n const notifications = await this.#deps.notifications();\n const records = notifications\n ? await notifications.listNotifications({\n threadId: scope.threadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: NOTIFICATION_SCAN,\n })\n : [];\n\n const sideRows = await this.#deps.sessions.listByOrigin({ orgId: scope.orgId, originSessionId: scope.sessionId });\n const sideSessions: Array<SideSessionRow & { active: boolean }> = [];\n for (const row of sideRows) {\n if (row.factoryProjectId !== scope.factoryProjectId) continue;\n const last = newest(records, record => record.attributes?.['from-session'] === row.sessionId);\n const running = await this.#deps.isRunning(row.sessionId);\n const lastActivity = last?.createdAt.getTime() ?? row.updatedAt.getTime();\n if (!running && now - lastActivity > FACTORY_SESSIONS_MAX_AGE_MS) continue;\n const state: FactorySessionRunState = running ? 'running' : last?.kind === 'blocked' ? 'parked' : 'idle';\n sideSessions.push({\n sessionId: row.sessionId,\n title: row.title ?? row.sessionId,\n state,\n lastReport: report(last),\n active: state !== 'idle',\n });\n }\n\n const subscriptions = await this.#deps.workItems.listSessionSubscriptions({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n const following: Array<FollowedRow & { active: boolean }> = [];\n for (const subscription of subscriptions) {\n const item = await this.#deps.workItems.getForProject(\n scope.orgId,\n scope.factoryProjectId,\n subscription.workItemId,\n );\n if (!item) continue;\n const stage = item.stages[0] ?? 'unknown';\n const terminal = this.#deps.boards.get(boardForWorkItem(item))?.isTerminal(stage) ?? false;\n if (terminal && now - item.updatedAt.getTime() > FACTORY_SESSIONS_MAX_AGE_MS) continue;\n const bindings = await this.#deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const active = bindings.find(binding => binding.status === 'active');\n const label = itemLabel(item);\n const last = newest(\n records,\n record =>\n record.attributes?.['work-item'] === label || (record.kind === 'stage' && record.sourceId === item.id),\n );\n const running = active ? await this.#deps.isRunning(active.sessionId) : false;\n const lastFromWorker = active\n ? newest(records, record => record.attributes?.['from-session'] === active.sessionId)\n : undefined;\n following.push({\n workItemId: item.id,\n label,\n title: item.title,\n stage,\n worker: active\n ? {\n role: active.role,\n sessionId: active.sessionId,\n state: running ? 'running' : lastFromWorker?.kind === 'blocked' ? 'parked' : 'idle',\n }\n : null,\n lastReport: report(last),\n active: running && !terminal,\n });\n }\n\n // Active entries first, then within the cap; the rest collapse into \"+N more\".\n const ordered = [\n ...sideSessions.map(row => ({ section: 'side' as const, row })),\n ...following.map(row => ({ section: 'following' as const, row })),\n ].sort((a, b) => Number(b.row.active) - Number(a.row.active));\n const shown = ordered.slice(0, FACTORY_SESSIONS_MAX_ROWS);\n const strip = <T extends { active: boolean }>({ active: _active, ...row }: T) => row;\n return {\n sideSessions: shown.flatMap(entry =>\n entry.section === 'side' ? [strip(entry.row as SideSessionRow & { active: boolean })] : [],\n ),\n following: shown.flatMap(entry =>\n entry.section === 'following' ? [strip(entry.row as FollowedRow & { active: boolean })] : [],\n ),\n more: ordered.length - shown.length,\n };\n }\n\n #render(state: SessionsState): string {\n const lastReport = (last: LastReport | null) => (last ? `; last report ${last.kind} at ${last.at}` : '');\n const lines: string[] = [];\n if (state.sideSessions.length > 0) {\n lines.push('Side sessions you started:');\n for (const row of state.sideSessions) {\n lines.push(\n `- \"${escapeText(row.title)}\" (session ${row.sessionId}): ${row.state}${lastReport(row.lastReport)}`,\n );\n }\n }\n if (state.following.length > 0) {\n lines.push('Work items you follow:');\n for (const row of state.following) {\n const worker = row.worker\n ? `; ${escapeText(row.worker.role)} session ${row.worker.sessionId} ${row.worker.state}`\n : '';\n lines.push(\n `- ${escapeText(row.label)} \"${escapeText(row.title)}\": ${escapeText(row.stage)}${worker}${lastReport(row.lastReport)}`,\n );\n }\n }\n if (state.more > 0) lines.push(`+${state.more} more`);\n lines.push('Message any of these sessions with factory_signal_session; read full reports with notification_inbox.');\n return lines.join('\\n');\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Factory tools that change work items: file one, move it, settle its
3
+ * decisions, and follow it. They act as the session's person (a human actor),
4
+ * through the same code the board's routes use.
5
+ */
6
+ import type { BoardRegistry } from '../boards/index.js';
7
+ import type { IntegrationTools } from '../integrations/base.js';
8
+ import type { FactoryTransitionService } from '../rules/transition-service.js';
9
+ import type { SourceControlSessionLookup } from '../session/factory-session.js';
10
+ import type { AuditRecorder } from '../storage/domains/audit/domain.js';
11
+ import type { CommentsDomain } from '../storage/domains/comments/domain.js';
12
+ import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
13
+ import type { FactoryToolScope } from './scope.js';
14
+ export interface FactoryWorkItemToolDependencies {
15
+ workItems: WorkItemsStorage;
16
+ boards: BoardRegistry;
17
+ /** Governed stage moves; filing and moving items refuse while it is unavailable. */
18
+ transitionService: Pick<FactoryTransitionService, 'transition'> | undefined;
19
+ /** Writes audit rows with an explicit actor; tool calls have no HTTP request to read one from. */
20
+ auditRecorder: AuditRecorder;
21
+ /** The feed's front door: validates, touches open boards and mirrors to linked channels. */
22
+ feed: Pick<CommentsDomain, 'createComment'>;
23
+ /** Finds the caller session's source-control provider, whose linked repositories a card can name. */
24
+ sessions: Pick<SourceControlSessionLookup, 'getSourceControlBySessionId'>;
25
+ }
26
+ /** The work-item write and subscription tools for one user session's turn. */
27
+ export declare function createFactoryWorkItemTools(scope: FactoryToolScope, deps: FactoryWorkItemToolDependencies): IntegrationTools;
28
+ //# sourceMappingURL=work-item-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"work-item-tools.d.ts","sourceRoot":"","sources":["../../src/tools/work-item-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC/E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,KAAK,EAAoC,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAkBhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,oFAAoF;IACpF,iBAAiB,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,GAAG,SAAS,CAAC;IAC5E,kGAAkG;IAClG,aAAa,EAAE,aAAa,CAAC;IAC7B,4FAA4F;IAC5F,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAC5C,qGAAqG;IACrG,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,6BAA6B,CAAC,CAAC;CAC3E;AA2BD,8EAA8E;AAC9E,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,+BAA+B,GACpC,gBAAgB,CA6RlB"}
@@ -0,0 +1,322 @@
1
+ import { boardForWorkItem } from "../boards/semantics.js";
2
+ import "../boards/index.js";
3
+ import { WorkItemRelationError } from "../storage/domains/work-items/base.js";
4
+ import { findWorkItem, truncate, workItemNotFound, workItemNumber, workItemRefSchema, workItemStage } from "./read-tools.js";
5
+ import { resolveWorkItemRepository } from "../session/work-item-repository.js";
6
+ import { transitionBodySchema } from "../routes/contracts.js";
7
+ import { createFactoryWorkItem } from "../work-items/create-work-item.js";
8
+ import { dismissFactoryDecision, resolveFactoryProposal, retryFactoryDecision } from "../work-items/decision-actions.js";
9
+ import { randomUUID } from "crypto";
10
+ import { z } from "zod";
11
+ import { createTool } from "@mastra/core/tools";
12
+ //#region src/tools/work-item-tools.ts
13
+ /**
14
+ * Factory tools that change work items: file one, move it, settle its
15
+ * decisions, and follow it. They act as the session's person (a human actor),
16
+ * through the same code the board's routes use.
17
+ */
18
+ const repositorySchema = z.string().trim().min(3).max(200).regex(/^[^/\s]+\/[^/\s]+$/, "Use the owner/name form, e.g. \"mastra-ai/mastra\".").describe("A repository linked to this Factory, as owner/name.");
19
+ const decisionId = z.string().trim().uuid().describe("Decision id, from factory_list_attention or inspect.");
20
+ function briefItem(item) {
21
+ return {
22
+ id: item.id,
23
+ number: workItemNumber(item),
24
+ title: truncate(item.title, 600),
25
+ stage: workItemStage(item)
26
+ };
27
+ }
28
+ function settled(result) {
29
+ if (!result.ok) throw new Error(result.message);
30
+ const { decision } = result;
31
+ return {
32
+ decisionId: decision.id,
33
+ status: decision.status,
34
+ workItemId: decision.workItemId
35
+ };
36
+ }
37
+ /** The work-item write and subscription tools for one user session's turn. */
38
+ function createFactoryWorkItemTools(scope, deps) {
39
+ const project = {
40
+ orgId: scope.orgId,
41
+ factoryProjectId: scope.factoryProjectId
42
+ };
43
+ const audit = async (input) => {
44
+ await deps.auditRecorder.record({
45
+ orgId: scope.orgId,
46
+ actorId: scope.userId,
47
+ actorType: "human",
48
+ ...scope.actorProfile ? { actorProfile: scope.actorProfile } : {},
49
+ ...input
50
+ });
51
+ };
52
+ const requireItem = async (ref) => {
53
+ const item = await findWorkItem(deps.workItems, scope, ref);
54
+ if (!item) throw workItemNotFound(ref);
55
+ return item;
56
+ };
57
+ /**
58
+ * Check `slug` is a repository linked to this Factory and that a card with
59
+ * `metadata` would run in it (a card synced from an issue follows the
60
+ * issue's repository, which a slug can't override).
61
+ */
62
+ const requireRepository = async (slug, metadata = null) => {
63
+ const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);
64
+ if (!sourceControl) throw new Error("This session has no source-control provider to look repositories up in.");
65
+ const resolve = (repository) => resolveWorkItemRepository({
66
+ sourceControl,
67
+ ...project,
68
+ item: { metadata: repository ? {
69
+ ...metadata,
70
+ repository
71
+ } : metadata }
72
+ });
73
+ const resolution = await resolve(slug);
74
+ if (resolution.status === "resolved" && resolution.slug === slug) return;
75
+ if (resolution.status === "resolved") throw new Error(`This card runs in ${resolution.slug}, which comes from its linked issue and can't be changed.`);
76
+ const linked = await resolve();
77
+ const candidates = linked.status === "ambiguous" ? linked.candidates : linked.status === "resolved" ? [linked.slug] : [];
78
+ throw new Error(candidates.length > 0 ? `${slug} is not linked to this Factory. Linked repositories: ${candidates.join(", ")}.` : `${slug} is not linked to this Factory, and it has no linked repositories.`);
79
+ };
80
+ const follow = (item, reason) => deps.workItems.subscribeToWorkItem({
81
+ ...project,
82
+ workItemId: item.id,
83
+ sessionId: scope.sessionId,
84
+ threadId: scope.threadId,
85
+ resourceId: scope.resourceId,
86
+ userId: scope.userId,
87
+ reason
88
+ });
89
+ return {
90
+ factory_create_work_item: createTool({
91
+ id: "factory_create_work_item",
92
+ description: "File a new work item on this Factory's Work board for the person you are working with. It lands in the board's first phase, exactly like pressing New card, and this session follows it. Check factory_list_work_items for an existing item first. Name the repository when the Factory links more than one; runs refuse to start on a card that doesn't say which.",
93
+ inputSchema: z.object({
94
+ title: z.string().trim().min(1).max(512).describe("Short title, as it should read on the card."),
95
+ brief: z.string().trim().min(1).max(8e3).describe("What needs doing and why; posted as the first comment on the card."),
96
+ repository: repositorySchema.optional()
97
+ }).strict(),
98
+ execute: async ({ title, brief, repository }) => {
99
+ if (repository) await requireRepository(repository);
100
+ const created = await createFactoryWorkItem({
101
+ workItems: deps.workItems,
102
+ boards: deps.boards,
103
+ transitionService: deps.transitionService,
104
+ audit,
105
+ ...project,
106
+ userId: scope.userId,
107
+ ...scope.actorProfile ? { origin: { actorProfile: scope.actorProfile } } : {},
108
+ input: {
109
+ title,
110
+ ...repository ? { metadata: { repository } } : {}
111
+ }
112
+ });
113
+ if (created.status === "refused") throw new Error(created.message);
114
+ if (created.status === "rejected") throw new Error(`The board refused the new item: ${created.reason}`);
115
+ const { item } = created;
116
+ const warnings = [];
117
+ try {
118
+ const comment = await deps.feed.createComment({
119
+ orgId: scope.orgId,
120
+ workItemId: item.id,
121
+ author: {
122
+ kind: "user",
123
+ id: scope.userId
124
+ },
125
+ body: brief
126
+ });
127
+ if (comment.status !== "created") warnings.push(`The brief was not posted (${comment.status === "invalid" ? comment.message : comment.status}).`);
128
+ } catch (error) {
129
+ warnings.push(`The brief was not posted: ${error instanceof Error ? error.message : String(error)}`);
130
+ }
131
+ try {
132
+ await follow(item, "created");
133
+ } catch (error) {
134
+ warnings.push(`This session does not follow it yet: ${error instanceof Error ? error.message : String(error)}`);
135
+ }
136
+ return warnings.length ? {
137
+ ...briefItem(item),
138
+ warnings
139
+ } : briefItem(item);
140
+ }
141
+ }),
142
+ factory_transition_work_item: createTool({
143
+ id: "factory_transition_work_item",
144
+ description: "Move a work item to another stage of its board, as the person you are working with. The board's rules decide whether the move is allowed and what it starts.",
145
+ inputSchema: z.object({
146
+ workItem: workItemRefSchema,
147
+ stage: z.string().trim().min(1).max(64).describe("Destination stage, e.g. \"planning\" or \"done\".")
148
+ }).strict(),
149
+ execute: async ({ workItem, stage }) => {
150
+ if (!deps.transitionService) throw new Error("Factory stage transitions are not available right now.");
151
+ const item = await requireItem(workItem);
152
+ const parsed = transitionBodySchema.safeParse({
153
+ board: boardForWorkItem(item),
154
+ stage,
155
+ expectedRevision: item.revision,
156
+ requestId: randomUUID(),
157
+ cause: "factory_tools"
158
+ });
159
+ if (!parsed.success) throw new Error(`"${stage}" is not a valid stage name.`);
160
+ const result = await deps.transitionService.transition({
161
+ ...parsed.data,
162
+ ...project,
163
+ workItemId: item.id,
164
+ actor: {
165
+ type: "human",
166
+ id: scope.userId
167
+ },
168
+ ...scope.actorProfile ? { actorProfile: scope.actorProfile } : {},
169
+ ingress: {
170
+ ...parsed.data.ingress,
171
+ identity: `human:${scope.userId}:${parsed.data.ingress.identity}`
172
+ }
173
+ });
174
+ if (result.status === "rejected") throw new Error(`The move was refused (${result.code}): ${result.reason}`);
175
+ return {
176
+ workItemId: item.id,
177
+ stage: result.stage,
178
+ revision: result.revision
179
+ };
180
+ }
181
+ }),
182
+ factory_update_work_item: createTool({
183
+ id: "factory_update_work_item",
184
+ description: "Edit a work item's fields as the person you are working with: its title, the repository its runs use, its parent item, or pre-approving its plans. Move stages with factory_transition_work_item and start roles with factory_start_session; post to the card's feed for anything else.",
185
+ inputSchema: z.object({
186
+ workItem: workItemRefSchema,
187
+ title: z.string().trim().min(1).max(500).optional().describe("New card title."),
188
+ repository: repositorySchema.optional(),
189
+ parentWorkItem: workItemRefSchema.nullable().optional().describe("Parent work item (id or #number), or null to detach it from its parent."),
190
+ plansPreapproved: z.literal(true).optional().describe("Pre-approve the item's plans, so its runs no longer stop for plan approval. Can't be undone here.")
191
+ }).strict().refine((input) => Object.keys(input).some((key) => key !== "workItem"), "Pass at least one field to change."),
192
+ execute: async ({ workItem, title, repository, parentWorkItem, plansPreapproved }) => {
193
+ const item = await requireItem(workItem);
194
+ if (repository) await requireRepository(repository, item.metadata);
195
+ let parentWorkItemId;
196
+ if (parentWorkItem === null) parentWorkItemId = null;
197
+ else if (parentWorkItem !== void 0) parentWorkItemId = (await requireItem(parentWorkItem)).id;
198
+ const patch = {
199
+ ...title !== void 0 ? { title } : {},
200
+ ...repository ? { metadata: { repository } } : {},
201
+ ...parentWorkItemId !== void 0 ? { parentWorkItemId } : {},
202
+ ...plansPreapproved ? { plansPreapproved } : {}
203
+ };
204
+ let updated;
205
+ try {
206
+ updated = await deps.workItems.update({
207
+ orgId: scope.orgId,
208
+ id: item.id,
209
+ userId: scope.userId,
210
+ patch
211
+ });
212
+ } catch (error) {
213
+ if (error instanceof WorkItemRelationError) throw new Error(`The parent was refused: ${error.message}`);
214
+ throw error;
215
+ }
216
+ if (!updated) throw workItemNotFound(workItem);
217
+ const fields = [
218
+ ...title !== void 0 ? ["title"] : [],
219
+ ...repository ? ["metadata"] : [],
220
+ ...parentWorkItemId !== void 0 ? ["parentWorkItemId"] : [],
221
+ ...plansPreapproved ? ["plansPreapproved"] : []
222
+ ];
223
+ await audit({
224
+ action: "factory.work_item.updated",
225
+ factoryProjectId: scope.factoryProjectId,
226
+ targets: [{
227
+ type: "work_item",
228
+ id: updated.item.id,
229
+ name: updated.item.title
230
+ }],
231
+ metadata: {
232
+ fields,
233
+ cause: "factory_tools"
234
+ }
235
+ });
236
+ const metadata = updated.item.metadata ?? {};
237
+ return {
238
+ workItem: briefItem(updated.item),
239
+ updated: fields.map((field) => field === "metadata" ? "repository" : field),
240
+ repository: typeof metadata.repository === "string" ? metadata.repository : null,
241
+ parentWorkItemId: updated.item.parentWorkItemId,
242
+ plansPreapproved: updated.item.plansPreapprovedAt !== null
243
+ };
244
+ }
245
+ }),
246
+ factory_retry_decision: createTool({
247
+ id: "factory_retry_decision",
248
+ description: "Retry one failed Factory run once its cause is fixed. Refused when the failure cannot be fixed by retrying or the item has moved on.",
249
+ inputSchema: z.object({ decisionId }).strict(),
250
+ execute: async ({ decisionId }) => settled(await retryFactoryDecision({
251
+ workItems: deps.workItems,
252
+ boards: deps.boards,
253
+ ...project,
254
+ decisionId
255
+ }))
256
+ }),
257
+ factory_dismiss_decision: createTool({
258
+ id: "factory_dismiss_decision",
259
+ description: "Dismiss one proposed Factory run that is no longer wanted.",
260
+ inputSchema: z.object({ decisionId }).strict(),
261
+ execute: async ({ decisionId }) => settled(await dismissFactoryDecision({
262
+ workItems: deps.workItems,
263
+ audit,
264
+ ...project,
265
+ userId: scope.userId,
266
+ decisionId
267
+ }))
268
+ }),
269
+ factory_resolve_proposal: createTool({
270
+ id: "factory_resolve_proposal",
271
+ description: "Approve or dismiss one proposed Factory run. Approving starts the run and lets the item's later runs start without asking.",
272
+ inputSchema: z.object({
273
+ decisionId,
274
+ action: z.enum(["approve", "dismiss"])
275
+ }).strict(),
276
+ execute: async ({ decisionId, action }) => settled(await resolveFactoryProposal({
277
+ workItems: deps.workItems,
278
+ audit,
279
+ ...project,
280
+ userId: scope.userId,
281
+ decisionId,
282
+ action
283
+ }))
284
+ }),
285
+ factory_subscribe_work_item: createTool({
286
+ id: "factory_subscribe_work_item",
287
+ description: "Follow a work item from this session, so you hear about its progress and its sessions report to you. Items you file or start are followed already.",
288
+ inputSchema: z.object({ workItem: workItemRefSchema }).strict(),
289
+ execute: async ({ workItem }) => {
290
+ const item = await requireItem(workItem);
291
+ const subscription = await follow(item, "subscribed");
292
+ return {
293
+ workItem: briefItem(item),
294
+ subscribed: true,
295
+ reason: subscription.reason
296
+ };
297
+ }
298
+ }),
299
+ factory_unsubscribe_work_item: createTool({
300
+ id: "factory_unsubscribe_work_item",
301
+ description: "Stop following a work item from this session.",
302
+ inputSchema: z.object({ workItem: workItemRefSchema }).strict(),
303
+ execute: async ({ workItem }) => {
304
+ const item = await requireItem(workItem);
305
+ const removed = await deps.workItems.unsubscribeFromWorkItem({
306
+ orgId: scope.orgId,
307
+ workItemId: item.id,
308
+ sessionId: scope.sessionId
309
+ });
310
+ return {
311
+ workItem: briefItem(item),
312
+ subscribed: false,
313
+ wasSubscribed: removed
314
+ };
315
+ }
316
+ })
317
+ };
318
+ }
319
+ //#endregion
320
+ export { createFactoryWorkItemTools };
321
+
322
+ //# sourceMappingURL=work-item-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"work-item-tools.js","names":[],"sources":["../../src/tools/work-item-tools.ts"],"sourcesContent":["/**\n * Factory tools that change work items: file one, move it, settle its\n * decisions, and follow it. They act as the session's person (a human actor),\n * through the same code the board's routes use.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { transitionBodySchema } from '../routes/contracts.js';\nimport type { FactoryTransitionService } from '../rules/transition-service.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { CommentsDomain } from '../storage/domains/comments/domain.js';\nimport { WorkItemRelationError } from '../storage/domains/work-items/base.js';\nimport type { UpdateWorkItemInput, WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { createFactoryWorkItem } from '../work-items/create-work-item.js';\nimport type { FactoryAuditEmit } from '../work-items/create-work-item.js';\nimport {\n dismissFactoryDecision,\n resolveFactoryProposal,\n retryFactoryDecision,\n} from '../work-items/decision-actions.js';\nimport type { FactoryDecisionActionResult } from '../work-items/decision-actions.js';\nimport {\n MAX_TEXT,\n findWorkItem,\n workItemRefSchema,\n truncate,\n workItemNotFound,\n workItemNumber,\n workItemStage,\n} from './read-tools.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport interface FactoryWorkItemToolDependencies {\n workItems: WorkItemsStorage;\n boards: BoardRegistry;\n /** Governed stage moves; filing and moving items refuse while it is unavailable. */\n transitionService: Pick<FactoryTransitionService, 'transition'> | undefined;\n /** Writes audit rows with an explicit actor; tool calls have no HTTP request to read one from. */\n auditRecorder: AuditRecorder;\n /** The feed's front door: validates, touches open boards and mirrors to linked channels. */\n feed: Pick<CommentsDomain, 'createComment'>;\n /** Finds the caller session's source-control provider, whose linked repositories a card can name. */\n sessions: Pick<SourceControlSessionLookup, 'getSourceControlBySessionId'>;\n}\n\nconst repositorySchema = z\n .string()\n .trim()\n .min(3)\n .max(200)\n .regex(/^[^/\\s]+\\/[^/\\s]+$/, 'Use the owner/name form, e.g. \"mastra-ai/mastra\".')\n .describe('A repository linked to this Factory, as owner/name.');\n\nconst decisionId = z.string().trim().uuid().describe('Decision id, from factory_list_attention or inspect.');\n\nfunction briefItem(item: WorkItemRow) {\n return {\n id: item.id,\n number: workItemNumber(item),\n title: truncate(item.title, MAX_TEXT),\n stage: workItemStage(item),\n };\n}\n\nfunction settled(result: FactoryDecisionActionResult) {\n if (!result.ok) throw new Error(result.message);\n const { decision } = result;\n return { decisionId: decision.id, status: decision.status, workItemId: decision.workItemId };\n}\n\n/** The work-item write and subscription tools for one user session's turn. */\nexport function createFactoryWorkItemTools(\n scope: FactoryToolScope,\n deps: FactoryWorkItemToolDependencies,\n): IntegrationTools {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const audit: FactoryAuditEmit = async input => {\n await deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: scope.userId,\n actorType: 'human',\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n ...input,\n });\n };\n const requireItem = async (ref: string) => {\n const item = await findWorkItem(deps.workItems, scope, ref);\n if (!item) throw workItemNotFound(ref);\n return item;\n };\n /**\n * Check `slug` is a repository linked to this Factory and that a card with\n * `metadata` would run in it (a card synced from an issue follows the\n * issue's repository, which a slug can't override).\n */\n const requireRepository = async (slug: string, metadata: Record<string, unknown> | null = null) => {\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!sourceControl) throw new Error('This session has no source-control provider to look repositories up in.');\n const resolve = (repository?: string) =>\n resolveWorkItemRepository({\n sourceControl,\n ...project,\n item: { metadata: repository ? { ...metadata, repository } : metadata },\n });\n const resolution = await resolve(slug);\n if (resolution.status === 'resolved' && resolution.slug === slug) return;\n if (resolution.status === 'resolved') {\n throw new Error(`This card runs in ${resolution.slug}, which comes from its linked issue and can't be changed.`);\n }\n const linked = await resolve();\n const candidates =\n linked.status === 'ambiguous' ? linked.candidates : linked.status === 'resolved' ? [linked.slug] : [];\n throw new Error(\n candidates.length > 0\n ? `${slug} is not linked to this Factory. Linked repositories: ${candidates.join(', ')}.`\n : `${slug} is not linked to this Factory, and it has no linked repositories.`,\n );\n };\n const follow = (item: WorkItemRow, reason: 'created' | 'subscribed') =>\n deps.workItems.subscribeToWorkItem({\n ...project,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason,\n });\n\n return {\n factory_create_work_item: createTool({\n id: 'factory_create_work_item',\n description:\n \"File a new work item on this Factory's Work board for the person you are working with. It lands in the board's first phase, exactly like pressing New card, and this session follows it. Check factory_list_work_items for an existing item first. Name the repository when the Factory links more than one; runs refuse to start on a card that doesn't say which.\",\n inputSchema: z\n .object({\n title: z.string().trim().min(1).max(512).describe('Short title, as it should read on the card.'),\n brief: z\n .string()\n .trim()\n .min(1)\n .max(8000)\n .describe('What needs doing and why; posted as the first comment on the card.'),\n repository: repositorySchema.optional(),\n })\n .strict(),\n execute: async ({ title, brief, repository }) => {\n if (repository) await requireRepository(repository);\n const created = await createFactoryWorkItem({\n workItems: deps.workItems,\n boards: deps.boards,\n transitionService: deps.transitionService,\n audit,\n ...project,\n userId: scope.userId,\n ...(scope.actorProfile ? { origin: { actorProfile: scope.actorProfile } } : {}),\n input: { title, ...(repository ? { metadata: { repository } } : {}) },\n });\n if (created.status === 'refused') throw new Error(created.message);\n if (created.status === 'rejected') throw new Error(`The board refused the new item: ${created.reason}`);\n const { item } = created;\n // The card exists from here on: report follow-up failures alongside it\n // rather than failing the call, so the agent doesn't file it twice.\n const warnings: string[] = [];\n try {\n const comment = await deps.feed.createComment({\n orgId: scope.orgId,\n workItemId: item.id,\n author: { kind: 'user', id: scope.userId },\n body: brief,\n });\n if (comment.status !== 'created') {\n warnings.push(\n `The brief was not posted (${comment.status === 'invalid' ? comment.message : comment.status}).`,\n );\n }\n } catch (error) {\n warnings.push(`The brief was not posted: ${error instanceof Error ? error.message : String(error)}`);\n }\n try {\n await follow(item, 'created');\n } catch (error) {\n warnings.push(\n `This session does not follow it yet: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n return warnings.length ? { ...briefItem(item), warnings } : briefItem(item);\n },\n }),\n\n factory_transition_work_item: createTool({\n id: 'factory_transition_work_item',\n description:\n \"Move a work item to another stage of its board, as the person you are working with. The board's rules decide whether the move is allowed and what it starts.\",\n inputSchema: z\n .object({\n workItem: workItemRefSchema,\n stage: z.string().trim().min(1).max(64).describe('Destination stage, e.g. \"planning\" or \"done\".'),\n })\n .strict(),\n execute: async ({ workItem, stage }) => {\n if (!deps.transitionService) throw new Error('Factory stage transitions are not available right now.');\n const item = await requireItem(workItem);\n const parsed = transitionBodySchema.safeParse({\n board: boardForWorkItem(item),\n stage,\n expectedRevision: item.revision,\n requestId: randomUUID(),\n cause: 'factory_tools',\n });\n if (!parsed.success) throw new Error(`\"${stage}\" is not a valid stage name.`);\n const result = await deps.transitionService.transition({\n ...parsed.data,\n ...project,\n workItemId: item.id,\n actor: { type: 'human', id: scope.userId },\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n ingress: { ...parsed.data.ingress, identity: `human:${scope.userId}:${parsed.data.ingress.identity}` },\n });\n if (result.status === 'rejected') throw new Error(`The move was refused (${result.code}): ${result.reason}`);\n return { workItemId: item.id, stage: result.stage, revision: result.revision };\n },\n }),\n\n factory_update_work_item: createTool({\n id: 'factory_update_work_item',\n description:\n \"Edit a work item's fields as the person you are working with: its title, the repository its runs use, its parent item, or pre-approving its plans. Move stages with factory_transition_work_item and start roles with factory_start_session; post to the card's feed for anything else.\",\n inputSchema: z\n .object({\n workItem: workItemRefSchema,\n title: z.string().trim().min(1).max(500).optional().describe('New card title.'),\n repository: repositorySchema.optional(),\n parentWorkItem: workItemRefSchema\n .nullable()\n .optional()\n .describe('Parent work item (id or #number), or null to detach it from its parent.'),\n plansPreapproved: z\n .literal(true)\n .optional()\n .describe(\n \"Pre-approve the item's plans, so its runs no longer stop for plan approval. Can't be undone here.\",\n ),\n })\n .strict()\n .refine(input => Object.keys(input).some(key => key !== 'workItem'), 'Pass at least one field to change.'),\n execute: async ({ workItem, title, repository, parentWorkItem, plansPreapproved }) => {\n const item = await requireItem(workItem);\n if (repository) await requireRepository(repository, item.metadata);\n let parentWorkItemId: string | null | undefined;\n if (parentWorkItem === null) parentWorkItemId = null;\n else if (parentWorkItem !== undefined) parentWorkItemId = (await requireItem(parentWorkItem)).id;\n const patch: UpdateWorkItemInput = {\n ...(title !== undefined ? { title } : {}),\n ...(repository ? { metadata: { repository } } : {}),\n ...(parentWorkItemId !== undefined ? { parentWorkItemId } : {}),\n ...(plansPreapproved ? { plansPreapproved } : {}),\n };\n let updated: Awaited<ReturnType<WorkItemsStorage['update']>>;\n try {\n updated = await deps.workItems.update({ orgId: scope.orgId, id: item.id, userId: scope.userId, patch });\n } catch (error) {\n if (error instanceof WorkItemRelationError) throw new Error(`The parent was refused: ${error.message}`);\n throw error;\n }\n if (!updated) throw workItemNotFound(workItem);\n const fields = [\n ...(title !== undefined ? ['title'] : []),\n ...(repository ? ['metadata'] : []),\n ...(parentWorkItemId !== undefined ? ['parentWorkItemId'] : []),\n ...(plansPreapproved ? ['plansPreapproved'] : []),\n ];\n await audit({\n action: 'factory.work_item.updated',\n factoryProjectId: scope.factoryProjectId,\n targets: [{ type: 'work_item', id: updated.item.id, name: updated.item.title }],\n metadata: { fields, cause: 'factory_tools' },\n });\n const metadata = updated.item.metadata ?? {};\n return {\n workItem: briefItem(updated.item),\n updated: fields.map(field => (field === 'metadata' ? 'repository' : field)),\n repository: typeof metadata.repository === 'string' ? metadata.repository : null,\n parentWorkItemId: updated.item.parentWorkItemId,\n plansPreapproved: updated.item.plansPreapprovedAt !== null,\n };\n },\n }),\n\n factory_retry_decision: createTool({\n id: 'factory_retry_decision',\n description:\n 'Retry one failed Factory run once its cause is fixed. Refused when the failure cannot be fixed by retrying or the item has moved on.',\n inputSchema: z.object({ decisionId }).strict(),\n execute: async ({ decisionId }) =>\n settled(await retryFactoryDecision({ workItems: deps.workItems, boards: deps.boards, ...project, decisionId })),\n }),\n\n factory_dismiss_decision: createTool({\n id: 'factory_dismiss_decision',\n description: 'Dismiss one proposed Factory run that is no longer wanted.',\n inputSchema: z.object({ decisionId }).strict(),\n execute: async ({ decisionId }) =>\n settled(\n await dismissFactoryDecision({\n workItems: deps.workItems,\n audit,\n ...project,\n userId: scope.userId,\n decisionId,\n }),\n ),\n }),\n\n factory_resolve_proposal: createTool({\n id: 'factory_resolve_proposal',\n description:\n \"Approve or dismiss one proposed Factory run. Approving starts the run and lets the item's later runs start without asking.\",\n inputSchema: z.object({ decisionId, action: z.enum(['approve', 'dismiss']) }).strict(),\n execute: async ({ decisionId, action }) =>\n settled(\n await resolveFactoryProposal({\n workItems: deps.workItems,\n audit,\n ...project,\n userId: scope.userId,\n decisionId,\n action,\n }),\n ),\n }),\n\n factory_subscribe_work_item: createTool({\n id: 'factory_subscribe_work_item',\n description:\n 'Follow a work item from this session, so you hear about its progress and its sessions report to you. Items you file or start are followed already.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await requireItem(workItem);\n const subscription = await follow(item, 'subscribed');\n return { workItem: briefItem(item), subscribed: true, reason: subscription.reason };\n },\n }),\n\n factory_unsubscribe_work_item: createTool({\n id: 'factory_unsubscribe_work_item',\n description: 'Stop following a work item from this session.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await requireItem(workItem);\n const removed = await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n return { workItem: briefItem(item), subscribed: false, wasSubscribed: removed };\n },\n }),\n };\n}\n"],"mappings":"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"}