@agent-native/core 0.136.5 → 0.137.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 (157) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
  3. package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
  4. package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
  5. package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
  6. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
  7. package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
  8. package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
  9. package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
  10. package/corpus/templates/content/actions/_database-utils.ts +6 -0
  11. package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
  12. package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
  13. package/corpus/templates/content/actions/configure-document-property.ts +113 -32
  14. package/corpus/templates/content/actions/delete-content-database.ts +5 -7
  15. package/corpus/templates/content/actions/delete-document-property.ts +109 -62
  16. package/corpus/templates/content/actions/delete-document.ts +178 -36
  17. package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
  18. package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
  19. package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
  20. package/corpus/templates/content/actions/remove-database-items.ts +13 -0
  21. package/corpus/templates/content/actions/set-document-property.ts +69 -1
  22. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
  23. package/corpus/templates/content/parity/matrix.md +1 -1
  24. package/corpus/templates/content/parity/matrix.ts +1 -0
  25. package/corpus/templates/content/server/db/schema.ts +27 -0
  26. package/corpus/templates/content/server/plugins/db.ts +24 -0
  27. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
  28. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  29. package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
  30. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
  31. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
  32. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
  33. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
  34. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
  35. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
  36. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  37. package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
  38. package/corpus/templates/design/app/i18n-data.ts +8 -0
  39. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
  40. package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
  41. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
  42. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
  43. package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
  44. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
  45. package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
  46. package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
  47. package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
  48. package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
  49. package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
  50. package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
  51. package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
  52. package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
  53. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
  54. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
  57. package/corpus/templates/factory/.env.example +17 -0
  58. package/corpus/templates/factory/AGENTS.md +27 -17
  59. package/corpus/templates/factory/README.md +19 -9
  60. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
  61. package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
  62. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
  63. package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
  64. package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
  65. package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
  66. package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
  67. package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
  68. package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
  69. package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
  70. package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
  71. package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
  72. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
  73. package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
  74. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
  75. package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
  76. package/corpus/templates/factory/app/routes/factory.tsx +462 -9
  77. package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
  78. package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
  79. package/corpus/templates/factory/netlify.toml +2 -2
  80. package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
  81. package/corpus/templates/factory/server/connectors/slack.ts +133 -5
  82. package/corpus/templates/factory/server/db/schema.ts +6 -0
  83. package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
  84. package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
  85. package/corpus/templates/factory/server/plugins/auth.ts +1 -0
  86. package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
  87. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
  88. package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
  89. package/corpus/templates/factory/server/triage/contracts.ts +14 -1
  90. package/corpus/templates/factory/server/triage/github-client.ts +404 -0
  91. package/corpus/templates/factory/server/triage/metadata.ts +44 -0
  92. package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
  93. package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
  94. package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
  95. package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
  96. package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
  97. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
  98. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
  99. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
  100. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
  101. package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
  102. package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
  103. package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
  104. package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
  105. package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
  106. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
  107. package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
  108. package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
  109. package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
  110. package/corpus/templates/plan/shared/plan-content.ts +17 -2
  111. package/dist/cli/skills-content/canvas.d.ts +3 -3
  112. package/dist/cli/skills-content/canvas.js +16 -5
  113. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  114. package/dist/cli/skills-content/visual-plan-skill.js +27 -0
  115. package/dist/cli/skills-content/wireframe.d.ts +2 -2
  116. package/dist/cli/skills-content/wireframe.js +7 -0
  117. package/dist/client/AgentPanel.js +23 -8
  118. package/dist/client/api-path.d.ts +6 -0
  119. package/dist/client/api-path.js +55 -0
  120. package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
  121. package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
  122. package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
  123. package/dist/client/onboarding/first-run-enabled.js +9 -0
  124. package/dist/client/onboarding/index.d.ts +2 -1
  125. package/dist/client/onboarding/index.js +1 -0
  126. package/dist/client/onboarding/use-onboarding.d.ts +6 -1
  127. package/dist/client/onboarding/use-onboarding.js +32 -1
  128. package/dist/client/route-state.js +10 -0
  129. package/dist/connections/catalog.d.ts +4 -4
  130. package/dist/connections/catalog.js +3 -3
  131. package/dist/db/index.d.ts +1 -1
  132. package/dist/db/index.js +1 -1
  133. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  134. package/dist/notifications/routes.d.ts +5 -5
  135. package/dist/observability/routes.d.ts +3 -3
  136. package/dist/onboarding/app-profile.d.ts +4 -0
  137. package/dist/onboarding/app-profile.js +378 -0
  138. package/dist/onboarding/index.d.ts +2 -1
  139. package/dist/onboarding/index.js +1 -0
  140. package/dist/onboarding/plugin.d.ts +2 -0
  141. package/dist/onboarding/plugin.js +21 -1
  142. package/dist/onboarding/types.d.ts +19 -0
  143. package/dist/progress/routes.d.ts +1 -1
  144. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  145. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  146. package/dist/resources/handlers.d.ts +1 -1
  147. package/dist/server/auth.js +21 -1
  148. package/dist/server/better-auth-instance.d.ts +3 -2
  149. package/dist/server/better-auth-instance.js +9 -7
  150. package/dist/server/email-markdown.js +3 -8
  151. package/dist/server/transcribe-voice.d.ts +1 -1
  152. package/dist/shared/first-run-onboarding.d.ts +3 -0
  153. package/dist/shared/first-run-onboarding.js +3 -0
  154. package/dist/triggers/index.d.ts +3 -0
  155. package/dist/triggers/index.js +5 -0
  156. package/docs/content/cross-app-sso.mdx +26 -0
  157. package/package.json +1 -1
