@agent-native/core 0.164.12 → 0.164.17

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 (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/netlify.toml +4 -0
  3. package/corpus/templates/clips/actions/request-transcript.ts +10 -6
  4. package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
  5. package/corpus/templates/content/actions/_database-utils.ts +2 -0
  6. package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
  7. package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
  8. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
  9. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
  10. package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
  11. package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
  12. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +298 -3
  13. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
  14. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
  15. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +81 -1
  16. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
  17. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +18 -4
  18. package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
  19. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
  20. package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
  21. package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
  22. package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
  23. package/corpus/templates/content/app/i18n-data.ts +239 -0
  24. package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
  25. package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
  26. package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
  27. package/corpus/templates/content/app/root.tsx +2 -1
  28. package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
  29. package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
  30. package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
  31. package/corpus/templates/content/docs/product/roadmap.md +2 -2
  32. package/corpus/templates/content/docs/solutions/2026-08-18-colored-text-highlighting-shape.md +693 -0
  33. package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
  34. package/corpus/templates/content/package.json +1 -0
  35. package/corpus/templates/content/shared/api.ts +25 -0
  36. package/corpus/templates/content/shared/nfm.ts +14 -3
  37. package/corpus/templates/factory/AGENTS.md +24 -29
  38. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +8 -28
  39. package/corpus/templates/factory/actions/get-triage-config.ts +1 -0
  40. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +67 -59
  41. package/corpus/templates/factory/actions/list-triage-items.ts +6 -1
  42. package/corpus/templates/factory/actions/reconcile-triage-run.ts +7 -1
  43. package/corpus/templates/factory/actions/save-triage-config.ts +27 -7
  44. package/corpus/templates/factory/actions/start-builder-for-item.ts +113 -31
  45. package/corpus/templates/factory/actions/view-screen.ts +12 -0
  46. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +457 -0
  47. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +53 -0
  48. package/corpus/templates/factory/app/components/layout/Layout.tsx +2 -0
  49. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +3 -1
  50. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +10 -7
  51. package/corpus/templates/factory/app/i18n/en-US.ts +3 -0
  52. package/corpus/templates/factory/app/routes/agents.tsx +22 -1
  53. package/corpus/templates/factory/app/routes/factory-settings.tsx +34 -0
  54. package/corpus/templates/factory/app/routes/factory.tsx +33 -323
  55. package/corpus/templates/factory/server/connectors/slack.ts +62 -0
  56. package/corpus/templates/factory/server/db/schema.ts +1 -0
  57. package/corpus/templates/factory/server/lib/slack-feedback-prompt.ts +30 -0
  58. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -8
  59. package/corpus/templates/factory/server/plugins/factory-migrations.ts +7 -0
  60. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +43 -25
  61. package/corpus/templates/factory/server/triage/ai-services-git.ts +2 -0
  62. package/corpus/templates/factory/server/triage/pr-monitor.ts +2 -1
  63. package/corpus/templates/factory/server/triage/pr-policy.ts +197 -19
  64. package/corpus/templates/factory/server/triage/review-skill-alignment.ts +127 -0
  65. package/corpus/templates/factory/server/triage/slack-client.ts +79 -6
  66. package/dist/app-config/auth.d.ts +4 -0
  67. package/dist/app-config/auth.js +7 -0
  68. package/dist/app-config/schema.d.ts +3 -0
  69. package/dist/app-config/schema.js +2 -0
  70. package/dist/cli/code-agent-executor.js +6 -1
  71. package/dist/cli/code-agent-mcp-config.d.ts +2 -2
  72. package/dist/cli/code-agent-mcp-config.js +34 -9
  73. package/dist/client/MultiTabAssistantChat.js +28 -1
  74. package/dist/client/org/OrgSwitcher.js +8 -4
  75. package/dist/client/use-chat-threads.d.ts +1 -0
  76. package/dist/client/use-chat-threads.js +13 -3
  77. package/dist/collab/awareness.d.ts +2 -2
  78. package/dist/collab/routes.d.ts +1 -1
  79. package/dist/mcp-client/config.d.ts +11 -0
  80. package/dist/mcp-client/config.js +33 -26
  81. package/dist/notifications/routes.d.ts +3 -3
  82. package/dist/observability/routes.d.ts +5 -5
  83. package/dist/progress/routes.d.ts +1 -1
  84. package/dist/resources/handlers.d.ts +2 -2
  85. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  86. package/dist/server/auth.js +17 -3
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/vite/client.js +2 -0
  90. package/package.json +1 -1
@@ -2,7 +2,6 @@ import { defineAction } from "@agent-native/core/action";
2
2
  import { and, eq } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
 
5
- import type { SlackMessage } from "../server/connectors/slack.js";
6
5
  import { getDb } from "../server/db/index.js";
7
6
  import { triageConfig, triageItems } from "../server/db/schema.js";
8
7
  import {
@@ -12,8 +11,6 @@ import {
12
11
  import { recordFactoryAudit } from "../server/triage/audit.js";
13
12
  import { createSlackReader } from "../server/triage/slack-client.js";
14
13
 
15
- const MAX_THREAD_PAGES = 5;
16
-
17
14
  export default defineAction({
18
15
  description:
19
16
  "Read the bounded full Slack thread for one Factory feedback item. Use this before deciding whether a report is a clear bug; unreadable or truncated context is not a green light.",
@@ -46,31 +43,11 @@ export default defineAction({
46
43
  const workspace =
47
44
  config?.slackWorkspace === "secondary" ? "secondary" : "primary";
48
45
  const slack = createSlackReader({ ownerEmail: userEmail, orgId });
49
- const messages: SlackMessage[] = [];
50
- let cursor: string | undefined;
51
- let hasMore = false;
52
- for (let page = 0; page < MAX_THREAD_PAGES; page += 1) {
53
- const result = await slack.getThread(
54
- workspace,
55
- item.channelId,
56
- item.threadTs,
57
- 100,
58
- cursor,
59
- );
60
- messages.push(...result.messages);
61
- if (result.has_more && !result.next_cursor) {
62
- throw new Error(
63
- "Slack thread pagination is incomplete because the provider omitted its next cursor.",
64
- );
65
- }
66
- if (!result.has_more) {
67
- hasMore = false;
68
- break;
69
- }
70
- hasMore = page === MAX_THREAD_PAGES - 1;
71
- if (hasMore) break;
72
- cursor = result.next_cursor;
73
- }
46
+ const { messages, hasMore } = await slack.getCompleteThread(
47
+ workspace,
48
+ item.channelId,
49
+ item.threadTs,
50
+ );
74
51
 
75
52
  await recordFactoryAudit(
76
53
  context,
@@ -100,10 +77,13 @@ export default defineAction({
100
77
  coverage: hasMore ? "partial" : "complete",
101
78
  messages: messages.map((message) => ({
102
79
  user: message.user ?? message.username ?? message.bot_id ?? null,
80
+ username: message.username ?? null,
81
+ botId: message.bot_id ?? null,
103
82
  text: message.text,
104
83
  ts: message.ts,
105
84
  threadTs: message.thread_ts ?? item.threadTs,
106
85
  replyCount: message.reply_count ?? 0,
86
+ reactions: message.reactions ?? [],
107
87
  })),
108
88
  };
109
89
  },
@@ -33,6 +33,7 @@ export default defineAction({
33
33
  slackWorkspace: "primary",
34
34
  slackChannelId: null,
35
35
  slackChannelName: null,
36
+ builderSlackUserId: null,
36
37
  pollingEnabled: false,
37
38
  lastSlackTs: null,
38
39
  slackHistoryCursor: null,
@@ -24,7 +24,10 @@ import {
24
24
  serializeTriageMetadata,
25
25
  } from "../server/triage/metadata.js";
26
26
  import { reconcileBabysitState } from "../server/triage/pr-babysit.js";
27
- import { decidePullRequestGovernance } from "../server/triage/pr-policy.js";
27
+ import {
28
+ decidePullRequestGovernance,
29
+ hasCurrentPullRequestApproval,
30
+ } from "../server/triage/pr-policy.js";
28
31
 
29
32
  function repositoryRef(value: string): { owner: string; repo: string } {
30
33
  const match = /^([^/\s]+)\/([^/\s]+)$/.exec(value.trim());
@@ -103,7 +106,7 @@ async function hasVerifiedFactoryRun(input: {
103
106
 
104
107
  export default defineAction({
105
108
  description:
106
- "Govern one agent-native pull request after fetching bounded GitHub and ai-services evidence. Auto-approve only clear internal bug fixes with clean CI and handled review feedback; auto-merge only the verified Factory Builder flow. Clips, Design, and Content are always owner-managed.",
109
+ "Govern one agent-native pull request after fetching bounded GitHub and ai-services evidence. Auto-approve only under the current review-prs membership, owner, evidence, and ultra-scary gates. Never auto-merge. Clips, Design, and Content feedback remains owner-managed while their verified PR-owner exceptions still apply.",
107
110
  schema: z.object({
108
111
  repo: z.string().trim().min(1).max(256),
109
112
  pullRequestNumber: z.number().int().positive(),
@@ -167,6 +170,24 @@ export default defineAction({
167
170
  repository,
168
171
  pullRequestNumber,
169
172
  );
173
+ if (pullRequest.state !== "open" || pullRequest.draft) {
174
+ if (itemId) {
175
+ await getDb()
176
+ .update(triageItems)
177
+ .set({
178
+ status: "pr_observed",
179
+ updatedAt: new Date().toISOString(),
180
+ })
181
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
182
+ }
183
+ return {
184
+ ok: true,
185
+ action: "skipped",
186
+ reason: pullRequest.draft
187
+ ? "Pull request is a draft and is excluded before evidence review."
188
+ : "Pull request is not open and is excluded before evidence review.",
189
+ };
190
+ }
170
191
  const privateKey = await resolveConnectorSecret(
171
192
  "BUILDER_PRIVATE_KEY",
172
193
  userEmail,
@@ -186,6 +207,38 @@ export default defineAction({
186
207
  `PR evidence is stale: GitHub reports ${pullRequest.headSha}, ai-services reports ${snapshot.headSha}.`,
187
208
  );
188
209
  }
210
+ if (hasCurrentPullRequestApproval(snapshot.reviews)) {
211
+ await recordFactoryAudit(
212
+ context,
213
+ { userEmail, orgId },
214
+ {
215
+ action: "govern-agent-native-pull-request",
216
+ kind: "governance",
217
+ status: "skipped",
218
+ itemId: itemId ?? null,
219
+ source: "github",
220
+ sourceUrl: pullRequest.htmlUrl,
221
+ summary:
222
+ "Skipped the pull request because it already has a current non-dismissed approval.",
223
+ details: { repo, pullRequestNumber },
224
+ },
225
+ );
226
+ if (itemId) {
227
+ await getDb()
228
+ .update(triageItems)
229
+ .set({
230
+ status: "pr_observed",
231
+ updatedAt: new Date().toISOString(),
232
+ })
233
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
234
+ }
235
+ return {
236
+ ok: true,
237
+ action: "skipped",
238
+ reason:
239
+ "Pull request already has a current non-dismissed approval; no duplicate review was created.",
240
+ };
241
+ }
189
242
 
190
243
  const checksPassed =
191
244
  snapshot.coverage === "complete" &&
@@ -254,6 +307,7 @@ export default defineAction({
254
307
  reviewFeedbackHandled,
255
308
  autoApprove: governance.autoApprove,
256
309
  autoMerge: governance.autoMerge,
310
+ ownerException: governance.ownerException,
257
311
  ownerOwnedArea: governance.ownerOwnedArea ?? null,
258
312
  guardResults: governance.guardResults,
259
313
  },
@@ -341,7 +395,9 @@ export default defineAction({
341
395
  const approval = await github.approvePullRequest(
342
396
  repository,
343
397
  pullRequestNumber,
344
- "Factory auto-approved: clear internal bug fix with passing CI and handled review feedback.",
398
+ governance.ownerException
399
+ ? `Factory auto-approved under the verified ${governance.ownerException} owner exception; ordinary check and review states remain recorded.`
400
+ : "Factory auto-approved under verified BuilderIO membership; ordinary check and review states remain recorded.",
345
401
  );
346
402
  approvalUrl = approval.htmlUrl;
347
403
  metadata.autoApprovedAt = new Date().toISOString();
@@ -361,61 +417,13 @@ export default defineAction({
361
417
  await github.approvePullRequest(
362
418
  repository,
363
419
  pullRequestNumber,
364
- "Factory auto-approved: clear internal bug fix with passing CI and handled review feedback.",
420
+ governance.ownerException
421
+ ? `Factory auto-approved under the verified ${governance.ownerException} owner exception; ordinary check and review states remain recorded.`
422
+ : "Factory auto-approved under verified BuilderIO membership; ordinary check and review states remain recorded.",
365
423
  )
366
424
  ).htmlUrl;
367
425
  }
368
426
 
369
- if (!governance.autoMerge) {
370
- await recordFactoryAudit(
371
- context,
372
- { userEmail, orgId },
373
- {
374
- action: "govern-agent-native-pull-request",
375
- kind: "external_action",
376
- itemId: itemId ?? null,
377
- source: "github",
378
- sourceUrl: approvalUrl,
379
- summary: "Approved the pull request after governance checks passed.",
380
- details: { repo, pullRequestNumber, approvalUrl },
381
- },
382
- );
383
- return {
384
- ok: true,
385
- action: "approved",
386
- approvalUrl,
387
- checksPassed,
388
- reviewFeedbackHandled,
389
- };
390
- }
391
-
392
- const merge = await github.mergePullRequest(
393
- repository,
394
- pullRequestNumber,
395
- `Merge Factory bug fix #${pullRequestNumber}`,
396
- snapshot.headSha,
397
- );
398
- if (itemId) {
399
- const item = (
400
- await getDb()
401
- .select({ metadataJson: triageItems.metadataJson })
402
- .from(triageItems)
403
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
404
- .limit(1)
405
- )[0];
406
- if (!item) throw new Error("Factory item disappeared after PR merge.");
407
- const metadata = parseTriageMetadata(item.metadataJson);
408
- metadata.autoMergedAt = new Date().toISOString();
409
- metadata.mergeSha = merge.sha;
410
- await getDb()
411
- .update(triageItems)
412
- .set({
413
- metadataJson: serializeTriageMetadata(metadata),
414
- status: "merged",
415
- updatedAt: new Date().toISOString(),
416
- })
417
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
418
- }
419
427
  await recordFactoryAudit(
420
428
  context,
421
429
  { userEmail, orgId },
@@ -424,16 +432,16 @@ export default defineAction({
424
432
  kind: "external_action",
425
433
  itemId: itemId ?? null,
426
434
  source: "github",
427
- sourceUrl: pullRequest.htmlUrl,
428
- summary: "Merged the pull request after governance checks passed.",
429
- details: { repo, pullRequestNumber, approvalUrl, mergeSha: merge.sha },
435
+ sourceUrl: approvalUrl,
436
+ summary:
437
+ "Approved the pull request under the current review-prs policy.",
438
+ details: { repo, pullRequestNumber, approvalUrl },
430
439
  },
431
440
  );
432
441
  return {
433
442
  ok: true,
434
- action: "merged",
443
+ action: "approved",
435
444
  approvalUrl,
436
- mergeSha: merge.sha,
437
445
  checksPassed,
438
446
  reviewFeedbackHandled,
439
447
  };
@@ -30,7 +30,12 @@ export default defineAction({
30
30
  workspaceMemberIdentityFromContext(context),
31
31
  );
32
32
  const db = getDb();
33
- const reviewStatuses = source === "github" ? ["pr_observed"] : ["received"];
33
+ const reviewStatuses =
34
+ source === "github"
35
+ ? ["pr_observed"]
36
+ : source === "slack"
37
+ ? ["received", "automation_started", "evidence_ready"]
38
+ : ["received"];
34
39
  const items = await db
35
40
  .select()
36
41
  .from(triageItems)
@@ -20,7 +20,13 @@ import {
20
20
 
21
21
  const reviewSchema = z.object({
22
22
  author: z.string(),
23
- state: z.enum(["approved", "changes_requested", "commented", "pending"]),
23
+ state: z.enum([
24
+ "approved",
25
+ "changes_requested",
26
+ "commented",
27
+ "pending",
28
+ "dismissed",
29
+ ]),
24
30
  observedAt: z.string().datetime(),
25
31
  });
26
32
  const checkSchema = z.object({
@@ -13,11 +13,19 @@ const workspaceSchema = z.enum(["primary", "secondary"]);
13
13
 
14
14
  export default defineAction({
15
15
  description:
16
- "Save Factory automation alert settings. Provider credentials live in shared workspace integrations; legacy source routing metadata is preserved for the default observer adapters.",
16
+ "Save Factory observation and automation alert settings. Provider credentials live in shared workspace integrations; source routing metadata including the Builder Slack member id is stored on Factory config.",
17
17
  schema: z.object({
18
18
  slackWorkspace: workspaceSchema.optional(),
19
- slackChannelId: z.string().trim().min(1).max(128).optional(),
19
+ slackChannelId: z.string().trim().max(128).optional(),
20
20
  slackChannelName: z.string().trim().max(200).optional(),
21
+ builderSlackUserId: z
22
+ .string()
23
+ .trim()
24
+ .max(32)
25
+ .refine((value) => value === "" || /^[UW][A-Z0-9]+$/i.test(value), {
26
+ message: "Builder Slack member id must look like U01234567.",
27
+ })
28
+ .optional(),
21
29
  pollingEnabled: z.boolean().optional(),
22
30
  githubPollingEnabled: z.boolean().optional(),
23
31
  sentryPollingEnabled: z.boolean().optional(),
@@ -26,7 +34,9 @@ export default defineAction({
26
34
  sentryEnvironment: z.string().trim().max(200).optional(),
27
35
  repository: z.string().trim().max(256).optional(),
28
36
  automationFailureAlertsEnabled: z.boolean().optional(),
29
- automationFailureAlertEmail: z.string().trim().email().optional(),
37
+ automationFailureAlertEmail: z
38
+ .union([z.string().trim().email(), z.literal("")])
39
+ .optional(),
30
40
  }),
31
41
  http: { method: "POST" },
32
42
  run: async (
@@ -34,6 +44,7 @@ export default defineAction({
34
44
  slackWorkspace,
35
45
  slackChannelId,
36
46
  slackChannelName,
47
+ builderSlackUserId,
37
48
  pollingEnabled,
38
49
  githubPollingEnabled,
39
50
  sentryPollingEnabled,
@@ -72,6 +83,12 @@ export default defineAction({
72
83
  slackChannelName,
73
84
  existing?.slackChannelName,
74
85
  );
86
+ const persistedBuilderSlackUserId = persistText(
87
+ builderSlackUserId === undefined
88
+ ? undefined
89
+ : builderSlackUserId.toUpperCase(),
90
+ existing?.builderSlackUserId,
91
+ );
75
92
  const persistedRepository = persistText(repository, existing?.repository);
76
93
  const persistedSentryOrgSlug = persistText(
77
94
  sentryOrgSlug,
@@ -109,10 +126,10 @@ export default defineAction({
109
126
  : automationFailureAlertsEnabled
110
127
  ? 1
111
128
  : 0;
112
- const persistedAutomationFailureAlertEmail =
113
- automationFailureAlertEmail ??
114
- existing?.automationFailureAlertEmail ??
115
- null;
129
+ const persistedAutomationFailureAlertEmail = persistText(
130
+ automationFailureAlertEmail,
131
+ existing?.automationFailureAlertEmail,
132
+ );
116
133
  await db
117
134
  .insert(triageConfig)
118
135
  .values({
@@ -120,6 +137,7 @@ export default defineAction({
120
137
  slackWorkspace: persistedSlackWorkspace,
121
138
  slackChannelId: persistedSlackChannelId,
122
139
  slackChannelName: persistedSlackChannelName,
140
+ builderSlackUserId: persistedBuilderSlackUserId,
123
141
  pollingEnabled: persistedPollingEnabled,
124
142
  githubPollingEnabled: persistedGithubPollingEnabled,
125
143
  sentryPollingEnabled: persistedSentryPollingEnabled,
@@ -140,6 +158,7 @@ export default defineAction({
140
158
  slackWorkspace: persistedSlackWorkspace,
141
159
  slackChannelId: persistedSlackChannelId,
142
160
  slackChannelName: persistedSlackChannelName,
161
+ builderSlackUserId: persistedBuilderSlackUserId,
143
162
  pollingEnabled: persistedPollingEnabled,
144
163
  githubPollingEnabled: persistedGithubPollingEnabled,
145
164
  sentryPollingEnabled: persistedSentryPollingEnabled,
@@ -160,6 +179,7 @@ export default defineAction({
160
179
  githubPollingEnabled: persistedGithubPollingEnabled === 1,
161
180
  sentryPollingEnabled: persistedSentryPollingEnabled === 1,
162
181
  slackChannelId: persistedSlackChannelId,
182
+ builderSlackUserId: persistedBuilderSlackUserId,
163
183
  automationFailureAlertsEnabled:
164
184
  persistedAutomationFailureAlertsEnabled === 1,
165
185
  automationFailureAlertEmail: persistedAutomationFailureAlertEmail,
@@ -25,10 +25,40 @@ import {
25
25
  import { detectOwnerOwnedArea } from "../server/triage/pr-policy.js";
26
26
  import { createSlackReader } from "../server/triage/slack-client.js";
27
27
 
28
- const replyTextPrefix =
29
- "@builder.io please run /address-feedback in the repo to address this feedback. Read the address-feedback, address-feedback-with-replies, review-latest-feedback, and review-prs skills as relevant, inspect the full thread and linked evidence, and fix the owning boundary. Please send a PR when ready.";
28
+ /** Slack notifies only with `<@USERID>`. Plaintext @handles do not ping anyone. */
29
+ const REPLY_INSTRUCTION =
30
+ "please run /address-feedback in the repo to address this feedback. Read the address-feedback, address-feedback-with-replies, review-latest-feedback, and review-prs skills as relevant, inspect the full thread and linked evidence, and fix the owning boundary. Please send a PR when ready, then have the @agent-native bot post a concise Fixed, In progress, or Clarification needed disposition in this same thread; an 👀 reaction or this handoff alone is not completion.";
31
+ const plaintextBuilderReplyPrefix =
32
+ "@builder.io please run /address-feedback in the repo to address this feedback.";
30
33
  const legacyReplyTextPrefix = "@builderio please fix this in a reply.";
31
34
 
35
+ export function requireBuilderSlackUserId(
36
+ value: string | null | undefined,
37
+ ): string {
38
+ const id = typeof value === "string" ? value.trim() : "";
39
+ if (!/^[UW][A-Z0-9]+$/i.test(id)) {
40
+ throw new Error(
41
+ "Configure a Builder Slack member id in Factory settings before tagging Builder.",
42
+ );
43
+ }
44
+ return id.toUpperCase();
45
+ }
46
+
47
+ export function replyTextPrefixFor(builderSlackUserId: string): string {
48
+ return `<@${requireBuilderSlackUserId(builderSlackUserId)}> ${REPLY_INSTRUCTION}`;
49
+ }
50
+
51
+ function messageHasBuilderHandoff(
52
+ text: string,
53
+ builderSlackUserId: string,
54
+ ): boolean {
55
+ return (
56
+ text.includes(replyTextPrefixFor(builderSlackUserId)) ||
57
+ text.includes(plaintextBuilderReplyPrefix) ||
58
+ text.includes(legacyReplyTextPrefix)
59
+ );
60
+ }
61
+
32
62
  type RelatedFeedbackItem = {
33
63
  id: string;
34
64
  sourceUrl: string | null;
@@ -93,6 +123,7 @@ export function replyTextForItem(
93
123
  sourceUrl: string | null;
94
124
  },
95
125
  relatedItems: RelatedFeedbackItem[] = [],
126
+ builderSlackUserId: string,
96
127
  ): string {
97
128
  const relatedText =
98
129
  relatedItems.length > 0
@@ -105,7 +136,7 @@ export function replyTextForItem(
105
136
  ]
106
137
  : [];
107
138
  return [
108
- replyTextPrefix,
139
+ replyTextPrefixFor(builderSlackUserId),
109
140
  `Factory item: ${item.id}`,
110
141
  item.sourceUrl ? `Source: ${item.sourceUrl}` : "",
111
142
  ...relatedText,
@@ -199,7 +230,7 @@ export async function recordAutomaticBuilderDecision(input: {
199
230
 
200
231
  export default defineAction({
201
232
  description:
202
- "Start the governed clear-bug Builder flow for a Factory item, or record a skip with a reason when clearBug is false. Slack items are kept in-thread by adding 👀 and tagging @builder.io; grouped Slack repeats share one Builder thread. GitHub issues and Sentry errors use the Builder agent run API. Owner-managed Clips, Design, and Content items are always left for their owner.",
233
+ "Start the governed clear-bug Builder flow for a Factory item, or record a skip with a reason when clearBug is false. Slack items stay in-thread: this action adds 👀 and pings Builder with the configured Slack member id; do not post Slack messages or @handles yourself. Grouped Slack repeats share one Builder thread. GitHub issues and Sentry errors use the Builder agent run API. Owner-managed Clips, Design, and Content items are always left for their owner.",
203
234
  schema: z.object({
204
235
  itemId: z.string().min(1),
205
236
  clearBug: z.boolean(),
@@ -464,7 +495,10 @@ export default defineAction({
464
495
  }
465
496
  const config = (
466
497
  await db
467
- .select({ slackWorkspace: triageConfig.slackWorkspace })
498
+ .select({
499
+ slackWorkspace: triageConfig.slackWorkspace,
500
+ builderSlackUserId: triageConfig.builderSlackUserId,
501
+ })
468
502
  .from(triageConfig)
469
503
  .where(
470
504
  and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)),
@@ -473,67 +507,115 @@ export default defineAction({
473
507
  )[0];
474
508
  const workspace =
475
509
  config?.slackWorkspace === "secondary" ? "secondary" : "primary";
510
+ const builderSlackUserId = requireBuilderSlackUserId(
511
+ config?.builderSlackUserId,
512
+ );
476
513
  const slack = createSlackReader({ ownerEmail: userEmail, orgId });
477
- for (const feedbackItem of [item, ...relatedItems]) {
478
- const feedbackMetadata =
479
- feedbackItem.id === itemId
480
- ? metadata
481
- : parseTriageMetadata(feedbackItem.metadataJson);
482
- if (metadataBoolean(feedbackMetadata, "slackEyesReactedAt")) {
483
- continue;
514
+ const thread = await slack.getCompleteThread(
515
+ workspace,
516
+ item.channelId,
517
+ item.threadTs,
518
+ );
519
+ const completeThreads = new Map([[item.id, thread]]);
520
+ if (thread.hasMore) {
521
+ throw new Error(
522
+ "Slack thread is truncated; refusing to dispatch without complete evidence.",
523
+ );
524
+ }
525
+ for (const related of relatedItems) {
526
+ if (!related.channelId || !related.threadTs) {
527
+ throw new Error(
528
+ "Grouped Slack feedback is missing a channel or thread identity.",
529
+ );
530
+ }
531
+ const relatedThread = await slack.getCompleteThread(
532
+ workspace,
533
+ related.channelId,
534
+ related.threadTs,
535
+ );
536
+ completeThreads.set(related.id, relatedThread);
537
+ if (relatedThread.hasMore) {
538
+ throw new Error(
539
+ "A grouped Slack thread is truncated; refusing to dispatch without complete evidence.",
540
+ );
484
541
  }
485
- const reaction = await slack.addEyesReaction(
542
+ }
543
+ for (const feedbackItem of [item, ...relatedItems]) {
544
+ const reactionState = await slack.getEyesReaction(
486
545
  workspace,
487
546
  feedbackItem.channelId!,
488
547
  feedbackItem.threadTs!,
489
548
  );
549
+ const reaction = reactionState.eyesPresent
550
+ ? { added: false, already_present: true }
551
+ : await slack.addEyesReaction(
552
+ workspace,
553
+ feedbackItem.channelId!,
554
+ feedbackItem.threadTs!,
555
+ );
490
556
  await writeMetadata(feedbackItem.id, orgId, {
491
557
  slackEyesReactedAt: new Date().toISOString(),
492
558
  slackEyesReactionAlreadyPresent: reaction.already_present,
493
559
  });
494
560
  }
495
- const thread = await slack.getThread(
496
- workspace,
497
- item.channelId,
498
- item.threadTs,
499
- 100,
500
- );
501
- const hasCurrentBuilderReply = thread.messages.some((message) =>
502
- message.text.includes(replyTextPrefix),
503
- );
504
- const hasLegacyBuilderReply = thread.messages.some((message) =>
505
- message.text.includes(legacyReplyTextPrefix),
561
+ const hasBuilderReply = thread.messages.some((message) =>
562
+ messageHasBuilderHandoff(message.text, builderSlackUserId),
506
563
  );
507
564
  const hasRelatedClusterDetails = thread.messages.some((message) =>
508
565
  message.text.includes(
509
566
  "Related feedback in the same issue cluster - inspect and fix all of these too:",
510
567
  ),
511
568
  );
512
- if (
513
- thread.has_more &&
514
- !hasCurrentBuilderReply &&
515
- !hasLegacyBuilderReply
516
- ) {
569
+ if (thread.hasMore && !hasBuilderReply) {
517
570
  throw new Error(
518
571
  "Slack thread is truncated; refusing to risk a duplicate Builder handoff.",
519
572
  );
520
573
  }
521
574
  if (
522
575
  !metadataBoolean(metadata, "slackBuilderReplyAt") &&
523
- ((!hasCurrentBuilderReply && !hasLegacyBuilderReply) ||
576
+ (!hasBuilderReply ||
524
577
  (relatedItems.length > 0 && !hasRelatedClusterDetails))
525
578
  ) {
526
579
  const posted = await slack.postThreadReply(
527
580
  workspace,
528
581
  item.channelId,
529
582
  item.threadTs,
530
- replyTextForItem(item, relatedItems),
583
+ replyTextForItem(item, relatedItems, builderSlackUserId),
531
584
  );
532
585
  await writeMetadata(itemId, orgId, {
533
586
  slackBuilderReplyAt: new Date().toISOString(),
534
587
  slackBuilderReplyTs: posted.ts,
535
588
  });
536
589
  }
590
+ const agentNative = await slack.getAgentNativeIdentity(workspace);
591
+ for (const feedbackItem of [item, ...relatedItems]) {
592
+ const feedbackThread = completeThreads.get(feedbackItem.id);
593
+ if (!feedbackThread) {
594
+ throw new Error(
595
+ `Slack thread evidence is missing for Factory item ${feedbackItem.id}.`,
596
+ );
597
+ }
598
+ const hasAgentNativeDisposition = feedbackThread.messages.some(
599
+ (message) =>
600
+ (message.user === agentNative.userId ||
601
+ message.username?.trim().toLowerCase() === "agent-native") &&
602
+ /^(Fixed|In progress|Clarification needed):/i.test(
603
+ message.text.trim(),
604
+ ),
605
+ );
606
+ if (hasAgentNativeDisposition) continue;
607
+ const disposition = await slack.postThreadReply(
608
+ workspace,
609
+ feedbackItem.channelId!,
610
+ feedbackItem.threadTs!,
611
+ `Thanks - Builder owns this feedback cluster and will follow up after verification. In progress: ${reason}`,
612
+ );
613
+ await writeMetadata(feedbackItem.id, orgId, {
614
+ slackDispositionAt: new Date().toISOString(),
615
+ slackDispositionTs: disposition.ts,
616
+ slackDisposition: "In progress",
617
+ });
618
+ }
537
619
  const clusterItemIds = [itemId, ...relatedItems.map(({ id }) => id)];
538
620
  const clusterMetadata = {
539
621
  feedbackClusterRepresentativeId: itemId,
@@ -41,6 +41,18 @@ export default defineAction({
41
41
  const screen: Record<string, unknown> = {};
42
42
  if (navigation) screen.navigation = navigation;
43
43
 
44
+ if (
45
+ navigation &&
46
+ typeof navigation === "object" &&
47
+ "view" in navigation &&
48
+ navigation.view === "factory-settings"
49
+ ) {
50
+ screen.factorySettings = {
51
+ scope: "workspace",
52
+ path: "/factory-settings",
53
+ };
54
+ }
55
+
44
56
  if (
45
57
  navigation &&
46
58
  typeof navigation === "object" &&