@agent-native/core 0.168.6 → 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 (87) 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/list-recordings.ts +14 -0
  11. package/corpus/templates/clips/actions/sync-calendars.ts +10 -29
  12. package/corpus/templates/clips/app/components/library/recording-card.tsx +2 -2
  13. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +20 -3
  14. package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
  15. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +12 -5
  16. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +28 -12
  17. package/corpus/templates/clips/desktop/src/lib/meeting-stop.ts +16 -0
  18. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +19 -9
  19. package/corpus/templates/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
  20. package/corpus/templates/factory/AGENTS.md +12 -22
  21. package/corpus/templates/factory/README.md +11 -8
  22. package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
  23. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
  24. package/corpus/templates/factory/actions/create-factory.ts +211 -0
  25. package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
  26. package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
  27. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
  28. package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
  29. package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
  30. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
  31. package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
  32. package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
  33. package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
  34. package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
  35. package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
  36. package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
  37. package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
  38. package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
  39. package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
  40. package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
  41. package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
  42. package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
  43. package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
  44. package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
  45. package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
  46. package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
  47. package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
  48. package/corpus/templates/factory/actions/view-screen.ts +22 -30
  49. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
  50. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
  51. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
  52. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  53. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
  54. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
  55. package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
  56. package/corpus/templates/factory/app/routes/factory.tsx +78 -62
  57. package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
  58. package/corpus/templates/factory/server/db/schema.ts +18 -3
  59. package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
  60. package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
  61. package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
  62. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
  63. package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
  64. package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
  65. package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
  66. package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
  67. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
  68. package/corpus/templates/factory/server/triage/audit.ts +4 -0
  69. package/corpus/templates/factory/server/triage/ids.ts +4 -0
  70. package/dist/cli/native-dependencies.js +4 -1
  71. package/dist/client/AgentPanel.d.ts +5 -1
  72. package/dist/client/AgentPanel.js +9 -4
  73. package/dist/client/agent-chat/index.d.ts +1 -1
  74. package/dist/client/agent-chat/index.js +1 -1
  75. package/dist/client/chat-first/apps-rail.js +6 -6
  76. package/dist/client/chat-first/primary-nav.js +14 -6
  77. package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
  78. package/dist/client/integrations/IntegrationsPanel.js +29 -21
  79. package/dist/client/integrations/index.d.ts +1 -1
  80. package/dist/client/integrations/index.js +1 -1
  81. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  82. package/dist/collab/routes.d.ts +1 -1
  83. package/dist/notifications/routes.d.ts +1 -1
  84. package/dist/progress/routes.d.ts +1 -1
  85. package/dist/resources/handlers.d.ts +3 -3
  86. package/dist/server/ssr-handler.js +19 -1
  87. package/package.json +1 -1
@@ -23,6 +23,8 @@ import {
23
23
 
24
24
  export const CALENDAR_MEETING_ID_PREFIX = "gcal";
25
25
 
26
+ const CALENDAR_REQUEST_SAFETY_MARGIN_MS = 60 * 1000;
27
+
26
28
  export interface CalendarAccountForEvents {
27
29
  id: string;
28
30
  provider: string;
@@ -74,8 +76,7 @@ export function shouldMarkNeedsReauth(message: string): boolean {
74
76
  lower.includes("google calendar event failed (401)") ||
75
77
  lower.includes("invalid_grant") ||
76
78
  lower.includes("invalid_token") ||
77
- lower.includes("insufficient_scope") ||
78
- lower.includes("token refresh failed")
79
+ lower.includes("insufficient_scope")
79
80
  );
80
81
  }
81
82
 
@@ -122,10 +123,16 @@ export async function resolveCalendarAccessToken(
122
123
  });
123
124
  } catch (err) {
124
125
  // Only a permanent failure (dead refresh token / bad OAuth client) means
125
- // "needs-reauth" — collapse those to `null` as before. A transient
126
- // failure (network error, 429, 5xx, timeout) is rethrown so callers can
127
- // record it as a soft sync error without flipping account status.
126
+ // "needs-reauth" — collapse those to `null` as before. During the refresh
127
+ // buffer, a transient failure can safely reuse the still-valid token.
128
128
  if (isPermanentRefreshFailure(err)) return null;
129
+ if (
130
+ bundle?.accessToken &&
131
+ bundle.expiresAt &&
132
+ bundle.expiresAt > Date.now() + CALENDAR_REQUEST_SAFETY_MARGIN_MS
133
+ ) {
134
+ return bundle.accessToken;
135
+ }
129
136
  throw err;
130
137
  }
131
138
  if (!refreshed.access_token) return null;