@@ -0,0 +1,411 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { and, eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+
5
+ import { getDb } from "../server/db/index.js";
6
+ import {
7
+ triageConfig,
8
+ triageDecisions,
9
+ triageItems,
10
+ triageRuns,
11
+ } from "../server/db/schema.js";
12
+ import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
13
+ import {
14
+ requireWorkspaceMember,
15
+ workspaceMemberIdentityFromContext,
16
+ } from "../server/lib/require-workspace-member.js";
17
+ import { startBuilderRun } from "../server/triage/builder-executor.js";
18
+ import { stableId } from "../server/triage/ids.js";
19
+ import {
20
+ metadataBoolean,
21
+ parseTriageMetadata,
22
+ serializeTriageMetadata,
23
+ } from "../server/triage/metadata.js";
24
+ import { detectOwnerOwnedArea } from "../server/triage/pr-policy.js";
25
+ import { createSlackReader } from "../server/triage/slack-client.js";
26
+
27
+ const replyTextPrefix =
28
+ "@builderio please fix this in a reply. This is a clear bug based on the feedback above. Please send a PR when ready.";
29
+
30
+ function replyTextForItem(item: {
31
+ id: string;
32
+ sourceUrl: string | null;
33
+ }): string {
34
+ return [
35
+ replyTextPrefix,
36
+ `Factory item: ${item.id}`,
37
+ item.sourceUrl ? `Source: ${item.sourceUrl}` : "",
38
+ "Please include the Factory item and source link in the PR description.",
39
+ ]
40
+ .filter(Boolean)
41
+ .join("\n");
42
+ }
43
+
44
+ async function writeMetadata(
45
+ itemId: string,
46
+ orgId: string,
47
+ patch: Record<string, unknown>,
48
+ ): Promise<void> {
49
+ const db = getDb();
50
+ const item = (
51
+ await db
52
+ .select({ metadataJson: triageItems.metadataJson })
53
+ .from(triageItems)
54
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
55
+ .limit(1)
56
+ )[0];
57
+ if (!item)
58
+ throw new Error("Factory item disappeared while recording dispatch.");
59
+ const metadata = parseTriageMetadata(item.metadataJson);
60
+ Object.assign(metadata, patch);
61
+ await db
62
+ .update(triageItems)
63
+ .set({
64
+ metadataJson: serializeTriageMetadata(metadata),
65
+ updatedAt: new Date().toISOString(),
66
+ })
67
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
68
+ }
69
+
70
+ async function recordDecision(input: {
71
+ itemId: string;
72
+ userEmail: string;
73
+ orgId: string;
74
+ outcome: "propose_fix" | "needs_manual";
75
+ reason: string;
76
+ guardResults: Array<{ code: string; passed: boolean; reason: string }>;
77
+ }) {
78
+ const id = stableId(
79
+ "decision",
80
+ input.orgId,
81
+ input.itemId,
82
+ "automatic-builder",
83
+ );
84
+ await getDb()
85
+ .insert(triageDecisions)
86
+ .values({
87
+ id,
88
+ itemId: input.itemId,
89
+ ruleId: null,
90
+ mode: "automation",
91
+ outcome: input.outcome,
92
+ reason: input.reason,
93
+ guardResultsJson: JSON.stringify(input.guardResults),
94
+ model: "factory-automation",
95
+ promptVersion: 1,
96
+ createdAt: new Date().toISOString(),
97
+ ownerEmail: input.userEmail,
98
+ orgId: input.orgId,
99
+ })
100
+ .onConflictDoNothing();
101
+ return id;
102
+ }
103
+
104
+ export default defineAction({
105
+ description:
106
+ "Start the governed clear-bug Builder flow for a Factory item. Slack items are kept in-thread by adding 👀 and tagging @builderio; GitHub issues and Sentry errors use the Builder agent run API. Owner-managed Clips, Design, and Content items are always left for their owner.",
107
+ schema: z.object({
108
+ itemId: z.string().min(1),
109
+ clearBug: z.boolean(),
110
+ reason: z.string().trim().min(1).max(4_000),
111
+ productUxImplications: z.boolean().default(false),
112
+ clearErrorReport: z.string().trim().max(8_000).optional(),
113
+ }),
114
+ http: false,
115
+ run: async (
116
+ { itemId, clearBug, reason, productUxImplications, clearErrorReport },
117
+ context,
118
+ ) => {
119
+ const { userEmail, orgId } = await requireWorkspaceMember(
120
+ workspaceMemberIdentityFromContext(context),
121
+ );
122
+ await requireFactoryAutomation(
123
+ context,
124
+ { userEmail, orgId },
125
+ "builderDispatch",
126
+ );
127
+ const db = getDb();
128
+ const item = (
129
+ await db
130
+ .select()
131
+ .from(triageItems)
132
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
133
+ .limit(1)
134
+ )[0];
135
+ if (!item) throw new Error("Factory item not found.");
136
+ const metadata = parseTriageMetadata(item.metadataJson);
137
+ const ownerOwnedArea = detectOwnerOwnedArea([
138
+ item.title,
139
+ item.summary,
140
+ item.repository,
141
+ typeof metadata.productArea === "string"
142
+ ? metadata.productArea
143
+ : undefined,
144
+ typeof metadata.path === "string" ? metadata.path : undefined,
145
+ ]);
146
+ const guardResults = [
147
+ {
148
+ code: "unknown_change",
149
+ passed: clearBug,
150
+ reason: clearBug
151
+ ? "The automation classified a concrete, reproducible bug or error report."
152
+ : "The report is not a clear bug, so no external work was started.",
153
+ },
154
+ {
155
+ code: "unknown_change",
156
+ passed: !productUxImplications,
157
+ reason: productUxImplications
158
+ ? "Product or UX implications require manual ownership."
159
+ : "No product or UX decision was detected.",
160
+ },
161
+ ...(ownerOwnedArea
162
+ ? [
163
+ {
164
+ code: "owner_owned",
165
+ passed: false,
166
+ reason: `${ownerOwnedArea} is fully owned by its product owner and is excluded from autonomous Builder work.`,
167
+ },
168
+ ]
169
+ : []),
170
+ ];
171
+ const blocked = guardResults.some((guard) => !guard.passed);
172
+ const decisionId = await recordDecision({
173
+ itemId,
174
+ userEmail,
175
+ orgId,
176
+ outcome: blocked ? "needs_manual" : "propose_fix",
177
+ reason: blocked
178
+ ? guardResults
179
+ .filter((guard) => !guard.passed)
180
+ .map((guard) => guard.reason)
181
+ .join(" ")
182
+ : reason,
183
+ guardResults,
184
+ });
185
+ if (blocked) {
186
+ await db
187
+ .update(triageItems)
188
+ .set({ status: "needs_manual", updatedAt: new Date().toISOString() })
189
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
190
+ return {
191
+ ok: true,
192
+ started: false,
193
+ needsManual: true,
194
+ decisionId,
195
+ reason: guardResults
196
+ .filter((guard) => !guard.passed)
197
+ .map((guard) => guard.reason)
198
+ .join(" "),
199
+ };
200
+ }
201
+
202
+ const dedupeKey = stableId("automatic-builder", orgId, itemId);
203
+ const runId = stableId("run", dedupeKey);
204
+ const now = new Date().toISOString();
205
+ const isSlack = item.source === "slack";
206
+ const existing = (
207
+ await db
208
+ .select()
209
+ .from(triageRuns)
210
+ .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)))
211
+ .limit(1)
212
+ )[0];
213
+ const retryableSlackRun =
214
+ isSlack &&
215
+ existing &&
216
+ (existing.status === "failed" || existing.status === "cancelled");
217
+ if (existing && !retryableSlackRun) {
218
+ return {
219
+ ok: true,
220
+ started: true,
221
+ deduplicated: true,
222
+ runId,
223
+ status: existing.status,
224
+ };
225
+ }
226
+
227
+ if (retryableSlackRun) {
228
+ await db
229
+ .update(triageRuns)
230
+ .set({
231
+ status: "submitted",
232
+ error: null,
233
+ completedAt: null,
234
+ heartbeatAt: now,
235
+ dispatchAttempts: (existing.dispatchAttempts ?? 0) + 1,
236
+ })
237
+ .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)));
238
+ } else {
239
+ await db.insert(triageRuns).values({
240
+ id: runId,
241
+ itemId,
242
+ source: item.source,
243
+ provider: isSlack ? "bot-tag" : "builder-http",
244
+ dedupeKey,
245
+ approvalEmail: null,
246
+ status: "submitted",
247
+ progressLogJson: JSON.stringify([
248
+ {
249
+ at: now,
250
+ state: "submitted",
251
+ reason: `Factory automation ${context?.automation?.triggerName ?? "unknown"} recorded a clear bug.`,
252
+ },
253
+ ]),
254
+ dispatchAttempts: 1,
255
+ needsContinuation: 0,
256
+ startedAt: now,
257
+ heartbeatAt: now,
258
+ completedAt: null,
259
+ error: null,
260
+ ownerEmail: item.ownerEmail,
261
+ orgId,
262
+ });
263
+ }
264
+
265
+ try {
266
+ if (isSlack) {
267
+ if (!item.channelId || !item.threadTs) {
268
+ throw new Error(
269
+ "Slack feedback is missing its channel or thread identity.",
270
+ );
271
+ }
272
+ const config = (
273
+ await db
274
+ .select({ slackWorkspace: triageConfig.slackWorkspace })
275
+ .from(triageConfig)
276
+ .where(
277
+ and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)),
278
+ )
279
+ .limit(1)
280
+ )[0];
281
+ const workspace =
282
+ config?.slackWorkspace === "secondary" ? "secondary" : "primary";
283
+ const slack = createSlackReader({ ownerEmail: userEmail, orgId });
284
+ if (!metadataBoolean(metadata, "slackEyesReactedAt")) {
285
+ const reaction = await slack.addEyesReaction(
286
+ workspace,
287
+ item.channelId,
288
+ item.threadTs,
289
+ );
290
+ await writeMetadata(itemId, orgId, {
291
+ slackEyesReactedAt: new Date().toISOString(),
292
+ slackEyesReactionAlreadyPresent: reaction.already_present,
293
+ });
294
+ }
295
+ const thread = await slack.getThread(
296
+ workspace,
297
+ item.channelId,
298
+ item.threadTs,
299
+ 100,
300
+ );
301
+ if (
302
+ thread.has_more &&
303
+ !thread.messages.some((message) =>
304
+ message.text.includes(replyTextPrefix),
305
+ )
306
+ ) {
307
+ throw new Error(
308
+ "Slack thread is truncated; refusing to risk a duplicate Builder handoff.",
309
+ );
310
+ }
311
+ if (
312
+ !metadataBoolean(metadata, "slackBuilderReplyAt") &&
313
+ !thread.messages.some((message) =>
314
+ message.text.includes(replyTextPrefix),
315
+ )
316
+ ) {
317
+ const posted = await slack.postThreadReply(
318
+ workspace,
319
+ item.channelId,
320
+ item.threadTs,
321
+ replyTextForItem(item),
322
+ );
323
+ await writeMetadata(itemId, orgId, {
324
+ slackBuilderReplyAt: new Date().toISOString(),
325
+ slackBuilderReplyTs: posted.ts,
326
+ });
327
+ }
328
+ await db
329
+ .update(triageItems)
330
+ .set({
331
+ status: "automation_started",
332
+ updatedAt: new Date().toISOString(),
333
+ })
334
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
335
+ return {
336
+ ok: true,
337
+ started: true,
338
+ deduplicated: false,
339
+ runId,
340
+ provider: "bot-tag",
341
+ awaitingBuilderReply: true,
342
+ };
343
+ }
344
+
345
+ const result = await startBuilderRun({
346
+ runId,
347
+ itemId,
348
+ ownerEmail: userEmail,
349
+ orgId,
350
+ repository: item.repository,
351
+ summary: item.summary,
352
+ sourceUrl: item.sourceUrl,
353
+ instructions: [
354
+ reason,
355
+ clearErrorReport ? `Error report:\n${clearErrorReport}` : "",
356
+ ]
357
+ .filter(Boolean)
358
+ .join("\n\n"),
359
+ });
360
+ await db
361
+ .update(triageRuns)
362
+ .set({
363
+ status: "acknowledged",
364
+ providerTaskId: result.providerTaskId ?? null,
365
+ progressLogJson: JSON.stringify([
366
+ { at: now, state: "submitted", reason },
367
+ {
368
+ at: new Date().toISOString(),
369
+ state: "acknowledged",
370
+ reason:
371
+ "Builder accepted the fire-and-forget request; waiting for the signed callback.",
372
+ },
373
+ ]),
374
+ heartbeatAt: new Date().toISOString(),
375
+ })
376
+ .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)));
377
+ await writeMetadata(itemId, orgId, {
378
+ builderProviderTaskId: result.providerTaskId ?? null,
379
+ builderBranchName: result.branchName,
380
+ });
381
+ await db
382
+ .update(triageItems)
383
+ .set({
384
+ status: "automation_started",
385
+ updatedAt: new Date().toISOString(),
386
+ })
387
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
388
+ return {
389
+ ok: true,
390
+ started: true,
391
+ deduplicated: false,
392
+ runId,
393
+ provider: "builder-http",
394
+ };
395
+ } catch (error) {
396
+ const message = error instanceof Error ? error.message : String(error);
397
+ await db
398
+ .update(triageRuns)
399
+ .set({
400
+ status: "failed",
401
+ error: message,
402
+ completedAt: new Date().toISOString(),
403
+ heartbeatAt: new Date().toISOString(),
404
+ })
405
+ .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)));
406
+ throw new Error(
407
+ `Factory Builder dispatch failed after recording the run: ${message}`,
408
+ );
409
+ }
410
+ },
411
+ });
@@ -89,8 +89,11 @@ type DragState = {
89
89
  nodeId: string;
90
90
  offsetX: number;
91
91
  offsetY: number;
92
+ startPosition: { x: number; y: number };
92
93
  };
