@mastra/factory 0.13.0-alpha.8 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +232 -10
  2. package/dist/auth.d.ts +2 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +29 -4
  5. package/dist/auth.js.map +1 -1
  6. package/dist/boards/define-board.d.ts +36 -2
  7. package/dist/boards/define-board.d.ts.map +1 -1
  8. package/dist/boards/define-board.js +58 -1
  9. package/dist/boards/define-board.js.map +1 -1
  10. package/dist/boards/index.d.ts +4 -2
  11. package/dist/boards/index.d.ts.map +1 -1
  12. package/dist/boards/index.js +2 -1
  13. package/dist/boards/index.js.map +1 -1
  14. package/dist/boards/relocate.d.ts +17 -0
  15. package/dist/boards/relocate.d.ts.map +1 -0
  16. package/dist/boards/relocate.js +53 -0
  17. package/dist/boards/relocate.js.map +1 -0
  18. package/dist/boards/review.d.ts +1 -1
  19. package/dist/boards/review.d.ts.map +1 -1
  20. package/dist/boards/review.js +17 -4
  21. package/dist/boards/review.js.map +1 -1
  22. package/dist/boards/semantics.d.ts +28 -0
  23. package/dist/boards/semantics.d.ts.map +1 -0
  24. package/dist/boards/semantics.js +41 -0
  25. package/dist/boards/semantics.js.map +1 -0
  26. package/dist/boards/work-tool-rules.d.ts +8 -0
  27. package/dist/boards/work-tool-rules.d.ts.map +1 -0
  28. package/dist/boards/work-tool-rules.js +20 -0
  29. package/dist/boards/work-tool-rules.js.map +1 -0
  30. package/dist/boards/work.d.ts +1 -1
  31. package/dist/boards/work.d.ts.map +1 -1
  32. package/dist/boards/work.js +13 -0
  33. package/dist/boards/work.js.map +1 -1
  34. package/dist/capabilities/intake.d.ts +2 -0
  35. package/dist/capabilities/intake.d.ts.map +1 -1
  36. package/dist/factory.d.ts +5 -6
  37. package/dist/factory.d.ts.map +1 -1
  38. package/dist/factory.js +41 -12
  39. package/dist/factory.js.map +1 -1
  40. package/dist/integrations/base.d.ts +7 -4
  41. package/dist/integrations/base.d.ts.map +1 -1
  42. package/dist/integrations/github/default-rules.d.ts +2 -2
  43. package/dist/integrations/github/default-rules.d.ts.map +1 -1
  44. package/dist/integrations/github/default-rules.js +2 -2
  45. package/dist/integrations/github/default-rules.js.map +1 -1
  46. package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
  47. package/dist/integrations/github/issue-reconciler.js +4 -3
  48. package/dist/integrations/github/issue-reconciler.js.map +1 -1
  49. package/dist/integrations/github/rules.d.ts +6 -2
  50. package/dist/integrations/github/rules.d.ts.map +1 -1
  51. package/dist/integrations/github/rules.js +96 -15
  52. package/dist/integrations/github/rules.js.map +1 -1
  53. package/dist/integrations/github/sandbox.d.ts +6 -3
  54. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  55. package/dist/integrations/github/sandbox.js +21 -2
  56. package/dist/integrations/github/sandbox.js.map +1 -1
  57. package/dist/integrations/issue-reconciler.d.ts +2 -0
  58. package/dist/integrations/issue-reconciler.d.ts.map +1 -1
  59. package/dist/integrations/issue-reconciler.js +1 -3
  60. package/dist/integrations/issue-reconciler.js.map +1 -1
  61. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  62. package/dist/integrations/linear/agent-tools.js.map +1 -1
  63. package/dist/integrations/linear/default-rules.d.ts +2 -2
  64. package/dist/integrations/linear/default-rules.d.ts.map +1 -1
  65. package/dist/integrations/linear/default-rules.js +2 -2
  66. package/dist/integrations/linear/default-rules.js.map +1 -1
  67. package/dist/integrations/linear/integration.js +1 -0
  68. package/dist/integrations/linear/integration.js.map +1 -1
  69. package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
  70. package/dist/integrations/linear/issue-reconciler.js +9 -4
  71. package/dist/integrations/linear/issue-reconciler.js.map +1 -1
  72. package/dist/integrations/linear/reconciliation-config.d.ts.map +1 -1
  73. package/dist/integrations/linear/reconciliation-config.js.map +1 -1
  74. package/dist/integrations/linear/routes.d.ts.map +1 -1
  75. package/dist/integrations/linear/routes.js +19 -26
  76. package/dist/integrations/linear/routes.js.map +1 -1
  77. package/dist/integrations/linear/rules.d.ts +7 -2
  78. package/dist/integrations/linear/rules.d.ts.map +1 -1
  79. package/dist/integrations/linear/rules.js +20 -8
  80. package/dist/integrations/linear/rules.js.map +1 -1
  81. package/dist/integrations/platform/api-client.d.ts.map +1 -1
  82. package/dist/integrations/platform/api-client.js +25 -4
  83. package/dist/integrations/platform/api-client.js.map +1 -1
  84. package/dist/integrations/platform/github/integration.d.ts +1 -0
  85. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  86. package/dist/integrations/platform/github/integration.js +5 -0
  87. package/dist/integrations/platform/github/integration.js.map +1 -1
  88. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  89. package/dist/integrations/platform/linear/integration.js +5 -2
  90. package/dist/integrations/platform/linear/integration.js.map +1 -1
  91. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts +8 -0
  92. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts.map +1 -0
  93. package/dist/integrations/slack/emoji-shortcodes.generated.js +1985 -0
  94. package/dist/integrations/slack/emoji-shortcodes.generated.js.map +1 -0
  95. package/dist/integrations/slack/emoji.d.ts +3 -0
  96. package/dist/integrations/slack/emoji.d.ts.map +1 -0
  97. package/dist/integrations/slack/emoji.js +12 -0
  98. package/dist/integrations/slack/emoji.js.map +1 -0
  99. package/dist/integrations/slack/integration.js +1 -1
  100. package/dist/integrations/slack/integration.js.map +1 -1
  101. package/dist/integrations/slack/slack.d.ts.map +1 -1
  102. package/dist/integrations/slack/slack.js +16 -2
  103. package/dist/integrations/slack/slack.js.map +1 -1
  104. package/dist/routes/attention-activity.d.ts +2 -3
  105. package/dist/routes/attention-activity.d.ts.map +1 -1
  106. package/dist/routes/attention-activity.js +12 -3
  107. package/dist/routes/attention-activity.js.map +1 -1
  108. package/dist/routes/attention-parked.d.ts +22 -0
  109. package/dist/routes/attention-parked.d.ts.map +1 -0
  110. package/dist/routes/attention-parked.js +136 -0
  111. package/dist/routes/attention-parked.js.map +1 -0
  112. package/dist/routes/attention-providers.js +8 -8
  113. package/dist/routes/attention-providers.js.map +1 -1
  114. package/dist/routes/attention.d.ts +2 -0
  115. package/dist/routes/attention.d.ts.map +1 -1
  116. package/dist/routes/attention.js +47 -118
  117. package/dist/routes/attention.js.map +1 -1
  118. package/dist/routes/contracts.d.ts +627 -0
  119. package/dist/routes/contracts.d.ts.map +1 -0
  120. package/dist/routes/contracts.js +468 -0
  121. package/dist/routes/contracts.js.map +1 -0
  122. package/dist/routes/intake.d.ts +18 -0
  123. package/dist/routes/intake.d.ts.map +1 -1
  124. package/dist/routes/intake.js +265 -15
  125. package/dist/routes/intake.js.map +1 -1
  126. package/dist/routes/projects.d.ts.map +1 -1
  127. package/dist/routes/projects.js +26 -76
  128. package/dist/routes/projects.js.map +1 -1
  129. package/dist/routes/supervisor.d.ts +2 -0
  130. package/dist/routes/supervisor.d.ts.map +1 -1
  131. package/dist/routes/supervisor.js +7 -6
  132. package/dist/routes/supervisor.js.map +1 -1
  133. package/dist/routes/surface.d.ts +7 -4
  134. package/dist/routes/surface.d.ts.map +1 -1
  135. package/dist/routes/surface.js +17 -11
  136. package/dist/routes/surface.js.map +1 -1
  137. package/dist/routes/telemetry.d.ts +13 -0
  138. package/dist/routes/telemetry.d.ts.map +1 -0
  139. package/dist/routes/telemetry.js +60 -0
  140. package/dist/routes/telemetry.js.map +1 -0
  141. package/dist/routes/work-items.d.ts +3 -3
  142. package/dist/routes/work-items.d.ts.map +1 -1
  143. package/dist/routes/work-items.js +134 -250
  144. package/dist/routes/work-items.js.map +1 -1
  145. package/dist/rules/dispatch-errors.d.ts +2 -2
  146. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  147. package/dist/rules/dispatch-errors.js +10 -6
  148. package/dist/rules/dispatch-errors.js.map +1 -1
  149. package/dist/rules/dispatcher.d.ts +9 -2
  150. package/dist/rules/dispatcher.d.ts.map +1 -1
  151. package/dist/rules/dispatcher.js +76 -23
  152. package/dist/rules/dispatcher.js.map +1 -1
  153. package/dist/rules/index.d.ts +4 -5
  154. package/dist/rules/index.d.ts.map +1 -1
  155. package/dist/rules/index.js +3 -4
  156. package/dist/rules/processor.d.ts +3 -3
  157. package/dist/rules/processor.d.ts.map +1 -1
  158. package/dist/rules/processor.js +24 -31
  159. package/dist/rules/processor.js.map +1 -1
  160. package/dist/rules/resolve.d.ts +1 -3
  161. package/dist/rules/resolve.d.ts.map +1 -1
  162. package/dist/rules/resolve.js +1 -4
  163. package/dist/rules/resolve.js.map +1 -1
  164. package/dist/rules/start-coordinator.d.ts.map +1 -1
  165. package/dist/rules/start-coordinator.js +3 -1
  166. package/dist/rules/start-coordinator.js.map +1 -1
  167. package/dist/rules/tools.d.ts.map +1 -1
  168. package/dist/rules/tools.js +20 -10
  169. package/dist/rules/tools.js.map +1 -1
  170. package/dist/rules/transition-service.d.ts +7 -7
  171. package/dist/rules/transition-service.d.ts.map +1 -1
  172. package/dist/rules/transition-service.js +21 -25
  173. package/dist/rules/transition-service.js.map +1 -1
  174. package/dist/rules/types.d.ts +16 -11
  175. package/dist/rules/types.d.ts.map +1 -1
  176. package/dist/rules/types.js.map +1 -1
  177. package/dist/rules/validation.d.ts +12 -2
  178. package/dist/rules/validation.d.ts.map +1 -1
  179. package/dist/rules/validation.js +29 -20
  180. package/dist/rules/validation.js.map +1 -1
  181. package/dist/session/live-sessions.d.ts +33 -3
  182. package/dist/session/live-sessions.d.ts.map +1 -1
  183. package/dist/session/live-sessions.js +67 -5
  184. package/dist/session/live-sessions.js.map +1 -1
  185. package/dist/storage/domains/intake/base.d.ts +54 -1
  186. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  187. package/dist/storage/domains/intake/base.js +136 -4
  188. package/dist/storage/domains/intake/base.js.map +1 -1
  189. package/dist/storage/domains/work-items/base.d.ts +22 -7
  190. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  191. package/dist/storage/domains/work-items/base.js +41 -26
  192. package/dist/storage/domains/work-items/base.js.map +1 -1
  193. package/dist/supervisor/health-worker.d.ts +2 -0
  194. package/dist/supervisor/health-worker.d.ts.map +1 -1
  195. package/dist/supervisor/health-worker.js +3 -1
  196. package/dist/supervisor/health-worker.js.map +1 -1
  197. package/dist/supervisor/health.d.ts +12 -14
  198. package/dist/supervisor/health.d.ts.map +1 -1
  199. package/dist/supervisor/health.js +24 -52
  200. package/dist/supervisor/health.js.map +1 -1
  201. package/dist/supervisor/instructions.d.ts +1 -1
  202. package/dist/supervisor/instructions.d.ts.map +1 -1
  203. package/dist/supervisor/instructions.js +10 -8
  204. package/dist/supervisor/instructions.js.map +1 -1
  205. package/dist/supervisor/read-tools.d.ts +2 -0
  206. package/dist/supervisor/read-tools.d.ts.map +1 -1
  207. package/dist/supervisor/read-tools.js +2 -2
  208. package/dist/supervisor/read-tools.js.map +1 -1
  209. package/dist/supervisor/write-tools.d.ts.map +1 -1
  210. package/dist/supervisor/write-tools.js +6 -4
  211. package/dist/supervisor/write-tools.js.map +1 -1
  212. package/dist/telemetry-types.d.ts +7 -0
  213. package/dist/telemetry-types.d.ts.map +1 -0
  214. package/dist/telemetry-types.js +24 -0
  215. package/dist/telemetry-types.js.map +1 -0
  216. package/dist/telemetry.d.ts +9 -0
  217. package/dist/telemetry.d.ts.map +1 -0
  218. package/dist/telemetry.js +47 -0
  219. package/dist/telemetry.js.map +1 -0
  220. package/dist/work-item-branch.d.ts +2 -0
  221. package/dist/work-item-branch.d.ts.map +1 -1
  222. package/dist/work-item-branch.js +6 -1
  223. package/dist/work-item-branch.js.map +1 -1
  224. package/dist/workspace.d.ts.map +1 -1
  225. package/dist/workspace.js +3 -1
  226. package/dist/workspace.js.map +1 -1
  227. package/factory-skills/configure-factory-rules/SKILL.md +30 -10
  228. package/package.json +11 -9
  229. package/dist/rules/defaults.d.ts +0 -9
  230. package/dist/rules/defaults.d.ts.map +0 -1
  231. package/dist/rules/defaults.js +0 -50
  232. package/dist/rules/defaults.js.map +0 -1
