@agent-native/core 0.141.3 → 0.141.4

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 (152) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  7. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  8. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  9. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  10. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  12. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  13. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  15. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  16. package/corpus/templates/analytics/server/plugins/db.ts +11 -3
  17. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  18. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  19. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  20. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  21. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  22. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  23. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  24. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  25. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  28. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  31. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  32. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  33. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  34. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  35. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  36. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  37. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  38. package/corpus/templates/content/server/lib/emails.ts +70 -0
  39. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  40. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  41. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  42. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  43. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  45. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  46. package/corpus/templates/factory/AGENTS.md +18 -17
  47. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  48. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  49. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  50. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  51. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  52. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  53. package/corpus/templates/factory/server/db/schema.ts +6 -0
  54. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  55. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  56. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  57. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  58. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  59. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  60. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  61. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  62. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  63. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  64. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  65. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  66. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  67. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  68. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  70. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  71. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  73. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  74. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  75. package/corpus/templates/slides/app/root.tsx +8 -0
  76. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  79. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  80. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  81. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  82. package/dist/brand-kit/index.d.ts +1 -0
  83. package/dist/brand-kit/index.js +1 -0
  84. package/dist/brand-kit/tokens.d.ts +91 -0
  85. package/dist/brand-kit/tokens.js +279 -0
  86. package/dist/brand-kit/types.d.ts +25 -0
  87. package/dist/client/AssistantChat.js +7 -7
  88. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  89. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  90. package/dist/client/onboarding/first-run-registry.js +18 -0
  91. package/dist/client/onboarding/index.d.ts +2 -0
  92. package/dist/client/onboarding/index.js +1 -0
  93. package/dist/collab/routes.d.ts +1 -1
  94. package/dist/deploy/build.d.ts +8 -0
  95. package/dist/deploy/build.js +92 -3
  96. package/dist/deploy/workspace-deploy.js +86 -3
  97. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  98. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  99. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  100. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  101. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  102. package/dist/email-catalog/actions/list-email-log.js +23 -0
  103. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  104. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  105. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  106. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  107. package/dist/email-catalog/authorize.d.ts +1 -0
  108. package/dist/email-catalog/authorize.js +9 -0
  109. package/dist/email-catalog/log.d.ts +60 -0
  110. package/dist/email-catalog/log.js +142 -0
  111. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  112. package/dist/email-catalog/provider-metrics.js +182 -0
  113. package/dist/email-catalog/registry.d.ts +78 -0
  114. package/dist/email-catalog/registry.js +70 -0
  115. package/dist/email-catalog/schema.d.ts +231 -0
  116. package/dist/email-catalog/schema.js +53 -0
  117. package/dist/email-catalog/system-emails.d.ts +13 -0
  118. package/dist/email-catalog/system-emails.js +67 -0
  119. package/dist/jobs/index.d.ts +2 -0
  120. package/dist/jobs/index.js +2 -0
  121. package/dist/jobs/run-history.js +48 -1
  122. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  123. package/dist/jobs/scheduler-dispatch.js +7 -0
  124. package/dist/jobs/scheduler-health.d.ts +22 -0
  125. package/dist/jobs/scheduler-health.js +172 -0
  126. package/dist/jobs/scheduler.js +67 -2
  127. package/dist/localization/default-messages.d.ts +45 -0
  128. package/dist/localization/default-messages.js +45 -0
  129. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  130. package/dist/notifications/routes.d.ts +3 -3
  131. package/dist/observability/routes.d.ts +5 -5
  132. package/dist/org/handlers.js +9 -1
  133. package/dist/progress/routes.d.ts +1 -1
  134. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  135. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  136. package/dist/secrets/routes.d.ts +9 -9
  137. package/dist/server/action-discovery.js +22 -0
  138. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  139. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  140. package/dist/server/agent-chat-plugin.d.ts +2 -1
  141. package/dist/server/agent-chat-plugin.js +39 -1
  142. package/dist/server/better-auth-instance.js +17 -2
  143. package/dist/server/design-token-utils.d.ts +6 -1
  144. package/dist/server/design-token-utils.js +252 -59
  145. package/dist/server/email.d.ts +11 -0
  146. package/dist/server/email.js +54 -3
  147. package/dist/server/index.d.ts +1 -0
  148. package/dist/server/index.js +1 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/server/transcribe-voice.d.ts +1 -1
  151. package/dist/vite/client.js +20 -14
  152. package/package.json +2 -1