93
94
 
95
+ const DRAG_THRESHOLD = 3;
96
+
94
97
  export function FactoryCanvas({
95
98
  graph,
96
99
  nodeMetrics = {},
@@ -171,6 +174,7 @@ export function FactoryCanvas({
171
174
  nodeId: node.id,
172
175
  offsetX: (event.clientX - rect.left) / scale - node.position.x,
173
176
  offsetY: (event.clientY - rect.top) / scale - node.position.y,
177
+ startPosition: node.position,
174
178
  });
175
179
  dragMoved.current = false;
176
180
  event.currentTarget.setPointerCapture(event.pointerId);
@@ -186,10 +190,17 @@ export function FactoryCanvas({
186
190
  (event.clientX - rect.left) / scale - dragState.offsetX,
187
191
  (event.clientY - rect.top) / scale - dragState.offsetY,
188
192
  );
189
- if (Math.abs(position.x) + Math.abs(position.y) > 0) {
190
- dragMoved.current = true;
191
- onMoveNode(dragState.nodeId, position);
193
+ const movedX = Math.abs(position.x - dragState.startPosition.x);
194
+ const movedY = Math.abs(position.y - dragState.startPosition.y);
195
+ if (
196
+ !dragMoved.current &&
197
+ movedX <= DRAG_THRESHOLD &&
198
+ movedY <= DRAG_THRESHOLD
199
+ ) {
200
+ return;
192
201
  }
202
+ dragMoved.current = true;
203
+ onMoveNode(dragState.nodeId, position);
193
204
  }
194
205
 
195
206
  function endDrag(event: React.PointerEvent<HTMLButtonElement>) {
@@ -0,0 +1 @@
1
+ export * from "@agent-native/toolkit/ui/checkbox";
@@ -9,6 +9,7 @@ export interface NavigationState {
9
9
  threadId?: string;
10
10
  factoryId?: string;
11
11
  factoryTab?: string;
12
+ factoryAutomationId?: string;
12
13
  factoryNodeId?: string;
13
14
  factoryEdgeId?: string;
14
15
  }
@@ -29,6 +30,12 @@ export function useNavigationState() {
29
30
  ...(pathname.startsWith("/factory") && searchParams.get("tab")
30
31
  ? { factoryTab: searchParams.get("tab") ?? undefined }
31
32
  : {}),
33
+ ...(pathname.startsWith("/factory") && searchParams.get("automationId")
34
+ ? {
35
+ factoryAutomationId:
36
+ searchParams.get("automationId") ?? undefined,
37
+ }
38
+ : {}),
32
39
  ...(pathname.startsWith("/factory") && searchParams.get("node")
33
40
  ? { factoryNodeId: searchParams.get("node") ?? undefined }
34
41
  : {}),
@@ -109,6 +109,14 @@ const messages = {
109
109
  repository: "Repository",
110
110
  repositoryPlaceholder: "For example, BuilderIO/agent-native",
111
111
  enablePolling: "Enable one-minute observation polling",
112
+ enableGithubPolling: "Enable GitHub issue and pull-request polling",
113
+ enableSentryPolling: "Enable daily Sentry error polling",
114
+ sentryOrgSlug: "Sentry organization slug",
115
+ sentryProjectSlug: "Sentry project slug",
116
+ sentryEnvironment: "Sentry environment",
117
+ sentryOrgPlaceholder: "For example, builder",
118
+ sentryProjectPlaceholder: "Optional project slug",
119
+ sentryEnvironmentPlaceholder: "For example, production",
112
120
  saveSettings: "Save settings",
113
121
  settingsError: "Could not save observation settings.",
114
122
  settingsSaved: "Observation settings saved.",
@@ -137,13 +145,39 @@ const messages = {
137
145
  rulesDescription: "Every rule is shadow-only until deliberately promoted.",
138
146
  editRule: "Edit triage rule",
139
147
  settingsDescription:
140
- "Connect the sources that feed this Factory. These settings only observe; they do not send provider messages.",
148
+ "Connect Slack, GitHub, and Sentry sources. Automations can dispatch governed work and expose their prompts and runs below.",
141
149
  blueprintTitle: "Blueprint mode.",
142
150
  blueprintDescription:
143
151
  "The current runtime evaluates enabled rules in parallel and stays in shadow mode. This map records the intended handoffs; it does not silently change execution behavior.",
144
152
  metricSignals: "Signals",
145
153
  metricDecisions: "Decisions",
146
154
  metricRuns: "Runs",
155
+ rulesTab: "Rules",
156
+ automationsTab: "Automations",
157
+ automationsTitle: "Automations",
158
+ automationsDescription: "Schedule and run Factory prompts.",
159
+ automationsLoading: "Loading automations...",
160
+ automationsEmpty: "No automations configured yet.",
161
+ automationEnabled: "Enabled",
162
+ automationDisabled: "Disabled",
163
+ automationEditorTitle: "Automation editor",
164
+ automationEditorDescription: "Configure the prompt, model, and schedule.",
165
+ automationTrigger: "Trigger",
166
+ automationEvent: "Event",
167
+ automationTimezone: "Timezone",
168
+ selectAutomation: "Select an automation to edit.",
169
+ automationModel: "Model",
170
+ automationModelPlaceholder: "For example, gpt-5",
171
+ automationSchedule: "Schedule",
172
+ automationSchedulePlaceholder: "For example, every weekday at 9:00 AM",
173
+ automationEnabledLabel: "Automation is enabled",
174
+ automationPrompt: "Prompt",
175
+ automationPromptPlaceholder: "What should Factory do when this runs?",
176
+ promptEditorHint: "Edit here, then save the automation.",
177
+ pastRuns: "Past runs",
178
+ pastRunsEmpty: "No runs recorded yet.",
179
+ saveAutomation: "Save",
180
+ runNow: "Run now",
147
181
  },
148
182
  factoryCanvas: {
149
183
  dragHint: "Drag to arrange",