package/README.md CHANGED
@@ -29,6 +29,32 @@ A host application calls `MastraFactory.prepare()`, constructs its `Mastra` inst
29
29
 
30
30
  `prepare()` initializes the Factory-owned resources needed before Mastra is constructed. `finalize()` connects those resources to the completed host, including Factory routes, integrations, storage-backed behavior, and agent-controller features. Consumers should keep frontend concerns in `factory-ui` and host-specific environment or deployment wiring in `web` rather than adding them to this package.
31
31
 
32
+ ### Product telemetry
33
+
34
+ Factory records `factory_web_activity` in Mastra's existing PostHog project for users signed in through the default `mastra-studio` auth provider. The browser sends only a known page category and an activity type to the authenticated `/web/telemetry/activity` endpoint. The server adds the verified account and deployment context.
35
+
36
+ | Property | Meaning |
37
+ | ------------------------------------- | -------------------------------------------------------------------------------- |
38
+ | `activity` | `page_view` for a visible screen, or `interaction` for pointer/keyboard activity |
39
+ | `page` | A bounded category such as `work`, `review`, or `settings`; never a URL |
40
+ | `platform_user_id`, `platform_org_id` | Opaque IDs from the authenticated platform account |
41
+ | `platform_project_id` | `MASTRA_PROJECT_ID`, when configured |
42
+ | `platform_hosted` | Whether a nonempty `MASTRA_DEPLOYMENT_ID` is present |
43
+ | `deployment_id`, `platform_region` | `MASTRA_DEPLOYMENT_ID` and `MASTRA_PLATFORM_REGION`, when configured |
44
+ | `schema_version` | `1` |
45
+
46
+ The person ID is `factory:platform:<user ID>`, consistent across local and hosted servers. This does not merge existing CLI or platform analytics profiles. A local server can have a platform project ID and use platform services while `platform_hosted` remains `false`. Non-platform hosting includes both local and other self-hosted servers; this event does not distinguish them.
47
+
48
+ Count unique people per day for visitors, and filter to `activity = interaction` for engaged users. Group by `platform_project_id` for project adoption. Interaction events are limited to once per minute per mounted browser app, and the server caps captures at 60 per minute per account/organization per process. Hidden tabs do not capture activity, and there is no background heartbeat. These are best-effort usage signals, not a record of successful product actions.
49
+
50
+ To disable collection, set this on the Factory Server:
51
+
52
+ ```bash
53
+ MASTRA_TELEMETRY_DISABLED=true
54
+ ```
55
+
56
+ `1`, `true`, and `yes` are accepted, ignoring case and surrounding whitespace. Older servers without the explicit capability do not receive browser telemetry requests. Custom auth providers are outside this initial measurement scope until they have a stable identity namespace. No names, emails, tokens, prompts, input values, raw URLs, session replay, or anonymous browser identity are collected by this event. Account IDs are identifiable account data, not anonymous data.
57
+
32
58
  ### Board lifecycle rules