@@ -86,6 +86,29 @@ export type ClipsTransactionalEmailInput =
86
86
  videoTimestampMs?: number | null;
87
87
  });
88
88
 
89
+ export const CLIPS_FIRST_VIEW_EMAIL_ID = "clips.first-view";
90
+ export const CLIPS_UNVIEWED_REMINDER_EMAIL_ID = "clips.unviewed-reminder";
91
+ export const CLIPS_FIRST_AGENT_VIEW_EMAIL_ID = "clips.first-agent-view";
92
+ export const CLIPS_FIRST_IMPORT_EMAIL_ID = "clips.first-import";
93
+ export const CLIPS_MONTHLY_RECAP_EMAIL_ID = "clips.monthly-recap";
94
+ export const CLIPS_TWO_CLIPS_EMAIL_ID = "clips.two-clips";
95
+ export const CLIPS_ACTIVITY_COMMENT_EMAIL_ID = "clips.activity-comment";
96
+ export const CLIPS_ACTIVITY_REACTION_EMAIL_ID = "clips.activity-reaction";
97
+
98
+ export const CLIPS_EMAIL_ID_BY_KIND: Record<
99
+ ClipsTransactionalEmailInput["kind"],
100
+ string
101
+ > = {
102
+ "first-view": CLIPS_FIRST_VIEW_EMAIL_ID,
103
+ "unviewed-reminder": CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
104
+ "first-agent-view": CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
105
+ "first-import": CLIPS_FIRST_IMPORT_EMAIL_ID,
106
+ "monthly-recap": CLIPS_MONTHLY_RECAP_EMAIL_ID,
107
+ "two-clips": CLIPS_TWO_CLIPS_EMAIL_ID,
108
+ "activity-comment": CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
109
+ "activity-reaction": CLIPS_ACTIVITY_REACTION_EMAIL_ID,
110
+ };
111
+
89
112
  export interface RecapTopClipInput {
90
113
  recordingId: string;
91
114
  title?: string | null;
@@ -613,6 +636,7 @@ export async function sendClipsTransactionalEmail(
613
636
  ? (validReplyTo(input.senderEmail) ?? FRIENDLY_REPLY_TO)
614
637
  : FRIENDLY_REPLY_TO,
615
638
  timeoutMs: EMAIL_SEND_TIMEOUT_MS,
639
+ templateId: CLIPS_EMAIL_ID_BY_KIND[input.kind],
616
640
  });
617
641
  }
618
642
 
