@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
- package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
- package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
- package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +81 -30
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
- package/corpus/templates/clips/server/plugins/auth.ts +9 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
- package/corpus/templates/content/actions/_database-utils.ts +6 -0
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
- package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
- package/corpus/templates/content/actions/configure-document-property.ts +113 -32
- package/corpus/templates/content/actions/delete-content-database.ts +5 -7
- package/corpus/templates/content/actions/delete-document-property.ts +109 -62
- package/corpus/templates/content/actions/delete-document.ts +178 -36
- package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
- package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
- package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
- package/corpus/templates/content/actions/remove-database-items.ts +13 -0
- package/corpus/templates/content/actions/set-document-property.ts +69 -1
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +1 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +24 -0
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
- package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
- package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
- package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
- package/corpus/templates/design/app/i18n-data.ts +8 -0
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
- package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
- package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
- package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
- package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
- package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
- package/corpus/templates/factory/.env.example +17 -0
- package/corpus/templates/factory/AGENTS.md +27 -17
- package/corpus/templates/factory/README.md +19 -9
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
- package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
- package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
- package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
- package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
- package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
- package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
- package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
- package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
- package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
- package/corpus/templates/factory/app/routes/factory.tsx +462 -9
- package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
- package/corpus/templates/factory/netlify.toml +2 -2
- package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
- package/corpus/templates/factory/server/connectors/slack.ts +133 -5
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
- package/corpus/templates/factory/server/plugins/auth.ts +1 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
- package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
- package/corpus/templates/factory/server/triage/contracts.ts +14 -1
- package/corpus/templates/factory/server/triage/github-client.ts +404 -0
- package/corpus/templates/factory/server/triage/metadata.ts +44 -0
- package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
- package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
- package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
- package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
- package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
- package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
- package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
- package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
- package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
- package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
- package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +17 -2
- package/dist/cli/skills-content/canvas.d.ts +3 -3
- package/dist/cli/skills-content/canvas.js +16 -5
- package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-plan-skill.js +27 -0
- package/dist/cli/skills-content/wireframe.d.ts +2 -2
- package/dist/cli/skills-content/wireframe.js +7 -0
- package/dist/client/AgentPanel.js +23 -8
- package/dist/client/api-path.d.ts +6 -0
- package/dist/client/api-path.js +55 -0
- package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
- package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
- package/dist/client/onboarding/first-run-enabled.js +9 -0
- package/dist/client/onboarding/index.d.ts +2 -1
- package/dist/client/onboarding/index.js +1 -0
- package/dist/client/onboarding/use-onboarding.d.ts +6 -1
- package/dist/client/onboarding/use-onboarding.js +32 -1
- package/dist/client/route-state.js +10 -0
- package/dist/client/settings/SecretsSection.js +32 -8
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +4 -4
- package/dist/connections/catalog.js +3 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/localization/default-messages.d.ts +5 -0
- package/dist/localization/default-messages.js +5 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/app-profile.d.ts +4 -0
- package/dist/onboarding/app-profile.js +378 -0
- package/dist/onboarding/index.d.ts +2 -1
- package/dist/onboarding/index.js +1 -0
- package/dist/onboarding/plugin.d.ts +2 -0
- package/dist/onboarding/plugin.js +21 -1
- package/dist/onboarding/types.d.ts +19 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +12 -12
- package/dist/secrets/routes.d.ts +2 -2
- package/dist/secrets/routes.js +41 -17
- package/dist/server/auth.js +21 -1
- package/dist/server/better-auth-instance.d.ts +3 -2
- package/dist/server/better-auth-instance.js +9 -7
- package/dist/server/email-markdown.js +3 -8
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/first-run-onboarding.d.ts +3 -0
- package/dist/shared/first-run-onboarding.js +3 -0
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +5 -0
- package/docs/content/cross-app-sso.mdx +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { resolveConnectorSecret } from "../connectors/credentials.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BASE_URL = "https://api.github.com";
|
|
4
|
+
const MAX_PAGE_SIZE = 100;
|
|
5
|
+
|
|
6
|
+
type FetchLike = typeof fetch;
|
|
7
|
+
|
|
8
|
+
export interface GitHubClientIdentity {
|
|
9
|
+
ownerEmail: string;
|
|
10
|
+
orgId?: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GitHubClientOptions extends GitHubClientIdentity {
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
fetchImpl?: FetchLike;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GitHubRepositoryRef {
|
|
19
|
+
owner: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GitHubPullRequest {
|
|
24
|
+
number: number;
|
|
25
|
+
title: string;
|
|
26
|
+
body: string | null;
|
|
27
|
+
state: string;
|
|
28
|
+
draft: boolean;
|
|
29
|
+
htmlUrl: string;
|
|
30
|
+
userLogin: string;
|
|
31
|
+
headSha: string;
|
|
32
|
+
headRef: string;
|
|
33
|
+
baseRef: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GitHubIssue {
|
|
39
|
+
number: number;
|
|
40
|
+
title: string;
|
|
41
|
+
body: string | null;
|
|
42
|
+
state: string;
|
|
43
|
+
htmlUrl: string;
|
|
44
|
+
userLogin: string;
|
|
45
|
+
labels: readonly string[];
|
|
46
|
+
createdAt: string;
|
|
47
|
+
updatedAt: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GitHubPullRequestSummary extends GitHubPullRequest {
|
|
51
|
+
additions: number;
|
|
52
|
+
deletions: number;
|
|
53
|
+
changedFiles: number;
|
|
54
|
+
mergeable: boolean | null;
|
|
55
|
+
mergeableState: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GitHubMemberCheck {
|
|
59
|
+
username: string;
|
|
60
|
+
isMember: boolean;
|
|
61
|
+
permission: "admin" | "maintain" | "push" | "triage" | "pull" | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GitHubApproval {
|
|
65
|
+
id: number;
|
|
66
|
+
state: "APPROVED";
|
|
67
|
+
htmlUrl: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GitHubMergeResult {
|
|
71
|
+
sha: string;
|
|
72
|
+
merged: true;
|
|
73
|
+
message: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface JsonResponse {
|
|
77
|
+
ok: boolean;
|
|
78
|
+
status: number;
|
|
79
|
+
json(): Promise<unknown>;
|
|
80
|
+
text(): Promise<string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requiredString(value: unknown, field: string): string {
|
|
84
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
85
|
+
throw new Error(`GitHub response is missing ${field}`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requiredNumber(value: unknown, field: string): number {
|
|
91
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
92
|
+
throw new Error(`GitHub response is missing ${field}`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function requiredBoolean(value: unknown, field: string): boolean {
|
|
98
|
+
if (typeof value !== "boolean") {
|
|
99
|
+
throw new Error(`GitHub response is missing ${field}`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function record(value: unknown): Record<string, unknown> {
|
|
105
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
106
|
+
throw new Error("GitHub response was not an object");
|
|
107
|
+
}
|
|
108
|
+
return value as Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pageSize(limit?: number): number {
|
|
112
|
+
if (limit === undefined) return MAX_PAGE_SIZE;
|
|
113
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE_SIZE) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`GitHub limit must be an integer from 1 to ${MAX_PAGE_SIZE}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return limit;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function repositoryPath(repository: GitHubRepositoryRef): string {
|
|
122
|
+
const owner = repository.owner.trim();
|
|
123
|
+
const repo = repository.repo.trim();
|
|
124
|
+
if (!owner || !repo) throw new Error("GitHub owner and repo are required");
|
|
125
|
+
return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parsePullRequest(value: unknown): GitHubPullRequest {
|
|
129
|
+
const item = record(value);
|
|
130
|
+
const user = record(item.user);
|
|
131
|
+
const head = record(item.head);
|
|
132
|
+
const base = record(item.base);
|
|
133
|
+
return {
|
|
134
|
+
number: requiredNumber(item.number, "pull request number"),
|
|
135
|
+
title: requiredString(item.title, "pull request title"),
|
|
136
|
+
body:
|
|
137
|
+
item.body === null
|
|
138
|
+
? null
|
|
139
|
+
: requiredString(item.body, "pull request body"),
|
|
140
|
+
state: requiredString(item.state, "pull request state"),
|
|
141
|
+
draft: requiredBoolean(item.draft, "pull request draft state"),
|
|
142
|
+
htmlUrl: requiredString(item.html_url, "pull request URL"),
|
|
143
|
+
userLogin: requiredString(user.login, "pull request author"),
|
|
144
|
+
headSha: requiredString(head.sha, "pull request head SHA"),
|
|
145
|
+
headRef: requiredString(head.ref, "pull request head branch"),
|
|
146
|
+
baseRef: requiredString(base.ref, "pull request base ref"),
|
|
147
|
+
createdAt: requiredString(item.created_at, "pull request created time"),
|
|
148
|
+
updatedAt: requiredString(item.updated_at, "pull request updated time"),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseIssue(value: unknown): GitHubIssue | null {
|
|
153
|
+
const item = record(value);
|
|
154
|
+
if (item.pull_request !== undefined) return null;
|
|
155
|
+
const user = record(item.user);
|
|
156
|
+
const labels = item.labels;
|
|
157
|
+
if (!Array.isArray(labels))
|
|
158
|
+
throw new Error("GitHub issue response is missing labels");
|
|
159
|
+
return {
|
|
160
|
+
number: requiredNumber(item.number, "issue number"),
|
|
161
|
+
title: requiredString(item.title, "issue title"),
|
|
162
|
+
body: item.body === null ? null : requiredString(item.body, "issue body"),
|
|
163
|
+
state: requiredString(item.state, "issue state"),
|
|
164
|
+
htmlUrl: requiredString(item.html_url, "issue URL"),
|
|
165
|
+
userLogin: requiredString(user.login, "issue author"),
|
|
166
|
+
labels: labels.map((label) =>
|
|
167
|
+
requiredString(record(label).name, "issue label"),
|
|
168
|
+
),
|
|
169
|
+
createdAt: requiredString(item.created_at, "issue created time"),
|
|
170
|
+
updatedAt: requiredString(item.updated_at, "issue updated time"),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function createGitHubClient(options: GitHubClientOptions) {
|
|
175
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
176
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
177
|
+
|
|
178
|
+
async function token(): Promise<string> {
|
|
179
|
+
const value = await resolveConnectorSecret(
|
|
180
|
+
"GITHUB_TOKEN",
|
|
181
|
+
options.ownerEmail,
|
|
182
|
+
{
|
|
183
|
+
orgId: options.orgId,
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
if (!value)
|
|
187
|
+
throw new Error("GITHUB_TOKEN is not configured for this workspace");
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function request<T>(
|
|
192
|
+
path: string,
|
|
193
|
+
init: RequestInit = {},
|
|
194
|
+
options: { allowEmpty?: boolean } = {},
|
|
195
|
+
): Promise<T> {
|
|
196
|
+
const response = (await fetchImpl(`${baseUrl}${path}`, {
|
|
197
|
+
...init,
|
|
198
|
+
headers: {
|
|
199
|
+
Accept: "application/vnd.github+json",
|
|
200
|
+
Authorization: `Bearer ${await token()}`,
|
|
201
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
202
|
+
...init.headers,
|
|
203
|
+
},
|
|
204
|
+
})) as JsonResponse;
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const detail = (await response.text()).slice(0, 500);
|
|
207
|
+
throw new Error(
|
|
208
|
+
`GitHub API request failed: HTTP ${response.status}${detail ? ` - ${detail}` : ""}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (response.status === 204 && options.allowEmpty) return undefined as T;
|
|
212
|
+
return (await response.json()) as T;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
async listOpenPullRequests(
|
|
217
|
+
repository: GitHubRepositoryRef,
|
|
218
|
+
limit?: number,
|
|
219
|
+
) {
|
|
220
|
+
const value = await request<unknown>(
|
|
221
|
+
`${repositoryPath(repository)}/pulls?state=open&per_page=${pageSize(limit)}`,
|
|
222
|
+
);
|
|
223
|
+
if (!Array.isArray(value))
|
|
224
|
+
throw new Error("GitHub pull request response was not an array");
|
|
225
|
+
return value.map(parsePullRequest);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
async listOpenIssues(repository: GitHubRepositoryRef, limit?: number) {
|
|
229
|
+
const value = await request<unknown>(
|
|
230
|
+
`${repositoryPath(repository)}/issues?state=open&per_page=${pageSize(limit)}`,
|
|
231
|
+
);
|
|
232
|
+
if (!Array.isArray(value))
|
|
233
|
+
throw new Error("GitHub issue response was not an array");
|
|
234
|
+
return value.flatMap((item) => {
|
|
235
|
+
const issue = parseIssue(item);
|
|
236
|
+
return issue ? [issue] : [];
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async getPullRequestSummary(
|
|
241
|
+
repository: GitHubRepositoryRef,
|
|
242
|
+
pullRequestNumber: number,
|
|
243
|
+
) {
|
|
244
|
+
if (!Number.isInteger(pullRequestNumber) || pullRequestNumber < 1) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"GitHub pull request number must be a positive integer",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const item = record(
|
|
250
|
+
await request<unknown>(
|
|
251
|
+
`${repositoryPath(repository)}/pulls/${pullRequestNumber}`,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
const pullRequest = parsePullRequest(item);
|
|
255
|
+
return {
|
|
256
|
+
...pullRequest,
|
|
257
|
+
additions: requiredNumber(item.additions, "pull request additions"),
|
|
258
|
+
deletions: requiredNumber(item.deletions, "pull request deletions"),
|
|
259
|
+
changedFiles: requiredNumber(
|
|
260
|
+
item.changed_files,
|
|
261
|
+
"pull request changed files",
|
|
262
|
+
),
|
|
263
|
+
mergeable:
|
|
264
|
+
item.mergeable === null
|
|
265
|
+
? null
|
|
266
|
+
: requiredBoolean(item.mergeable, "pull request mergeable state"),
|
|
267
|
+
mergeableState:
|
|
268
|
+
item.mergeable_state === null
|
|
269
|
+
? null
|
|
270
|
+
: requiredString(
|
|
271
|
+
item.mergeable_state,
|
|
272
|
+
"pull request mergeable state",
|
|
273
|
+
),
|
|
274
|
+
} satisfies GitHubPullRequestSummary;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async checkMember(
|
|
278
|
+
repository: GitHubRepositoryRef,
|
|
279
|
+
username: string,
|
|
280
|
+
): Promise<GitHubMemberCheck> {
|
|
281
|
+
const member = username.trim();
|
|
282
|
+
if (!member) throw new Error("GitHub member username is required");
|
|
283
|
+
const path = `${repositoryPath(repository)}/collaborators/${encodeURIComponent(member)}/permission`;
|
|
284
|
+
try {
|
|
285
|
+
const item = record(await request<unknown>(path));
|
|
286
|
+
const permission = item.permission;
|
|
287
|
+
if (
|
|
288
|
+
permission !== "admin" &&
|
|
289
|
+
permission !== "maintain" &&
|
|
290
|
+
permission !== "push" &&
|
|
291
|
+
permission !== "triage" &&
|
|
292
|
+
permission !== "pull"
|
|
293
|
+
) {
|
|
294
|
+
throw new Error("GitHub member response has an invalid permission");
|
|
295
|
+
}
|
|
296
|
+
return { username: member, isMember: true, permission };
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (
|
|
299
|
+
error instanceof Error &&
|
|
300
|
+
error.message.startsWith("GitHub API request failed: HTTP 404")
|
|
301
|
+
) {
|
|
302
|
+
return { username: member, isMember: false, permission: null };
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
async checkOrganizationMember(
|
|
309
|
+
organization: string,
|
|
310
|
+
username: string,
|
|
311
|
+
): Promise<GitHubMemberCheck> {
|
|
312
|
+
const org = organization.trim();
|
|
313
|
+
const member = username.trim();
|
|
314
|
+
if (!org || !member) {
|
|
315
|
+
throw new Error("GitHub organization and member username are required");
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await request<undefined>(
|
|
319
|
+
`/orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(member)}`,
|
|
320
|
+
{},
|
|
321
|
+
{ allowEmpty: true },
|
|
322
|
+
);
|
|
323
|
+
return { username: member, isMember: true, permission: null };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (
|
|
326
|
+
error instanceof Error &&
|
|
327
|
+
error.message.startsWith("GitHub API request failed: HTTP 404")
|
|
328
|
+
) {
|
|
329
|
+
return { username: member, isMember: false, permission: null };
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
async approvePullRequest(
|
|
336
|
+
repository: GitHubRepositoryRef,
|
|
337
|
+
pullRequestNumber: number,
|
|
338
|
+
body?: string,
|
|
339
|
+
): Promise<GitHubApproval> {
|
|
340
|
+
if (!Number.isInteger(pullRequestNumber) || pullRequestNumber < 1)
|
|
341
|
+
throw new Error(
|
|
342
|
+
"GitHub pull request number must be a positive integer",
|
|
343
|
+
);
|
|
344
|
+
const item = record(
|
|
345
|
+
await request<unknown>(
|
|
346
|
+
`${repositoryPath(repository)}/pulls/${pullRequestNumber}/reviews`,
|
|
347
|
+
{
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: { "Content-Type": "application/json" },
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
event: "APPROVE",
|
|
352
|
+
...(body ? { body: body.slice(0, 4_000) } : {}),
|
|
353
|
+
}),
|
|
354
|
+
},
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
return {
|
|
358
|
+
id: requiredNumber(item.id, "approval id"),
|
|
359
|
+
state: "APPROVED",
|
|
360
|
+
htmlUrl: requiredString(item.html_url, "approval URL"),
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async mergePullRequest(
|
|
365
|
+
repository: GitHubRepositoryRef,
|
|
366
|
+
pullRequestNumber: number,
|
|
367
|
+
commitMessage?: string,
|
|
368
|
+
expectedHeadSha?: string,
|
|
369
|
+
): Promise<GitHubMergeResult> {
|
|
370
|
+
if (!Number.isInteger(pullRequestNumber) || pullRequestNumber < 1)
|
|
371
|
+
throw new Error(
|
|
372
|
+
"GitHub pull request number must be a positive integer",
|
|
373
|
+
);
|
|
374
|
+
const item = record(
|
|
375
|
+
await request<unknown>(
|
|
376
|
+
`${repositoryPath(repository)}/pulls/${pullRequestNumber}/merge`,
|
|
377
|
+
{
|
|
378
|
+
method: "PUT",
|
|
379
|
+
headers: { "Content-Type": "application/json" },
|
|
380
|
+
body: JSON.stringify(
|
|
381
|
+
commitMessage
|
|
382
|
+
? {
|
|
383
|
+
commit_message: commitMessage.slice(0, 4_000),
|
|
384
|
+
...(expectedHeadSha ? { sha: expectedHeadSha } : {}),
|
|
385
|
+
}
|
|
386
|
+
: expectedHeadSha
|
|
387
|
+
? { sha: expectedHeadSha }
|
|
388
|
+
: {},
|
|
389
|
+
),
|
|
390
|
+
},
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
if (item.merged !== true)
|
|
394
|
+
throw new Error(
|
|
395
|
+
`GitHub merge was not completed: ${requiredString(item.message, "merge message")}`,
|
|
396
|
+
);
|
|
397
|
+
return {
|
|
398
|
+
sha: requiredString(item.sha, "merge SHA"),
|
|
399
|
+
merged: true,
|
|
400
|
+
message: requiredString(item.message, "merge message"),
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type TriageMetadata = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export function parseTriageMetadata(value: string): TriageMetadata {
|
|
4
|
+
let parsed: unknown;
|
|
5
|
+
try {
|
|
6
|
+
parsed = JSON.parse(value);
|
|
7
|
+
} catch {
|
|
8
|
+
throw new Error("Factory item metadata is unreadable.");
|
|
9
|
+
}
|
|
10
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11
|
+
throw new Error("Factory item metadata must be an object.");
|
|
12
|
+
}
|
|
13
|
+
return parsed as TriageMetadata;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function serializeTriageMetadata(value: TriageMetadata): string {
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mergeTriageMetadata(
|
|
21
|
+
existingJson: string,
|
|
22
|
+
incoming: TriageMetadata,
|
|
23
|
+
): string {
|
|
24
|
+
return serializeTriageMetadata({
|
|
25
|
+
...parseTriageMetadata(existingJson),
|
|
26
|
+
...incoming,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function metadataString(
|
|
31
|
+
metadata: TriageMetadata,
|
|
32
|
+
key: string,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
const value = metadata[key];
|
|
35
|
+
return typeof value === "string" ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function metadataBoolean(
|
|
39
|
+
metadata: TriageMetadata,
|
|
40
|
+
key: string,
|
|
41
|
+
): boolean | undefined {
|
|
42
|
+
const value = metadata[key];
|
|
43
|
+
return typeof value === "boolean" ? value : undefined;
|
|
44
|
+
}
|
|
@@ -41,16 +41,16 @@ function parseMissingChangesetPackages(log: string | undefined): string[] {
|
|
|
41
41
|
.filter((pkg) => pkg.length > 0);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// resolution lookup fails.
|
|
44
|
+
// A reply is an explicit human response even when the provider still reports
|
|
45
|
+
// the thread as unresolved. That covers a deliberate "won't fix" explanation;
|
|
46
|
+
// provider resolution handles outdated threads with no reply.
|
|
48
47
|
function isAnswered(
|
|
49
48
|
comment: ReviewCommentObservation,
|
|
50
49
|
repliedToIds: ReadonlySet<string>,
|
|
51
50
|
): boolean {
|
|
51
|
+
if (repliedToIds.has(comment.id)) return true;
|
|
52
52
|
if (comment.isResolved !== undefined) return comment.isResolved;
|
|
53
|
-
return
|
|
53
|
+
return false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function reconcileBabysitState(input: BabysitInput): BabysitProposal {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { GuardResult } from "./contracts.js";
|
|
2
|
+
|
|
3
|
+
export type OwnerOwnedArea = "clips" | "design" | "content";
|
|
4
|
+
|
|
5
|
+
export interface PullRequestGovernanceInput {
|
|
6
|
+
author: string;
|
|
7
|
+
repository: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
summary?: string | null;
|
|
10
|
+
changedFiles: readonly string[];
|
|
11
|
+
clearBug: boolean;
|
|
12
|
+
productUxImplications: boolean;
|
|
13
|
+
checksPassed: boolean;
|
|
14
|
+
reviewFeedbackHandled: boolean;
|
|
15
|
+
openNonDraft: boolean;
|
|
16
|
+
internalBuilderMember: boolean;
|
|
17
|
+
factoryTriggered: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PullRequestGovernanceDecision {
|
|
21
|
+
ownerOwnedArea: OwnerOwnedArea | null;
|
|
22
|
+
autoApprove: boolean;
|
|
23
|
+
autoMerge: boolean;
|
|
24
|
+
reason: string;
|
|
25
|
+
guardResults: GuardResult[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function detectOwnerOwnedArea(
|
|
29
|
+
values: readonly (string | null | undefined)[],
|
|
30
|
+
): OwnerOwnedArea | null {
|
|
31
|
+
const text = values
|
|
32
|
+
.filter((value): value is string => typeof value === "string")
|
|
33
|
+
.join("\n")
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
const paths = text.split(/\s+/).filter((value) => value.includes("/"));
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
/(^|\b)(clips app|clips desktop|clips chrome extension|clips bug)\b/.test(
|
|
39
|
+
text,
|
|
40
|
+
) ||
|
|
41
|
+
/(^|\n)\s*clips(?:\s+app)?\s*[:\-]/m.test(text) ||
|
|
42
|
+
paths.some((path) => /(^|[/_-])clips([/_-]|$)/.test(path))
|
|
43
|
+
) {
|
|
44
|
+
return "clips";
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
/(^|\b)(design app|design bug)\b/.test(text) ||
|
|
48
|
+
/(^|\n)\s*design(?:\s+(?:app|generation))?\s*[:\-]/m.test(text) ||
|
|
49
|
+
paths.some((path) => /(^|[/_-])design([/_-]|$)/.test(path))
|
|
50
|
+
) {
|
|
51
|
+
return "design";
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
/(^|\b)(content app|content bug)\b/.test(text) ||
|
|
55
|
+
/(^|\n)\s*content(?:\s+app)?\s*[:\-]/m.test(text) ||
|
|
56
|
+
paths.some((path) => /(^|[/_-])content([/_-]|$)/.test(path))
|
|
57
|
+
) {
|
|
58
|
+
return "content";
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decidePullRequestGovernance(
|
|
64
|
+
input: PullRequestGovernanceInput,
|
|
65
|
+
): PullRequestGovernanceDecision {
|
|
66
|
+
const ownerOwnedArea = detectOwnerOwnedArea([
|
|
67
|
+
input.repository,
|
|
68
|
+
input.title,
|
|
69
|
+
input.summary,
|
|
70
|
+
...input.changedFiles,
|
|
71
|
+
]);
|
|
72
|
+
const gates: GuardResult[] = [
|
|
73
|
+
{
|
|
74
|
+
code: "identity",
|
|
75
|
+
passed: input.internalBuilderMember,
|
|
76
|
+
reason: input.internalBuilderMember
|
|
77
|
+
? "The pull-request author is a member of the BuilderIO organization."
|
|
78
|
+
: "The pull-request author is not verified as a BuilderIO organization member.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: "unknown_change",
|
|
82
|
+
passed: input.clearBug,
|
|
83
|
+
reason: input.clearBug
|
|
84
|
+
? "The automation classified this as a clear bug with a concrete failure signal."
|
|
85
|
+
: "The automation did not establish a clear bug; product requests and guesses stay manual.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: "security",
|
|
89
|
+
passed: input.checksPassed,
|
|
90
|
+
reason: input.checksPassed
|
|
91
|
+
? "All observed CI checks passed."
|
|
92
|
+
: "CI is failing, cancelled, pending, or unavailable.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
code: "unknown_change",
|
|
96
|
+
passed: input.openNonDraft,
|
|
97
|
+
reason: input.openNonDraft
|
|
98
|
+
? "The pull request is open and ready for review."
|
|
99
|
+
: "Draft or closed pull requests are not eligible for autonomous approval.",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
code: "unknown_change",
|
|
103
|
+
passed: input.reviewFeedbackHandled,
|
|
104
|
+
reason: input.reviewFeedbackHandled
|
|
105
|
+
? "All observed review feedback is fixed, resolved, replied to, or outdated."
|
|
106
|
+
: "Review feedback is unanswered, unresolved, truncated, or otherwise unknown.",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
if (ownerOwnedArea) {
|
|
111
|
+
gates.push({
|
|
112
|
+
code: "owner_owned",
|
|
113
|
+
passed: false,
|
|
114
|
+
reason: `${ownerOwnedArea} is owner-managed and is never auto-approved, auto-merged, or dispatched by this Factory.`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (input.productUxImplications) {
|
|
118
|
+
gates.push({
|
|
119
|
+
code: "unknown_change",
|
|
120
|
+
passed: false,
|
|
121
|
+
reason: "Product or UX implications require the owning human to decide.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const baseEligible = gates.every((gate) => gate.passed);
|
|
126
|
+
const autoApprove = baseEligible;
|
|
127
|
+
const autoMerge = baseEligible && input.factoryTriggered;
|
|
128
|
+
const reason = autoMerge
|
|
129
|
+
? "Clear internal bug fix, all CI and review gates are clean, and the PR came from the Factory Builder flow."
|
|
130
|
+
: autoApprove
|
|
131
|
+
? "Clear internal bug fix with clean CI and review gates; approval is safe to automate."
|
|
132
|
+
: gates
|
|
133
|
+
.filter((gate) => !gate.passed)
|
|
134
|
+
.map((gate) => gate.reason)
|
|
135
|
+
.join(" ");
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
ownerOwnedArea,
|
|
139
|
+
autoApprove,
|
|
140
|
+
autoMerge,
|
|
141
|
+
reason,
|
|
142
|
+
guardResults: gates,
|
|
143
|
+
};
|
|
144
|
+
}
|