@agent-native/core 0.168.7 → 0.168.10

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 (91) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/mutate-dashboard.ts +8 -1
  3. package/corpus/templates/calendar/actions/delete-event.ts +34 -18
  4. package/corpus/templates/calendar/actions/delete-events.ts +13 -1
  5. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +22 -3
  6. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +19 -0
  7. package/corpus/templates/calendar/server/lib/google-api.ts +18 -1
  8. package/corpus/templates/clips/actions/finalize-meeting.ts +16 -0
  9. package/corpus/templates/clips/actions/list-meetings.ts +1 -0
  10. package/corpus/templates/clips/actions/sync-calendars.ts +10 -29
  11. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +28 -12
  12. package/corpus/templates/clips/desktop/src/lib/meeting-stop.ts +16 -0
  13. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +19 -9
  14. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +1 -0
  15. package/corpus/templates/content/app/global.css +5 -0
  16. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-slash-production-regression-shape.md +492 -0
  17. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-surface-background-shape.md +273 -0
  18. package/corpus/templates/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
  19. package/corpus/templates/factory/AGENTS.md +12 -22
  20. package/corpus/templates/factory/README.md +11 -8
  21. package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
  22. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
  23. package/corpus/templates/factory/actions/create-factory.ts +211 -0
  24. package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
  25. package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
  26. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
  27. package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
  28. package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
  29. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
  30. package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
  31. package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
  32. package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
  33. package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
  34. package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
  35. package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
  36. package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
  37. package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
  38. package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
  39. package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
  40. package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
  41. package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
  43. package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
  44. package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
  45. package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
  46. package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
  47. package/corpus/templates/factory/actions/view-screen.ts +22 -30
  48. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
  49. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
  50. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
  51. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  52. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
  53. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
  54. package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
  55. package/corpus/templates/factory/app/routes/factory.tsx +78 -62
  56. package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
  57. package/corpus/templates/factory/server/db/schema.ts +18 -3
  58. package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
  59. package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
  60. package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
  61. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
  62. package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
  63. package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
  64. package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
  65. package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
  66. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
  67. package/corpus/templates/factory/server/triage/audit.ts +4 -0
  68. package/corpus/templates/factory/server/triage/ids.ts +4 -0
  69. package/dist/cli/native-dependencies.js +4 -1
  70. package/dist/client/AgentPanel.d.ts +5 -1
  71. package/dist/client/AgentPanel.js +9 -4
  72. package/dist/client/agent-chat/index.d.ts +1 -1
  73. package/dist/client/agent-chat/index.js +1 -1
  74. package/dist/client/chat-first/apps-rail.js +6 -6
  75. package/dist/client/chat-first/primary-nav.js +14 -6
  76. package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
  77. package/dist/client/integrations/IntegrationsPanel.js +29 -21
  78. package/dist/client/integrations/index.d.ts +1 -1
  79. package/dist/client/integrations/index.js +1 -1
  80. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  81. package/dist/collab/struct-routes.d.ts +1 -1
  82. package/dist/deploy/build.js +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  84. package/dist/observability/routes.d.ts +3 -3
  85. package/dist/progress/routes.d.ts +1 -1
  86. package/dist/secrets/routes.d.ts +6 -6
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  90. package/dist/shared/mcp-embed-headers.js +10 -3
  91. package/package.json +1 -1
@@ -3,6 +3,8 @@ import { z } from "zod";
3
3
 
4
4
  import { getDb } from "../server/db/index.js";
5
5
  import { triageItems } from "../server/db/schema.js";
