@agent-native/core 0.168.7 → 0.168.9

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 (84) 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/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
  15. package/corpus/templates/factory/AGENTS.md +12 -22
  16. package/corpus/templates/factory/README.md +11 -8
  17. package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
  18. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
  19. package/corpus/templates/factory/actions/create-factory.ts +211 -0
  20. package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
  21. package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
  22. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
  23. package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
  24. package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
  25. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
  26. package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
  27. package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
  28. package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
  29. package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
  30. package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
  31. package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
  32. package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
  33. package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
  34. package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
  35. package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
  36. package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
  37. package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
  38. package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
  39. package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
  40. package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
  41. package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
  42. package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
  43. package/corpus/templates/factory/actions/view-screen.ts +22 -30
  44. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
  45. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
  46. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
  47. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  48. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
  49. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
  50. package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
  51. package/corpus/templates/factory/app/routes/factory.tsx +78 -62
  52. package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
  53. package/corpus/templates/factory/server/db/schema.ts +18 -3
  54. package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
  55. package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
  56. package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
  57. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
  58. package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
  59. package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
  60. package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
  61. package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
  62. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
  63. package/corpus/templates/factory/server/triage/audit.ts +4 -0
  64. package/corpus/templates/factory/server/triage/ids.ts +4 -0
  65. package/dist/cli/native-dependencies.js +4 -1
  66. package/dist/client/AgentPanel.d.ts +5 -1
  67. package/dist/client/AgentPanel.js +9 -4
  68. package/dist/client/agent-chat/index.d.ts +1 -1
  69. package/dist/client/agent-chat/index.js +1 -1
  70. package/dist/client/chat-first/apps-rail.js +6 -6
  71. package/dist/client/chat-first/primary-nav.js +14 -6
  72. package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
  73. package/dist/client/integrations/IntegrationsPanel.js +29 -21
  74. package/dist/client/integrations/index.d.ts +1 -1
  75. package/dist/client/integrations/index.js +1 -1
  76. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  77. package/dist/collab/struct-routes.d.ts +1 -1
  78. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  79. package/dist/observability/routes.d.ts +3 -3
  80. package/dist/progress/routes.d.ts +1 -1
  81. package/dist/secrets/routes.d.ts +6 -6
  82. package/dist/server/realtime-token.d.ts +1 -1
  83. package/dist/server/transcribe-voice.d.ts +1 -1
  84. package/package.json +1 -1
@@ -8,6 +8,13 @@ import {
8
8
  triageItems,
9
9
  triageRuns,
10
10
  } from "../server/db/schema.js";