@@ -0,0 +1,5 @@
1
+ import { registerClipsEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmailCatalog(): void {
4
+ registerClipsEmails();
5
+ }
@@ -269,6 +269,7 @@ export default defineEventHandler(async (event: H3Event) => {
269
269
  const query = getQuery(event) as {
270
270
  password?: unknown;
271
271
  t?: unknown;
272
+ animated?: unknown;
272
273
  };
273
274
  if (recording.password && loaded.role !== "owner") {
274
275
  const queryToken = typeof query.t === "string" ? query.t : "";
@@ -287,7 +288,9 @@ export default defineEventHandler(async (event: H3Event) => {
287
288
  }
288
289
 
289
290
  const sourceUrl =
290
- recording.thumbnailUrl || recording.animatedThumbnailUrl;
291
+ query.animated === "1"
292
+ ? recording.animatedThumbnailUrl || recording.thumbnailUrl
293
+ : recording.thumbnailUrl || recording.animatedThumbnailUrl;
291
294
  if (!sourceUrl) {
292
295
  setResponseStatus(event, 404);
293
296
  return { error: "Thumbnail not found" };
@@ -0,0 +1,61 @@
1
+ export interface EmailPreviewMarkup {
2
+ html: string;
3
+ plainText: string;
4
+ }
5
+
6
+ export interface EmailPreviewOptions {
7
+ title: string;
8
+ shareUrl: string;
9
+ thumbnailUrl: string;
10
+ }
11
+
12
+ const HTML_ESCAPES: Record<string, string> = {
13
+ "&": "&amp;",
14
+ "<": "&lt;",
15
+ ">": "&gt;",
16
+ '"': "&quot;",
17
+ "'": "&#39;",
18
+ };
19
+
20
+ function escapeHtml(value: string): string {
21
+ return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
22
+ }
23
+
24
+ function requireHttpUrl(value: string, field: string): string {
25
+ let parsed: URL;
26
+ try {
27
+ parsed = new URL(value);
28
+ } catch {
29
+ throw new Error(`${field} must be an absolute HTTP URL`);
30
+ }
31
+
32
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
33
+ throw new Error(`${field} must be an absolute HTTP URL`);
34
+ }
35
+
36
+ return parsed.toString();
37
+ }
38
+
39
+ export function buildEmailPreviewMarkup(
40
+ options: EmailPreviewOptions,
41
+ ): EmailPreviewMarkup {
42
+ const title = options.title.trim();
43
+ if (!title) throw new Error("Email preview title cannot be empty");
44
+
45
+ const shareUrl = requireHttpUrl(options.shareUrl, "shareUrl");
46
+ const thumbnailUrl = requireHttpUrl(options.thumbnailUrl, "thumbnailUrl");
47
+ const escapedTitle = escapeHtml(title);
48
+ const escapedShareUrl = escapeHtml(shareUrl);
49
+ const escapedThumbnailUrl = escapeHtml(thumbnailUrl);
50
+ const escapedAlt = escapeHtml(`Play ${title}`);
51
+
52
+ return {
53
+ plainText: `${title}\n${shareUrl}`,
54
+ html: [
55
+ '<div style="max-width:640px">',
56
+ `<p style="margin:0 0 12px;font-size:16px;font-weight:600"><a href="${escapedShareUrl}" style="text-decoration:none">${escapedTitle}</a></p>`,
57
+ `<a href="${escapedShareUrl}" style="display:block;text-decoration:none"><img src="${escapedThumbnailUrl}" alt="${escapedAlt}" width="640" style="display:block;width:100%;max-width:640px;height:auto;border:0" /></a>`,
58
+ "</div>",
59
+ ].join(""),
60
+ };
61
+ }
@@ -21,6 +21,10 @@ import { and, eq } from "drizzle-orm";
21
21
 
22
22
  import { CONTENT_USER_PREFS_KEY } from "../../shared/content-user-prefs.js";
23
23
  import { getDb, schema } from "../db/index.js";
24
+ import {
25
+ CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
26
+ CONTENT_DOCUMENT_MENTION_EMAIL_ID,
27
+ } from "./emails.js";
24
28
 
25
29
  export type DocumentCommentNotificationResult = ActivityNotificationResult;
26
30
 
@@ -70,6 +74,48 @@ export interface DocumentCommentNotificationInput {
70
74
  isReply: boolean;
71
75
  }
72
76
 
77
+ export function renderDocumentCommentEmail({
78
+ actor,
79
+ title,
80
+ url,
81
+ content,
82
+ isReply,
83
+ wasMentioned,
84
+ }: {
85
+ actor: string;
86
+ title: string;
87
+ url: string;
88
+ content: string;
89
+ isReply: boolean;
90
+ wasMentioned: boolean;
91
+ }) {
92
+ const lead = wasMentioned
93
+ ? `${emailStrong(actor)} mentioned you in a comment on ${emailStrong(title)}.`
94
+ : isReply
95
+ ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
96
+ : `${emailStrong(actor)} commented on ${emailStrong(title)}.`;
97
+
98
+ return {
99
+ subject: wasMentioned
100
+ ? `${actor} mentioned you on "${title}"`
101
+ : isReply
102
+ ? `${actor} replied to a comment on "${title}"`
103
+ : `${actor} commented on "${title}"`,
104
+ ...renderEmail({
105
+ preheader: `${actor} commented on ${title}.`,
106
+ heading: wasMentioned
107
+ ? "You were mentioned"
108
+ : isReply
109
+ ? "New reply on your document"
110
+ : "New comment",
111
+ paragraphs: [lead, `"${excerpt(content)}"`],
112
+ cta: { label: "Open document", url },
113
+ footer:
114
+ "You received this because you own, were mentioned in, or participated in this thread. Turn these off in Documents settings.",
115
+ }),
116
+ };
117
+ }
118
+
73
119
  export async function notifyDocumentComment(
74
120
  input: DocumentCommentNotificationInput,
75
121
  ): Promise<DocumentCommentNotificationResult> {
@@ -112,34 +158,19 @@ async function deliverDocumentCommentEmails(
112
158
  logLabel: LOG_LABEL,
113
159
  send: async (to) => {
114
160
  const wasMentioned = mentioned.has(to);
115
- const lead = wasMentioned
116
- ? `${emailStrong(actor)} mentioned you in a comment on ${emailStrong(title)}.`
117
- : input.isReply
118
- ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
119
- : `${emailStrong(actor)} commented on ${emailStrong(title)}.`;
120
-
121
- const { html, text } = renderEmail({
122
- preheader: `${actor} commented on ${title}.`,
123
- heading: wasMentioned
124
- ? "You were mentioned"
125
- : input.isReply
126
- ? "New reply on your document"
127
- : "New comment",
128
- paragraphs: [lead, `"${excerpt(input.content)}"`],
129
- cta: { label: "Open document", url },
130
- footer:
131
- "You received this because you own, were mentioned in, or participated in this thread. Turn these off in Documents settings.",
132
- });
133
-
134
161
  await sendEmail({
162
+ ...renderDocumentCommentEmail({
163
+ actor,
164
+ title,
165
+ url,
166
+ content: input.content,
167
+ isReply: input.isReply,
168
+ wasMentioned,
169
+ }),
135
170
  to,
136
- subject: wasMentioned
137
- ? `${actor} mentioned you on "${title}"`
138
- : input.isReply
139
- ? `${actor} replied to a comment on "${title}"`
140
- : `${actor} commented on "${title}"`,
141
- html,
142
- text,
171
+ templateId: wasMentioned
172
+ ? CONTENT_DOCUMENT_MENTION_EMAIL_ID
173
+ : CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
143
174
  });
144
175
  },
145
176
  });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Documents sends.
3
+ *
4
+ * Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
5
+ * and preview them without the app having sent anything yet.
6
+ */
7
+
8
+ import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
9
+
10
+ import { renderDocumentCommentEmail } from "./comment-notifications.js";
11
+
12
+ /** Obviously-fake sample data — these render in a preview pane, never send. */
13
+ const SAMPLE_TITLE = "Q3 launch brief";
14
+ const SAMPLE_URL = "https://example.com/page/doc_sample";
15
+ const SAMPLE_COMMENT =
16
+ "Can we tighten the second paragraph? It repeats the intro.";
17
+
18
+ export const CONTENT_DOCUMENT_COMMENT_EMAIL_ID = "content.document-comment";
19
+ export const CONTENT_DOCUMENT_MENTION_EMAIL_ID = "content.document-mention";
20
+
21
+ let registered = false;
22
+
23
+ export function registerContentEmails(): void {
24
+ if (registered) return;
25
+ registered = true;
26
+
27
+ defineTransactionalEmail({
28
+ id: CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
29
+ name: "Document comment",
30
+ trigger:
31
+ "Someone posts a comment or a thread reply on a document. Sent to recipients who were not mentioned in it; the copy and subject differ slightly for a reply.",
32
+ recipientLabel: "Owner and thread authors",
33
+ recipient:
34
+ "The document owner, plus every prior author in the thread when the new comment is a reply. The list is re-checked against the document's live ACL and filtered by each user's `emailNotifications` preference; the comment's own author never receives it.",
35
+ senderLabel: "Default sender",
36
+ sender:
37
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
38
+ preview: () =>
39
+ renderDocumentCommentEmail({
40
+ actor: "Sam Rivera",
41
+ title: SAMPLE_TITLE,
42
+ url: SAMPLE_URL,
43
+ content: SAMPLE_COMMENT,
44
+ isReply: false,
45
+ wasMentioned: false,
46
+ }),
47
+ });
48
+
49
+ defineTransactionalEmail({
50
+ id: CONTENT_DOCUMENT_MENTION_EMAIL_ID,
51
+ name: "Document mention",
52
+ trigger:
53
+ "A comment or reply mentions someone by email. Sent instead of the plain comment notification to each mentioned recipient.",
54
+ recipientLabel: "Mentioned users",
55
+ recipient:
56
+ "The mentioned addresses supplied by the caller, re-checked against the document's live ACL and filtered by each user's `emailNotifications` preference.",
57
+ senderLabel: "Default sender",
58
+ sender:
59
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
60
+ preview: () =>
61
+ renderDocumentCommentEmail({
62
+ actor: "Sam Rivera",
63
+ title: SAMPLE_TITLE,
64
+ url: SAMPLE_URL,
65
+ content: SAMPLE_COMMENT,
66
+ isReply: false,
67
+ wasMentioned: true,
68
+ }),
69
+ });
70
+ }
@@ -0,0 +1,5 @@
1
+ import { registerContentEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmails(): void {
4
+ registerContentEmails();
5
+ }
@@ -118,20 +118,23 @@ function designDeepLink(designId: string): string {
118
118
 
119
119
  function classifyVar(name: string, value: string): ImportedTokenType {
120
120
  const n = name.toLowerCase();
121
+ if (isColorValue(value)) return "color";
122
+ if (/radius|rounded/i.test(n)) return "radius";
123
+ if (/font|size|leading|tracking|weight|heading|body|type/i.test(n)) {
124
+ return "typography";
125
+ }
126
+ if (/spacing|gap|padding|margin|space/i.test(n)) return "spacing";
127
+ if (/shadow|blur|drop/i.test(n)) return "shadow";
121
128
  if (
122
129
  /color|bg|background|text|border|accent|primary|secondary|surface|muted|foreground|fill|stroke/i.test(
123
130
  n,
124
- ) ||
125
- isColorValue(value)
131
+ ) &&
132
+ !/^-?\d*\.?\d+(px|rem|em|ex|ch|%|vh|vw|vmin|vmax|pt|pc|cm|mm|in|s|ms|deg|fr)?$/i.test(
133
+ value.trim(),
134
+ )
126
135
  ) {
127
136
  return "color";
128
137
  }
129
- if (/font|size|leading|tracking|weight|heading|body|type/i.test(n)) {
130
- return "typography";
131
- }
132
- if (/radius|rounded/i.test(n)) return "radius";
133
- if (/spacing|gap|padding|margin|space/i.test(n)) return "spacing";
134
- if (/shadow|blur|drop/i.test(n)) return "shadow";
135
138
  return "other";
136
139
  }
137
140
 
@@ -22,25 +22,32 @@ function classifyVar(
22
22
  ): "color" | "typography" | "spacing" | "radius" | "shadow" | "other" {
23
23
  const n = name.toLowerCase();
24
24
  if (
25
- /color|bg|background|text|border|accent|primary|secondary|surface|muted|foreground|fill|stroke/i.test(
26
- n,
27
- ) ||
28
25
  /^(#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\(|oklch\(|color\()/.test(value.trim())
29
26
  ) {
30
27
  return "color";
31
28
  }
32
- if (/font|size|leading|tracking|weight|heading|body|type/i.test(n)) {
33
- return "typography";
34
- }
35
29
  if (/radius|rounded/i.test(n)) {
36
30
  return "radius";
37
31
  }
32
+ if (/font|size|leading|tracking|weight|heading|body|type/i.test(n)) {
33
+ return "typography";
34
+ }
38
35
  if (/spacing|gap|padding|margin|space/i.test(n)) {
39
36
  return "spacing";
40
37
  }
41
38
  if (/shadow|blur|drop/i.test(n)) {
42
39
  return "shadow";
43
40
  }
41
+ if (
42
+ /color|bg|background|text|border|accent|primary|secondary|surface|muted|foreground|fill|stroke/i.test(
43
+ n,
44
+ ) &&
45
+ !/^-?\d*\.?\d+(px|rem|em|ex|ch|%|vh|vw|vmin|vmax|pt|pc|cm|mm|in|s|ms|deg|fr)?$/i.test(
46
+ value.trim(),
47
+ )
48
+ ) {
49
+ return "color";
50
+ }
44
51
  return "other";
45
52
  }
46
53
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Design token imports classify typography dimensions correctly.
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ meta,
4
+ } from "@agent-native/dispatch/routes/pages/transactional-email.$appId.$id";
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ meta,
4
+ } from "@agent-native/dispatch/routes/pages/transactional-email";
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Transactional email catalogs and delivery activity are available in Dispatch again.
@@ -3,7 +3,7 @@
3
3
  Factory is the visual workspace for building agent factories from incoming work
4
4
  to governed delivery. The map is the source of truth; Dispatch owns the shared
5
5
  inbox and routing, while Factory owns graph versions, queue state, rules,
6
- decisions, feedback, agent runs, and provider-action audit records.
6
+ decisions, feedback, agent runs, and provider audit records.
7
7
 
8
8
  Before building common workspace or agent UI, read `agent-native-toolkit`; use
9
9
  `customizing-agent-native` for the configure → compose → eject → propose
@@ -12,7 +12,7 @@ ladder.
12
12
  ## Core rules
13
13
 
14
14
  - Keep app state in SQL via Drizzle and scope every read/write by org and
15
- authenticated member. Use actions as the UI, agent, CLI, MCP, and A2A surface.
15
+ member. Use actions as the UI, agent, CLI, MCP, and A2A surface.
16
16
  - Keep migrations additive and portable. These tables intentionally use explicit
17
17
  `ownerEmail`/`orgId` columns for org-visible data, not `ownableColumns()`;
18
18
  do not call `accessFilter` on them without adding deliberate visibility data.
@@ -31,23 +31,23 @@ ladder.
31
31
  automations add 👀 and tag `@builderio` in the source thread; GitHub and Sentry
32
32
  clear bugs use the Builder run API. Clips, Design, and Content are always
33
33
  owner-managed and never enter autonomous dispatch or PR governance.
34
- - PR governance requires verified internal BuilderIO membership, a clear bug,
35
- passing CI, and handled review feedback. Product or UX implications stay
34
+ - PR governance requires verified BuilderIO membership, a clear bug, passing
35
+ CI, and handled review feedback. Product or UX implications stay
36
36
  manual. Auto-merge additionally requires a verified Factory Builder run.
37
37
  - Reuse the existing ai-services GitHub read and Builder execution APIs. Do not
38
38
  duplicate GitHub installation/webhook infrastructure in this template.
39
39
  - Do not add CRUD routes under `server/routes/api/`; actions are the domain
40
40
  surface. Provider callbacks are the only exception and must verify signatures.
41
- - Factory graph edits create immutable blueprint versions. AI proposes a
42
- complete graph with `source=ai`; a person reviews and publishes it through
41
+ - Factory graph edits create immutable blueprint versions. AI proposes a graph
42
+ with `source=ai`; a person reviews and publishes it through
43
43
  the same action surface as manual edits.
44
44
 
45
45
  ## Application state
46
46
 
47
- - `navigation.view`: `factory` when the visual workspace is open.
47
+ - `navigation.view`: `factory` when the workspace is open.
48
48
  - `navigation.factoryId`: selected Factory id when present.
49
49
  - `navigation.factoryTab`: `map` | `inbox` | `rules` | `automations` | `settings`.
50
- - `navigation.factoryNodeId` / `navigation.factoryEdgeId`: selected graph target.
50
+ - `navigation.factoryNodeId` / `navigation.factoryEdgeId`: selected graph item.
51
51
  - A selected graph node or edge is part of `navigation` context. Read
52
52
  `view-screen` before answering why a route exists or changing the selected
53
53
  Factory.
@@ -56,18 +56,19 @@ ladder.
56
56
 
57
57
  | Action | Purpose |
58
58
  | --- | --- |
59
- | `list-triage-items` / `get-triage-item` | Inspect the queue and evidence. |
59
+ | `list-triage-items` / `get-triage-item` | Inspect queue and evidence. |
60
60
  | `poll-slack-channel` | Observe Slack history; never writes to Slack. |
61
61
  | `get-slack-feedback-context` | Read the bounded full Slack thread before classification. |
62
62
  | `poll-github-sources` / `poll-sentry-errors` | Observe bounded GitHub and Sentry source queues. |
63
63
  | `ingest-github-observation` | Store read-only PR evidence. |
64
64
  | `list-triage-rules` / `save-triage-rule` | Tune prompt rules and guards. |
65
- | `evaluate-triage-item` | Append a structured decision. |
65
+ | `evaluate-triage-item` | Append a decision. |
66
66
  | `record-triage-feedback` | Capture human correction for learning. |
67
67
  | `approve-factory-item` | Explicitly authorize one bounded run. |
68
68
  | `start-builder-for-item` | Govern clear-bug dispatch; Slack tags Builder in-thread, other sources use Builder API. |
69
69
  | `govern-agent-native-pull-request` | Apply CI, review, internal-author, product, and owner gates to PR approval/merge. |
70
- | `list-factory-automations` / `save-factory-automation` / `run-factory-automation` | Inspect and edit organization-owned Factory prompts, models, schedules, and runs. |
70
+ | `list-factory-automations` / `save-factory-automation` / `run-factory-automation` | Inspect and edit org-owned Factory prompts, models, schedules, and runs. |
71
+ | `get-factory-automation-health` | Inspect the durable scheduler heartbeat and last scheduler error when runs appear stale. |
71
72
  | `suggest-factory-rules` | Mine feedback and fast approvals into proposals. |
72
73
  | `reconcile-triage-run` | Persist callback/provider reconciliation. |
73
74
  | `list-factories` / `get-factory-graph` | Inspect Factory definitions, graph versions, and live evidence metrics. |
@@ -77,7 +78,7 @@ ladder.
77
78
  Rules start in shadow mode; hard guards always apply. Editable organization
78
79
  automations execute stored prompts; every external mutation needs a durable run,
79
80
  idempotency key, and provider confirmation. The legacy observer is disabled
80
- when organization automations are seeded.
81
+ once organization automations are seeded.
81
82
 
82
83
  Use the visual editor for direct blueprint changes. Use the agent chat for
83
84
  natural language design, explanations, and proposals; it must preserve a
@@ -87,14 +88,14 @@ actions, never through graph JSON.
87
88
 
88
89
  ## Scheduler identity
89
90
 
90
- `WORKSPACE_OWNER_EMAIL` is read only at startup to find the deployment
91
- organization and stamp seeded automation `createdBy`; it is never caller
92
- identity and must not enter request authorization or credential resolution.
91
+ `WORKSPACE_OWNER_EMAIL` is read only at startup to find the deployment org and
92
+ stamp seeded automation `createdBy`; it is never caller identity and must not
93
+ enter request authorization or credential resolution.
93
94
 
94
95
  ## Hosting
95
96
 
96
97
  Production needs `DATABASE_URL`, `WORKSPACE_OWNER_EMAIL`, and
97
- `FACTORY_PUBLIC_URL`. Builder execution additionally needs the service URL and
98
- project ID plus workspace-resolved Builder credentials. GitHub and Sentry
98
+ `FACTORY_PUBLIC_URL`. Builder execution additionally needs the service URL,
99
+ project ID, and workspace-resolved Builder credentials. GitHub and Sentry
99
100
  polling use workspace-resolved provider credentials. Provider callbacks and
100
101
  external writes must remain auditable and fail closed when evidence is partial.
@@ -0,0 +1,50 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { getAutomationSchedulerHealth } from "@agent-native/core/jobs";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ requireWorkspaceMember,
7
+ workspaceMemberIdentityFromContext,
8
+ } from "../server/lib/require-workspace-member.js";
9
+
10
+ const STALE_AFTER_MS = 3 * 60_000;
11
+
12
+ export default defineAction({
13
+ description:
14
+ "Inspect the Factory recurring-automation scheduler heartbeat and last error. Use this when an automation appears not to be running.",
15
+ schema: z.object({}),
16
+ http: { method: "GET" },
17
+ readOnly: true,
18
+ run: async (_, context) => {
19
+ const { orgId } = await requireWorkspaceMember(
20
+ workspaceMemberIdentityFromContext(context),
21
+ );
22
+ const health = await getAutomationSchedulerHealth({
23
+ appId: "factory",
24
+ orgId,
25
+ });
26
+ if (!health?.lastCheckedAt) {
27
+ return {
28
+ status: "no-data" as const,
29
+ lastCheckedAt: null,
30
+ lastDispatchedAt: health?.lastDispatchedAt ?? null,
31
+ lastError: health?.lastError ?? null,
32
+ runtime: health?.runtime ?? null,
33
+ staleAfterMs: STALE_AFTER_MS,
34
+ };
35
+ }
36
+ const stale = Date.now() - health.lastCheckedAt > STALE_AFTER_MS;
37
+ return {
38
+ status: health.lastError
39
+ ? ("error" as const)
40
+ : stale
41
+ ? ("stale" as const)
42
+ : ("healthy" as const),
43
+ lastCheckedAt: health.lastCheckedAt,
44
+ lastDispatchedAt: health.lastDispatchedAt,
45
+ lastError: health.lastError,
46
+ runtime: health.runtime,
47
+ staleAfterMs: STALE_AFTER_MS,
48
+ };
49
+ },
50
+ });
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
+ import { getEmailReadiness } from "@agent-native/core/server";
2
3
  import { and, eq } from "drizzle-orm";
3
4
  import { z } from "zod";
4
5
 
@@ -19,6 +20,7 @@ export default defineAction({
19
20
  const { orgId } = await requireWorkspaceMember(
20
21
  workspaceMemberIdentityFromContext(context),
21
22
  );
23
+ const emailReadiness = await getEmailReadiness();
22
24
  const row = (
23
25
  await getDb()
24
26
  .select()
@@ -41,6 +43,9 @@ export default defineAction({
41
43
  sentryProjectSlug: null,
42
44
  sentryEnvironment: null,
43
45
  lastSentrySeenAt: null,
46
+ automationFailureAlertsEnabled: true,
47
+ automationFailureAlertEmail: null,
48
+ emailReadiness,
44
49
  };
45
50
  }
46
51
  return {
@@ -48,6 +53,9 @@ export default defineAction({
48
53
  pollingEnabled: row.pollingEnabled === 1,
49
54
  githubPollingEnabled: row.githubPollingEnabled === 1,
50
55
  sentryPollingEnabled: row.sentryPollingEnabled === 1,
56
+ automationFailureAlertsEnabled: row.automationFailureAlertsEnabled === 1,
57
+ automationFailureAlertEmail: row.automationFailureAlertEmail,
58
+ emailReadiness,
51
59
  };
52
60
  },
53
61
  });
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
+ import { and, eq } from "drizzle-orm";
2
3
  import { z } from "zod";
3
4
 
4
5
  import { getDb } from "../server/db/index.js";
@@ -24,6 +25,8 @@ export default defineAction({
24
25
  sentryProjectSlug: z.string().trim().max(200).optional(),
25
26
  sentryEnvironment: z.string().trim().max(200).optional(),
26
27
  repository: z.string().trim().max(256).optional(),
28
+ automationFailureAlertsEnabled: z.boolean().default(true),
29
+ automationFailureAlertEmail: z.string().trim().email().optional(),
27
30
  }),
28
31
  http: { method: "POST" },
29
32
  run: async (
@@ -38,6 +41,8 @@ export default defineAction({
38
41
  sentryProjectSlug,
39
42
  sentryEnvironment,
40
43
  repository,
44
+ automationFailureAlertsEnabled,
45
+ automationFailureAlertEmail,
41
46
  },
42
47
  context,
43
48
  ) => {
@@ -46,6 +51,19 @@ export default defineAction({
46
51
  );
47
52
  const now = new Date().toISOString();
48
53
  const db = getDb();
54
+ const existing = (
55
+ await db
56
+ .select({
57
+ automationFailureAlertEmail: triageConfig.automationFailureAlertEmail,
58
+ })
59
+ .from(triageConfig)
60
+ .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
61
+ .limit(1)
62
+ )[0];
63
+ const persistedAutomationFailureAlertEmail =
64
+ automationFailureAlertEmail ??
65
+ existing?.automationFailureAlertEmail ??
66
+ null;
49
67
  await db
50
68
  .insert(triageConfig)
51
69
  .values({
@@ -60,6 +78,8 @@ export default defineAction({
60
78
  sentryProjectSlug: sentryProjectSlug ?? null,
61
79
  sentryEnvironment: sentryEnvironment ?? null,
62
80
  repository: repository ?? null,
81
+ automationFailureAlertsEnabled: automationFailureAlertsEnabled ? 1 : 0,
82
+ automationFailureAlertEmail: persistedAutomationFailureAlertEmail,
63
83
  createdAt: now,
64
84
  updatedAt: now,
65
85
  ownerEmail: userEmail,
@@ -78,6 +98,10 @@ export default defineAction({
78
98
  sentryProjectSlug: sentryProjectSlug ?? null,
79
99
  sentryEnvironment: sentryEnvironment ?? null,
80
100
  repository: repository ?? null,
101
+ automationFailureAlertsEnabled: automationFailureAlertsEnabled
102
+ ? 1
103
+ : 0,
104
+ automationFailureAlertEmail: persistedAutomationFailureAlertEmail,
81
105
  updatedAt: now,
82
106
  ownerEmail: userEmail,
83
107
  },
@@ -88,6 +112,8 @@ export default defineAction({
88
112
  githubPollingEnabled,
89
113
  sentryPollingEnabled,
90
114
  slackChannelId: slackChannelId ?? null,
115
+ automationFailureAlertsEnabled,
116
+ automationFailureAlertEmail: persistedAutomationFailureAlertEmail,
91
117
  };
92
118
  },
93
119
  });