33
59
 
34
60
  Installed board definitions exclusively own phase entry and exit handlers. Work and Review are installed automatically with Mastra's preferred defaults; no rule configuration is needed. Custom boards declare source-specific `onEnter` and `onExit` handlers through `defineBoard()`:
@@ -43,9 +69,10 @@ const releaseBoard = defineBoard({
43
69
  title: 'Release',
44
70
  initialPhase: 'queued',
45
71
  phases: {
46
- queued: { title: 'Queued', next: 'shipped' },
72
+ queued: { title: 'Queued', kind: 'resting', next: 'shipped' },
47
73
  shipped: {
48
74
  title: 'Shipped',
75
+ kind: 'terminal',
49
76
  onEnter: {
50
77
  manual: () => ({ type: 'reject', code: 'release_held', reason: 'Release is held.' }),
51
78
  },
@@ -64,15 +91,56 @@ Handlers return one typed decision or `undefined`. Supported sources are `issue`
64
91
 
65
92
  **Migration:** Remove former global `rules.work` and `rules.review` configuration. Built-in customization is deferred; there is no built-in override or replacement API. Define custom-board handlers on their phases instead. The web deployment now uses the guarded Work default rather than its former unconditional intake handler, so noncandidate or manual arrivals no longer start merely from entering Intake.
66
93
 
67
- Global rules now contain only the shared audit `version` and tool-result handlers:
94
+ There is no global rules object. Every rule has one owner: boards own lifecycle handlers, transition policy, phase semantics, and tool-result rules; integrations own their event handlers. The runtime only executes rules.
95
+
96
+ ### Board tool-result rules
97
+
98
+ A board may react to a tool result produced inside one of its seats. Declare handlers under `tools`, keyed by tool name:
99
+
100
+ ```typescript
101
+ import { defineBoard } from '@mastra/factory/boards';
102
+ import type { BoardToolResultRuleHandler } from '@mastra/factory/boards';
103
+
104
+ const shipIt: BoardToolResultRuleHandler = context => {
105
+ if (context.result.status !== 'success' || context.item.stages[0] !== 'queued') return;
106
+ return { type: 'notify', idempotencyKey: `${context.ingress.id}:shipped`, title: 'Release shipped' };
107
+ };
108
+
109
+ const releaseBoard = defineBoard({
110
+ id: 'release',
111
+ title: 'Release',
112
+ initialPhase: 'queued',
113
+ phases: {
114
+ queued: { title: 'Queued', kind: 'resting', next: 'shipped' },
115
+ shipped: { title: 'Shipped', kind: 'terminal' },
116
+ },
117
+ tools: { ship_it: { onResult: shipIt } },
118
+ });
119
+ ```
120
+
121
+ The handler receives the bound item, actor, board, tool name, normalized result, and `configVersion`, and returns one decision or `undefined`. Tool names follow the identifier rules for decision roles; `onResult` must be a function and the leaf may contain nothing else. Violations are `BoardDefinitionError`s at definition time.
122
+
123
+ Work declares one rule: `submit_plan`. When a `plan`-seated agent on a Planning card reports a result starting with `Plan approved.`, the card transitions to Execute. Review declares none. Resolution is fail-closed: a tool result on a card whose board is not installed, or whose board does not declare that tool, fires no rule — a custom board inherits nothing from Work even if it reuses Work's phase names.
124
+
125
+ ### Config version
126
+
127
+ `configVersion` is an operator-maintained deployment label stamped onto transition audit rows, deferred decisions, reconciler audit, and the session kickoff header (`Config: …`). Nothing branches on it; it exists so an audit row can be traced back to the deployment that produced it. It defaults to `factory-config-v1` and must be a non-empty bounded string. The storage column keeps its shipped name, `rule_set_version`.
68
128
 
69
129
  ```typescript
70
- import { defaultFactoryRules } from '@mastra/factory/rules/defaults';
130
+ new MastraFactory({ storage, configVersion: 'deployment-v2' });
131
+ ```
132
+
133
+ **Migration:** The `rules` option, `FactoryRules`, `defaultFactoryRules`, and the `@mastra/factory/rules/defaults` subpath are gone. Passing `rules` throws at construction with a pointer to the replacements.
71
134
 
72
- const rules = defaultFactoryRules({ version: 'deployment-v2' });
73
- // Pass rules to MastraFactory. Optional tool overrides remain under overrides.tools.
135
+ ```typescript
136
+ // before
137
+ new MastraFactory({ storage, rules: defaultFactoryRules({ version: 'v2', overrides: { tools: { my_tool: { onResult } } } }) });
138
+ // after
139
+ new MastraFactory({ storage, configVersion: 'v2', boards: [defineBoard({ ..., tools: { my_tool: { onResult } } })] });
74
140
  ```
75
141
 
142
+ Contexts that carried `ruleSetVersion` now carry `configVersion`. Work's `submit_plan` rule cannot be replaced from config; built-in customization remains deferred.
143
+
76
144
  ### Board transition policy
77
145
 
78
146
  Boards own three separate concerns: topology declares which moves exist, `transitionPolicy` restricts those moves, and lifecycle handlers return entry/exit effects. The transition service uses the policy on the item's persisted, installed board, with no fallback to Work policy.
@@ -95,8 +163,8 @@ const releaseBoard = defineBoard({
95
163
  initialPhase: 'approval',
96
164
  transitionPolicy: releasePolicy,
97
165
  phases: {
98
- approval: { title: 'Approval', next: 'shipped' },
99
- shipped: { title: 'Shipped' },
166
+ approval: { title: 'Approval', kind: 'resting', next: 'shipped' },
167
+ shipped: { title: 'Shipped', kind: 'working', role: 'release' },
100
168
  },
101
169
  });
102
170
  // Install through new MastraFactory({ storage, boards: [releaseBoard] }).
@@ -110,7 +178,161 @@ Policies must be side-effect-free. They run on initial entry, reentry, and same-
110
178
 
111
179
  A policy cannot bypass topology, ingress authorization, board ownership, external-author safety, revision checks, decision validation, replay handling, or atomic persistence. Returning `allow` is not an authorization override.
112
180
 
113
- **Remaining limitations:** Phase execution semantics are not generalized by this API. Working/resting interpretation, role routing, terminal cleanup, consent, and kickoff behavior still include built-in naming assumptions. For example, naming a phase `shipped` does not make it a runtime terminal phase. Built-in board replacement and customization remain unsupported.
181
+ **Remaining limitations:** Built-in board replacement and customization remain unsupported.
182
+
183
+ ### Board phase semantics
184
+
185
+ Every phase declares what it _is_ with a required `kind`; `defineBoard()` rejects a phase without one.
186
+
187
+ - `resting` — the card is parked. A human move out of a resting phase arms autonomy; a move back into one disarms it. `initialPhase` must be resting: a card cannot arrive already seated or already finished.
188
+ - `working` — an agent seat carries the card. `role` is required (same identifier rules as decision roles) and names the seat a human kickoff opens and the lane a rule-started run leaves rest for. Two working phases may share a role; `phaseForRole` returns the first in declaration order.
189
+ - `terminal` — the card is finished. Entering it releases the sandbox and lets sweeps supersede stale decisions and revoke run bindings. `role` is not allowed.
190
+
191
+ ```typescript
192
+ const releaseBoard = defineBoard({
193
+ id: 'release',
194
+ title: 'Release',
195
+ initialPhase: 'queued',
196
+ phases: {
197
+ queued: { title: 'Queued', kind: 'resting', next: 'shipping' },
198
+ shipping: { title: 'Shipping', kind: 'working', role: 'release', next: 'shipped' },
199
+ shipped: { title: 'Shipped', kind: 'terminal' },
200
+ },
201
+ });
202
+ ```
203
+
204
+ Work declares `intake` resting; `triage`, `planning`, `execute`, and `review` working with roles `triage`, `plan`, `work`, and `work`; `done` and `canceled` terminal. Review declares `intake` resting, `review` working with role `review`, and `done`/`canceled` terminal. The definition exposes the derived helpers `phaseKind`, `isWorking`, `isTerminal`, `roleForPhase`, and `phaseForRole`.
205
+
206
+ Consent, the external-author guard, kickoff seating, run-start lanes, terminal cleanup, the closed-PR and issue sweeps, and supervisor findings all read the installed board's declarations; nothing name-matches phases. A board that reuses Work's phase names gets exactly what it declared. Persisted `board` is authoritative; rows without one are read as Review for pull requests and Work otherwise.
207
+
208
+ Unknown semantics fail closed. When the board is not installed or the phase is not declared: external-event transitions ask for consent, no sweep or cleanup treats the card as finished, the supervisor neither revokes nor starts a seat for it, and a rule-started run from rest with no lane for its role is rejected.
209
+
210
+ **Migration:** Existing `defineBoard()` calls must add `kind` to every phase and `role` to working phases.
211
+
212
+ ```typescript
213
+ // before
214
+ phases: { queued: { title: 'Queued', next: 'shipped' }, shipped: { title: 'Shipped' } }
215
+ // after
216
+ phases: {
217
+ queued: { title: 'Queued', kind: 'resting', next: 'shipped' },
218
+ shipped: { title: 'Shipped', kind: 'terminal' },
219
+ }
220
+ ```
221
+
222
+ **Remaining limitations:** The `held-waiting` supervisor finding stays Work-specific. Throughput and lead-time metrics still count completions by Work's `done` phase. `factory-ui` still renders the built-in stage and role pipeline. Built-in board replacement and customization remain unsupported.
223
+
224
+ ### Execute a custom board
225
+
226
+ Installed custom boards can create linked items, start working roles, transition through bound tools, and react to completed tool results. Board and phase identifiers contain 1–128 letters, digits, underscores, or hyphens and start with a letter or digit. Identifiers are case-sensitive and cannot contain surrounding whitespace.
227
+
228
+ This configuration runs a release rehearsal through `queued → preparing → shipping → shipped`. Add it to an existing Factory host with configured storage, a GitHub integration, a connected project repository, a sandbox, and organization-scoped model credentials. Enable automatic runs for the project. The repository must contain a `release:check` package script; it should validate the release without publishing it.
229
+
230
+ ```typescript
231
+ import { MastraFactory } from '@mastra/factory';
232
+ import type { MastraFactoryConfig } from '@mastra/factory';
233
+ import { defineBoard } from '@mastra/factory/boards';
234
+ import type { BoardPhaseDefinition } from '@mastra/factory/boards';
235
+
236
+ type ReleasePhase = 'queued' | 'preparing' | 'shipping' | 'shipped';
237
+
238
+ const releaseBoard = defineBoard<'release', Record<ReleasePhase, BoardPhaseDefinition<ReleasePhase>>>({
239
+ id: 'release',
240
+ title: 'Release',
241
+ initialPhase: 'queued',
242
+ transitionPolicy: context => {
243
+ if (context.fromStage === 'queued' && context.toStage === 'preparing' && !context.isHumanTransition) {
244
+ return {
245
+ type: 'reject',
246
+ code: 'approval_required',
247
+ reason: 'A person must start the release rehearsal.',
248
+ };
249
+ }
250
+ },
251
+ phases: {
252
+ queued: { title: 'Queued', kind: 'resting', next: 'preparing' },
253
+ preparing: {
254
+ title: 'Preparing',
255
+ kind: 'working',
256
+ role: 'release-preparer',
257
+ next: 'shipping',
258
+ onEnter: {
259
+ issue: context => ({
260
+ type: 'invokeSkill',
261
+ idempotencyKey: `${context.ingress.id}:prepare`,
262
+ role: 'release-preparer',
263
+ prompt:
264
+ 'Inspect the release changes. When ready, call factory_transition_work_item with stage shipping and the current expectedRevision from the Factory phase signal.',
265
+ }),
266
+ },
267
+ },
268
+ shipping: {
269
+ title: 'Shipping',
270
+ kind: 'working',
271
+ role: 'release-publisher',
272
+ next: 'shipped',
273
+ onEnter: {
274
+ issue: context => ({
275
+ type: 'invokeSkill',
276
+ idempotencyKey: `${context.ingress.id}:check`,
277
+ role: 'release-publisher',
278
+ prompt:
279
+ 'Run npm run release:check && printf "RELEASE_CHECK_PASSED\\n" with execute_command. This is a rehearsal; do not publish anything.',
280
+ }),
281
+ },
282
+ },
283
+ shipped: { title: 'Shipped', kind: 'terminal' },
284
+ },
285
+ tools: {
286
+ execute_command: {
287
+ onResult: context => {
288
+ if (
289
+ context.item.stages[0] !== 'shipping' ||
290
+ context.result.status !== 'success' ||
291
+ typeof context.result.value !== 'string' ||
292
+ !context.result.value.trimEnd().endsWith('RELEASE_CHECK_PASSED')
293
+ ) {
294
+ return;
295
+ }
296
+ return {
297
+ type: 'transition',
298
+ idempotencyKey: `${context.ingress.id}:checked`,
299
+ board: 'release',
300
+ stage: 'shipped',
301
+ };
302
+ },
303
+ },
304
+ },
305
+ });
306
+
307
+ export function createFactory(config: Omit<MastraFactoryConfig, 'boards' | 'includeDefaultBoards'>) {
308
+ return new MastraFactory({ ...config, boards: [releaseBoard], includeDefaultBoards: true });
309
+ }
310
+ ```
311
+
312
+ Use the host's normal `prepare()`, `new Mastra(...)`, and `finalize()` sequence. Change the hardcoded `includeDefaultBoards: true` in `createFactory` to `false` to run without Work or Review. Working roles name bindings on the shared Code Agent; they do not register separate agents. Factory has no per-role agent configuration option. The board's kickoff prompts supply the role-specific instructions.
313
+
314
+ Create a card with `POST /web/factory/projects/:id/work-items`, passing `board: 'release'`, a title, and the GitHub issue's `externalSource`. The card starts in `queued`. Then use `POST /web/factory/projects/:id/work-items/:workItemId/transition` with `board: 'release'`, `stage: 'preparing'`, the returned `expectedRevision`, a unique `requestId`, and a `cause`. Send these requests as an authorized user using the host's authentication. The human transition satisfies this board's policy and starts the preparer. Its bound tool advances to `shipping`; the publisher's completed check produces a deferred transition to `shipped`. Terminal entry revokes the binding and releases the sandbox. The Factory UI performs the same operations against installed boards (see below); verify the persisted card's board, phase, and deferred decision status either way.
315
+
316
+ Lifecycle, tool-result, and integration handlers may return `upsertLinkedWorkItem` targeting a different installed board. Linked cards enter that board's declared initial phase before moving to the requested phase. Existing cards cannot be reassigned to another board through either decision type. Targets are validated against their own installed board before acceptance and again before uncommitted deferred effects execute; a phase declared only on another board is not valid. Committed replay retains its recorded result and original `configVersion`.
317
+
318
+ Custom phase signals and persisted tool-result ingestion use the item's installed board. Bound tools re-resolve the live binding, retain revision and topology checks, and reject session reassignment. A custom role named `triage` does not inherit Work's classification requirements. Existing authorization, external-author safeguards, and board-owned policies still apply.
319
+
320
+ ### Custom boards in the Factory UI
321
+
322
+ `GET /web/factory/projects/:id/boards` returns the installed board catalog for an authorized project: each board's `id`, `title`, `initialPhase`, declaration-ordered `phases` (with `kind` and working `role`), and transition topology. Handlers, policies, and prompts are never serialized. The Factory UI reads this catalog instead of assuming Work and Review:
323
+
324
+ - Installed boards appear in the sidebar — built-ins first, then custom boards in registry order. Work and Review keep `/factories/:id/work` and `/factories/:id/review`; custom boards open at `/factories/:id/boards/:boardId`. An unknown board ID or a failed catalog request shows an explicit unavailable state rather than falling back to Work.
325
+ - Columns, phase labels, and working/terminal presentation come from the phase definitions. Card creation sends the selected `board`, so the server picks the initial phase. Card menus offer only moves the topology declares; the server still enforces policy, authorization, and revisions.
326
+ - A card's persisted `board` decides which board it belongs to. UI actions cannot move a card to a different board.
327
+ - Settings › Skills groups built-in skills under Work and Review and lists each custom board's declared roles. Custom-board kickoff instructions live in code; there is no skill or per-role agent configuration for them.
328
+ - Not yet board-aware: the Overview funnel and stage-hold metrics (Work-only), search scopes (Work/Review sessions), and the automation toggles that are specific to Work triage and Review. Custom-board cards still count toward in-flight totals and appear in activity and audit logs.
329
+
330
+ ### Route intake to custom boards
331
+
332
+ Intake bindings are explicit. A Linear project (Settings › Intake › Linear routing) or GitHub repository is only offered as a candidate feed once it is bound to a board; nothing is materialized just by opening a board. Review only accepts pull requests and is not offered for issue routing.
333
+
334
+ - **Linear:** each project binding selects one installed board. Changing the board moves that source's existing cards to the new board's initial phase, skipping terminal cards and cards with an active session.
335
+ - **GitHub:** Settings › Intake › GitHub routing maps a label to a board per Factory project (`GET`/`PUT /web/intake/label-routes`). Labels match case-insensitively; unrouted issues go to Work. Saving a route relocates matching cards the same way, and `issues.labeled` / `issues.unlabeled` webhooks move a card between its routed board and Work while refreshing its label metadata. Routes apply to every repository linked to the project; label input is free text (no repository label autocomplete yet).
114
336
 
115
337
  ### GitHub event rules
116
338
 
@@ -143,7 +365,7 @@ const overrides = { github: { issueCommentCreated: { onEvent: null } } };
143
365
  const github = new PlatformGithubIntegration({ rules: { issueCommentCreated: null } });
144
366
  ```
145
367
 
146
- Board definitions own lifecycle handlers; only tool-result configuration remains global. The global rule version remains shared audit metadata, including for GitHub evaluations; it is not a hash of custom handler code and does not change delivery replay semantics. Update the deployment-owned version when changing handler behavior.
368
+ Board definitions own lifecycle, transition-policy, phase-semantics, and tool-result rules; nothing is configured globally. `MastraFactory({ configVersion })` supplies the deployment-owned label stamped on audit records and GitHub evaluations; it is not a hash of custom handler code, not ingress identity, and does not change delivery replay semantics. Update `configVersion` when changing handler behavior.
147
369
 
148
370
  Handlers receive the existing typed GitHub context and return one decision or `undefined`. External titles, bodies, and comments remain untrusted data after webhook authentication. Custom handlers must preserve any required actor-permission checks explicitly.
149
371
 
@@ -180,7 +402,7 @@ const linear = new PlatformLinearIntegration({ rules: { issueClosed: null } });
180
402
 
181
403
  Linear event handlers are configured exclusively on the integration. Fetched issues, platform polling, and issue reconciliation use that instance's handlers. Defaults create intake items for observed open issues and close linked non-terminal Work items as Done or Canceled; closed unlinked issues do not create new items. Custom handlers receive the existing typed Linear context and return one decision or `undefined`. Treat issue titles, descriptions, and other external content as untrusted data.
182
404
 
183
- The global rule version remains shared audit metadata for Linear evaluations. Update the deployment-owned version when handler behavior changes; it does not change ingress identity or replay semantics.
405
+ `MastraFactory({ configVersion })` is the deployment-owned label stamped on Linear evaluations and audit records. Update it when handler behavior changes; it is neither ingress identity nor replay state.
184
406
 
185
407
  ### GitHub review commands
186
408
 
package/dist/auth.d.ts CHANGED
@@ -41,6 +41,8 @@ export interface FactoryAuthUser {
41
41
  * isolated building instances. Absent for personal (no-org) accounts.
42
42
  */
43
43
  organizationId?: string;
44
+ /** Organization ids proven by the provider's authenticated membership response. */
45
+ organizationMembershipIds?: string[];
44
46
  }
45
47
  /**
46
48
  * Tenant identity: the org is the top-level tenant, and each user inside it is
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/auth.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { actorFromAuthUser } from "./storage/domains/comments/actor.js";
2
+ import { isFactoryTelemetryEnabled } from "./telemetry.js";
2
3
  import { timedAboveThreshold } from "./timing.js";
3
4
  import { MastraAuthWorkos } from "@mastra/auth-workos";
4
5
  import { MASTRA_MESSAGE_AUTHOR_KEY } from "@mastra/core/request-context";
5
6
  import { isAuthHttpHandler, isCredentialsProvider, isOrganizationsProvider, isSSOProvider, isSessionProvider, registerApiRoute } from "@mastra/core/server";
7
+ import { HTTPException } from "hono/http-exception";
6
8
  //#region src/auth.ts
9
+ const ORGANIZATION_ID_HEADER = "X-Mastra-Organization-Id";
7
10
  /**
8
11
  * Validate that a `returnTo` value is a safe same-site path, to prevent
9
12
  * open-redirect attacks. Only absolute local paths (`/foo`) are allowed;
@@ -136,13 +139,21 @@ function toFactoryAuthUser(result) {
136
139
  const id = typeof flat.id === "string" ? flat.id : void 0;
137
140
  const workosId = typeof flat.workosId === "string" ? flat.workosId : void 0;
138
141
  if (!id && !workosId) return null;
142
+ const membershipOrganizationIds = Array.isArray(flat.memberships) ? flat.memberships.flatMap((membership) => {
143
+ if (!membership || typeof membership !== "object") return [];
144
+ const organizationId = membership.organizationId;
145
+ return typeof organizationId === "string" ? [organizationId] : [];
146
+ }) : [];
147
+ const memberOrgIds = Array.isArray(flat.memberOrgIds) ? flat.memberOrgIds.filter((organizationId) => typeof organizationId === "string") : [];
148
+ const organizationMembershipIds = [.../* @__PURE__ */ new Set([...membershipOrganizationIds, ...memberOrgIds])];
139
149
  return {
140
150
  id,
141
151
  workosId,
142
152
  email: typeof flat.email === "string" ? flat.email : void 0,
143
153
  name: typeof flat.name === "string" ? flat.name : void 0,
144
154
  avatarUrl: typeof flat.avatarUrl === "string" ? flat.avatarUrl : void 0,
145
- organizationId: typeof flat.organizationId === "string" ? flat.organizationId : void 0
155
+ organizationId: typeof flat.organizationId === "string" ? flat.organizationId : void 0,
156
+ organizationMembershipIds
146
157
  };
147
158
  }
148
159
  /**
@@ -174,6 +185,12 @@ async function ensureUserOrg(provider, user) {
174
185
  if (orgId) user.organizationId = orgId;
175
186
  } catch {}
176
187
  }
188
+ function selectRequestedOrganization(user, requestedOrganizationId) {
189
+ if (user.organizationId === requestedOrganizationId) return true;
190
+ if (!user.organizationMembershipIds?.includes(requestedOrganizationId)) return false;
191
+ user.organizationId = requestedOrganizationId;
192
+ return true;
193
+ }
177
194
  /**
178
195
  * `Set-Cookie` values that clear the provider's session cookie(s), from the
179
196
  * provider's (possibly partial) `ISessionProvider.getClearSessionHeaders`.
@@ -244,9 +261,13 @@ async function ensureFactoryAuthUser(provider, c) {
244
261
  const existing = getFactoryAuthUser(c);
245
262
  if (existing) return existing;
246
263
  if (!provider) return void 0;
247
- const user = await authenticateRequest(provider, getBearerToken(c.req.header("Authorization")), c.req.raw);
264
+ const token = getBearerToken(c.req.header("Authorization"));
265
+ const user = await authenticateRequest(provider, token, c.req.raw);
248
266
  if (!user) return void 0;
249
- await ensureUserOrg(provider, user);
267
+ const requestedOrganizationId = token ? c.req.header(ORGANIZATION_ID_HEADER)?.trim() : void 0;
268
+ if (requestedOrganizationId) {
269
+ if (!selectRequestedOrganization(user, requestedOrganizationId)) throw new HTTPException(403, { message: "organization_forbidden" });
270
+ } else await ensureUserOrg(provider, user);
250
271
  c.set(FACTORY_AUTH_USER_KEY, user);
251
272
  return user;
252
273
  }
@@ -280,6 +301,7 @@ async function handleAuthMe(provider, c) {
280
301
  await ensureUserOrg(provider, user);
281
302
  return c.json({
282
303
  authenticated: true,
304
+ telemetryEnabled: isFactoryTelemetryEnabled(provider.name),
283
305
  user: {
284
306
  userId: getFactoryAuthUserId(user),
285
307
  email: user.email,
@@ -516,7 +538,10 @@ function createFactoryAuthGate(provider) {
516
538
  const token = getBearerToken(c.req.header("Authorization"));
517
539
  const user = await timedAboveThreshold("auth.gate.authenticate", 1e3, () => authenticateRequest(provider, token, c.req.raw));
518
540
  if (user) {
519
- await ensureUserOrg(provider, user);
541
+ const requestedOrganizationId = token ? c.req.header(ORGANIZATION_ID_HEADER)?.trim() : void 0;
542
+ if (requestedOrganizationId) {
543
+ if (!selectRequestedOrganization(user, requestedOrganizationId)) return c.json({ error: "organization_forbidden" }, 403);
544
+ } else await ensureUserOrg(provider, user);
520
545
  c.set(FACTORY_AUTH_USER_KEY, user);
521
546
  const requestContext = c.get("requestContext");
522
547
  requestContext?.set("user", user);