11
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
12
+ import {
13
+ factoryIdSchema,
14
+ orgFactoryDecisionFilter,
15
+ orgFactoryItemFilter,
16
+ orgFactoryRunFilter,
17
+ } from "../server/lib/factory-scope.js";
11
18
  import {
12
19
  requireWorkspaceMember,
13
20
  workspaceMemberIdentityFromContext,
@@ -19,6 +26,7 @@ export default defineAction({
19
26
  description:
20
27
  "Explicitly approve one Factory item and start one deduplicated Builder coding run. The approver, decision, guard results, and callback state are recorded before provider work is considered successful.",
21
28
  schema: z.object({
29
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
22
30
  itemId: z.string().min(1),
23
31
  decisionId: z.string().min(1).optional(),
24
32
  confirm: z.literal(true),
@@ -27,7 +35,7 @@ export default defineAction({
27
35
  // must remain paused until a human approves this exact agent tool call.
28
36
  needsApproval: true,
29
37
  http: { method: "POST" },
30
- run: async ({ itemId, decisionId, confirm }, context) => {
38
+ run: async ({ factoryId, itemId, decisionId, confirm }, context) => {
31
39
  if (!confirm) throw new Error("Explicit confirmation is required.");
32
40
  const { userEmail, orgId } = await requireWorkspaceMember(
33
41
  workspaceMemberIdentityFromContext(context),
@@ -37,7 +45,12 @@ export default defineAction({
37
45
  await db
38
46
  .select()
39
47
  .from(triageItems)
40
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
48
+ .where(
49
+ and(
50
+ eq(triageItems.id, itemId),
51
+ orgFactoryItemFilter(orgId, factoryId),
52
+ ),
53
+ )
41
54
  .limit(1)
42
55
  )[0];
43
56
  if (!item) throw new Error("Factory item not found.");
@@ -47,8 +60,8 @@ export default defineAction({
47
60
  .from(triageDecisions)
48
61
  .where(
49
62
  and(
50
- eq(triageDecisions.orgId, orgId),
51
63
  eq(triageDecisions.itemId, itemId),
64
+ orgFactoryDecisionFilter(orgId, factoryId),
52
65
  ),
53
66
  )
54
67
  .orderBy(asc(triageDecisions.createdAt));
@@ -72,7 +85,9 @@ export default defineAction({
72
85
  await db
73
86
  .select()
74
87
  .from(triageRuns)
75
- .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)))
88
+ .where(
89
+ and(eq(triageRuns.id, runId), orgFactoryRunFilter(orgId, factoryId)),
90
+ )
76
91
  .limit(1)
77
92
  )[0];
78
93
  if (
@@ -110,6 +125,7 @@ export default defineAction({
110
125
  error: null,
111
126
  ownerEmail: item.ownerEmail,
112
127
  orgId,
128
+ factoryId,
113
129
  })
114
130
  .onConflictDoNothing();
115
131
 
@@ -117,7 +133,9 @@ export default defineAction({
117
133
  await db
118
134
  .select()
119
135
  .from(triageRuns)
120
- .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)))
136
+ .where(
137
+ and(eq(triageRuns.id, runId), orgFactoryRunFilter(orgId, factoryId)),
138
+ )
121
139
  .limit(1)
122
140
  )[0];
123
141
  if (!run) throw new Error("Factory run could not be recorded.");
@@ -155,11 +173,18 @@ export default defineAction({
155
173
  progressLogJson: JSON.stringify(progress),
156
174
  heartbeatAt: new Date().toISOString(),
157
175
  })
158
- .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)));
176
+ .where(
177
+ and(eq(triageRuns.id, runId), orgFactoryRunFilter(orgId, factoryId)),
178
+ );
159
179
  await db
160
180
  .update(triageItems)
161
181
  .set({ status: "classified", updatedAt: new Date().toISOString() })
162
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
182
+ .where(
183
+ and(
184
+ eq(triageItems.id, itemId),
185
+ orgFactoryItemFilter(orgId, factoryId),
186
+ ),
187
+ );
163
188
  return {
164
189
  ok: true,
165
190
  deduplicated: false,
@@ -176,7 +201,9 @@ export default defineAction({
176
201
  completedAt: new Date().toISOString(),
177
202
  heartbeatAt: new Date().toISOString(),
178
203
  })
179
- .where(and(eq(triageRuns.id, runId), eq(triageRuns.orgId, orgId)));
204
+ .where(
205
+ and(eq(triageRuns.id, runId), orgFactoryRunFilter(orgId, factoryId)),
206
+ );
180
207
  throw new Error(
181
208
  `Factory approval was recorded, but Builder dispatch failed: ${message}`,
182
209
  );
@@ -4,7 +4,12 @@ import { z } from "zod";
4
4
 
5
5
  import { resolveConnectorSecret } from "../server/connectors/credentials.js";
6
6
  import { getDb } from "../server/db/index.js";
7
- import { triageConfig, triageItems } from "../server/db/schema.js";
7
+ import { triageItems } from "../server/db/schema.js";
8
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
9
+ import {
10
+ factoryIdSchema,
11
+ readTriageConfigRow,
12
+ } from "../server/lib/factory-scope.js";
8
13
  import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
9
14
  import {
10
15
  requireWorkspaceMember,
@@ -85,13 +90,13 @@ export default defineAction({
85
90
  "Watch one builder-io-bot pull request, post the bounded feedback-fix request when CI, mergeability, or review feedback needs work, and stop after 20 quiet minutes. The action never merges or approves.",
86
91
  schema: z.object({
87
92
  itemId: z.string().min(1),
93
+ factoryId: factoryIdSchema.optional(),
88
94
  }),
89
95
  http: false,
90
- run: async ({ itemId }, context) => {
96
+ run: async ({ itemId, factoryId: factoryIdInput }, context) => {
91
97
  const { userEmail, orgId } = await requireWorkspaceMember(
92
98
  workspaceMemberIdentityFromContext(context),
93
99
  );
94
- await requireFactoryAutomation(context, { userEmail, orgId }, "prBabysit");
95
100
 
96
101
  const db = getDb();
97
102
  const item = (
@@ -102,6 +107,16 @@ export default defineAction({
102
107
  .limit(1)
103
108
  )[0];
104
109
  if (!item) throw new Error("Factory item not found for PR babysitting.");
110
+ const factoryId = factoryIdInput ?? item.factoryId ?? DEFAULT_FACTORY_ID;
111
+ if ((item.factoryId ?? DEFAULT_FACTORY_ID) !== factoryId) {
112
+ throw new Error("Factory item does not belong to this factory.");
113
+ }
114
+ await requireFactoryAutomation(
115
+ context,
116
+ { userEmail, orgId },
117
+ "prBabysit",
118
+ factoryId,
119
+ );
105
120
  if (
106
121
  item.source !== "github" ||
107
122
  !item.repository ||
@@ -115,12 +130,8 @@ export default defineAction({
115
130
  }
116
131
 
117
132
  const configuredRepository = (
118
- await db
119
- .select({ repository: triageConfig.repository })
120
- .from(triageConfig)
121
- .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
122
- .limit(1)
123
- )[0]?.repository;
133
+ await readTriageConfigRow(db, orgId, factoryId)
134
+ )?.repository;
124
135
  if (
125
136
  !configuredRepository ||
126
137
  configuredRepository.trim() !== item.repository.trim()
@@ -154,6 +165,7 @@ export default defineAction({
154
165
  summary: "Skipped because the pull request is closed or a draft.",
155
166
  details: { state: pullRequest.state, draft: pullRequest.draft },
156
167
  },
168
+ factoryId,
157
169
  );
158
170
  return { ok: true, action: "skipped", reason: "PR is closed or draft." };
159
171
  }
@@ -182,6 +194,7 @@ export default defineAction({
182
194
  summary: `${ownerOwnedArea} is owner-managed; no bot feedback was posted.`,
183
195
  details: { ownerOwnedArea },
184
196
  },
197
+ factoryId,
185
198
  );
186
199
  return {
187
200
  ok: true,
@@ -257,6 +270,7 @@ export default defineAction({
257
270
  reviewFeedbackClean: signal.isClean,
258
271
  },
259
272
  },
273
+ factoryId,
260
274
  );
261
275
  const now = new Date();
262
276
  const nowIso = now.toISOString();
@@ -326,6 +340,7 @@ export default defineAction({
326
340
  summary: "Posted the bounded feedback-fix request to builder-io-bot.",
327
341
  details: { commentUrl: comment.htmlUrl, quietForMinutes: 0 },
328
342
  },
343
+ factoryId,
329
344
  );
330
345
  await updateBabysitMetadata(itemId, orgId, {
331
346
  prBabysitState: "active",
@@ -0,0 +1,211 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { buildDeepLink } from "@agent-native/core/server";
3
+ import { and, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb } from "../server/db/index.js";
7
+ import {
8
+ factoryDefinitions,
9
+ factoryGraphVersions,
10
+ triageConfig,
11
+ } from "../server/db/schema.js";
12
+ import {
13
+ minimalFactoryGraph,
14
+ normalizeFactoryGraph,
15
+ } from "../server/factory-graph/contracts.js";
16
+ import {
17
+ resolveEnabledAutomations,
18
+ isFactoryIdConflict,
19
+ isFactorySlackChannelConflict,
20
+ } from "../server/lib/factory-automation-plan.js";
21
+ import {
22
+ assertUniqueSlackChannelForFactory,
23
+ factoryConfigRowId,
24
+ resolveUniqueFactoryId,
25
+ } from "../server/lib/factory-scope.js";
26
+ import {
27
+ requireWorkspaceMember,
28
+ workspaceMemberIdentityFromContext,
29
+ } from "../server/lib/require-workspace-member.js";
30
+ import {
31
+ ensureFactoryAutomations,
32
+ removeFactoryAutomationResources,
33
+ syncFactoryAutomationEnabledStates,
34
+ } from "../server/plugins/factory-scheduler-job.js";
35
+ import { stableId } from "../server/triage/ids.js";
36
+
37
+ export default defineAction({
38
+ description:
39
+ "Create a new Factory with a minimal blueprint, optional observation settings, and seeded automations. Name is required; sources and automations are optional.",
40
+ schema: z.object({
41
+ name: z.string().trim().min(1).max(120),
42
+ description: z.string().trim().max(500).optional(),
43
+ slackChannelId: z.string().trim().max(128).optional(),
44
+ slackChannelName: z.string().trim().max(200).optional(),
45
+ observeSlack: z.boolean().optional(),
46
+ repository: z.string().trim().max(256).optional(),
47
+ observeGithub: z.boolean().optional(),
48
+ sentryOrgSlug: z.string().trim().max(200).optional(),
49
+ sentryProjectSlug: z.string().trim().max(200).optional(),
50
+ sentryEnvironment: z.string().trim().max(200).optional(),
51
+ observeSentry: z.boolean().optional(),
52
+ }),
53
+ http: { method: "POST" },
54
+ link: ({ result }) => ({
55
+ url: buildDeepLink({
56
+ app: "factory",
57
+ view: "factory",
58
+ params: { factoryId: result.factoryId },
59
+ }),
60
+ label: `Open ${result.name} in Factory`,
61
+ }),
62
+ run: async (input, context) => {
63
+ const { userEmail, orgId } = await requireWorkspaceMember(
64
+ workspaceMemberIdentityFromContext(context),
65
+ );
66
+ const db = getDb();
67
+ const description = input.description?.trim() ?? "";
68
+ const graph = normalizeFactoryGraph(
69
+ minimalFactoryGraph(input.name, description),
70
+ );
71
+ const now = new Date().toISOString();
72
+ const automationPlan = resolveEnabledAutomations({
73
+ observeSlack: input.observeSlack === true,
74
+ slackChannelId: input.slackChannelId,
75
+ observeGithub: input.observeGithub === true,
76
+ repository: input.repository,
77
+ observeSentry: input.observeSentry === true,
78
+ sentryOrgSlug: input.sentryOrgSlug,
79
+ sentryProjectSlug: input.sentryProjectSlug,
80
+ });
81
+
82
+ const MAX_FACTORY_CREATE_ATTEMPTS = 5;
83
+ let factoryId = "";
84
+ for (let attempt = 0; attempt < MAX_FACTORY_CREATE_ATTEMPTS; attempt++) {
85
+ factoryId = await resolveUniqueFactoryId(db, orgId, input.name);
86
+ const versionId = stableId("factory-graph", orgId, factoryId, "1");
87
+ try {
88
+ await db.transaction(async (tx) => {
89
+ await tx.insert(factoryDefinitions).values({
90
+ id: factoryId,
91
+ name: input.name,
92
+ description,
93
+ prompt: "",
94
+ graphVersion: 1,
95
+ graphJson: JSON.stringify(graph),
96
+ createdAt: now,
97
+ updatedAt: now,
98
+ ownerEmail: userEmail,
99
+ orgId,
100
+ });
101
+ await tx.insert(factoryGraphVersions).values({
102
+ id: versionId,
103
+ factoryId,
104
+ version: 1,
105
+ graphJson: JSON.stringify(graph),
106
+ source: "manual",
107
+ changeSummary: "Created from the new factory dialog.",
108
+ createdAt: now,
109
+ createdBy: userEmail,
110
+ ownerEmail: userEmail,
111
+ orgId,
112
+ });
113
+
114
+ if (automationPlan.hasConfig) {
115
+ const slackChannelId = input.slackChannelId?.trim() || null;
116
+ await assertUniqueSlackChannelForFactory(
117
+ tx as unknown as typeof db,
118
+ orgId,
119
+ factoryId,
120
+ slackChannelId,
121
+ );
122
+ await tx.insert(triageConfig).values({
123
+ id: factoryConfigRowId(orgId, factoryId),
124
+ factoryId,
125
+ slackWorkspace: "primary",
126
+ slackChannelId,
127
+ slackChannelName: input.slackChannelName?.trim() || null,
128
+ builderSlackUserId: null,
129
+ pollingEnabled: automationPlan.pollingEnabled ? 1 : 0,
130
+ githubPollingEnabled: automationPlan.githubPollingEnabled ? 1 : 0,
131
+ sentryPollingEnabled: automationPlan.sentryPollingEnabled ? 1 : 0,
132
+ sentryOrgSlug: input.sentryOrgSlug?.trim() || null,
133
+ sentryProjectSlug: input.sentryProjectSlug?.trim() || null,
134
+ sentryEnvironment: input.sentryEnvironment?.trim() || null,
135
+ repository: input.repository?.trim() || null,
136
+ automationFailureAlertsEnabled: 1,
137
+ automationFailureAlertEmail: null,
138
+ createdAt: now,
139
+ updatedAt: now,
140
+ ownerEmail: userEmail,
141
+ orgId,
142
+ });
143
+ }
144
+ });
145
+ break;
146
+ } catch (error) {
147
+ if (isFactorySlackChannelConflict(error)) {
148
+ throw new Error(
149
+ "That Slack channel is already used by another Factory in this workspace.",
150
+ );
151
+ }
152
+ if (
153
+ isFactoryIdConflict(error) &&
154
+ attempt < MAX_FACTORY_CREATE_ATTEMPTS - 1
155
+ ) {
156
+ continue;
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+ if (!factoryId) {
162
+ throw new Error("Could not allocate a unique factory id.");
163
+ }
164
+
165
+ try {
166
+ await ensureFactoryAutomations(userEmail, orgId, factoryId, {
167
+ enabledNames: automationPlan.enabledNames,
168
+ });
169
+ if (automationPlan.enabledNames.size > 0) {
170
+ await syncFactoryAutomationEnabledStates(userEmail, orgId, factoryId, [
171
+ ...automationPlan.enabledNames,
172
+ ]);
173
+ }
174
+ } catch (error) {
175
+ await removeFactoryAutomationResources(orgId, factoryId);
176
+ await db
177
+ .delete(triageConfig)
178
+ .where(
179
+ and(
180
+ eq(triageConfig.orgId, orgId),
181
+ eq(triageConfig.factoryId, factoryId),
182
+ ),
183
+ );
184
+ await db
185
+ .delete(factoryGraphVersions)
186
+ .where(
187
+ and(
188
+ eq(factoryGraphVersions.orgId, orgId),
189
+ eq(factoryGraphVersions.factoryId, factoryId),
190
+ ),
191
+ );
192
+ await db
193
+ .delete(factoryDefinitions)
194
+ .where(
195
+ and(
196
+ eq(factoryDefinitions.id, factoryId),
197
+ eq(factoryDefinitions.orgId, orgId),
198
+ ),
199
+ );
200
+ throw error;
201
+ }
202
+
203
+ return {
204
+ ok: true,
205
+ factoryId,
206
+ name: input.name,
207
+ graphVersion: 1,
208
+ enabledAutomations: [...automationPlan.enabledNames],
209
+ };
210
+ },
211
+ });
@@ -11,6 +11,12 @@ import {
11
11
  triageItems,
12
12
  triageRules,
13
13
  } from "../server/db/schema.js";
14
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
15
+ import {
16
+ factoryIdSchema,
17
+ orgFactoryItemFilter,
18
+ orgFactoryRuleFilter,
19
+ } from "../server/lib/factory-scope.js";
14
20
  import {
15
21
  requireWorkspaceMember,
16
22
  workspaceMemberIdentityFromContext,
@@ -25,9 +31,12 @@ import { evaluateStructuredGuards } from "../server/triage/guards.js";
25
31
  export default defineAction({
26
32
  description:
27
33
  "Triage one Factory item against enabled rules and append structured guard decisions. This records proposals only and never changes a provider.",
28
- schema: z.object({ itemId: z.string().min(1) }),
34
+ schema: z.object({
35
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
36
+ itemId: z.string().min(1),
37
+ }),
29
38
  http: { method: "POST" },
30
- run: async ({ itemId }, context) => {
39
+ run: async ({ factoryId, itemId }, context) => {
31
40
  const { userEmail, orgId } = await requireWorkspaceMember(
32
41
  workspaceMemberIdentityFromContext(context),
33
42
  );
@@ -36,7 +45,12 @@ export default defineAction({
36
45
  await db
37
46
  .select()
38
47
  .from(triageItems)
39
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
48
+ .where(
49
+ and(
50
+ eq(triageItems.id, itemId),
51
+ orgFactoryItemFilter(orgId, factoryId),
52
+ ),
53
+ )
40
54
  .limit(1)
41
55
  )[0];
42
56
  if (!item) throw new Error("Triage item not found");
@@ -44,7 +58,9 @@ export default defineAction({
44
58
  const rules = await db
45
59
  .select()
46
60
  .from(triageRules)
47
- .where(and(eq(triageRules.orgId, orgId), eq(triageRules.enabled, 1)));
61
+ .where(
62
+ and(eq(triageRules.enabled, 1), orgFactoryRuleFilter(orgId, factoryId)),
63
+ );
48
64
  const metadata = parseMetadata(item.metadataJson);
49
65
  const evidence = {
50
66
  repository: item.repository ?? undefined,
@@ -77,6 +93,7 @@ export default defineAction({
77
93
  {
78
94
  action: "evaluate-triage-item",
79
95
  kind: "decision",
96
+ factoryId,
80
97
  status: "skipped",
81
98
  itemId,
82
99
  source: item.source,
@@ -116,6 +133,7 @@ export default defineAction({
116
133
  createdAt: now,
117
134
  ownerEmail: userEmail,
118
135
  orgId,
136
+ factoryId,
119
137
  };
120
138
  });
121
139
 
@@ -131,7 +149,12 @@ export default defineAction({
131
149
  : "shadow_decided",
132
150
  updatedAt: now,
133
151
  })
134
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
152
+ .where(
153
+ and(
154
+ eq(triageItems.id, itemId),
155
+ orgFactoryItemFilter(orgId, factoryId),
156
+ ),
157
+ );
135
158
  });
136
159
 
137
160
  for (const decision of decisions) {
@@ -141,6 +164,7 @@ export default defineAction({
141
164
  {
142
165
  action: "evaluate-triage-item",
143
166
  kind: "decision",
167
+ factoryId,
144
168
  itemId,
145
169
  source: item.source,
146
170
  sourceUrl: item.sourceUrl,
@@ -28,7 +28,7 @@ export default defineAction({
28
28
  const row = await readFactoryDefinition(orgId, factoryId);
29
29
  const fallback = defaultFactoryDefinition();
30
30
  const graph = row ? parseFactoryGraph(row.graphJson) : fallback.graph;
31
- const metrics = await readFactoryMetrics(orgId);
31
+ const metrics = await readFactoryMetrics(orgId, factoryId);
32
32
 
33
33
  const metricValues: Record<string, number> = {
34
34
  items: metrics.totalItems,
@@ -3,7 +3,13 @@ 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 { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
8
+ import {
9
+ factoryIdSchema,
10
+ orgFactoryItemFilter,
11
+ readTriageConfigRow,
12
+ } from "../server/lib/factory-scope.js";
7
13
  import {
8
14
  requireWorkspaceMember,
9
15
  workspaceMemberIdentityFromContext,
@@ -14,10 +20,13 @@ import { createSlackReader } from "../server/triage/slack-client.js";
14
20
  export default defineAction({
15
21
  description:
16
22
  "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.",
17
- schema: z.object({ itemId: z.string().min(1) }),
23
+ schema: z.object({
24
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
25
+ itemId: z.string().min(1),
26
+ }),
18
27
  http: { method: "GET" },
19
28
  readOnly: true,
20
- run: async ({ itemId }, context) => {
29
+ run: async ({ factoryId, itemId }, context) => {
21
30
  const { userEmail, orgId } = await requireWorkspaceMember(
22
31
  workspaceMemberIdentityFromContext(context),
23
32
  );
@@ -25,7 +34,12 @@ export default defineAction({
25
34
  await getDb()
26
35
  .select()
27
36
  .from(triageItems)
28
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
37
+ .where(
38
+ and(
39
+ eq(triageItems.id, itemId),
40
+ orgFactoryItemFilter(orgId, factoryId),
41
+ ),
42
+ )
29
43
  .limit(1)
30
44
  )[0];
31
45
  if (!item) throw new Error("Factory item not found.");
@@ -33,13 +47,7 @@ export default defineAction({
33
47
  throw new Error("Factory item is not a readable Slack feedback item.");
34
48
  }
35
49
 
36
- const config = (
37
- await getDb()
38
- .select({ slackWorkspace: triageConfig.slackWorkspace })
39
- .from(triageConfig)
40
- .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
41
- .limit(1)
42
- )[0];
50
+ const config = await readTriageConfigRow(getDb(), orgId, factoryId);
43
51
  const workspace =
44
52
  config?.slackWorkspace === "secondary" ? "secondary" : "primary";
45
53
  const slack = createSlackReader({ ownerEmail: userEmail, orgId });
@@ -66,6 +74,7 @@ export default defineAction({
66
74
  messageCount: messages.length,
67
75
  },
68
76
  },
77
+ factoryId,
69
78
  );
70
79
 
71
80
  return {
@@ -1,10 +1,13 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { getEmailReadiness } from "@agent-native/core/server";
3
- import { and, eq } from "drizzle-orm";
4
3
  import { z } from "zod";
5
4
 
6
5
  import { getDb } from "../server/db/index.js";
7
- import { triageConfig } from "../server/db/schema.js";
6
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
7
+ import {
8
+ factoryIdSchema,
9
+ readTriageConfigRow,
10
+ } from "../server/lib/factory-scope.js";
8
11
  import {
9
12
  requireWorkspaceMember,
10
13
  workspaceMemberIdentityFromContext,
@@ -12,24 +15,21 @@ import {
12
15
 
13
16
  export default defineAction({
14
17
  description:
15
- "Read Factory observation settings for the active workspace. Secret values are never returned.",
16
- schema: z.object({}),
18
+ "Read Factory observation settings for the selected factory. Secret values are never returned.",
19
+ schema: z.object({
20
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
21
+ }),
17
22
  http: { method: "GET" },
18
23
  readOnly: true,
19
- run: async (_, context) => {
24
+ run: async ({ factoryId }, context) => {
20
25
  const { orgId } = await requireWorkspaceMember(
21
26
  workspaceMemberIdentityFromContext(context),
22
27
  );
23
28
  const emailReadiness = await getEmailReadiness();
24
- const row = (
25
- await getDb()
26
- .select()
27
- .from(triageConfig)
28
- .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
29
- .limit(1)
30
- )[0];
29
+ const row = await readTriageConfigRow(getDb(), orgId, factoryId);
31
30
  if (!row) {
32
31
  return {
32
+ factoryId,
33
33
  slackWorkspace: "primary",
34
34
  slackChannelId: null,
35
35
  slackChannelName: null,
@@ -51,6 +51,7 @@ export default defineAction({
51
51
  }
52
52
  return {
53
53
  ...row,
54
+ factoryId,
54
55
  pollingEnabled: row.pollingEnabled === 1,
55
56
  githubPollingEnabled: row.githubPollingEnabled === 1,
56
57
  sentryPollingEnabled: row.sentryPollingEnabled === 1,