@@ -158,13 +165,13 @@ export async function recordCalendarFetchSuccess(
158
165
  .set({
159
166
  lastSyncedAt: now,
160
167
  lastSyncError: null,
161
- status: "connected",
162
168
  updatedAt: now,
163
169
  })
164
170
  .where(
165
171
  and(
166
172
  eq(schema.calendarAccounts.id, account.id),
167
173
  eq(schema.calendarAccounts.ownerEmail, account.ownerEmail),
174
+ eq(schema.calendarAccounts.status, "connected"),
168
175
  ),
169
176
  );
170
177
  }
@@ -172,10 +179,11 @@ export async function recordCalendarFetchSuccess(
172
179
  export async function recordCalendarFetchError(
173
180
  account: CalendarAccountForEvents,
174
181
  error: unknown,
182
+ options: { needsReauth?: boolean } = {},
175
183
  ): Promise<CalendarFetchError> {
176
184
  const message =
177
185
  error instanceof Error ? error.message : String(error || "Calendar failed");
178
- const needsReauth = shouldMarkNeedsReauth(message);
186
+ const needsReauth = options.needsReauth ?? shouldMarkNeedsReauth(message);
179
187
  if (!account.ownerEmail) {
180
188
  return { accountId: account.id, error: message, needsReauth };
181
189
  }
@@ -183,7 +191,7 @@ export async function recordCalendarFetchError(
183
191
  await db
184
192
  .update(schema.calendarAccounts)
185
193
  .set({
186
- status: needsReauth ? "needs-reauth" : "connected",
194
+ ...(needsReauth ? { status: "needs-reauth" as const } : {}),
187
195
  lastSyncError: needsReauth
188
196
  ? "Google Calendar needs to be reconnected."
189
197
  : message,
@@ -320,7 +328,9 @@ export async function fetchLiveCalendarEventFromId(virtualId: string) {
320
328
  return null;
321
329
  }
322
330
  if (!accessToken) {
323
- await recordCalendarFetchError(account, new Error("Token refresh failed"));
331
+ await recordCalendarFetchError(account, new Error("Token refresh failed"), {
332
+ needsReauth: true,
333
+ });
324
334
  return null;
325
335
  }
326
336
 
@@ -16,6 +16,11 @@ explains them, but never hides a topology change in prose. Until an execution
16
16
  binding is explicitly added, the blueprint must not be described as the runtime
17
17
  router.
18
18
 
19
+ Runtime work — settings, inbox, rules, automations, and activity — is scoped by
20
+ `factoryId`. Reusable agents stay workspace-wide; graph nodes may reference any
21
+ workspace agent as blueprint only. Pass `factoryId` on triage, config, automation,
22
+ and audit actions for the factory the user is viewing.
23
+
19
24
  ## Workflow
20
25
 
21
26
  1. Read `view-screen` to identify the selected Factory, tab, node, or edge.
@@ -21,22 +21,11 @@ decisions, feedback, agent runs, and provider audit records.
21
21
  reconciliation is not success; preserve typed failure or
22
22
  `reconciliation_required` state.
23
23
  - Deduplicate by Factory item and rule/run identity, not provider comment ID.
24
- - Slack clear bugs go through `start-builder-for-item` (👀 and a Slack user
25
- mention for `/address-feedback`); never post Slack messages or `@handles`.
26
- Group repeats into one Builder thread. GitHub/Sentry use the Builder run
27
- API. Follow `review-latest-feedback` for full-thread evidence, existing
28
- reaction and owner checks, answered-clarification priority, one-question
29
- clarification limits, durable rechecks, and the required `@agent-native`
30
- disposition after every actionable 👀. Clips, Design, and Content stay
31
- owner-managed outside autonomous dispatch and PR governance.
32
- - PR governance follows `review-prs`: verify BuilderIO membership and complete
33
- diff/review/check evidence; skip drafts/current approvals and never approve
34
- external authors. Keep ultra-scary security, auth, tenant-isolation,
35
- secrets, data-loss, execution, payment, and deployment risks manual.
36
- Verified members may pass ordinary uncertain checks or review feedback, but
37
- record exact states and never call them clean. Apply the Alice/Content,
38
- Nick/Slides, Enzo/Factory, Sid/Design, and docs-only exceptions only after
39
- membership and the ultra-scary gate. Never auto-merge.
24
+ - Slack clear bugs go through `start-builder-for-item`; never post Slack
25
+ messages or `@handles`. GitHub/Sentry use the Builder run API. Read
26
+ `review-latest-feedback` for thread evidence and disposition rules.
27
+ - PR governance follows `review-prs`: verify membership and evidence; skip
28
+ drafts and external authors; keep ultra-scary risks manual; never auto-merge.
40
29
  - Graph edits create immutable blueprint versions. AI proposes with `source=ai`;
41
30
  a person reviews and publishes through the same action surface.
42
31
  - Provider credentials belong to Dispatch/shared workspace integrations, never
@@ -46,16 +35,16 @@ decisions, feedback, agent runs, and provider audit records.
46
35
 
47
36
  ## Application state
48
37
 
49
- - `navigation.view` is `factory`, `agents`, or `factory-settings`.
50
- Observation settings are workspace-scoped at `/factory-settings`.
51
- `factoryId`, `factoryTab`, `factoryAuditRunId`, `factoryNodeId`, and
52
- `factoryEdgeId` hold Factory context. Read `view-screen` first.
38
+ - `navigation.view` is `factory` or `agents`. Runtime data is scoped by
39
+ `factoryId`; reusable agents stay workspace-wide. Read `view-screen` and
40
+ `factory-graphs` for tab and selection keys.
53
41
 
54
42
  ## Action contract
55
43
 
56
44
  | Action | Purpose |
57
45
  | --- | --- |
58
- | `list-triage-items` / `get-triage-item` | Inspect bounded queue evidence; scheduled reviewers pass `needsReview: true`, `source`, and `limit`. |
46
+ | `list-triage-items` / `get-triage-item` | Inspect queue evidence; pass `factoryId`. |
47
+ | `get-triage-config` / `save-triage-config` | Read or save observation settings for one factory. |
59
48
  | `poll-slack-channel` | Observe Slack history; never writes to Slack. |
60
49
  | `get-slack-feedback-context` | Read the bounded full Slack thread before classification. |
61
50
  | `poll-github-sources` / `poll-sentry-errors` | Observe bounded source queues. |
@@ -67,11 +56,12 @@ decisions, feedback, agent runs, and provider audit records.
67
56
  | `start-builder-for-item` | Govern clear-bug dispatch through Slack or Builder API, or record a skip reason. |
68
57
  | `govern-agent-native-pull-request` | Apply PR evidence and ownership gates. |
69
58
  | `list-factory-automations` / `save-factory-automation` / `run-factory-automation` | Inspect or edit org-owned automations. |
70
- | `list-factory-audit` | Inspect automation runs, evidence, decisions, and provider actions. |
59
+ | `list-factory-audit` | Inspect runs, evidence, and provider actions for one factory. |
71
60
  | `get-factory-automation-health` | Inspect scheduler heartbeat and last error. |
72
61
  | `suggest-factory-rules` | Mine feedback into proposals. |
73
62
  | `reconcile-triage-run` | Persist callback/provider reconciliation. |
74
63
  | `list-factories` / `get-factory-graph` | Inspect definitions, versions, and metrics. |
64
+ | `create-factory` | Create a factory from `/new-factory` with optional sources. |
75
65
  | `save-factory-graph` | Create/version a graph with inspected `expectedGraphVersion`; never starts provider work. |
76
66
  | graph history actions | Factory graph version history. |
77
67
  | `list-factory-comments` / `add-factory-comment` | Read or attach comments to a canvas, node, or edge. |
@@ -44,19 +44,22 @@ shared `app_secrets` rows must use the same
44
44
  encryption fallback). Never copy raw tokens between apps or add a second
45
45
  env-only read in a provider client.
46
46
 
47
- Factory's default observer still keeps organization-scoped source metadata -
48
- such as a Slack channel, repository, or Sentry project - for its normalized
49
- queue adapters. That metadata is not a credential or a per-factory integration
50
- setup. GitHub issue polling defaults to hourly; Slack and pull-request checks
51
- use shorter bounded cadences because those sources can need faster feedback.
52
- Every poll preserves errors for reconciliation.
47
+ Factory's observer keeps **per-factory** source metadata — Slack channel,
48
+ repository, Sentry project, and related polling settings for its normalized
49
+ queue adapters. Each factory has its own inbox, rules, automations, and activity.
50
+ Reusable agents and workspace credentials stay shared. Each factory's observation
51
+ settings live on its Settings tab in the factory detail view. That metadata is
52
+ not a credential or a per-factory
53
+ integration setup. GitHub issue polling defaults to hourly; Slack and
54
+ pull-request checks use shorter bounded cadences because those sources can need
55
+ faster feedback. Every poll preserves errors for reconciliation.
53
56
 
54
57
  Factory agents can discover connected provider APIs with
55
58
  `provider-api-catalog`, inspect their docs with `provider-api-docs`, and call
56
59
  them through `provider-api-request`. Scheduled Factory runs also receive the
57
60
  workspace's connected MCP tools, subject to the same workspace and request
58
- scope gates. The three normalized pollers are compatibility adapters for the
59
- default triage queue, not the agent's capability limit.
61
+ scope gates. The three normalized pollers are compatibility adapters scoped by
62
+ `factoryId`, not the agent's capability limit.
60
63
 
61
64
  The generic Slack bot is wired to Factory. Mention `@agent-native` in a feedback
62
65
  thread to inspect the linked item, explain its decision, tune a rule, or say
@@ -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
+ });