@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
@@ -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,
@@ -4,6 +4,11 @@ import { z } from "zod";
4
4
 
5
5
  import { getDb } from "../server/db/index.js";
6
6
  import { triageItems, triageRuns } from "../server/db/schema.js";
7
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
8
+ import {
9
+ factoryIdSchema,
10
+ orgFactoryScopedItemWhere,
11
+ } from "../server/lib/factory-scope.js";
7
12
  import {
8
13
  requireWorkspaceMember,
9
14
  workspaceMemberIdentityFromContext,
@@ -48,6 +53,7 @@ export default defineAction({
48
53
  "Reconcile an observe-only pull-request monitoring run from ai-services callback and provider observations. Missing callbacks or provider reads remain typed failure states; no executor or GitHub write occurs.",
49
54
  schema: z.object({
50
55
  itemId: z.string().min(1),
56
+ factoryId: factoryIdSchema.optional(),
51
57
  runId: z.string().min(1),
52
58
  source: triageSourceSchema.default("github"),
53
59
  callback: z
@@ -70,6 +76,7 @@ export default defineAction({
70
76
  run: async (
71
77
  {
72
78
  itemId,
79
+ factoryId: factoryIdInput,
73
80
  runId,
74
81
  source,
75
82
  callback,
@@ -82,8 +89,25 @@ export default defineAction({
82
89
  const { userEmail, orgId } = await requireWorkspaceMember(
83
90
  workspaceMemberIdentityFromContext(context),
84
91
  );
92
+ const db = getDb();
93
+ const item = (
94
+ await db
95
+ .select({ factoryId: triageItems.factoryId })
96
+ .from(triageItems)
97
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
98
+ .limit(1)
99
+ )[0];
100
+ if (!item)
101
+ throw new Error("Factory item not found for run reconciliation.");
102
+ const itemFactoryId = item.factoryId ?? DEFAULT_FACTORY_ID;
103
+ if (factoryIdInput && factoryIdInput !== itemFactoryId) {
104
+ throw new Error("Factory item does not belong to this factory.");
105
+ }
106
+ const factoryId = itemFactoryId;
85
107
  const now = new Date().toISOString();
86
- const databaseRunId = stableId("run", orgId, runId);
108
+ const scopedRunId = stableId("run", orgId, factoryId, runId);
109
+ const legacyRunId =
110
+ factoryId === DEFAULT_FACTORY_ID ? stableId("run", orgId, runId) : null;
87
111
  const result = reconcilePullRequestRun({
88
112
  triageItemId: itemId,
89
113
  runId,
@@ -93,17 +117,42 @@ export default defineAction({
93
117
  now,
94
118
  timeoutMs,
95
119
  });
96
- const db = getDb();
97
120
  const existingRun = (
98
121
  await db
99
- .select({ progressLogJson: triageRuns.progressLogJson })
122
+ .select({
123
+ id: triageRuns.id,
124
+ progressLogJson: triageRuns.progressLogJson,
125
+ factoryId: triageRuns.factoryId,
126
+ })
100
127
  .from(triageRuns)
101
- .where(
102
- and(eq(triageRuns.id, databaseRunId), eq(triageRuns.orgId, orgId)),
103
- )
128
+ .where(and(eq(triageRuns.id, scopedRunId), eq(triageRuns.orgId, orgId)))
104
129
  .limit(1)
105
130
  )[0];
106
- const progressLog = appendProgressLog(existingRun?.progressLogJson, {
131
+ let databaseRunId = scopedRunId;
132
+ let existing = existingRun;
133
+ if (!existing && legacyRunId) {
134
+ const legacyRun = (
135
+ await db
136
+ .select({
137
+ id: triageRuns.id,
138
+ progressLogJson: triageRuns.progressLogJson,
139
+ factoryId: triageRuns.factoryId,
140
+ })
141
+ .from(triageRuns)
142
+ .where(
143
+ and(eq(triageRuns.id, legacyRunId), eq(triageRuns.orgId, orgId)),
144
+ )
145
+ .limit(1)
146
+ )[0];
147
+ if (legacyRun) {
148
+ existing = legacyRun;
149
+ databaseRunId = legacyRunId;
150
+ }
151
+ }
152
+ if (existing && (existing.factoryId ?? DEFAULT_FACTORY_ID) !== factoryId) {
153
+ throw new Error("Run id is already used by another factory.");
154
+ }
155
+ const progressLog = appendProgressLog(existing?.progressLogJson, {
107
156
  at: now,
108
157
  state: result.state,
109
158
  reason: result.reason,
@@ -131,6 +180,7 @@ export default defineAction({
131
180
  : null,
132
181
  ownerEmail: userEmail,
133
182
  orgId,
183
+ factoryId,
134
184
  })
135
185
  .onConflictDoUpdate({
136
186
  target: triageRuns.id,
@@ -154,12 +204,12 @@ export default defineAction({
154
204
  await tx
155
205
  .update(triageItems)
156
206
  .set(result.triageItemPatch)
157
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
207
+ .where(orgFactoryScopedItemWhere(itemId, orgId, factoryId));
158
208
  } else if (result.state === "reconciliation_required") {
159
209
  await tx
160
210
  .update(triageItems)
161
211
  .set({ status: "reconciliation_required", updatedAt: now })
162
- .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
212
+ .where(orgFactoryScopedItemWhere(itemId, orgId, factoryId));
163
213
  }
164
214
  });
165
215
 
@@ -6,6 +6,11 @@ import { z } from "zod";
6
6
 
7
7
  import { getDb } from "../server/db/index.js";
8
8
  import { triageDecisions, triageFeedback } from "../server/db/schema.js";
9
+ import { DEFAULT_FACTORY_ID } from "../server/factory-graph/store.js";
10
+ import {
11
+ factoryIdSchema,
12
+ orgFactoryDecisionFilter,
13
+ } from "../server/lib/factory-scope.js";
9
14
  import {
10
15
  requireWorkspaceMember,
11
16
  workspaceMemberIdentityFromContext,
@@ -18,27 +23,32 @@ export default defineAction({
18
23
  "Record human feedback on a Factory shadow decision. Feedback is append-only and changes no provider or executor state.",
19
24
  schema: z.object({
20
25
  decisionId: z.string().min(1),
26
+ factoryId: factoryIdSchema.default(DEFAULT_FACTORY_ID),
21
27
  verdict: verdictSchema,
22
28
  note: z.string().trim().max(2_000).optional(),
23
29
  }),
24
30
  http: { method: "POST" },
25
- run: async ({ decisionId, verdict, note }, context) => {
31
+ run: async ({ decisionId, factoryId, verdict, note }, context) => {
26
32
  const { userEmail, orgId } = await requireWorkspaceMember(
27
33
  workspaceMemberIdentityFromContext(context),
28
34
  );
29
35
  const decision = (
30
36
  await getDb()
31
- .select({ id: triageDecisions.id })
37
+ .select({
38
+ id: triageDecisions.id,
39
+ factoryId: triageDecisions.factoryId,
40
+ })
32
41
  .from(triageDecisions)
33
42
  .where(
34
43
  and(
35
44
  eq(triageDecisions.id, decisionId),
36
- eq(triageDecisions.orgId, orgId),
45
+ orgFactoryDecisionFilter(orgId, factoryId),
37
46
  ),
38
47
  )
39
48
  .limit(1)
40
49
  )[0];
41
50
  if (!decision) throw new Error("Triage decision not found");
51
+ const resolvedFactoryId = decision.factoryId ?? factoryId;
42
52
 
43
53
  const createdAt = new Date().toISOString();
44
54
  const id = randomUUID();
@@ -52,6 +62,7 @@ export default defineAction({
52
62
  createdAt,
53
63
  ownerEmail: userEmail,
54
64
  orgId,
65
+ factoryId: resolvedFactoryId,
55
66
  });
56
67
  return { ok: true, id };
57
68
  },
@@ -5,6 +5,10 @@ import {
5
5
  } from "@agent-native/core/triggers";
6
6
  import { z } from "zod";
7
7
 
8
+ import {
9
+ factoryIdSchema,
10
+ readAutomationFactoryId,
11
+ } from "../server/lib/factory-scope.js";
8
12
  import {
9
13
  requireWorkspaceMember,
10
14
  workspaceMemberIdentityFromContext,
@@ -15,11 +19,11 @@ export default defineAction({
15
19
  "Queue one organization-scoped Factory automation for an immediate run and return its durable run id.",
16
20
  agentTool: false,
17
21
  schema: z.object({
18
- factoryId: z.string().trim().min(1).optional(),
22
+ factoryId: factoryIdSchema,
19
23
  automationId: z.string().trim().min(1),
20
24
  }),
21
25
  http: { method: "POST" },
22
- run: async ({ automationId }, context) => {
26
+ run: async ({ factoryId, automationId }, context) => {
23
27
  const { userEmail, orgId } = await requireWorkspaceMember(
24
28
  workspaceMemberIdentityFromContext(context),
25
29
  );
@@ -32,6 +36,12 @@ export default defineAction({
32
36
  entry.meta.domain === "factory" && entry.resource.id === automationId,
33
37
  );
34
38
  if (!definition) throw new Error("Factory automation not found.");
39
+ if (
40
+ readAutomationFactoryId(definition.meta, definition.resource.content) !==
41
+ factoryId
42
+ ) {
43
+ throw new Error("Factory automation not found.");
44
+ }
35
45
  return queueAutomationRunNow({
36
46
  userEmail,
37
47
  orgId,
@@ -1,10 +1,22 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
+ import { isValidCron, nextOccurrence } from "@agent-native/core/jobs";
2
3
  import {
3
- listAutomationDefinitions,
4
- updateAutomation,
5
- } from "@agent-native/core/triggers";
4
+ resourceGetByPath,
5
+ resourcePutIfCurrent,
6
+ } from "@agent-native/core/resources";
7
+ import { listAutomationDefinitions } from "@agent-native/core/triggers";
6
8
  import { z } from "zod";
7
9
 
10
+ import {
11
+ factoryIdSchema,
12
+ patchAutomationResource,
13
+ readAutomationEnabled,
14
+ readAutomationFactoryId,
15
+ readAutomationModel,
16
+ readAutomationSchedule,
17
+ resolveAutomationDisplayName,
18
+ setAutomationFrontmatterField,
19
+ } from "../server/lib/factory-scope.js";
8
20
  import {
9
21
  requireWorkspaceMember,
10
22
  workspaceMemberIdentityFromContext,
@@ -12,12 +24,13 @@ import {
12
24
 
13
25
  export default defineAction({
14
26
  description:
15
- "Edit a Factory automation's prompt, model, schedule, or enabled state in its organization-owned markdown resource.",
27
+ "Edit a Factory automation's display name, prompt, model, schedule, or enabled state in its organization-owned markdown resource.",
16
28
  agentTool: false,
17
29
  schema: z.object({
18
- factoryId: z.string().trim().min(1).optional(),
30
+ factoryId: factoryIdSchema,
19
31
  automationId: z.string().trim().min(1),
20
32
  name: z.string().trim().min(1).max(120),
33
+ displayName: z.string().trim().max(120).optional(),
21
34
  prompt: z.string().trim().min(1).max(20_000),
22
35
  model: z.string().trim().max(200).optional(),
23
36
  schedule: z.string().trim().min(1).max(100),
@@ -25,7 +38,16 @@ export default defineAction({
25
38
  }),
26
39
  http: { method: "POST" },
27
40
  run: async (
28
- { automationId, name, prompt, model, schedule, enabled },
41
+ {
42
+ factoryId,
43
+ automationId,
44
+ name,
45
+ displayName,
46
+ prompt,
47
+ model,
48
+ schedule,
49
+ enabled,
50
+ },
29
51
  context,
30
52
  ) => {
31
53
  const { userEmail, orgId } = await requireWorkspaceMember(
@@ -40,30 +62,69 @@ export default defineAction({
40
62
  entry.meta.domain === "factory" && entry.resource.id === automationId,
41
63
  );
42
64
  if (!definition) throw new Error("Factory automation not found.");
65
+ if (
66
+ readAutomationFactoryId(definition.meta, definition.resource.content) !==
67
+ factoryId
68
+ ) {
69
+ throw new Error("Factory automation not found.");
70
+ }
43
71
  if (definition.name !== name) {
44
72
  throw new Error(
45
73
  "Factory automation id and name do not refer to the same automation.",
46
74
  );
47
75
  }
48
- const updated = await updateAutomation(
49
- { userEmail, orgId, appId: "factory" },
50
- {
51
- name: definition.name,
52
- scope: "organization",
53
- body: prompt,
54
- model: model?.trim() || null,
55
- schedule,
56
- enabled,
57
- },
76
+ if (!definition.canUpdate) {
77
+ throw new Error(
78
+ "Only the automation's creator or an organization admin can update it.",
79
+ );
80
+ }
81
+ if (definition.meta.triggerType === "schedule" && !isValidCron(schedule)) {
82
+ throw new Error(`Invalid cron expression "${schedule}".`);
83
+ }
84
+ const resource = await resourceGetByPath(
85
+ definition.resource.owner,
86
+ definition.resource.path,
58
87
  );
88
+ if (!resource) throw new Error("Factory automation not found.");
89
+ let content = patchAutomationResource(resource.content, {
90
+ body: prompt,
91
+ enabled,
92
+ schedule,
93
+ model: model?.trim() || null,
94
+ ...(displayName !== undefined ? { displayName } : {}),
95
+ });
96
+ content = setAutomationFrontmatterField(content, "factoryId", factoryId);
97
+ if (definition.meta.triggerType === "schedule" && isValidCron(schedule)) {
98
+ const nextRun = nextOccurrence(
99
+ schedule,
100
+ undefined,
101
+ definition.meta.timezone,
102
+ ).toISOString();
103
+ content = setAutomationFrontmatterField(content, "nextRun", nextRun);
104
+ }
105
+ const updated = await resourcePutIfCurrent({
106
+ owner: definition.resource.owner,
107
+ path: definition.resource.path,
108
+ content,
109
+ mimeType: "text/markdown",
110
+ expectedId: resource.id,
111
+ expectedUpdatedAt: resource.updatedAt,
112
+ expectedContent: resource.content,
113
+ });
114
+ if (!updated) {
115
+ throw new Error(
116
+ "Factory automation changed concurrently. Refresh and try again.",
117
+ );
118
+ }
59
119
  return {
60
120
  ok: true,
61
121
  id: definition.resource.id,
62
- name: updated.name,
63
- prompt: updated.body,
64
- model: updated.meta.model ?? null,
65
- schedule: updated.meta.schedule || null,
66
- enabled: updated.meta.enabled,
122
+ name: definition.name,
123
+ displayName: resolveAutomationDisplayName(definition.name, content),
124
+ prompt,
125
+ model: readAutomationModel(content),
126
+ schedule: readAutomationSchedule(content),
127
+ enabled: readAutomationEnabled(content),
67
128
  };
68
129
  },
69
130
  });
@@ -17,6 +17,7 @@ import {
17
17
  requireWorkspaceMember,
18
18
  workspaceMemberIdentityFromContext,
19
19
  } from "../server/lib/require-workspace-member.js";
20
+ import { ensureFactoryAutomations } from "../server/plugins/factory-scheduler-job.js";
20
21
  import { stableId } from "../server/triage/ids.js";
21
22
 
22
23
  export default defineAction({
@@ -63,8 +64,9 @@ export default defineAction({
63
64
  workspaceMemberIdentityFromContext(context),
64
65
  );
65
66
  const db = getDb();
67
+ let createdNewFactory = false;
66
68
 
67
- return db.transaction(async (tx) => {
69
+ const result = await db.transaction(async (tx) => {
68
70
  const existing = (
69
71
  await tx
70
72
  .select()
@@ -123,6 +125,7 @@ export default defineAction({
123
125
  );
124
126
  }
125
127
  } else {
128
+ createdNewFactory = true;
126
129
  await tx.insert(factoryDefinitions).values({
127
130
  id: factoryId,
128
131
  name,
@@ -158,5 +161,11 @@ export default defineAction({
158
161
  source,
159
162
  };
160
163
  });
164
+ if (createdNewFactory) {
165
+ await ensureFactoryAutomations(userEmail, orgId, factoryId, {
166
+ enabled: false,
167
+ });
168
+ }
169
+ return result;
161
170
  },
162
171
  });