@agent-native/core 0.136.4 → 0.137.0

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 (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
  3. package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
  4. package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
  5. package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
  6. package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
  7. package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
  8. package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
  10. package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
  11. package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
  13. package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
  14. package/corpus/templates/clips/desktop/src/app.tsx +81 -30
  15. package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
  16. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
  17. package/corpus/templates/clips/server/plugins/auth.ts +9 -0
  18. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
  19. package/corpus/templates/content/actions/_database-utils.ts +6 -0
  20. package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
  21. package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
  22. package/corpus/templates/content/actions/configure-document-property.ts +113 -32
  23. package/corpus/templates/content/actions/delete-content-database.ts +5 -7
  24. package/corpus/templates/content/actions/delete-document-property.ts +109 -62
  25. package/corpus/templates/content/actions/delete-document.ts +178 -36
  26. package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
  27. package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
  28. package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
  29. package/corpus/templates/content/actions/remove-database-items.ts +13 -0
  30. package/corpus/templates/content/actions/set-document-property.ts +69 -1
  31. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
  32. package/corpus/templates/content/parity/matrix.md +1 -1
  33. package/corpus/templates/content/parity/matrix.ts +1 -0
  34. package/corpus/templates/content/server/db/schema.ts +27 -0
  35. package/corpus/templates/content/server/plugins/db.ts +24 -0
  36. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
  37. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  38. package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
  39. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
  40. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
  41. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
  42. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
  43. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
  44. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
  45. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  46. package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
  47. package/corpus/templates/design/app/i18n-data.ts +8 -0
  48. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
  49. package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
  50. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
  51. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
  52. package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
  53. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
  54. package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
  57. package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
  58. package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
  59. package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
  60. package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
  61. package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
  62. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
  63. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
  64. package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
  65. package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
  66. package/corpus/templates/factory/.env.example +17 -0
  67. package/corpus/templates/factory/AGENTS.md +27 -17
  68. package/corpus/templates/factory/README.md +19 -9
  69. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
  70. package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
  71. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
  72. package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
  73. package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
  74. package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
  75. package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
  76. package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
  77. package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
  78. package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
  79. package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
  80. package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
  81. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
  82. package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
  83. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
  84. package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
  85. package/corpus/templates/factory/app/routes/factory.tsx +462 -9
  86. package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
  87. package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
  88. package/corpus/templates/factory/netlify.toml +2 -2
  89. package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
  90. package/corpus/templates/factory/server/connectors/slack.ts +133 -5
  91. package/corpus/templates/factory/server/db/schema.ts +6 -0
  92. package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
  93. package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
  94. package/corpus/templates/factory/server/plugins/auth.ts +1 -0
  95. package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
  96. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
  97. package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
  98. package/corpus/templates/factory/server/triage/contracts.ts +14 -1
  99. package/corpus/templates/factory/server/triage/github-client.ts +404 -0
  100. package/corpus/templates/factory/server/triage/metadata.ts +44 -0
  101. package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
  102. package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
  103. package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
  104. package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
  105. package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
  106. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
  107. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
  108. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
  109. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
  110. package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
  111. package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
  112. package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
  113. package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
  114. package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
  115. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
  116. package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
  117. package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
  118. package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
  119. package/corpus/templates/plan/shared/plan-content.ts +17 -2
  120. package/dist/cli/skills-content/canvas.d.ts +3 -3
  121. package/dist/cli/skills-content/canvas.js +16 -5
  122. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  123. package/dist/cli/skills-content/visual-plan-skill.js +27 -0
  124. package/dist/cli/skills-content/wireframe.d.ts +2 -2
  125. package/dist/cli/skills-content/wireframe.js +7 -0
  126. package/dist/client/AgentPanel.js +23 -8
  127. package/dist/client/api-path.d.ts +6 -0
  128. package/dist/client/api-path.js +55 -0
  129. package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
  130. package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
  131. package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
  132. package/dist/client/onboarding/first-run-enabled.js +9 -0
  133. package/dist/client/onboarding/index.d.ts +2 -1
  134. package/dist/client/onboarding/index.js +1 -0
  135. package/dist/client/onboarding/use-onboarding.d.ts +6 -1
  136. package/dist/client/onboarding/use-onboarding.js +32 -1
  137. package/dist/client/route-state.js +10 -0
  138. package/dist/client/settings/SecretsSection.js +32 -8
  139. package/dist/collab/struct-routes.d.ts +1 -1
  140. package/dist/connections/catalog.d.ts +4 -4
  141. package/dist/connections/catalog.js +3 -3
  142. package/dist/db/index.d.ts +1 -1
  143. package/dist/db/index.js +1 -1
  144. package/dist/localization/default-messages.d.ts +5 -0
  145. package/dist/localization/default-messages.js +5 -0
  146. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  147. package/dist/observability/routes.d.ts +3 -3
  148. package/dist/onboarding/app-profile.d.ts +4 -0
  149. package/dist/onboarding/app-profile.js +378 -0
  150. package/dist/onboarding/index.d.ts +2 -1
  151. package/dist/onboarding/index.js +1 -0
  152. package/dist/onboarding/plugin.d.ts +2 -0
  153. package/dist/onboarding/plugin.js +21 -1
  154. package/dist/onboarding/types.d.ts +19 -0
  155. package/dist/progress/routes.d.ts +1 -1
  156. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  157. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  158. package/dist/secrets/routes.d.ts +2 -2
  159. package/dist/secrets/routes.js +41 -17
  160. package/dist/server/auth.js +21 -1
  161. package/dist/server/better-auth-instance.d.ts +3 -2
  162. package/dist/server/better-auth-instance.js +9 -7
  163. package/dist/server/email-markdown.js +3 -8
  164. package/dist/server/transcribe-voice.d.ts +1 -1
  165. package/dist/shared/first-run-onboarding.d.ts +3 -0
  166. package/dist/shared/first-run-onboarding.js +3 -0
  167. package/dist/triggers/index.d.ts +3 -0
  168. package/dist/triggers/index.js +5 -0
  169. package/docs/content/cross-app-sso.mdx +26 -0
  170. package/package.json +1 -1
@@ -0,0 +1,386 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { and, desc, eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+
5
+ import { resolveConnectorSecret } from "../server/connectors/credentials.js";
6
+ import { getDb } from "../server/db/index.js";
7
+ import {
8
+ triageDecisions,
9
+ triageConfig,
10
+ triageItems,
11
+ triageRuns,
12
+ } from "../server/db/schema.js";
13
+ import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
14
+ import {
15
+ requireWorkspaceMember,
16
+ workspaceMemberIdentityFromContext,
17
+ } from "../server/lib/require-workspace-member.js";
18
+ import { createAiServicesGitReadClient } from "../server/triage/ai-services-git.js";
19
+ import { createGitHubClient } from "../server/triage/github-client.js";
20
+ import { stableId } from "../server/triage/ids.js";
21
+ import {
22
+ parseTriageMetadata,
23
+ serializeTriageMetadata,
24
+ } from "../server/triage/metadata.js";
25
+ import { reconcileBabysitState } from "../server/triage/pr-babysit.js";
26
+ import { decidePullRequestGovernance } from "../server/triage/pr-policy.js";
27
+
28
+ function repositoryRef(value: string): { owner: string; repo: string } {
29
+ const match = /^([^/\s]+)\/([^/\s]+)$/.exec(value.trim());
30
+ if (!match)
31
+ throw new Error("Repository must use the owner/repository format.");
32
+ return { owner: match[1], repo: match[2] };
33
+ }
34
+
35
+ function requiredAiServicesEnv(
36
+ name: "BUILDER_AI_SERVICES_URL" | "BUILDER_PROJECT_ID",
37
+ ): string {
38
+ const value =
39
+ name === "BUILDER_AI_SERVICES_URL"
40
+ ? process.env.BUILDER_AI_SERVICES_URL // guard:allow-env-credential - non-secret deployment endpoint
41
+ : process.env.BUILDER_PROJECT_ID; // guard:allow-env-credential - non-secret deployment project id
42
+ if (!value) throw new Error(`${name} is required for PR governance.`);
43
+ return value;
44
+ }
45
+
46
+ async function hasVerifiedFactoryRun(input: {
47
+ itemId: string | undefined;
48
+ orgId: string;
49
+ pullRequestBody: string | null;
50
+ headRef: string;
51
+ }): Promise<boolean> {
52
+ const db = getDb();
53
+ const runConditions = [eq(triageRuns.orgId, input.orgId)];
54
+ if (input.itemId) {
55
+ runConditions.push(eq(triageRuns.itemId, input.itemId));
56
+ }
57
+ const runs = await db
58
+ .select({
59
+ itemId: triageRuns.itemId,
60
+ provider: triageRuns.provider,
61
+ providerTaskId: triageRuns.providerTaskId,
62
+ status: triageRuns.status,
63
+ })
64
+ .from(triageRuns)
65
+ .where(and(...runConditions))
66
+ .limit(200);
67
+ const verifiedItemIds = new Set(
68
+ runs
69
+ .filter(
70
+ (run) =>
71
+ (run.provider === "builder-http" || run.provider === "bot-tag") &&
72
+ run.status === "completed" &&
73
+ run.providerTaskId !== null,
74
+ )
75
+ .map((run) => run.itemId),
76
+ );
77
+ const body = input.pullRequestBody ?? "";
78
+ if (input.itemId && verifiedItemIds.has(input.itemId)) {
79
+ return (
80
+ input.headRef === `factory/${input.itemId.slice(0, 12)}` ||
81
+ body.includes(`Factory item: ${input.itemId}`)
82
+ );
83
+ }
84
+ if (input.itemId) return false;
85
+
86
+ if (!body && !input.headRef.startsWith("factory/")) return false;
87
+ const items = await db
88
+ .select({ id: triageItems.id, sourceUrl: triageItems.sourceUrl })
89
+ .from(triageItems)
90
+ .where(eq(triageItems.orgId, input.orgId))
91
+ .orderBy(desc(triageItems.updatedAt))
92
+ .limit(100);
93
+ return items.some((item) => {
94
+ if (!verifiedItemIds.has(item.id)) return false;
95
+ return (
96
+ body.includes(`Factory item: ${item.id}`) ||
97
+ (item.sourceUrl !== null && body.includes(item.sourceUrl)) ||
98
+ input.headRef === `factory/${item.id.slice(0, 12)}`
99
+ );
100
+ });
101
+ }
102
+
103
+ export default defineAction({
104
+ description:
105
+ "Govern one agent-native pull request after fetching bounded GitHub and ai-services evidence. Auto-approve only clear internal bug fixes with clean CI and handled review feedback; auto-merge only the verified Factory Builder flow. Clips, Design, and Content are always owner-managed.",
106
+ schema: z.object({
107
+ repo: z.string().trim().min(1).max(256),
108
+ pullRequestNumber: z.number().int().positive(),
109
+ itemId: z.string().min(1).optional(),
110
+ clearBug: z.boolean(),
111
+ productUxImplications: z.boolean().default(false),
112
+ reason: z.string().trim().min(1).max(4_000),
113
+ }),
114
+ http: false,
115
+ run: async (
116
+ {
117
+ repo,
118
+ pullRequestNumber,
119
+ itemId,
120
+ clearBug,
121
+ productUxImplications,
122
+ reason,
123
+ },
124
+ context,
125
+ ) => {
126
+ const { userEmail, orgId } = await requireWorkspaceMember(
127
+ workspaceMemberIdentityFromContext(context),
128
+ );
129
+ await requireFactoryAutomation(context, { userEmail, orgId }, "governance");
130
+ const repository = repositoryRef(repo);
131
+ const configuredRepository = (
132
+ await getDb()
133
+ .select({ repository: triageConfig.repository })
134
+ .from(triageConfig)
135
+ .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
136
+ .limit(1)
137
+ )[0]?.repository;
138
+ if (!configuredRepository || configuredRepository.trim() !== repo.trim()) {
139
+ throw new Error(
140
+ "PR governance is restricted to the configured Factory repository.",
141
+ );
142
+ }
143
+ if (itemId) {
144
+ const item = (
145
+ await getDb()
146
+ .select({
147
+ repository: triageItems.repository,
148
+ pullRequestNumber: triageItems.pullRequestNumber,
149
+ })
150
+ .from(triageItems)
151
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
152
+ .limit(1)
153
+ )[0];
154
+ if (
155
+ !item ||
156
+ item.repository !== repo.trim() ||
157
+ item.pullRequestNumber !== pullRequestNumber
158
+ ) {
159
+ throw new Error(
160
+ "Factory item does not match the governed repository and pull request.",
161
+ );
162
+ }
163
+ }
164
+ const github = createGitHubClient({ ownerEmail: userEmail, orgId });
165
+ const pullRequest = await github.getPullRequestSummary(
166
+ repository,
167
+ pullRequestNumber,
168
+ );
169
+ const privateKey = await resolveConnectorSecret(
170
+ "BUILDER_PRIVATE_KEY",
171
+ userEmail,
172
+ { orgId },
173
+ );
174
+ if (!privateKey)
175
+ throw new Error(
176
+ "BUILDER_PRIVATE_KEY is required to read PR review evidence.",
177
+ );
178
+ const projectId = requiredAiServicesEnv("BUILDER_PROJECT_ID");
179
+ const snapshot = await createAiServicesGitReadClient({
180
+ baseUrl: requiredAiServicesEnv("BUILDER_AI_SERVICES_URL"),
181
+ authorization: `Bearer ${privateKey.startsWith("bpk-") ? privateKey : `bpk-${privateKey}`}`,
182
+ }).fetchPullRequest({ projectId, repo, pullRequestNumber });
183
+ if (snapshot.headSha !== pullRequest.headSha) {
184
+ throw new Error(
185
+ `PR evidence is stale: GitHub reports ${pullRequest.headSha}, ai-services reports ${snapshot.headSha}.`,
186
+ );
187
+ }
188
+
189
+ const checksPassed =
190
+ snapshot.coverage === "complete" &&
191
+ snapshot.checks.length > 0 &&
192
+ snapshot.checks.every((check) => check.state === "passed");
193
+ const reviewStateClean = snapshot.reviews.every(
194
+ (review) =>
195
+ review.state !== "changes_requested" && review.state !== "pending",
196
+ );
197
+ const reviewFeedback = reconcileBabysitState({
198
+ comments: snapshot.comments,
199
+ checks: snapshot.checks,
200
+ commentsTruncated: snapshot.commentsTruncated,
201
+ botAuthors: [
202
+ "github-actions",
203
+ "github-actions[bot]",
204
+ "dependabot[bot]",
205
+ "builderio[bot]",
206
+ ],
207
+ });
208
+ const reviewFeedbackHandled = reviewStateClean && reviewFeedback.isClean;
209
+ const internalMember = await github.checkOrganizationMember(
210
+ "BuilderIO",
211
+ pullRequest.userLogin,
212
+ );
213
+ const factoryTriggered = await hasVerifiedFactoryRun({
214
+ itemId,
215
+ orgId,
216
+ pullRequestBody: pullRequest.body,
217
+ headRef: pullRequest.headRef,
218
+ });
219
+ const governance = decidePullRequestGovernance({
220
+ author: pullRequest.userLogin,
221
+ repository: repo,
222
+ title: pullRequest.title,
223
+ summary: pullRequest.body,
224
+ changedFiles: snapshot.changedFiles ?? [],
225
+ clearBug,
226
+ productUxImplications,
227
+ checksPassed,
228
+ reviewFeedbackHandled,
229
+ openNonDraft: pullRequest.state === "open" && !pullRequest.draft,
230
+ internalBuilderMember: internalMember.isMember,
231
+ factoryTriggered,
232
+ });
233
+
234
+ if (itemId) {
235
+ const item = (
236
+ await getDb()
237
+ .select()
238
+ .from(triageItems)
239
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
240
+ .limit(1)
241
+ )[0];
242
+ if (!item) throw new Error("Factory item not found for PR governance.");
243
+ const decisionId = stableId(
244
+ "pr-governance",
245
+ orgId,
246
+ itemId,
247
+ snapshot.headSha,
248
+ );
249
+ await getDb()
250
+ .insert(triageDecisions)
251
+ .values({
252
+ id: decisionId,
253
+ itemId,
254
+ ruleId: null,
255
+ mode: "automation",
256
+ outcome: governance.autoMerge
257
+ ? "auto_merge"
258
+ : governance.autoApprove
259
+ ? "auto_approve"
260
+ : "needs_manual",
261
+ reason: `${reason} ${governance.reason}`.trim(),
262
+ guardResultsJson: JSON.stringify(governance.guardResults),
263
+ model: "factory-pr-governance",
264
+ promptVersion: 1,
265
+ createdAt: new Date().toISOString(),
266
+ ownerEmail: userEmail,
267
+ orgId,
268
+ })
269
+ .onConflictDoNothing();
270
+ }
271
+
272
+ if (!governance.autoApprove) {
273
+ if (itemId) {
274
+ await getDb()
275
+ .update(triageItems)
276
+ .set({
277
+ status: governance.ownerOwnedArea ? "needs_manual" : "pr_observed",
278
+ updatedAt: new Date().toISOString(),
279
+ })
280
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
281
+ }
282
+ return {
283
+ ok: true,
284
+ action: "needs_manual",
285
+ ownerOwnedArea: governance.ownerOwnedArea,
286
+ reason: governance.reason,
287
+ checksPassed,
288
+ reviewFeedbackHandled,
289
+ };
290
+ }
291
+
292
+ let approvalUrl: string | null = null;
293
+ if (itemId) {
294
+ const item = (
295
+ await getDb()
296
+ .select({
297
+ metadataJson: triageItems.metadataJson,
298
+ status: triageItems.status,
299
+ })
300
+ .from(triageItems)
301
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
302
+ .limit(1)
303
+ )[0];
304
+ if (!item) throw new Error("Factory item disappeared after PR approval.");
305
+ const metadata = parseTriageMetadata(item.metadataJson);
306
+ const previouslyApproved =
307
+ item.status === "auto_approved" &&
308
+ metadata.autoApprovalHeadSha === snapshot.headSha &&
309
+ typeof metadata.autoApprovalUrl === "string";
310
+ if (previouslyApproved) approvalUrl = metadata.autoApprovalUrl as string;
311
+ if (!previouslyApproved) {
312
+ const approval = await github.approvePullRequest(
313
+ repository,
314
+ pullRequestNumber,
315
+ "Factory auto-approved: clear internal bug fix with passing CI and handled review feedback.",
316
+ );
317
+ approvalUrl = approval.htmlUrl;
318
+ metadata.autoApprovedAt = new Date().toISOString();
319
+ metadata.autoApprovalUrl = approval.htmlUrl;
320
+ metadata.autoApprovalHeadSha = snapshot.headSha;
321
+ await getDb()
322
+ .update(triageItems)
323
+ .set({
324
+ metadataJson: serializeTriageMetadata(metadata),
325
+ status: "auto_approved",
326
+ updatedAt: new Date().toISOString(),
327
+ })
328
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
329
+ }
330
+ } else {
331
+ approvalUrl = (
332
+ await github.approvePullRequest(
333
+ repository,
334
+ pullRequestNumber,
335
+ "Factory auto-approved: clear internal bug fix with passing CI and handled review feedback.",
336
+ )
337
+ ).htmlUrl;
338
+ }
339
+
340
+ if (!governance.autoMerge) {
341
+ return {
342
+ ok: true,
343
+ action: "approved",
344
+ approvalUrl,
345
+ checksPassed,
346
+ reviewFeedbackHandled,
347
+ };
348
+ }
349
+
350
+ const merge = await github.mergePullRequest(
351
+ repository,
352
+ pullRequestNumber,
353
+ `Merge Factory bug fix #${pullRequestNumber}`,
354
+ snapshot.headSha,
355
+ );
356
+ if (itemId) {
357
+ const item = (
358
+ await getDb()
359
+ .select({ metadataJson: triageItems.metadataJson })
360
+ .from(triageItems)
361
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
362
+ .limit(1)
363
+ )[0];
364
+ if (!item) throw new Error("Factory item disappeared after PR merge.");
365
+ const metadata = parseTriageMetadata(item.metadataJson);
366
+ metadata.autoMergedAt = new Date().toISOString();
367
+ metadata.mergeSha = merge.sha;
368
+ await getDb()
369
+ .update(triageItems)
370
+ .set({
371
+ metadataJson: serializeTriageMetadata(metadata),
372
+ status: "merged",
373
+ updatedAt: new Date().toISOString(),
374
+ })
375
+ .where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
376
+ }
377
+ return {
378
+ ok: true,
379
+ action: "merged",
380
+ approvalUrl,
381
+ mergeSha: merge.sha,
382
+ checksPassed,
383
+ reviewFeedbackHandled,
384
+ };
385
+ },
386
+ });
@@ -0,0 +1,53 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import {
3
+ listAutomationDefinitions,
4
+ listAutomationRuns,
5
+ } from "@agent-native/core/triggers";
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ requireWorkspaceMember,
10
+ workspaceMemberIdentityFromContext,
11
+ } from "../server/lib/require-workspace-member.js";
12
+
13
+ export default defineAction({
14
+ description:
15
+ "List the organization-scoped Factory automations with their trigger, editable prompt, model, schedule, enabled state, and recent runs.",
16
+ agentTool: false,
17
+ schema: z.object({ factoryId: z.string().trim().min(1).optional() }),
18
+ http: { method: "GET" },
19
+ readOnly: true,
20
+ run: async (_, context) => {
21
+ const { userEmail, orgId } = await requireWorkspaceMember(
22
+ workspaceMemberIdentityFromContext(context),
23
+ );
24
+ const definitions = await listAutomationDefinitions(
25
+ { userEmail, orgId },
26
+ "organization",
27
+ );
28
+ return Promise.all(
29
+ definitions
30
+ .filter(({ meta }) => meta.domain === "factory")
31
+ .map(async ({ resource, name, meta, body, canUpdate }) => ({
32
+ id: resource.id,
33
+ name,
34
+ prompt: body,
35
+ body,
36
+ model: meta.model ?? null,
37
+ schedule: meta.schedule || null,
38
+ enabled: meta.enabled,
39
+ triggerType: meta.triggerType,
40
+ event: meta.event ?? null,
41
+ timezone: meta.timezone ?? null,
42
+ condition: meta.condition ?? null,
43
+ createdBy: meta.createdBy ?? null,
44
+ canUpdate,
45
+ runs: await listAutomationRuns({
46
+ owners: [resource.owner],
47
+ automation: name,
48
+ limit: 20,
49
+ }),
50
+ })),
51
+ );
52
+ },
53
+ });
@@ -8,18 +8,22 @@ import {
8
8
  requireWorkspaceMember,
9
9
  workspaceMemberIdentityFromContext,
10
10
  } from "../server/lib/require-workspace-member.js";
11
- import { triageItemStatusSchema } from "../server/triage/contracts.js";
11
+ import {
12
+ triageItemStatusSchema,
13
+ triageSourceSchema,
14
+ } from "../server/triage/contracts.js";
12
15
 
13
16
  export default defineAction({
14
17
  description:
15
18
  "List the Factory observation queue. Results are scoped to the active workspace and include the latest shadow decision summary.",
16
19
  schema: z.object({
17
20
  status: triageItemStatusSchema.optional(),
21
+ source: triageSourceSchema.optional(),
18
22
  limit: z.coerce.number().int().min(1).max(100).default(50),
19
23
  }),
20
24
  http: { method: "GET" },
21
25
  readOnly: true,
22
- run: async ({ status, limit }, context) => {
26
+ run: async ({ status, source, limit }, context) => {
23
27
  const { orgId } = await requireWorkspaceMember(
24
28
  workspaceMemberIdentityFromContext(context),
25
29
  );
@@ -31,6 +35,7 @@ export default defineAction({
31
35
  and(
32
36
  eq(triageItems.orgId, orgId),
33
37
  status ? eq(triageItems.status, status) : undefined,
38
+ source ? eq(triageItems.source, source) : undefined,
34
39
  ),
35
40
  )
36
41
  .orderBy(desc(triageItems.updatedAt))
@@ -0,0 +1,203 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { and, eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+
5
+ import { getDb } from "../server/db/index.js";
6
+ import { triageConfig, triageItems } from "../server/db/schema.js";
7
+ import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
8
+ import {
9
+ requireWorkspaceMember,
10
+ workspaceMemberIdentityFromContext,
11
+ } from "../server/lib/require-workspace-member.js";
12
+ import { createGitHubClient } from "../server/triage/github-client.js";
13
+ import { itemDedupeKey } from "../server/triage/ids.js";
14
+ import { mergeTriageMetadata } from "../server/triage/metadata.js";
15
+
16
+ function parseRepository(value: string): { owner: string; repo: string } {
17
+ const match = /^([^/\s]+)\/([^/\s]+)$/.exec(value.trim());
18
+ if (!match) {
19
+ throw new Error("Factory repository must use the owner/repository format.");
20
+ }
21
+ return { owner: match[1], repo: match[2] };
22
+ }
23
+
24
+ export default defineAction({
25
+ description:
26
+ "Poll the configured GitHub repository for bounded open issues and pull requests and record them in the Factory queue. This does not write to GitHub.",
27
+ schema: z.object({
28
+ includeIssues: z.boolean().default(true),
29
+ includePullRequests: z.boolean().default(true),
30
+ }),
31
+ http: false,
32
+ run: async ({ includeIssues, includePullRequests }, context) => {
33
+ const { userEmail, orgId } = await requireWorkspaceMember(
34
+ workspaceMemberIdentityFromContext(context),
35
+ );
36
+ await requireFactoryAutomation(
37
+ context,
38
+ { userEmail, orgId },
39
+ "sourcePolling",
40
+ );
41
+ const config = (
42
+ await getDb()
43
+ .select()
44
+ .from(triageConfig)
45
+ .where(and(eq(triageConfig.id, orgId), eq(triageConfig.orgId, orgId)))
46
+ .limit(1)
47
+ )[0];
48
+ if (config?.githubPollingEnabled !== 1) {
49
+ throw new Error("Enable GitHub polling before polling GitHub sources.");
50
+ }
51
+ if (!config.repository) {
52
+ throw new Error("Configure a GitHub repository before polling GitHub.");
53
+ }
54
+
55
+ const repositoryName = config.repository;
56
+ const repository = parseRepository(repositoryName);
57
+ const client = createGitHubClient({ ownerEmail: userEmail, orgId });
58
+ const [issues, pullRequests] = await Promise.all([
59
+ includeIssues
60
+ ? client.listOpenIssues(repository, 50)
61
+ : Promise.resolve([]),
62
+ includePullRequests
63
+ ? client.listOpenPullRequests(repository, 50)
64
+ : Promise.resolve([]),
65
+ ]);
66
+ const db = getDb();
67
+ const now = new Date().toISOString();
68
+ let issueCount = 0;
69
+ let pullRequestCount = 0;
70
+
71
+ await db.transaction(async (tx) => {
72
+ for (const issue of issues) {
73
+ const id = itemDedupeKey(
74
+ {
75
+ source: "github_issue",
76
+ externalId: `${repositoryName}#${issue.number}`,
77
+ },
78
+ orgId,
79
+ );
80
+ const existing = (
81
+ await tx
82
+ .select()
83
+ .from(triageItems)
84
+ .where(and(eq(triageItems.id, id), eq(triageItems.orgId, orgId)))
85
+ .limit(1)
86
+ )[0];
87
+ const metadata = mergeTriageMetadata(existing?.metadataJson ?? "{}", {
88
+ kind: "github_issue",
89
+ author: issue.userLogin,
90
+ labels: [...issue.labels],
91
+ errorReport: [issue.title, issue.body ?? ""]
92
+ .filter(Boolean)
93
+ .join("\n\n"),
94
+ updatedAt: issue.updatedAt,
95
+ });
96
+ await tx
97
+ .insert(triageItems)
98
+ .values({
99
+ id,
100
+ source: "github_issue",
101
+ externalId: `${repositoryName}#${issue.number}`,
102
+ sourceUrl: issue.htmlUrl,
103
+ title: issue.title,
104
+ summary: issue.body?.slice(0, 4_000) ?? null,
105
+ status: existing?.status ?? "received",
106
+ risk: existing?.risk ?? "unknown",
107
+ coverage: existing?.coverage ?? "complete",
108
+ dedupeKey: id,
109
+ metadataJson: metadata,
110
+ lastSeenAt: issue.updatedAt,
111
+ createdAt: existing?.createdAt ?? now,
112
+ updatedAt: now,
113
+ ownerEmail: existing?.ownerEmail ?? userEmail,
114
+ orgId,
115
+ })
116
+ .onConflictDoUpdate({
117
+ target: triageItems.id,
118
+ set: {
119
+ sourceUrl: issue.htmlUrl,
120
+ title: issue.title,
121
+ summary: issue.body?.slice(0, 4_000) ?? null,
122
+ metadataJson: metadata,
123
+ lastSeenAt: issue.updatedAt,
124
+ updatedAt: now,
125
+ },
126
+ });
127
+ issueCount += 1;
128
+ }
129
+
130
+ for (const pullRequest of pullRequests) {
131
+ const id = itemDedupeKey(
132
+ {
133
+ source: "github",
134
+ externalId: `${repositoryName}#${pullRequest.number}`,
135
+ repository: repositoryName,
136
+ pullRequestNumber: pullRequest.number,
137
+ },
138
+ orgId,
139
+ );
140
+ const existing = (
141
+ await tx
142
+ .select()
143
+ .from(triageItems)
144
+ .where(and(eq(triageItems.id, id), eq(triageItems.orgId, orgId)))
145
+ .limit(1)
146
+ )[0];
147
+ const metadata = mergeTriageMetadata(existing?.metadataJson ?? "{}", {
148
+ kind: "pull_request",
149
+ author: pullRequest.userLogin,
150
+ headRef: pullRequest.headRef,
151
+ baseRef: pullRequest.baseRef,
152
+ draft: pullRequest.draft,
153
+ updatedAt: pullRequest.updatedAt,
154
+ });
155
+ await tx
156
+ .insert(triageItems)
157
+ .values({
158
+ id,
159
+ source: "github",
160
+ externalId: `${repositoryName}#${pullRequest.number}`,
161
+ sourceUrl: pullRequest.htmlUrl,
162
+ title: pullRequest.title,
163
+ summary: pullRequest.body?.slice(0, 4_000) ?? null,
164
+ status: existing?.status ?? "pr_observed",
165
+ risk: existing?.risk ?? "unknown",
166
+ repository: repositoryName,
167
+ pullRequestNumber: pullRequest.number,
168
+ headSha: pullRequest.headSha,
169
+ coverage: existing?.coverage ?? "partial",
170
+ dedupeKey: id,
171
+ metadataJson: metadata,
172
+ lastSeenAt: pullRequest.updatedAt,
173
+ createdAt: existing?.createdAt ?? now,
174
+ updatedAt: now,
175
+ ownerEmail: existing?.ownerEmail ?? userEmail,
176
+ orgId,
177
+ })
178
+ .onConflictDoUpdate({
179
+ target: triageItems.id,
180
+ set: {
181
+ sourceUrl: pullRequest.htmlUrl,
182
+ title: pullRequest.title,
183
+ summary: pullRequest.body?.slice(0, 4_000) ?? null,
184
+ repository: repositoryName,
185
+ pullRequestNumber: pullRequest.number,
186
+ headSha: pullRequest.headSha,
187
+ metadataJson: metadata,
188
+ lastSeenAt: pullRequest.updatedAt,
189
+ updatedAt: now,
190
+ },
191
+ });
192
+ pullRequestCount += 1;
193
+ }
194
+ });
195
+
196
+ return {
197
+ ok: true,
198
+ repository: repositoryName,
199
+ issues: issueCount,
200
+ pullRequests: pullRequestCount,
201
+ };
202
+ },
203
+ });