6
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
7
+ import { factoryIdSchema } from "../server/lib/factory-scope.js";
6
8
  import {
7
9
  requireWorkspaceMember,
8
10
  workspaceMemberIdentityFromContext,
@@ -31,6 +33,7 @@ export default defineAction({
31
33
  description:
32
34
  "Ingest one read-only GitHub pull-request observation from the existing ai-services boundary into Factory. This action records evidence only and never writes to GitHub.",
33
35
  schema: z.object({
36
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
34
37
  repo: z.string().trim().min(1).max(256),
35
38
  pullRequestNumber: z.number().int().positive(),
36
39
  headSha: z.string().trim().min(1).max(128),
@@ -50,8 +53,9 @@ export default defineAction({
50
53
  const { userEmail, orgId } = await requireWorkspaceMember(
51
54
  workspaceMemberIdentityFromContext(context),
52
55
  );
53
- const envelope = pullRequestSnapshotToEnvelope(input);
54
- const id = itemDedupeKey(envelope, orgId);
56
+ const { factoryId, ...observation } = input;
57
+ const envelope = pullRequestSnapshotToEnvelope(observation);
58
+ const id = itemDedupeKey(envelope, orgId, factoryId);
55
59
  const now = new Date().toISOString();
56
60
  const db = getDb();
57
61
 
@@ -77,6 +81,7 @@ export default defineAction({
77
81
  updatedAt: now,
78
82
  ownerEmail: userEmail,
79
83
  orgId,
84
+ factoryId,
80
85
  })
81
86
  .onConflictDoUpdate({
82
87
  target: triageItems.id,
@@ -105,10 +110,11 @@ export default defineAction({
105
110
  repository: envelope.repository,
106
111
  pullRequestNumber: envelope.pullRequestNumber,
107
112
  coverage: envelope.coverage,
108
- reviewCount: input.reviews.length,
109
- checkCount: input.checks.length,
113
+ reviewCount: observation.reviews.length,
114
+ checkCount: observation.checks.length,
110
115
  },
111
116
  },
117
+ factoryId,
112
118
  );
113
119
 
114
120
  return {
@@ -8,6 +8,10 @@ import { z } from "zod";
8
8
 
9
9
  import { getDb } from "../server/db/index.js";
10
10
  import { factoryAuditEvents } from "../server/db/schema.js";
11
+ import {
12
+ factoryIdSchema,
13
+ readAutomationFactoryId,
14
+ } from "../server/lib/factory-scope.js";
11
15
  import {
12
16
  requireWorkspaceMember,
13
17
  workspaceMemberIdentityFromContext,
@@ -18,13 +22,13 @@ export default defineAction({
18
22
  "List recent Factory automation runs with the bounded source observations, decisions, and external actions recorded for each run.",
19
23
  agentTool: false,
20
24
  schema: z.object({
21
- factoryId: z.string().trim().min(1).optional(),
25
+ factoryId: factoryIdSchema,
22
26
  automation: z.string().trim().min(1).optional(),
23
27
  limit: z.coerce.number().int().min(1).max(50).default(20),
24
28
  }),
25
29
  http: { method: "GET" },
26
30
  readOnly: true,
27
- run: async ({ automation, limit }, context) => {
31
+ run: async ({ factoryId, automation, limit }, context) => {
28
32
  const { userEmail, orgId } = await requireWorkspaceMember(
29
33
  workspaceMemberIdentityFromContext(context),
30
34
  );
@@ -33,8 +37,10 @@ export default defineAction({
33
37
  "organization",
34
38
  );
35
39
  const factoryDefinitions = definitions.filter(
36
- ({ meta, name }) =>
37
- meta.domain === "factory" && (!automation || name === automation),
40
+ ({ meta, name, resource }) =>
41
+ meta.domain === "factory" &&
42
+ readAutomationFactoryId(meta, resource.content) === factoryId &&
43
+ (!automation || name === automation),
38
44
  );
39
45
  const runGroups = await Promise.all(
40
46
  factoryDefinitions.map(async ({ name, resource }) =>
@@ -60,6 +66,7 @@ export default defineAction({
60
66
  .where(
61
67
  and(
62
68
  eq(factoryAuditEvents.orgId, orgId),
69
+ eq(factoryAuditEvents.factoryId, factoryId),
63
70
  inArray(factoryAuditEvents.automationRunId, runIds),
64
71
  ),
65
72
  )
@@ -5,32 +5,58 @@ import {
5
5
  } from "@agent-native/core/triggers";
6
6
  import { z } from "zod";
7
7
 
8
+ import { readFactoryDefinition } from "../server/factory-graph/store.js";
9
+ import {
10
+ DEFAULT_FACTORY_ID,
11
+ factoryIdSchema,
12
+ readAutomationFactoryId,
13
+ resolveAutomationDisplayName,
14
+ } from "../server/lib/factory-scope.js";
8
15
  import {
9
16
  requireWorkspaceMember,
10
17
  workspaceMemberIdentityFromContext,
11
18
  } from "../server/lib/require-workspace-member.js";
12
19
 
20
+ function automationRunHistoryKey(path: string): string {
21
+ return path.replace(/^jobs\//, "").replace(/\.md$/, "");
22
+ }
23
+
13
24
  export default defineAction({
14
25
  description:
15
26
  "List the organization-scoped Factory automations with their trigger, editable prompt, model, schedule, enabled state, and recent runs.",
16
27
  agentTool: false,
17
- schema: z.object({ factoryId: z.string().trim().min(1).optional() }),
28
+ schema: z.object({ factoryId: factoryIdSchema }),
18
29
  http: { method: "GET" },
19
30
  readOnly: true,
20
- run: async (_, context) => {
31
+ run: async ({ factoryId }, context) => {
21
32
  const { userEmail, orgId } = await requireWorkspaceMember(
22
33
  workspaceMemberIdentityFromContext(context),
23
34
  );
35
+ const factory = await readFactoryDefinition(orgId, factoryId);
36
+ if (!factory && factoryId !== DEFAULT_FACTORY_ID) {
37
+ throw new Error("Factory not found.");
38
+ }
24
39
  const definitions = await listAutomationDefinitions(
25
40
  { userEmail, orgId, appId: "factory" },
26
41
  "organization",
27
42
  );
43
+ const scoped = definitions.filter(
44
+ ({ meta, resource }) =>
45
+ meta.domain === "factory" &&
46
+ readAutomationFactoryId(meta, resource.content) === factoryId,
47
+ );
28
48
  return Promise.all(
29
- definitions
30
- .filter(({ meta }) => meta.domain === "factory")
31
- .map(async ({ resource, name, meta, body, canUpdate }) => ({
49
+ scoped.map(async ({ resource, name, meta, body, canUpdate }) => {
50
+ const runs = await listAutomationRuns({
51
+ owners: [resource.owner],
52
+ automation: automationRunHistoryKey(resource.path),
53
+ appId: "factory",
54
+ limit: 20,
55
+ });
56
+ return {
32
57
  id: resource.id,
33
58
  name,
59
+ displayName: resolveAutomationDisplayName(name, resource.content),
34
60
  prompt: body,
35
61
  body,
36
62
  model: meta.model ?? null,
@@ -46,13 +72,9 @@ export default defineAction({
46
72
  ? new Date(resource.updatedAt).toISOString()
47
73
  : null,
48
74
  canUpdate,
49
- runs: await listAutomationRuns({
50
- owners: [resource.owner],
51
- automation: name,
52
- appId: "factory",
53
- limit: 20,
54
- }),
55
- })),
75
+ runs: runs.filter((run) => run.path === resource.path),
76
+ };
77
+ }),
56
78
  );
57
79
  },
58
80
  });
@@ -4,6 +4,12 @@ import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
6
  import { triageItems, triageDecisions } from "../server/db/schema.js";
7
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
8
+ import {
9
+ factoryIdSchema,
10
+ orgFactoryDecisionFilter,
11
+ orgFactoryItemFilter,
12
+ } from "../server/lib/factory-scope.js";
7
13
  import {
8
14
  requireWorkspaceMember,
9
15
  workspaceMemberIdentityFromContext,
@@ -18,6 +24,7 @@ export default defineAction({
18
24
  description:
19
25
  "List the Factory observation queue. Results are scoped to the active workspace and include the latest shadow decision summary. Scheduled reviewers must pass needsReview true with a bounded source and limit so unchanged items are not re-reviewed.",
20
26
  schema: z.object({
27
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
21
28
  status: triageItemStatusSchema.optional(),
22
29
  source: triageSourceSchema.optional(),
23
30
  needsReview: z.boolean().default(false),
@@ -25,7 +32,7 @@ export default defineAction({
25
32
  }),
26
33
  http: { method: "GET" },
27
34
  readOnly: true,
28
- run: async ({ status, source, needsReview, limit }, context) => {
35
+ run: async ({ factoryId, status, source, needsReview, limit }, context) => {
29
36
  const { userEmail, orgId } = await requireWorkspaceMember(
30
37
  workspaceMemberIdentityFromContext(context),
31
38
  );
@@ -41,7 +48,7 @@ export default defineAction({
41
48
  .from(triageItems)
42
49
  .where(
43
50
  and(
44
- eq(triageItems.orgId, orgId),
51
+ orgFactoryItemFilter(orgId, factoryId),
45
52
  needsReview
46
53
  ? inArray(triageItems.status, reviewStatuses)
47
54
  : status
@@ -56,7 +63,7 @@ export default defineAction({
56
63
  const decisions = await db
57
64
  .select()
58
65
  .from(triageDecisions)
59
- .where(eq(triageDecisions.orgId, orgId))
66
+ .where(orgFactoryDecisionFilter(orgId, factoryId))
60
67
  .orderBy(desc(triageDecisions.createdAt))
61
68
  .limit(500);
62
69
  const latestByItem = new Map<string, (typeof decisions)[number]>();
@@ -116,6 +123,7 @@ export default defineAction({
116
123
  decision: item.latestDecision?.outcome ?? null,
117
124
  },
118
125
  },
126
+ factoryId,
119
127
  );
120
128
  }
121
129
  return listedItems;
@@ -1,9 +1,14 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
- import { desc, eq } from "drizzle-orm";
2
+ import { desc } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
6
  import { triageRules } from "../server/db/schema.js";
7
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
8
+ import {
9
+ factoryIdSchema,
10
+ orgFactoryRuleFilter,
11
+ } from "../server/lib/factory-scope.js";
7
12
  import {
8
13
  requireWorkspaceMember,
9
14
  workspaceMemberIdentityFromContext,
@@ -13,17 +18,19 @@ import { normalizeTriagePolicyGuards } from "../server/triage/contracts.js";
13
18
  export default defineAction({
14
19
  description:
15
20
  "List editable Factory rules for the active workspace. Rules are disabled or shadow-only until the owner explicitly promotes them.",
16
- schema: z.object({}),
21
+ schema: z.object({
22
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
23
+ }),
17
24
  http: { method: "GET" },
18
25
  readOnly: true,
19
- run: async (_, context) => {
26
+ run: async ({ factoryId }, context) => {
20
27
  const { orgId } = await requireWorkspaceMember(
21
28
  workspaceMemberIdentityFromContext(context),
22
29
  );
23
30
  const rows = await getDb()
24
31
  .select()
25
32
  .from(triageRules)
26
- .where(eq(triageRules.orgId, orgId))
33
+ .where(orgFactoryRuleFilter(orgId, factoryId))
27
34
  .orderBy(desc(triageRules.updatedAt));
28
35
  return rows.map((row) => ({
29
36
  ...row,
@@ -3,7 +3,12 @@ import { and, eq } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
- import { triageConfig, triageItems } from "../server/db/schema.js";
6
+ import { triageItems } from "../server/db/schema.js";
7
+ import { repairFactoryAutomationsFromConfig } from "../server/lib/factory-automation-repair.js";
8
+ import {
9
+ factoryIdSchema,
10
+ readTriageConfigRow,
11
+ } from "../server/lib/factory-scope.js";
7
12
  import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
8
13
  import {
9
14
  requireWorkspaceMember,
@@ -30,11 +35,12 @@ export default defineAction({
30
35
  description:
31
36
  "Poll the configured GitHub repository for bounded open issues and pull requests and record them in the Factory queue. This does not write to GitHub.",
32
37
  schema: z.object({
38
+ factoryId: factoryIdSchema,
33
39
  includeIssues: z.boolean().default(true),
34
40
  includePullRequests: z.boolean().default(true),
35
41
  }),
36
42
  http: false,
37
- run: async ({ includeIssues, includePullRequests }, context) => {
43
+ run: async ({ factoryId, includeIssues, includePullRequests }, context) => {
38
44
  const { userEmail, orgId } = await requireWorkspaceMember(
39
45
  workspaceMemberIdentityFromContext(context),
40
46
  );
@@ -42,14 +48,11 @@ export default defineAction({
42
48
  context,
43
49
  { userEmail, orgId },
44
50
  "sourcePolling",
51
+ factoryId,
45
52
  );
46
- const config = (
47
- await getDb()
48
- .select()
49
- .from(triageConfig)
50
- .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
51
- .limit(1)
52
- )[0];
53
+ const db = getDb();
54
+ const config = await readTriageConfigRow(db, orgId, factoryId);
55
+ await repairFactoryAutomationsFromConfig(userEmail, orgId, factoryId);
53
56
  if (config?.githubPollingEnabled !== 1) {
54
57
  throw new Error("Enable GitHub polling before polling GitHub sources.");
55
58
  }
@@ -68,7 +71,6 @@ export default defineAction({
68
71
  ? client.listOpenPullRequests(repository, 50)
69
72
  : Promise.resolve([]),
70
73
  ]);
71
- const db = getDb();
72
74
  const now = new Date().toISOString();
73
75
  let issueCount = 0;
74
76
  let pullRequestCount = 0;
@@ -81,6 +83,7 @@ export default defineAction({
81
83
  externalId: `${repositoryName}#${issue.number}`,
82
84
  },
83
85
  orgId,
86
+ factoryId,
84
87
  );
85
88
  const existing = (
86
89
  await tx
@@ -133,6 +136,7 @@ export default defineAction({
133
136
  updatedAt,
134
137
  ownerEmail: existing?.ownerEmail ?? userEmail,
135
138
  orgId,
139
+ factoryId,
136
140
  })
137
141
  .onConflictDoUpdate({
138
142
  target: triageItems.id,
@@ -144,6 +148,7 @@ export default defineAction({
144
148
  metadataJson: metadata,
145
149
  lastSeenAt,
146
150
  updatedAt,
151
+ factoryId,
147
152
  },
148
153
  });
149
154
  issueCount += 1;
@@ -158,6 +163,7 @@ export default defineAction({
158
163
  pullRequestNumber: pullRequest.number,
159
164
  },
160
165
  orgId,
166
+ factoryId,
161
167
  );
162
168
  const existing = (
163
169
  await tx
@@ -213,6 +219,7 @@ export default defineAction({
213
219
  updatedAt,
214
220
  ownerEmail: existing?.ownerEmail ?? userEmail,
215
221
  orgId,
222
+ factoryId,
216
223
  })
217
224
  .onConflictDoUpdate({
218
225
  target: triageItems.id,
@@ -227,6 +234,7 @@ export default defineAction({
227
234
  metadataJson: metadata,
228
235
  lastSeenAt,
229
236
  updatedAt,
237
+ factoryId,
230
238
  },
231
239
  });
232
240
  pullRequestCount += 1;
@@ -244,6 +252,7 @@ export default defineAction({
244
252
  summary: "No open GitHub issues or pull requests were observed.",
245
253
  details: { repository: repositoryName },
246
254
  },
255
+ factoryId,
247
256
  );
248
257
  } else {
249
258
  for (const issue of issues) {
@@ -259,12 +268,14 @@ export default defineAction({
259
268
  externalId: `${repositoryName}#${issue.number}`,
260
269
  },
261
270
  orgId,
271
+ factoryId,
262
272
  ),
263
273
  source: "github_issue",
264
274
  sourceUrl: issue.htmlUrl,
265
275
  summary: issue.title,
266
276
  details: { repository: repositoryName, number: issue.number },
267
277
  },
278
+ factoryId,
268
279
  );
269
280
  }
270
281
  for (const pullRequest of pullRequests) {
@@ -282,18 +293,21 @@ export default defineAction({
282
293
  pullRequestNumber: pullRequest.number,
283
294
  },
284
295
  orgId,
296
+ factoryId,
285
297
  ),
286
298
  source: "github",
287
299
  sourceUrl: pullRequest.htmlUrl,
288
300
  summary: pullRequest.title,
289
301
  details: { repository: repositoryName, number: pullRequest.number },
290
302
  },
303
+ factoryId,
291
304
  );
292
305
  }
293
306
  }
294
307
 
295
308
  return {
296
309
  ok: true,
310
+ factoryId,
297
311
  repository: repositoryName,
298
312
  issues: issueCount,
299
313
  pullRequests: pullRequestCount,
@@ -4,6 +4,12 @@ import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
6
  import { triageConfig, triageItems } from "../server/db/schema.js";
7
+ import { repairFactoryAutomationsFromConfig } from "../server/lib/factory-automation-repair.js";
8
+ import {
9
+ factoryIdSchema,
10
+ readTriageConfigRow,
11
+ triageConfigUpdateRowId,
12
+ } from "../server/lib/factory-scope.js";
7
13
  import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
8
14
  import {
9
15
  requireWorkspaceMember,
@@ -21,9 +27,12 @@ import { createSentryClient } from "../server/triage/sentry-client.js";
21
27
  export default defineAction({
22
28
  description:
23
29
  "Poll bounded unresolved Sentry issues for the configured organization and record them in the Factory queue. This does not change Sentry.",
24
- schema: z.object({ limit: z.number().int().min(1).max(50).default(25) }),
30
+ schema: z.object({
31
+ factoryId: factoryIdSchema,
32
+ limit: z.number().int().min(1).max(50).default(25),
33
+ }),
25
34
  http: false,
26
- run: async ({ limit }, context) => {
35
+ run: async ({ factoryId, limit }, context) => {
27
36
  const { userEmail, orgId } = await requireWorkspaceMember(
28
37
  workspaceMemberIdentityFromContext(context),
29
38
  );
@@ -31,14 +40,11 @@ export default defineAction({
31
40
  context,
32
41
  { userEmail, orgId },
33
42
  "sourcePolling",
43
+ factoryId,
34
44
  );
35
- const config = (
36
- await getDb()
37
- .select()
38
- .from(triageConfig)
39
- .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
40
- .limit(1)
41
- )[0];
45
+ const db = getDb();
46
+ const config = await readTriageConfigRow(db, orgId, factoryId);
47
+ await repairFactoryAutomationsFromConfig(userEmail, orgId, factoryId);
42
48
  if (config?.sentryPollingEnabled !== 1) {
43
49
  throw new Error("Enable Sentry polling before polling Sentry.");
44
50
  }
@@ -74,14 +80,15 @@ export default defineAction({
74
80
  );
75
81
  }
76
82
  const observedIssues = issues;
77
- const db = getDb();
78
83
  const now = new Date().toISOString();
84
+ const configRowId = triageConfigUpdateRowId(config, orgId, factoryId);
79
85
 
80
86
  await db.transaction(async (tx) => {
81
87
  for (const issue of observedIssues) {
82
88
  const id = itemDedupeKey(
83
89
  { source: "sentry", externalId: issue.id },
84
90
  orgId,
91
+ factoryId,
85
92
  );
86
93
  const existing = (
87
94
  await tx
@@ -140,6 +147,7 @@ export default defineAction({
140
147
  updatedAt,
141
148
  ownerEmail: existing?.ownerEmail ?? userEmail,
142
149
  orgId,
150
+ factoryId,
143
151
  })
144
152
  .onConflictDoUpdate({
145
153
  target: triageItems.id,
@@ -151,6 +159,7 @@ export default defineAction({
151
159
  metadataJson: metadata,
152
160
  lastSeenAt,
153
161
  updatedAt,
162
+ factoryId,
154
163
  },
155
164
  });
156
165
  }
@@ -167,7 +176,9 @@ export default defineAction({
167
176
  }, config.lastSentrySeenAt) ?? config.lastSentrySeenAt,
168
177
  updatedAt: now,
169
178
  })
170
- .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)));
179
+ .where(
180
+ and(eq(triageConfig.id, configRowId), eq(triageConfig.orgId, orgId)),
181
+ );
171
182
  });
172
183
 
173
184
  if (observedIssues.length === 0) {
@@ -181,6 +192,7 @@ export default defineAction({
181
192
  summary: "No unresolved Sentry errors were observed.",
182
193
  details: { sentryOrgSlug: config.sentryOrgSlug },
183
194
  },
195
+ factoryId,
184
196
  );
185
197
  } else {
186
198
  for (const issue of observedIssues) {
@@ -193,6 +205,7 @@ export default defineAction({
193
205
  itemId: itemDedupeKey(
194
206
  { source: "sentry", externalId: issue.id },
195
207
  orgId,
208
+ factoryId,
196
209
  ),
197
210
  source: "sentry",
198
211
  sourceUrl: issue.permalink,
@@ -204,12 +217,14 @@ export default defineAction({
204
217
  projectSlug: issue.projectSlug,
205
218
  },
206
219
  },
220
+ factoryId,
207
221
  );
208
222
  }
209
223
  }
210
224
 
211
225
  return {
212
226
  ok: true,
227
+ factoryId,
213
228
  observed: observedIssues.length,
214
229
  fetched: issues.length,
215
230
  sentryOrgSlug: config.sentryOrgSlug,
@@ -4,6 +4,12 @@ import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
6
  import { triageConfig, triageItems } from "../server/db/schema.js";
7
+ import { repairFactoryAutomationsFromConfig } from "../server/lib/factory-automation-repair.js";
8
+ import {
9
+ factoryIdSchema,
10
+ readTriageConfigRow,
11
+ triageConfigUpdateRowId,
12
+ } from "../server/lib/factory-scope.js";
7
13
  import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
8
14
  import {
9
15
  requireWorkspaceMember,
@@ -21,10 +27,11 @@ export default defineAction({
21
27
  description:
22
28
  "Poll the configured Slack channel and append new messages to the Factory queue. This action only observes and never posts, replies, starts work, or changes a provider.",
23
29
  schema: z.object({
30
+ factoryId: factoryIdSchema,
24
31
  channelId: z.string().trim().min(1).max(128).optional(),
25
32
  }),
26
33
  http: false,
27
- run: async ({ channelId: requestedChannelId }, context) => {
34
+ run: async ({ factoryId, channelId: requestedChannelId }, context) => {
28
35
  const { userEmail, orgId } = await requireWorkspaceMember(
29
36
  workspaceMemberIdentityFromContext(context),
30
37
  );
@@ -32,22 +39,24 @@ export default defineAction({
32
39
  context,
33
40
  { userEmail, orgId },
34
41
  "sourcePolling",
42
+ factoryId,
35
43
  );
36
44
  const db = getDb();
37
- const config = (
38
- await db
39
- .select()
40
- .from(triageConfig)
41
- .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
42
- .limit(1)
43
- )[0];
45
+ const config = await readTriageConfigRow(db, orgId, factoryId);
46
+ await repairFactoryAutomationsFromConfig(userEmail, orgId, factoryId);
44
47
  if (config?.pollingEnabled !== 1) {
45
48
  throw new Error("Enable Slack polling before polling Slack.");
46
49
  }
47
- const channelId = requestedChannelId ?? config?.slackChannelId;
48
- if (!channelId) {
50
+ const configuredChannelId = config?.slackChannelId;
51
+ if (!configuredChannelId) {
49
52
  throw new Error("Configure a Slack channel before polling.");
50
53
  }
54
+ if (requestedChannelId && requestedChannelId !== configuredChannelId) {
55
+ throw new Error(
56
+ "Poll only the Slack channel configured for this factory.",
57
+ );
58
+ }
59
+ const channelId = configuredChannelId;
51
60
 
52
61
  const result = await pollSlackChannel({
53
62
  workspace:
@@ -59,10 +68,11 @@ export default defineAction({
59
68
  orgId,
60
69
  });
61
70
  const now = new Date().toISOString();
71
+ const configRowId = triageConfigUpdateRowId(config, orgId, factoryId);
62
72
 
63
73
  await db.transaction(async (tx) => {
64
74
  for (const envelope of result.envelopes) {
65
- const id = itemDedupeKey(envelope, orgId);
75
+ const id = itemDedupeKey(envelope, orgId, factoryId);
66
76
  const existing = (
67
77
  await tx
68
78
  .select()
@@ -117,6 +127,7 @@ export default defineAction({
117
127
  updatedAt,
118
128
  ownerEmail: userEmail,
119
129
  orgId,
130
+ factoryId,
120
131
  })
121
132
  .onConflictDoUpdate({
122
133
  target: triageItems.id,
@@ -131,6 +142,7 @@ export default defineAction({
131
142
  status,
132
143
  lastSeenAt,
133
144
  updatedAt,
145
+ factoryId,
134
146
  },
135
147
  });
136
148
  }
@@ -144,7 +156,10 @@ export default defineAction({
144
156
  updatedAt: now,
145
157
  })
146
158
  .where(
147
- and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)),
159
+ and(
160
+ eq(triageConfig.id, configRowId),
161
+ eq(triageConfig.orgId, orgId),
162
+ ),
148
163
  );
149
164
  }
150
165
  });
@@ -163,6 +178,7 @@ export default defineAction({
163
178
  coverage: result.hasMore ? "partial" : "complete",
164
179
  },
165
180
  },
181
+ factoryId,
166
182
  );
167
183
  } else {
168
184
  for (const envelope of result.envelopes) {
@@ -172,7 +188,7 @@ export default defineAction({
172
188
  {
173
189
  action: "poll-slack-channel",
174
190
  kind: "observed",
175
- itemId: itemDedupeKey(envelope, orgId),
191
+ itemId: itemDedupeKey(envelope, orgId, factoryId),
176
192
  source: envelope.source,
177
193
  sourceUrl: envelope.sourceUrl ?? null,
178
194
  summary: envelope.summary ?? envelope.title,
@@ -182,6 +198,7 @@ export default defineAction({
182
198
  coverage: envelope.coverage,
183
199
  },
184
200
  },
201
+ factoryId,
185
202
  );
186
203
  }
187
204
  }
@@ -189,6 +206,7 @@ export default defineAction({
189
206
  return {
190
207
  ok: true,
191
208
  source: "slack",
209
+ factoryId,
192
210
  channelId,
193
211
  observed: result.envelopes.length,
194
212
  hasMore: result.hasMore,