@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
@@ -0,0 +1,136 @@
1
+ export const PR_AUTOMATIONS = [
2
+ "factory-pr-governance",
3
+ "factory-pr-babysit",
4
+ ] as const;
5
+
6
+ export function resolveEnabledAutomations(input: {
7
+ observeSlack: boolean;
8
+ slackChannelId?: string;
9
+ observeGithub: boolean;
10
+ repository?: string;
11
+ observeSentry: boolean;
12
+ sentryOrgSlug?: string;
13
+ sentryProjectSlug?: string;
14
+ }): {
15
+ enabledNames: Set<string>;
16
+ pollingEnabled: boolean;
17
+ githubPollingEnabled: boolean;
18
+ sentryPollingEnabled: boolean;
19
+ hasConfig: boolean;
20
+ } {
21
+ const enabledNames = new Set<string>();
22
+ let pollingEnabled = false;
23
+ let githubPollingEnabled = false;
24
+ let sentryPollingEnabled = false;
25
+ let hasConfig = false;
26
+
27
+ if (input.observeSlack) {
28
+ if (!input.slackChannelId?.trim()) {
29
+ throw new Error(
30
+ "Configure a Slack channel before enabling Slack observation.",
31
+ );
32
+ }
33
+ enabledNames.add("factory-slack-feedback");
34
+ pollingEnabled = true;
35
+ hasConfig = true;
36
+ }
37
+
38
+ if (input.observeGithub) {
39
+ if (!input.repository?.trim()) {
40
+ throw new Error(
41
+ "Configure a GitHub repository before enabling GitHub observation.",
42
+ );
43
+ }
44
+ enabledNames.add("factory-github-issues");
45
+ githubPollingEnabled = true;
46
+ hasConfig = true;
47
+ }
48
+
49
+ if (input.repository?.trim()) {
50
+ for (const name of PR_AUTOMATIONS) enabledNames.add(name);
51
+ hasConfig = true;
52
+ }
53
+
54
+ if (input.observeSentry) {
55
+ if (!input.sentryOrgSlug?.trim() || !input.sentryProjectSlug?.trim()) {
56
+ throw new Error(
57
+ "Configure Sentry organization and project slugs before enabling Sentry observation.",
58
+ );
59
+ }
60
+ enabledNames.add("factory-sentry-errors");
61
+ sentryPollingEnabled = true;
62
+ hasConfig = true;
63
+ }
64
+
65
+ return {
66
+ enabledNames,
67
+ pollingEnabled,
68
+ githubPollingEnabled,
69
+ sentryPollingEnabled,
70
+ hasConfig,
71
+ };
72
+ }
73
+
74
+ export function resolveEnabledAutomationsFromSavedConfig(config: {
75
+ pollingEnabled: boolean | number;
76
+ githubPollingEnabled: boolean | number;
77
+ sentryPollingEnabled: boolean | number;
78
+ slackChannelId?: string | null;
79
+ repository?: string | null;
80
+ sentryOrgSlug?: string | null;
81
+ sentryProjectSlug?: string | null;
82
+ }): Set<string> {
83
+ const enabledNames = new Set<string>();
84
+ const pollingEnabled =
85
+ config.pollingEnabled === true || config.pollingEnabled === 1;
86
+ const githubPollingEnabled =
87
+ config.githubPollingEnabled === true || config.githubPollingEnabled === 1;
88
+ const sentryPollingEnabled =
89
+ config.sentryPollingEnabled === true || config.sentryPollingEnabled === 1;
90
+
91
+ if (pollingEnabled && config.slackChannelId?.trim()) {
92
+ enabledNames.add("factory-slack-feedback");
93
+ }
94
+ if (githubPollingEnabled && config.repository?.trim()) {
95
+ enabledNames.add("factory-github-issues");
96
+ }
97
+ if (config.repository?.trim()) {
98
+ for (const name of PR_AUTOMATIONS) enabledNames.add(name);
99
+ }
100
+ if (
101
+ sentryPollingEnabled &&
102
+ config.sentryOrgSlug?.trim() &&
103
+ config.sentryProjectSlug?.trim()
104
+ ) {
105
+ enabledNames.add("factory-sentry-errors");
106
+ }
107
+ return enabledNames;
108
+ }
109
+
110
+ export function isFactoryIdConflict(error: unknown): boolean {
111
+ const message = String(
112
+ (error as { message?: unknown } | null)?.message ?? "",
113
+ ).toLowerCase();
114
+ const code = String((error as { code?: unknown } | null)?.code ?? "");
115
+ return (
116
+ code === "23505" ||
117
+ code === "SQLITE_CONSTRAINT_PRIMARYKEY" ||
118
+ code === "SQLITE_CONSTRAINT_UNIQUE" ||
119
+ message.includes("unique constraint") ||
120
+ message.includes("duplicate key") ||
121
+ message.includes("primary key")
122
+ );
123
+ }
124
+
125
+ export function isFactorySlackChannelConflict(error: unknown): boolean {
126
+ const message = String(
127
+ (error as { message?: unknown } | null)?.message ?? "",
128
+ ).toLowerCase();
129
+ const code = String((error as { code?: unknown } | null)?.code ?? "");
130
+ const unique =
131
+ code === "23505" ||
132
+ code === "SQLITE_CONSTRAINT_UNIQUE" ||
133
+ message.includes("unique constraint") ||
134
+ message.includes("duplicate key");
135
+ return unique && message.includes("slack_channel");
136
+ }
@@ -0,0 +1,31 @@
1
+ import { getDb } from "../db/index.js";
2
+ import {
3
+ ensureFactoryAutomations,
4
+ syncFactoryAutomationEnabledStates,
5
+ } from "../plugins/factory-scheduler-job.js";
6
+ import { resolveEnabledAutomationsFromSavedConfig } from "./factory-automation-plan.js";
7
+ import { readTriageConfigRow } from "./factory-scope.js";
8
+
9
+ export async function repairFactoryAutomationsFromConfig(
10
+ ownerEmail: string,
11
+ orgId: string,
12
+ factoryId: string,
13
+ ): Promise<void> {
14
+ const config = await readTriageConfigRow(getDb(), orgId, factoryId);
15
+ if (!config) return;
16
+ const enabledNames = resolveEnabledAutomationsFromSavedConfig({
17
+ pollingEnabled: config.pollingEnabled,
18
+ githubPollingEnabled: config.githubPollingEnabled,
19
+ sentryPollingEnabled: config.sentryPollingEnabled,
20
+ slackChannelId: config.slackChannelId,
21
+ repository: config.repository,
22
+ sentryOrgSlug: config.sentryOrgSlug,
23
+ sentryProjectSlug: config.sentryProjectSlug,
24
+ });
25
+ await ensureFactoryAutomations(ownerEmail, orgId, factoryId, {
26
+ enabledNames,
27
+ });
28
+ await syncFactoryAutomationEnabledStates(ownerEmail, orgId, factoryId, [
29
+ ...enabledNames,
30
+ ]);
31
+ }
@@ -0,0 +1,416 @@
1
+ import { and, eq, type AnyColumn, type SQL } from "drizzle-orm";
2
+ import { z } from "zod";
3
+
4
+ import type { getDb } from "../db/index.js";
5
+ import {
6
+ factoryDefinitions,
7
+ factoryGraphVersions,
8
+ triageConfig,
9
+ triageDecisions,
10
+ triageFeedback,
11
+ triageItems,
12
+ triageRules,
13
+ triageRuns,
14
+ } from "../db/schema.js";
15
+ import { DEFAULT_FACTORY_ID } from "../factory-graph/store.js";
16
+
17
+ export { DEFAULT_FACTORY_ID };
18
+
19
+ export const factoryIdSchema = z
20
+ .string()
21
+ .trim()
22
+ .min(1)
23
+ .max(120)
24
+ .regex(/^[a-z0-9][a-z0-9-]*$/);
25
+
26
+ export function factoryConfigRowId(orgId: string, factoryId: string): string {
27
+ return `${orgId}:${factoryId}`;
28
+ }
29
+
30
+ const MAX_FACTORY_ID_LENGTH = 120;
31
+
32
+ export function slugifyFactoryId(name: string): string {
33
+ const normalized = name
34
+ .toLowerCase()
35
+ .trim()
36
+ .replace(/[^a-z0-9]+/g, "-")
37
+ .replace(/^-+|-+$/g, "")
38
+ .replace(/-+/g, "-")
39
+ .slice(0, 100);
40
+ const base =
41
+ normalized && /^[a-z0-9]/.test(normalized)
42
+ ? normalized
43
+ : `factory-${normalized || "untitled"}`;
44
+ return base.slice(0, MAX_FACTORY_ID_LENGTH);
45
+ }
46
+
47
+ export function factoryIdCandidateWithSuffix(
48
+ base: string,
49
+ suffix: number,
50
+ ): string {
51
+ const suffixText = `-${suffix}`;
52
+ const maxBaseLength = MAX_FACTORY_ID_LENGTH - suffixText.length;
53
+ return `${base.slice(0, maxBaseLength)}${suffixText}`;
54
+ }
55
+
56
+ export async function resolveUniqueFactoryId(
57
+ db: Db,
58
+ orgId: string,
59
+ name: string,
60
+ ): Promise<string> {
61
+ const base = slugifyFactoryId(name);
62
+ let candidate = base;
63
+ let suffix = 2;
64
+ while (await factoryIdTaken(db, orgId, candidate)) {
65
+ candidate = factoryIdCandidateWithSuffix(base, suffix);
66
+ suffix += 1;
67
+ }
68
+ return candidate;
69
+ }
70
+
71
+ async function factoryIdTaken(
72
+ db: Db,
73
+ orgId: string,
74
+ factoryId: string,
75
+ ): Promise<boolean> {
76
+ const definition = (
77
+ await db
78
+ .select({ id: factoryDefinitions.id })
79
+ .from(factoryDefinitions)
80
+ .where(eq(factoryDefinitions.id, factoryId))
81
+ .limit(1)
82
+ )[0];
83
+ if (definition) return true;
84
+
85
+ const config = (
86
+ await db
87
+ .select({ factoryId: triageConfig.factoryId })
88
+ .from(triageConfig)
89
+ .where(
90
+ and(
91
+ eq(triageConfig.orgId, orgId),
92
+ eq(triageConfig.factoryId, factoryId),
93
+ ),
94
+ )
95
+ .limit(1)
96
+ )[0];
97
+ if (config) return true;
98
+
99
+ const graphVersion = (
100
+ await db
101
+ .select({ factoryId: factoryGraphVersions.factoryId })
102
+ .from(factoryGraphVersions)
103
+ .where(
104
+ and(
105
+ eq(factoryGraphVersions.orgId, orgId),
106
+ eq(factoryGraphVersions.factoryId, factoryId),
107
+ ),
108
+ )
109
+ .limit(1)
110
+ )[0];
111
+ return Boolean(graphVersion);
112
+ }
113
+
114
+ export function legacyFactoryConfigRowId(orgId: string): string {
115
+ return orgId;
116
+ }
117
+
118
+ /** Row id to use when persisting poll cursors — follows the loaded config row. */
119
+ export function triageConfigUpdateRowId(
120
+ config: { id: string } | null | undefined,
121
+ orgId: string,
122
+ factoryId: string,
123
+ ): string {
124
+ return config?.id ?? factoryConfigRowId(orgId, factoryId);
125
+ }
126
+
127
+ type Db = ReturnType<typeof getDb>;
128
+
129
+ export async function readTriageConfigRow(
130
+ db: Db,
131
+ orgId: string,
132
+ factoryId: string,
133
+ ) {
134
+ const scopedId = factoryConfigRowId(orgId, factoryId);
135
+ const scoped = (
136
+ await db
137
+ .select()
138
+ .from(triageConfig)
139
+ .where(and(eq(triageConfig.id, scopedId), eq(triageConfig.orgId, orgId)))
140
+ .limit(1)
141
+ )[0];
142
+ if (scoped) return scoped;
143
+ if (factoryId !== DEFAULT_FACTORY_ID) return undefined;
144
+ return (
145
+ await db
146
+ .select()
147
+ .from(triageConfig)
148
+ .where(
149
+ and(
150
+ eq(triageConfig.id, legacyFactoryConfigRowId(orgId)),
151
+ eq(triageConfig.orgId, orgId),
152
+ ),
153
+ )
154
+ .limit(1)
155
+ )[0];
156
+ }
157
+
158
+ export function orgFactoryFilter(
159
+ table: { orgId: AnyColumn; factoryId: AnyColumn },
160
+ orgId: string,
161
+ factoryId: string,
162
+ ): SQL {
163
+ return and(eq(table.orgId, orgId), eq(table.factoryId, factoryId))!;
164
+ }
165
+
166
+ export function orgFactoryItemFilter(orgId: string, factoryId: string): SQL {
167
+ return orgFactoryFilter(triageItems, orgId, factoryId);
168
+ }
169
+
170
+ export function orgFactoryScopedItemWhere(
171
+ itemId: string,
172
+ orgId: string,
173
+ factoryId: string,
174
+ ): SQL {
175
+ return and(
176
+ eq(triageItems.id, itemId),
177
+ orgFactoryItemFilter(orgId, factoryId),
178
+ )!;
179
+ }
180
+
181
+ export function orgFactoryRuleFilter(orgId: string, factoryId: string): SQL {
182
+ return orgFactoryFilter(triageRules, orgId, factoryId);
183
+ }
184
+
185
+ export function orgFactoryDecisionFilter(
186
+ orgId: string,
187
+ factoryId: string,
188
+ ): SQL {
189
+ return orgFactoryFilter(triageDecisions, orgId, factoryId);
190
+ }
191
+
192
+ export function orgFactoryRunFilter(orgId: string, factoryId: string): SQL {
193
+ return orgFactoryFilter(triageRuns, orgId, factoryId);
194
+ }
195
+
196
+ export function orgFactoryFeedbackFilter(
197
+ orgId: string,
198
+ factoryId: string,
199
+ ): SQL {
200
+ return orgFactoryFilter(triageFeedback, orgId, factoryId);
201
+ }
202
+
203
+ export function resolveAutomationFactoryId(
204
+ meta: Record<string, unknown> | object | undefined,
205
+ ): string {
206
+ const raw =
207
+ meta && typeof meta === "object" && "factoryId" in meta
208
+ ? (meta as { factoryId?: unknown }).factoryId
209
+ : undefined;
210
+ return typeof raw === "string" && raw.trim()
211
+ ? raw.trim()
212
+ : DEFAULT_FACTORY_ID;
213
+ }
214
+
215
+ function readFrontmatterFactoryId(content: string): string | undefined {
216
+ if (!content.startsWith("---\n")) return undefined;
217
+ const end = content.indexOf("\n---", 4);
218
+ if (end === -1) return undefined;
219
+ const match = content.slice(4, end).match(/^factoryId:\s*(.*)$/m);
220
+ const value = match?.[1]?.trim();
221
+ if (!value) return undefined;
222
+ return value.replace(/^(\"|')|((\"|')$)/g, "");
223
+ }
224
+
225
+ export function readAutomationFactoryId(meta: object, content: string): string {
226
+ const fromContent = readFrontmatterFactoryId(content);
227
+ return resolveAutomationFactoryId(
228
+ fromContent ? { ...meta, factoryId: fromContent } : meta,
229
+ );
230
+ }
231
+
232
+ export function factoryAutomationJobPath(
233
+ factoryId: string,
234
+ automationName: string,
235
+ ): string {
236
+ if (factoryId === DEFAULT_FACTORY_ID) {
237
+ return `jobs/${automationName}.md`;
238
+ }
239
+ return `jobs/factories/${factoryId}/${automationName}.md`;
240
+ }
241
+
242
+ export function legacyFactoryAutomationJobPath(automationName: string): string {
243
+ return `jobs/${automationName}.md`;
244
+ }
245
+
246
+ export function isLegacyFactoryAutomationPath(path: string): boolean {
247
+ return /^jobs\/factory-[^/]+\.md$/.test(path);
248
+ }
249
+
250
+ export function readFactoryIdFromAutomationPath(path: string): string | null {
251
+ const match = path.match(/^jobs\/factories\/([^/]+)\/factory-[^/]+\.md$/);
252
+ return match?.[1] ?? null;
253
+ }
254
+
255
+ export async function findFactoryIdForSlackChannel(
256
+ db: Db,
257
+ orgId: string,
258
+ channelId: string,
259
+ ): Promise<string | null> {
260
+ const rows = await db
261
+ .select({
262
+ factoryId: triageConfig.factoryId,
263
+ slackChannelId: triageConfig.slackChannelId,
264
+ })
265
+ .from(triageConfig)
266
+ .where(
267
+ and(
268
+ eq(triageConfig.orgId, orgId),
269
+ eq(triageConfig.slackChannelId, channelId),
270
+ ),
271
+ );
272
+ const factoryIds = rows
273
+ .map((row) => row.factoryId)
274
+ .filter((value): value is string => Boolean(value?.trim()));
275
+ if (factoryIds.length === 1) return factoryIds[0]!;
276
+ return null;
277
+ }
278
+
279
+ export async function assertUniqueSlackChannelForFactory(
280
+ db: Db,
281
+ orgId: string,
282
+ factoryId: string,
283
+ channelId: string | null | undefined,
284
+ ): Promise<void> {
285
+ const normalized = channelId?.trim();
286
+ if (!normalized) return;
287
+ const conflict = (
288
+ await db
289
+ .select({ factoryId: triageConfig.factoryId })
290
+ .from(triageConfig)
291
+ .where(
292
+ and(
293
+ eq(triageConfig.orgId, orgId),
294
+ eq(triageConfig.slackChannelId, normalized),
295
+ ),
296
+ )
297
+ ).find((row) => row.factoryId && row.factoryId !== factoryId);
298
+ if (conflict) {
299
+ throw new Error(
300
+ "That Slack channel is already used by another Factory in this workspace.",
301
+ );
302
+ }
303
+ }
304
+
305
+ function readFrontmatterField(
306
+ content: string,
307
+ key: string,
308
+ ): string | undefined {
309
+ if (!content.startsWith("---\n")) return undefined;
310
+ const end = content.indexOf("\n---", 4);
311
+ if (end === -1) return undefined;
312
+ const match = content
313
+ .slice(4, end)
314
+ .match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
315
+ const value = match?.[1]?.trim();
316
+ if (!value) return undefined;
317
+ return value.replace(/^(\"|')|((\"|')$)/g, "");
318
+ }
319
+
320
+ export function readAutomationDisplayName(content: string): string | null {
321
+ const value = readFrontmatterField(content, "displayName")?.trim();
322
+ return value || null;
323
+ }
324
+
325
+ export function resolveAutomationDisplayName(
326
+ automationName: string,
327
+ content: string,
328
+ ): string {
329
+ return readAutomationDisplayName(content) ?? automationName;
330
+ }
331
+
332
+ export function setAutomationFrontmatterField(
333
+ content: string,
334
+ key: string,
335
+ value: string,
336
+ ): string {
337
+ if (!content.startsWith("---\n")) return content;
338
+ const end = content.indexOf("\n---", 4);
339
+ if (end === -1) return content;
340
+ const trimmed = value.trim();
341
+ const frontmatter = content.slice(4, end);
342
+ const pattern = new RegExp(`^${key}:.*\\n?`, "m");
343
+ if (!trimmed) {
344
+ if (!pattern.test(frontmatter)) return content;
345
+ const nextFrontmatter = frontmatter.replace(pattern, "").trimEnd();
346
+ return nextFrontmatter
347
+ ? `---\n${nextFrontmatter}${content.slice(end)}`
348
+ : content;
349
+ }
350
+ if (pattern.test(frontmatter)) {
351
+ return `---\n${frontmatter.replace(pattern, `${key}: ${trimmed}\n`)}${content.slice(end)}`;
352
+ }
353
+ return `${content.slice(0, end)}\n${key}: ${trimmed}${content.slice(end)}`;
354
+ }
355
+
356
+ export function replaceAutomationBody(content: string, body: string): string {
357
+ if (!content.startsWith("---\n")) return content;
358
+ const end = content.indexOf("\n---", 4);
359
+ if (end === -1) return content;
360
+ const trimmedBody = body.trim();
361
+ return `${content.slice(0, end + 4)}\n${trimmedBody ? `${trimmedBody}\n` : ""}`;
362
+ }
363
+
364
+ export function patchAutomationResource(
365
+ content: string,
366
+ patch: {
367
+ body?: string;
368
+ enabled?: boolean;
369
+ schedule?: string;
370
+ model?: string | null;
371
+ displayName?: string;
372
+ },
373
+ ): string {
374
+ let next =
375
+ patch.body === undefined
376
+ ? content
377
+ : replaceAutomationBody(content, patch.body);
378
+ if (patch.enabled !== undefined) {
379
+ next = setAutomationFrontmatterField(
380
+ next,
381
+ "enabled",
382
+ patch.enabled ? "true" : "false",
383
+ );
384
+ }
385
+ if (patch.schedule !== undefined) {
386
+ next = setAutomationFrontmatterField(next, "schedule", patch.schedule);
387
+ }
388
+ if (patch.model !== undefined) {
389
+ next = setAutomationFrontmatterField(
390
+ next,
391
+ "model",
392
+ patch.model?.trim() ?? "",
393
+ );
394
+ }
395
+ if (patch.displayName !== undefined) {
396
+ next = setAutomationFrontmatterField(
397
+ next,
398
+ "displayName",
399
+ patch.displayName,
400
+ );
401
+ }
402
+ return next;
403
+ }
404
+
405
+ export function readAutomationEnabled(content: string): boolean {
406
+ const value = readFrontmatterField(content, "enabled")?.trim().toLowerCase();
407
+ return value === "true" || value === "1" || value === "yes";
408
+ }
409
+
410
+ export function readAutomationSchedule(content: string): string | null {
411
+ return readFrontmatterField(content, "schedule")?.trim() || null;
412
+ }
413
+
414
+ export function readAutomationModel(content: string): string | null {
415
+ return readFrontmatterField(content, "model")?.trim() || null;
416
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ActionRunContext } from "@agent-native/core/action";
2
2
  import { listAutomationDefinitions } from "@agent-native/core/triggers";
3
3
 
4
+ import { readAutomationFactoryId } from "./factory-scope.js";
4
5
  import type { WorkspaceMemberIdentity } from "./require-workspace-member.js";
5
6
 
6
7
  const FACTORY_AUTOMATION_NAMES = {
@@ -31,6 +32,7 @@ export async function requireFactoryAutomation(
31
32
  context: ActionRunContext | undefined,
32
33
  identity: Pick<WorkspaceMemberIdentity, "userEmail" | "orgId">,
33
34
  role: FactoryAutomationRole,
35
+ expectedFactoryId?: string,
34
36
  ): Promise<void> {
35
37
  if (context?.caller !== "automation") {
36
38
  throw new Error("This action is only available to Factory automations.");
@@ -67,4 +69,13 @@ export async function requireFactoryAutomation(
67
69
  "The action was not invoked by a governed Factory automation.",
68
70
  );
69
71
  }
72
+ if (
73
+ expectedFactoryId &&
74
+ readAutomationFactoryId(definition.meta, definition.resource.content) !==
75
+ expectedFactoryId
76
+ ) {
77
+ throw new Error(
78
+ "The action was not invoked by a governed Factory automation.",
79
+ );
80
+ }
70
81
  }
@@ -70,8 +70,10 @@ provider keys per factory. Start with provider-api-catalog to discover the
70
70
  workspace's connected provider APIs, use provider-api-docs when an endpoint is
71
71
  unclear, and use provider-api-request with the shared credentials. The normalized
72
72
  poll-slack-channel, poll-github-sources, and poll-sentry-errors actions are legacy
73
- observer adapters for the default triage queue, not a list of Factory integrations
74
- or a limit on what agents can use.
73
+ observer adapters scoped by factoryId (pass the current factory from automation
74
+ meta or navigation), not a list of Factory integrations or a limit on what agents
75
+ can use. Triage config, inbox, rules, automations, and activity are per-factory;
76
+ reusable agents and workspace integrations stay shared.
75
77
  For rule or guard changes, use the triage rule actions and preserve
76
78
  normalizeTriagePolicyGuards; do not encode policy in graph JSON.
77
79
  Use add-factory-comment for durable comments attached to the selected node or edge.