@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +8 -1
- package/corpus/templates/calendar/actions/delete-event.ts +34 -18
- package/corpus/templates/calendar/actions/delete-events.ts +13 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +22 -3
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +19 -0
- package/corpus/templates/calendar/server/lib/google-api.ts +18 -1
- package/corpus/templates/clips/actions/finalize-meeting.ts +16 -0
- package/corpus/templates/clips/actions/list-meetings.ts +1 -0
- package/corpus/templates/clips/actions/sync-calendars.ts +10 -29
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +28 -12
- package/corpus/templates/clips/desktop/src/lib/meeting-stop.ts +16 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +19 -9
- package/corpus/templates/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
- package/corpus/templates/factory/AGENTS.md +12 -22
- package/corpus/templates/factory/README.md +11 -8
- package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
- package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
- package/corpus/templates/factory/actions/create-factory.ts +211 -0
- package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
- package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
- package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
- package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
- package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
- package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
- package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
- package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
- package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
- package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
- package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
- package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
- package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
- package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
- package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
- package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
- package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
- package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
- package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
- package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
- package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
- package/corpus/templates/factory/actions/view-screen.ts +22 -30
- package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
- package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
- package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
- package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
- package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
- package/corpus/templates/factory/app/routes/factory.tsx +78 -62
- package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
- package/corpus/templates/factory/server/db/schema.ts +18 -3
- package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
- package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
- package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
- package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
- package/corpus/templates/factory/server/triage/audit.ts +4 -0
- package/corpus/templates/factory/server/triage/ids.ts +4 -0
- package/dist/cli/native-dependencies.js +4 -1
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +9 -4
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat/index.js +1 -1
- package/dist/client/chat-first/apps-rail.js +6 -6
- package/dist/client/chat-first/primary-nav.js +14 -6
- package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
- package/dist/client/integrations/IntegrationsPanel.js +29 -21
- package/dist/client/integrations/index.d.ts +1 -1
- package/dist/client/integrations/index.js +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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
|
|
74
|
-
or a limit on what agents
|
|
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.
|