@bridge-ds/mcp 0.2.0 → 0.3.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/README.md +5 -5
- package/dist/{chunk-6JO2AG6X.js → chunk-GABRGCKY.js} +632 -76
- package/dist/{chunk-FJL6UTZG.js → chunk-X455XY4E.js} +418 -17
- package/dist/drafts.d.ts +4 -6
- package/dist/explorations.d.ts +78 -5
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +16 -2
- package/dist/ops.d.ts +19 -1
- package/dist/server.d.ts +2 -1
- package/dist/{src-ABJPOTKE.js → src-XSBCZV4W.js} +1 -1
- package/package.json +3 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CONFIG_FILE,
|
|
3
3
|
DEFAULT_PROTECTED_BRANCHES,
|
|
4
|
+
DEFAULT_RIGHTS,
|
|
4
5
|
LayeredTokenSet,
|
|
5
6
|
ProtectedBranchError,
|
|
6
7
|
RISK_LABELS,
|
|
8
|
+
TeamDirectory,
|
|
7
9
|
__export,
|
|
8
10
|
aliasOf,
|
|
11
|
+
approverEntries,
|
|
9
12
|
assertSafeBranch,
|
|
10
13
|
baseBranchOf,
|
|
11
14
|
branchExists,
|
|
@@ -21,10 +24,12 @@ import {
|
|
|
21
24
|
contrastPair,
|
|
22
25
|
currentBranch,
|
|
23
26
|
diffLayered,
|
|
27
|
+
entryLabel,
|
|
28
|
+
expandOwners,
|
|
29
|
+
findPreviewComment,
|
|
24
30
|
git,
|
|
25
31
|
globToRegExp,
|
|
26
32
|
hexToRgba,
|
|
27
|
-
installCommand,
|
|
28
33
|
isGitRepo,
|
|
29
34
|
layeredImpact,
|
|
30
35
|
layeredParityIssues,
|
|
@@ -33,19 +38,23 @@ import {
|
|
|
33
38
|
originRepo,
|
|
34
39
|
parseContrastChecks,
|
|
35
40
|
previewLinks,
|
|
41
|
+
previewsSettled,
|
|
36
42
|
protectedBranchesOf,
|
|
43
|
+
readPreviewComment,
|
|
37
44
|
renderedColor,
|
|
45
|
+
resolveBranchPreviews,
|
|
38
46
|
rgbaToDtcg,
|
|
39
47
|
rgbaToHex,
|
|
40
48
|
runLayeredContrast,
|
|
41
49
|
selectTokenFiles,
|
|
50
|
+
shipPolicy,
|
|
42
51
|
showFile,
|
|
43
|
-
snapshotUrl,
|
|
44
|
-
tarballUrl,
|
|
45
52
|
toConfigJson,
|
|
46
53
|
tokensPath,
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
unreadableTeams,
|
|
55
|
+
validateLayered,
|
|
56
|
+
whoCanShipSentence
|
|
57
|
+
} from "./chunk-X455XY4E.js";
|
|
49
58
|
|
|
50
59
|
// src/lib.ts
|
|
51
60
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -704,15 +713,67 @@ function applyTokenEdits(base, edits) {
|
|
|
704
713
|
|
|
705
714
|
// ../core/src/github-client.ts
|
|
706
715
|
var GitHubError = class extends Error {
|
|
707
|
-
constructor(status, message, detail) {
|
|
716
|
+
constructor(status, message, detail, info = {}) {
|
|
708
717
|
super(message);
|
|
709
718
|
this.status = status;
|
|
710
719
|
this.detail = detail;
|
|
711
720
|
this.name = "GitHubError";
|
|
721
|
+
this.auth = info.auth;
|
|
722
|
+
this.permission = info.permission;
|
|
712
723
|
}
|
|
713
724
|
status;
|
|
714
725
|
detail;
|
|
726
|
+
/** Set when the failure is about the token: rejected (401) or missing a permission (403). */
|
|
727
|
+
auth;
|
|
728
|
+
/** With auth "missing-permission": the permission the call needed, when known. */
|
|
729
|
+
permission;
|
|
715
730
|
};
|
|
731
|
+
var PERMISSION_LABELS = {
|
|
732
|
+
issues: "Issues",
|
|
733
|
+
pull_requests: "Pull requests",
|
|
734
|
+
contents: "Contents",
|
|
735
|
+
checks: "Checks",
|
|
736
|
+
statuses: "Commit statuses",
|
|
737
|
+
metadata: "Metadata",
|
|
738
|
+
actions: "Actions",
|
|
739
|
+
deployments: "Deployments",
|
|
740
|
+
workflows: "Workflows",
|
|
741
|
+
administration: "Administration"
|
|
742
|
+
};
|
|
743
|
+
function permissionLabel(permission) {
|
|
744
|
+
return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
745
|
+
}
|
|
746
|
+
var TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
|
|
747
|
+
function missingPermissionMessage(permission, action) {
|
|
748
|
+
const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
|
|
749
|
+
if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
|
|
750
|
+
return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
|
|
751
|
+
}
|
|
752
|
+
function requiredPermission(path, acceptedHeader) {
|
|
753
|
+
if (acceptedHeader) {
|
|
754
|
+
const first = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
|
|
755
|
+
if (first.length) return first[0];
|
|
756
|
+
}
|
|
757
|
+
const p = path.replace(/[?#].*$/, "");
|
|
758
|
+
if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
|
|
759
|
+
if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
|
|
760
|
+
if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
|
|
761
|
+
if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
|
|
762
|
+
if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
|
|
763
|
+
if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
|
|
764
|
+
if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
|
|
765
|
+
if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
var PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
|
|
769
|
+
function classifyAuth(status, detail, opts = {}) {
|
|
770
|
+
if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
|
|
771
|
+
if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
|
|
772
|
+
if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
|
|
773
|
+
return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
|
|
774
|
+
}
|
|
775
|
+
return {};
|
|
776
|
+
}
|
|
716
777
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
717
778
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
718
779
|
var API = "https://api.github.com";
|
|
@@ -774,7 +835,7 @@ var GitHubClient = class _GitHubClient {
|
|
|
774
835
|
}
|
|
775
836
|
}
|
|
776
837
|
assertToken(what) {
|
|
777
|
-
if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only
|
|
838
|
+
if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only.`, "no-token");
|
|
778
839
|
}
|
|
779
840
|
get repoPath() {
|
|
780
841
|
return `/repos/${this.owner}/${this.name}`;
|
|
@@ -823,7 +884,12 @@ var GitHubClient = class _GitHubClient {
|
|
|
823
884
|
detail
|
|
824
885
|
);
|
|
825
886
|
}
|
|
826
|
-
|
|
887
|
+
const info = classifyAuth(res.status, detail, {
|
|
888
|
+
path,
|
|
889
|
+
acceptedPermissions: res.headers?.get("x-accepted-github-permissions"),
|
|
890
|
+
withToken: Boolean(this.token)
|
|
891
|
+
});
|
|
892
|
+
throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
|
|
827
893
|
}
|
|
828
894
|
return await res.json();
|
|
829
895
|
}
|
|
@@ -1002,6 +1068,11 @@ var GitHubClient = class _GitHubClient {
|
|
|
1002
1068
|
async addComment(issueNumber, body) {
|
|
1003
1069
|
await this.request("POST", `${this.repoPath}/issues/${issueNumber}/comments`, { body });
|
|
1004
1070
|
}
|
|
1071
|
+
/** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
|
|
1072
|
+
async listIssueComments(issueNumber) {
|
|
1073
|
+
const r = await this.request("GET", `${this.repoPath}/issues/${issueNumber}/comments?per_page=100`);
|
|
1074
|
+
return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at }));
|
|
1075
|
+
}
|
|
1005
1076
|
// --- Issues (component change requests) ---------------------------------------------------
|
|
1006
1077
|
async createIssue(input) {
|
|
1007
1078
|
const r = await this.request("POST", `${this.repoPath}/issues`, {
|
|
@@ -1022,6 +1093,67 @@ var GitHubClient = class _GitHubClient {
|
|
|
1022
1093
|
async getIssue(n) {
|
|
1023
1094
|
return toIssue(await this.request("GET", `${this.repoPath}/issues/${n}`));
|
|
1024
1095
|
}
|
|
1096
|
+
/** Edits an issue: its body, or closes it with a reason ("not_planned" for a declined request). */
|
|
1097
|
+
async updateIssue(n, patch) {
|
|
1098
|
+
this.assertToken("Editing an issue");
|
|
1099
|
+
return toIssue(await this.request("PATCH", `${this.repoPath}/issues/${n}`, patch));
|
|
1100
|
+
}
|
|
1101
|
+
/** Adds people to an issue's assignees (GitHub ignores logins that cannot be assigned: the result says who is). */
|
|
1102
|
+
async addAssignees(n, logins) {
|
|
1103
|
+
this.assertToken("Assigning an issue");
|
|
1104
|
+
const r = await this.request("POST", `${this.repoPath}/issues/${n}/assignees`, { assignees: logins });
|
|
1105
|
+
return (r.assignees ?? []).map((a) => a.login);
|
|
1106
|
+
}
|
|
1107
|
+
/** People with at least `permission` on the repository (reading the list needs push access). First 100. */
|
|
1108
|
+
async listCollaborators(permission = "push") {
|
|
1109
|
+
const r = await this.request("GET", `${this.repoPath}/collaborators?permission=${permission}&per_page=100`);
|
|
1110
|
+
return r.map((c) => ({ login: c.login, avatarUrl: c.avatar_url ?? "" }));
|
|
1111
|
+
}
|
|
1112
|
+
/** Whether a token is sent with each request (TeamReader). */
|
|
1113
|
+
get hasToken() {
|
|
1114
|
+
return Boolean(this.token);
|
|
1115
|
+
}
|
|
1116
|
+
/** Every page of a list endpoint (per_page 100), up to `maxPages`. */
|
|
1117
|
+
async paginate(path, maxPages = 20) {
|
|
1118
|
+
const out = [];
|
|
1119
|
+
const sep2 = path.includes("?") ? "&" : "?";
|
|
1120
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
1121
|
+
const r = await this.request("GET", `${path}${sep2}per_page=100&page=${page}`);
|
|
1122
|
+
out.push(...r);
|
|
1123
|
+
if (r.length < 100) break;
|
|
1124
|
+
}
|
|
1125
|
+
return out;
|
|
1126
|
+
}
|
|
1127
|
+
/** Every member login of an organization team (needs read:org, or Members: Read on a fine-grained token). */
|
|
1128
|
+
async listTeamMembers(org, slug2) {
|
|
1129
|
+
const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug2)}/members`);
|
|
1130
|
+
return r.map((m) => m.login);
|
|
1131
|
+
}
|
|
1132
|
+
/** A user's membership of a team: "active", "pending", or null when GitHub says they are not a member (404). */
|
|
1133
|
+
async getTeamMembership(org, slug2, login) {
|
|
1134
|
+
try {
|
|
1135
|
+
const r = await this.request("GET", `/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug2)}/memberships/${encodeURIComponent(login)}`);
|
|
1136
|
+
return r.state === "active" ? "active" : "pending";
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
1139
|
+
throw err;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/** "User" or "Organization" for a login, null when GitHub does not know it. */
|
|
1143
|
+
async getAccountType(login) {
|
|
1144
|
+
try {
|
|
1145
|
+
const r = await this.request("GET", `/users/${encodeURIComponent(login)}`);
|
|
1146
|
+
return r.type === "Organization" ? "Organization" : r.type === "User" ? "User" : null;
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
1149
|
+
throw err;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/** The organization's teams this token can see. */
|
|
1153
|
+
async listOrgTeams(org) {
|
|
1154
|
+
const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams`);
|
|
1155
|
+
return r.map((t) => ({ slug: t.slug, name: t.name }));
|
|
1156
|
+
}
|
|
1025
1157
|
/** Create or update a binary file (base64 content) on a branch, e.g. a Figma export attached to a request. */
|
|
1026
1158
|
async putBase64File(input) {
|
|
1027
1159
|
this.assertWritableBranch(input.branch);
|
|
@@ -1157,6 +1289,27 @@ var GitHubClient = class _GitHubClient {
|
|
|
1157
1289
|
const pending = checks.some((c) => c.status !== "completed");
|
|
1158
1290
|
return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
|
|
1159
1291
|
}
|
|
1292
|
+
/** Commit statuses of a commit, newest first (Vercel reports each project as "Vercel – <project>"). */
|
|
1293
|
+
async listCommitStatuses(sha) {
|
|
1294
|
+
const r = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/statuses?per_page=100`);
|
|
1295
|
+
return r.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url ?? null, description: s.description ?? null }));
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* GitHub deployments of a commit with their latest status (environment "Preview – <project>", environment_url the
|
|
1299
|
+
* preview). Latest per environment only, so it costs one read plus one per environment.
|
|
1300
|
+
*/
|
|
1301
|
+
async listDeployments(sha) {
|
|
1302
|
+
const list = await this.request("GET", `${this.repoPath}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
|
|
1303
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1304
|
+
for (const d of list) if (!latest.has(d.environment)) latest.set(d.environment, d);
|
|
1305
|
+
return Promise.all(
|
|
1306
|
+
[...latest.values()].map(async (d) => {
|
|
1307
|
+
const st = await this.request("GET", `${this.repoPath}/deployments/${d.id}/statuses?per_page=1`).catch(() => []);
|
|
1308
|
+
const s = st[0];
|
|
1309
|
+
return { id: d.id, environment: d.environment, state: s?.state ?? null, environmentUrl: s?.environment_url || null };
|
|
1310
|
+
})
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1160
1313
|
async getPullRequest(n) {
|
|
1161
1314
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
1162
1315
|
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null };
|
|
@@ -1183,7 +1336,44 @@ var GitHubClient = class _GitHubClient {
|
|
|
1183
1336
|
"GET",
|
|
1184
1337
|
`${this.repoPath}/commits/${encodeRef(ref)}/check-runs`
|
|
1185
1338
|
);
|
|
1186
|
-
return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url }));
|
|
1339
|
+
return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url, ...c.app?.slug ? { app: c.app.slug } : {} }));
|
|
1340
|
+
}
|
|
1341
|
+
/** Rulesets that apply to the repository, without their rules (GET /rulesets; read access is enough). */
|
|
1342
|
+
async listRulesets() {
|
|
1343
|
+
const r = await this.request("GET", `${this.repoPath}/rulesets?includes_parents=true&per_page=100`);
|
|
1344
|
+
return r.map(withRulesetUrl);
|
|
1345
|
+
}
|
|
1346
|
+
/** One ruleset with its rules. */
|
|
1347
|
+
async getRuleset(id) {
|
|
1348
|
+
return withRulesetUrl(await this.request("GET", `${this.repoPath}/rulesets/${id}`));
|
|
1349
|
+
}
|
|
1350
|
+
/** Creates a repository ruleset. Needs admin on the repo (fine-grained token: Administration read and write). */
|
|
1351
|
+
async createRuleset(payload) {
|
|
1352
|
+
this.assertToken("Protecting the library");
|
|
1353
|
+
return withRulesetUrl(await this.request("POST", `${this.repoPath}/rulesets`, payload));
|
|
1354
|
+
}
|
|
1355
|
+
/** Replaces a repository ruleset (same permissions as createRuleset). */
|
|
1356
|
+
async updateRuleset(id, payload) {
|
|
1357
|
+
this.assertToken("Protecting the library");
|
|
1358
|
+
return withRulesetUrl(await this.request("PUT", `${this.repoPath}/rulesets/${id}`, payload));
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Classic branch protection of `branch`: the settings, null when the branch has none (404), undefined when this
|
|
1362
|
+
* token cannot read them (403: the endpoint needs admin). On a 403 the public branch summary still tells
|
|
1363
|
+
* whether the branch is protected at all: `protected: false` means there is nothing to read, so null.
|
|
1364
|
+
*/
|
|
1365
|
+
async getBranchProtection(branch) {
|
|
1366
|
+
try {
|
|
1367
|
+
return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
if (!(err instanceof GitHubError)) throw err;
|
|
1370
|
+
if (err.status === 404) return null;
|
|
1371
|
+
if (err.status === 403 || err.status === 401) {
|
|
1372
|
+
const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
|
|
1373
|
+
return b?.protected === false ? null : void 0;
|
|
1374
|
+
}
|
|
1375
|
+
throw err;
|
|
1376
|
+
}
|
|
1187
1377
|
}
|
|
1188
1378
|
/** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
|
|
1189
1379
|
async createCheckRun(input) {
|
|
@@ -1225,7 +1415,9 @@ function toSummary(r) {
|
|
|
1225
1415
|
...typeof r.body === "string" ? { body: r.body } : {}
|
|
1226
1416
|
};
|
|
1227
1417
|
}
|
|
1228
|
-
function friendlyMessage(status, detail) {
|
|
1418
|
+
function friendlyMessage(status, detail, info = {}) {
|
|
1419
|
+
if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
|
|
1420
|
+
if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
|
|
1229
1421
|
switch (status) {
|
|
1230
1422
|
case 401:
|
|
1231
1423
|
return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
|
|
@@ -1245,6 +1437,10 @@ function friendlyMessage(status, detail) {
|
|
|
1245
1437
|
return `GitHub error ${status}${detail ? `: ${detail}` : ""}`;
|
|
1246
1438
|
}
|
|
1247
1439
|
}
|
|
1440
|
+
function withRulesetUrl(r) {
|
|
1441
|
+
const href = r._links?.html?.href;
|
|
1442
|
+
return href && !r.html_url ? { ...r, html_url: href } : r;
|
|
1443
|
+
}
|
|
1248
1444
|
function toIssue(r) {
|
|
1249
1445
|
return {
|
|
1250
1446
|
number: r.number,
|
|
@@ -1850,6 +2046,38 @@ async function loadGitHubHistory(client2, config, opts = {}) {
|
|
|
1850
2046
|
return buildReport({ source: "github", label: `${client2.fullName}@${ref}`, ref, entries, releases, ...currentVersion ? { currentVersion } : {}, truncated: listed >= limit });
|
|
1851
2047
|
}
|
|
1852
2048
|
|
|
2049
|
+
// ../core/src/previews-github.ts
|
|
2050
|
+
async function branchPreviewUrls(gh, ref, config, opts = {}) {
|
|
2051
|
+
const apps = config?.previews?.apps ?? {};
|
|
2052
|
+
if (!Object.values(apps).some(Boolean)) return [];
|
|
2053
|
+
let pr;
|
|
2054
|
+
let branch = opts.branch;
|
|
2055
|
+
let sha = opts.sha;
|
|
2056
|
+
if (typeof ref === "number") {
|
|
2057
|
+
pr = ref;
|
|
2058
|
+
if (!branch || !sha) {
|
|
2059
|
+
const p = await gh.getPullRequest(ref);
|
|
2060
|
+
branch ??= p.headRef;
|
|
2061
|
+
sha ??= p.headSha;
|
|
2062
|
+
}
|
|
2063
|
+
} else {
|
|
2064
|
+
sha = ref;
|
|
2065
|
+
if (!branch || !opts.comments) {
|
|
2066
|
+
const open = (await gh.listPullRequestsForCommit(ref).catch(() => [])).find((p) => p.state === "open");
|
|
2067
|
+
if (open) {
|
|
2068
|
+
pr = open.number;
|
|
2069
|
+
branch ??= open.headRef;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
const [comments, statuses, deployments] = await Promise.all([
|
|
2074
|
+
opts.comments ? Promise.resolve(opts.comments) : pr !== void 0 ? gh.listIssueComments(pr).catch(() => []) : Promise.resolve([]),
|
|
2075
|
+
sha ? gh.listCommitStatuses(sha).catch(() => []) : Promise.resolve([]),
|
|
2076
|
+
sha ? gh.listDeployments(sha).catch(() => []) : Promise.resolve([])
|
|
2077
|
+
]);
|
|
2078
|
+
return resolveBranchPreviews(apps, { branch: branch ?? "", ...pr !== void 0 ? { pr } : {}, ...sha ? { sha } : {} }, { comments, statuses, deployments });
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1853
2081
|
// ../core/src/ramps.js
|
|
1854
2082
|
var STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
1855
2083
|
var LIGHTNESS = { 50: 0.975, 100: 0.952, 200: 0.912, 300: 0.85, 400: 0.735, 500: 0.63, 600: 0.53, 700: 0.445, 800: 0.36, 900: 0.27, 950: 0.205 };
|
|
@@ -2378,6 +2606,7 @@ function composeNewBrandBody(plan, opts) {
|
|
|
2378
2606
|
}
|
|
2379
2607
|
|
|
2380
2608
|
// ../core/src/docs-draft.ts
|
|
2609
|
+
var DOCS_KEYS = ["summary", "preview", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines", "anatomy"];
|
|
2381
2610
|
var PROSE_KEYS = ["summary", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines"];
|
|
2382
2611
|
var ACRONYMS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "xs"]);
|
|
2383
2612
|
function anatomyLabel(className) {
|
|
@@ -2430,6 +2659,7 @@ function prefillDocs(input) {
|
|
|
2430
2659
|
contentGuidelines: existing?.contentGuidelines ?? [],
|
|
2431
2660
|
anatomy,
|
|
2432
2661
|
...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
|
|
2662
|
+
...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
|
|
2433
2663
|
...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
|
|
2434
2664
|
};
|
|
2435
2665
|
return {
|
|
@@ -2444,7 +2674,17 @@ function prefillDocs(input) {
|
|
|
2444
2674
|
draft
|
|
2445
2675
|
};
|
|
2446
2676
|
}
|
|
2447
|
-
|
|
2677
|
+
var SECTION_SHAPE = {
|
|
2678
|
+
summary: '"summary": "one or two sentences"',
|
|
2679
|
+
whenToUse: '"whenToUse": ["..."]',
|
|
2680
|
+
whenNotToUse: '"whenNotToUse": ["..."]',
|
|
2681
|
+
dos: '"dos": [{ "title": "...", "text": "..." }]',
|
|
2682
|
+
donts: '"donts": [{ "title": "...", "text": "..." }]',
|
|
2683
|
+
accessibility: '"accessibility": ["..."]',
|
|
2684
|
+
contentGuidelines: '"contentGuidelines": ["..."]'
|
|
2685
|
+
};
|
|
2686
|
+
var docsSectionShapes = (keys) => `{ ${keys.map((k) => SECTION_SHAPE[k]).join(", ")} }`;
|
|
2687
|
+
function docsPrompt(prefill, conventions = {}, options = {}) {
|
|
2448
2688
|
const facts = [
|
|
2449
2689
|
`Component: ${prefill.component}`,
|
|
2450
2690
|
prefill.propsTable ? `Props (parsed from TypeScript):
|
|
@@ -2459,15 +2699,33 @@ ${prefill.propsTable}` : "Props: none of its own.",
|
|
|
2459
2699
|
const block = (title, text) => text?.trim() ? `<${title}>
|
|
2460
2700
|
${text.trim()}
|
|
2461
2701
|
</${title}>` : "";
|
|
2702
|
+
const keys = options.keys;
|
|
2703
|
+
const check = keys ? (options.check ?? []).filter((k) => keys.includes(k)) : [];
|
|
2704
|
+
const write = keys ? keys.filter((k) => !check.includes(k)) : [];
|
|
2705
|
+
const task = keys ? [
|
|
2706
|
+
...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
|
|
2707
|
+
...check.length ? [
|
|
2708
|
+
`Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
|
|
2709
|
+
"Return a checked section ONLY when something in it is no longer true for the code (a wrong or removed prop, a removed state or variant, changed behavior). Then return the whole section with only the untrue parts fixed, keeping the writer's wording everywhere else.",
|
|
2710
|
+
"Leave a checked section out of the answer when it is still accurate: a missing key means unchanged. Never return a checked section just to improve its style."
|
|
2711
|
+
] : [],
|
|
2712
|
+
...options.rewrite ? ["Rewrite each one from the facts and the source: keep what is true in the current text, drop what the code does not support."] : []
|
|
2713
|
+
] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
|
|
2714
|
+
const shape = keys ? [
|
|
2715
|
+
`- ${check.length ? "Return" : "Write"} only these keys: ${keys.join(", ")}${check.length ? ` (the checked ones, ${check.join(", ")}, only when they need a fix)` : ""}. Never return anatomy, preview or any other key: code decides them.`,
|
|
2716
|
+
`- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
|
|
2717
|
+
] : [
|
|
2718
|
+
`- Fill only these keys: ${prefill.missing.join(", ") || "none (everything is written: improve nothing, return the draft as is)"}. Keep every other key exactly as given.`,
|
|
2719
|
+
"- Answer with the full docs.json as one JSON object, nothing else."
|
|
2720
|
+
];
|
|
2462
2721
|
return [
|
|
2463
|
-
|
|
2722
|
+
...task,
|
|
2464
2723
|
"",
|
|
2465
2724
|
"Rules:",
|
|
2466
2725
|
"- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
|
|
2467
2726
|
"- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
|
|
2468
2727
|
'- Keep the keys and shapes of the example docs.json. dos and donts are { "title", "text" } objects; add an "example" only with components and props that exist.',
|
|
2469
|
-
|
|
2470
|
-
"- Answer with the full docs.json as one JSON object, nothing else.",
|
|
2728
|
+
...shape,
|
|
2471
2729
|
"",
|
|
2472
2730
|
"Facts computed from the code:",
|
|
2473
2731
|
facts.join("\n\n"),
|
|
@@ -2476,11 +2734,179 @@ ${text.trim()}
|
|
|
2476
2734
|
${JSON.stringify(prefill.draft, null, 2)}`,
|
|
2477
2735
|
"",
|
|
2478
2736
|
block("source", conventions.source),
|
|
2737
|
+
block("styles", conventions.styles),
|
|
2479
2738
|
block("agents-md", conventions.agents),
|
|
2480
2739
|
block("voice", conventions.voice),
|
|
2481
2740
|
block("example-docs-json", conventions.example)
|
|
2482
2741
|
].filter((x, i, all) => x !== "" || all[i - 1] !== "").join("\n");
|
|
2483
2742
|
}
|
|
2743
|
+
function markClaudeDraft(d, sections2) {
|
|
2744
|
+
if (!sections2) {
|
|
2745
|
+
const { draftedSections: _all, ...rest } = d;
|
|
2746
|
+
return { ...rest, draftedBy: "claude", needsReview: true };
|
|
2747
|
+
}
|
|
2748
|
+
const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
|
|
2749
|
+
const union = PROSE_KEYS.filter((k) => waiting.includes(k) || sections2.includes(k));
|
|
2750
|
+
return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
|
|
2751
|
+
}
|
|
2752
|
+
function formatDocsJson(d) {
|
|
2753
|
+
const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k) => k !== "summary"), "draftedBy", "draftedSections", "needsReview"];
|
|
2754
|
+
const out = {};
|
|
2755
|
+
for (const k of order) if (d[k] !== void 0) out[k] = d[k];
|
|
2756
|
+
for (const [k, v] of Object.entries(d)) if (!(k in out) && v !== void 0) out[k] = v;
|
|
2757
|
+
return `${JSON.stringify(out, null, 2)}
|
|
2758
|
+
`;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// ../core/src/change-drafts.ts
|
|
2762
|
+
function cleanDraftText(text) {
|
|
2763
|
+
return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
|
|
2764
|
+
}
|
|
2765
|
+
var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
2766
|
+
var DRAFT_LINE_MAX = 120;
|
|
2767
|
+
function clipText(text, max) {
|
|
2768
|
+
const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
2769
|
+
if (t.length <= max) return t;
|
|
2770
|
+
const cut = t.slice(0, max + 1);
|
|
2771
|
+
const at = cut.lastIndexOf(" ");
|
|
2772
|
+
return (at > max * 0.5 ? cut.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
|
|
2773
|
+
}
|
|
2774
|
+
function sanitizeDraftLine(raw) {
|
|
2775
|
+
if (typeof raw !== "string") return "";
|
|
2776
|
+
return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// ../core/src/docs-ai.ts
|
|
2780
|
+
var DOCS_DRAFT_MODEL = "claude-sonnet-5-5";
|
|
2781
|
+
var ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
|
|
2782
|
+
var DOCS_SELECTION_MODES = {
|
|
2783
|
+
clearer: "Make it clearer: plain words, one idea per sentence, the most useful fact first.",
|
|
2784
|
+
shorter: "Make it shorter: keep every fact, drop filler, aim for half the words or fewer.",
|
|
2785
|
+
voice: "Put it in the Bridge voice: sentence case, verbs first, calm and precise, no marketing words, no em dash."
|
|
2786
|
+
};
|
|
2787
|
+
var isDocsSelectionMode = (m) => typeof m === "string" && m in DOCS_SELECTION_MODES;
|
|
2788
|
+
var isProseKey = (k) => typeof k === "string" && PROSE_KEYS.includes(k);
|
|
2789
|
+
function validateDocsTask(x) {
|
|
2790
|
+
if (!x || typeof x !== "object") return ["The docs task is missing."];
|
|
2791
|
+
const t = x;
|
|
2792
|
+
if (t.kind === "sections") {
|
|
2793
|
+
const keys = t.keys;
|
|
2794
|
+
if (!Array.isArray(keys) || !keys.length || !keys.every(isProseKey)) return [`The docs task lists no sections: use ${PROSE_KEYS.join(", ")}.`];
|
|
2795
|
+
if (t.check !== void 0 && (!Array.isArray(t.check) || !t.check.every(isProseKey))) return ["check must list prose sections."];
|
|
2796
|
+
return [];
|
|
2797
|
+
}
|
|
2798
|
+
if (t.kind === "selection") {
|
|
2799
|
+
const out = [];
|
|
2800
|
+
if (!isProseKey(t.key)) out.push(`The selection needs a section: ${PROSE_KEYS.join(", ")}.`);
|
|
2801
|
+
if (typeof t.text !== "string" || !t.text.trim()) out.push("The selection has no text.");
|
|
2802
|
+
if (!isDocsSelectionMode(t.mode)) out.push("The selection mode must be clearer, shorter or voice.");
|
|
2803
|
+
return out;
|
|
2804
|
+
}
|
|
2805
|
+
return ['The docs task kind must be "sections" or "selection".'];
|
|
2806
|
+
}
|
|
2807
|
+
var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
|
|
2808
|
+
function parseDocsReply(text) {
|
|
2809
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
2810
|
+
const body = fenced ? fenced[1] : text;
|
|
2811
|
+
const start = body.indexOf("{");
|
|
2812
|
+
const end = body.lastIndexOf("}");
|
|
2813
|
+
if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
|
|
2814
|
+
const json = JSON.parse(body.slice(start, end + 1));
|
|
2815
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("The reply is not a docs.json object.");
|
|
2816
|
+
return json;
|
|
2817
|
+
}
|
|
2818
|
+
var cleanDocsText = (s) => cleanDraftText(s.replace(/[\r\n]+/g, " "));
|
|
2819
|
+
var isExample = (x) => {
|
|
2820
|
+
if (typeof x === "string") return true;
|
|
2821
|
+
if (!x || typeof x !== "object" || Array.isArray(x)) return false;
|
|
2822
|
+
const n = x;
|
|
2823
|
+
if (typeof n.c !== "string" || !n.c) return false;
|
|
2824
|
+
if (n.p !== void 0 && (!n.p || typeof n.p !== "object" || Array.isArray(n.p))) return false;
|
|
2825
|
+
return n.children === void 0 || Array.isArray(n.children) && n.children.every(isExample);
|
|
2826
|
+
};
|
|
2827
|
+
var textList = (x) => {
|
|
2828
|
+
if (typeof x === "string") x = [x];
|
|
2829
|
+
if (!Array.isArray(x)) return void 0;
|
|
2830
|
+
const out = x.filter((s) => typeof s === "string").map(cleanDocsText).filter(Boolean);
|
|
2831
|
+
return out.length ? out : void 0;
|
|
2832
|
+
};
|
|
2833
|
+
var guidanceList = (x, examples) => {
|
|
2834
|
+
if (!Array.isArray(x)) return void 0;
|
|
2835
|
+
const out = [];
|
|
2836
|
+
for (const g of x) {
|
|
2837
|
+
if (!g || typeof g !== "object") continue;
|
|
2838
|
+
const r = g;
|
|
2839
|
+
const title = typeof r.title === "string" ? cleanDocsText(r.title) : "";
|
|
2840
|
+
const text = typeof r.text === "string" ? cleanDocsText(r.text) : "";
|
|
2841
|
+
if (!title || !text) continue;
|
|
2842
|
+
out.push({ title, text, ...examples && r.example !== void 0 && isExample(r.example) ? { example: r.example } : {} });
|
|
2843
|
+
}
|
|
2844
|
+
return out.length ? out : void 0;
|
|
2845
|
+
};
|
|
2846
|
+
function sanitizeDocsSections(raw, keys = PROSE_KEYS, opts = {}) {
|
|
2847
|
+
const out = {};
|
|
2848
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
|
2849
|
+
const r = raw;
|
|
2850
|
+
for (const k of keys) {
|
|
2851
|
+
const v = r[k];
|
|
2852
|
+
if (k === "summary") {
|
|
2853
|
+
const s = typeof v === "string" ? cleanDocsText(v) : "";
|
|
2854
|
+
if (s) out.summary = s;
|
|
2855
|
+
} else if (k === "dos" || k === "donts") {
|
|
2856
|
+
const g = guidanceList(v, opts.examples ?? false);
|
|
2857
|
+
if (g) out[k] = g;
|
|
2858
|
+
} else {
|
|
2859
|
+
const l = textList(v);
|
|
2860
|
+
if (l) out[k] = l;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
return out;
|
|
2864
|
+
}
|
|
2865
|
+
function sanitizeDocsReplacement(raw) {
|
|
2866
|
+
if (typeof raw !== "string") return "";
|
|
2867
|
+
return cleanDocsText(raw.trim().replace(/^["'“]+|["'”]+$/g, ""));
|
|
2868
|
+
}
|
|
2869
|
+
function docsSectionsAnswer(task, raw) {
|
|
2870
|
+
const check = (task.check ?? []).filter((k) => task.keys.includes(k));
|
|
2871
|
+
const sections2 = check.length ? sanitizeDocsSections(raw, task.keys) : docsSectionsResult(task, raw);
|
|
2872
|
+
return { sections: sections2, checked: check.filter((k) => sections2[k] === void 0) };
|
|
2873
|
+
}
|
|
2874
|
+
function docsSectionsResult(task, raw) {
|
|
2875
|
+
const sections2 = sanitizeDocsSections(raw, task.keys);
|
|
2876
|
+
if (!Object.keys(sections2).length) throw new Error(`Claude's answer holds none of the sections asked for (${task.keys.join(", ")}). Try again.`);
|
|
2877
|
+
return sections2;
|
|
2878
|
+
}
|
|
2879
|
+
function mergeDocsDraft(prefillDraft, reply) {
|
|
2880
|
+
const empty = PROSE_KEYS.filter((k) => {
|
|
2881
|
+
const v = prefillDraft[k];
|
|
2882
|
+
return v === void 0 || (typeof v === "string" ? !v.trim() : Array.isArray(v) && !v.length);
|
|
2883
|
+
});
|
|
2884
|
+
const sections2 = sanitizeDocsSections(reply, empty, { examples: true });
|
|
2885
|
+
const merged = { ...prefillDraft, ...sections2 };
|
|
2886
|
+
if (reply.preview && !prefillDraft.preview && isExample(reply.preview)) merged.preview = reply.preview;
|
|
2887
|
+
merged.anatomy = prefillDraft.anatomy;
|
|
2888
|
+
const drafted = PROSE_KEYS.filter((k) => sections2[k] !== void 0);
|
|
2889
|
+
return markClaudeDraft(merged, drafted.length && drafted.length < PROSE_KEYS.length ? drafted : void 0);
|
|
2890
|
+
}
|
|
2891
|
+
function anthropicRequest(input) {
|
|
2892
|
+
const headers = { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
|
|
2893
|
+
if (input.fallback) headers["anthropic-beta"] = "server-side-fallback-2026-07-01";
|
|
2894
|
+
if (input.browser) headers["anthropic-dangerous-direct-browser-access"] = "true";
|
|
2895
|
+
const body = {
|
|
2896
|
+
model: input.model || DOCS_DRAFT_MODEL,
|
|
2897
|
+
max_tokens: input.maxTokens ?? 16e3,
|
|
2898
|
+
...input.fallback ? { fallbacks: "default" } : {},
|
|
2899
|
+
...input.system ? { system: input.system } : {},
|
|
2900
|
+
messages: [{ role: "user", content: input.prompt }]
|
|
2901
|
+
};
|
|
2902
|
+
return { url: ANTHROPIC_MESSAGES_URL, init: { method: "POST", headers, body: JSON.stringify(body) } };
|
|
2903
|
+
}
|
|
2904
|
+
function anthropicReplyText(msg) {
|
|
2905
|
+
const m = msg ?? {};
|
|
2906
|
+
if (m.stop_reason === "refusal") throw new Error("Claude declined to draft this one.");
|
|
2907
|
+
if (m.stop_reason === "max_tokens") throw new Error("The reply was cut off (max_tokens).");
|
|
2908
|
+
return (m.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
2909
|
+
}
|
|
2484
2910
|
|
|
2485
2911
|
// ../core/src/explorations.ts
|
|
2486
2912
|
var EXPLORATION_LABEL = "exploration";
|
|
@@ -2535,16 +2961,6 @@ function explorationNote(e) {
|
|
|
2535
2961
|
""
|
|
2536
2962
|
].join("\n");
|
|
2537
2963
|
}
|
|
2538
|
-
function explorationInstall(input) {
|
|
2539
|
-
const packages = input.packages.map((name) => ({ name, url: snapshotUrl(input.owner, input.repo, name, input.pr) }));
|
|
2540
|
-
const named = (list) => installCommand(list.map((p) => `${p.name}@${p.url}`));
|
|
2541
|
-
const out = { command: named(packages), packages };
|
|
2542
|
-
if (input.sha) {
|
|
2543
|
-
const tarballs = input.packages.map((name) => ({ name, url: tarballUrl(input.owner, input.repo, input.pr, name, input.sha) }));
|
|
2544
|
-
out.fallback = { command: named(tarballs), packages: tarballs, release: `https://github.com/${input.owner}/${input.repo}/releases/tag/preview-pr-${input.pr}` };
|
|
2545
|
-
}
|
|
2546
|
-
return out;
|
|
2547
|
-
}
|
|
2548
2964
|
function explorationPrTitle(e) {
|
|
2549
2965
|
const n = e.name.trim().replace(/\.$/, "");
|
|
2550
2966
|
return `Exploration: ${n.charAt(0).toLowerCase()}${n.slice(1)}`;
|
|
@@ -2570,20 +2986,9 @@ function explorationPrBody(e, ctx) {
|
|
|
2570
2986
|
"### Try it in a product staging branch",
|
|
2571
2987
|
""
|
|
2572
2988
|
];
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
"In the product repo, on a branch that deploys to staging (production keeps the released version):",
|
|
2577
|
-
"",
|
|
2578
|
-
"```sh",
|
|
2579
|
-
install.command,
|
|
2580
|
-
"```",
|
|
2581
|
-
"",
|
|
2582
|
-
`The snapshots follow the latest commit of this pull request. The "Try this DS branch in your product" comment lists the exact commit, and the tarball URLs from the [\`preview-pr-${ctx.pr}\` prerelease](https://github.com/${ctx.owner}/${ctx.repo}/releases/tag/preview-pr-${ctx.pr}) when pkg.pr.new is not set up.`
|
|
2583
|
-
);
|
|
2584
|
-
} else {
|
|
2585
|
-
lines.push('The install command appears here and in the "Try this DS branch in your product" comment once the first snapshot is published.');
|
|
2586
|
-
}
|
|
2989
|
+
lines.push(
|
|
2990
|
+
'In the product repo, on a branch that deploys to staging (production keeps the released version), install the snapshot with the command in the "Try this exploration in your product" comment below. It appears a few minutes after each push and always matches the latest commit.'
|
|
2991
|
+
);
|
|
2587
2992
|
lines.push("", "### Previews", "");
|
|
2588
2993
|
const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
|
|
2589
2994
|
if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url} |`));
|
|
@@ -2652,6 +3057,13 @@ function promotionPlan(input) {
|
|
|
2652
3057
|
markReady: true
|
|
2653
3058
|
};
|
|
2654
3059
|
}
|
|
3060
|
+
function explorationTryIt(input) {
|
|
3061
|
+
const c = readPreviewComment(input.comment);
|
|
3062
|
+
if (!c) return { state: "building" };
|
|
3063
|
+
if (input.headSha && c.sha7 && !input.headSha.startsWith(c.sha7)) return { state: "building" };
|
|
3064
|
+
if (!c.command || c.source === "none") return { state: "failed" };
|
|
3065
|
+
return { state: "ready", command: c.command, source: c.source, sha7: c.sha7 };
|
|
3066
|
+
}
|
|
2655
3067
|
function explorationRules(branch) {
|
|
2656
3068
|
return [
|
|
2657
3069
|
`- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
|
|
@@ -2664,7 +3076,8 @@ function explorationRules(branch) {
|
|
|
2664
3076
|
var REQUEST_KINDS = [
|
|
2665
3077
|
{ value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
|
|
2666
3078
|
{ value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
|
|
2667
|
-
{ value: "prop-change", label: "
|
|
3079
|
+
{ value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
|
|
3080
|
+
{ value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
|
|
2668
3081
|
{ value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
|
|
2669
3082
|
{ value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
|
|
2670
3083
|
{ value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
|
|
@@ -2686,6 +3099,7 @@ function composeRequestIssue(r, opts = {}) {
|
|
|
2686
3099
|
"",
|
|
2687
3100
|
r.summary.trim(),
|
|
2688
3101
|
"",
|
|
3102
|
+
...r.line?.trim() ? [`**What changes:** ${r.line.trim()}`, ""] : [],
|
|
2689
3103
|
...rows.length ? [...rows, ""] : [],
|
|
2690
3104
|
...r.images?.length ? [...r.images.map((u, i) => ``), ""] : [],
|
|
2691
3105
|
...figma ? [`**Design:** ${figma}`, ""] : [],
|
|
@@ -2733,6 +3147,7 @@ function requestBrief(r, ctx) {
|
|
|
2733
3147
|
`Component: ${r.component}`,
|
|
2734
3148
|
`Kind: ${kindLabel(r.kind)}`,
|
|
2735
3149
|
`Request: ${r.title}`,
|
|
3150
|
+
...r.line?.trim() ? [`What changes: ${r.line.trim()}`] : [],
|
|
2736
3151
|
"",
|
|
2737
3152
|
r.summary.trim(),
|
|
2738
3153
|
"",
|
|
@@ -2750,11 +3165,6 @@ function requestBrief(r, ctx) {
|
|
|
2750
3165
|
var cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2751
3166
|
var sourceLabel = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
|
|
2752
3167
|
|
|
2753
|
-
// ../core/src/change-drafts.ts
|
|
2754
|
-
function cleanDraftText(text) {
|
|
2755
|
-
return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
3168
|
// ../core/src/draft-exchange.ts
|
|
2759
3169
|
var DRAFT_EXCHANGE_VERSION = 1;
|
|
2760
3170
|
var DRAFTS_DIR = ".bridge/drafts";
|
|
@@ -2768,12 +3178,16 @@ var DRAFT_VOICE_RULES = [
|
|
|
2768
3178
|
"Use only facts from the changes, the images and the designer's notes. Never invent numbers, names, usage or reasons. If the reason is unknown, say what changes and leave the reason out.",
|
|
2769
3179
|
"Keep the designer's intent from their notes.",
|
|
2770
3180
|
"Never use an em dash: use a colon, a comma or parentheses. No exclamation marks, no emoji, no Markdown.",
|
|
2771
|
-
"A request title never repeats the component name: the issue title already starts with it."
|
|
3181
|
+
"A request title never repeats the component name: the issue title already starts with it.",
|
|
3182
|
+
`For each change line, write one short sentence (at most ${DRAFT_LINE_MAX} characters, no trailing period) of what changed for developers, read from the before and after images: the part, the variant and the values when you can see them, for example "Icon added after the label in primary sm: Pencil, 16px". Never guess a value you cannot read.`,
|
|
3183
|
+
`For each change line, also write the title of the request it becomes: short, sentence case, starts with a verb, at most ${DRAFT_TITLE_MAX} characters, no trailing period, never the component name, for example "Add an icon after the label".`
|
|
2772
3184
|
];
|
|
2773
3185
|
var DRAFT_OUTPUT_CONTRACT = [
|
|
2774
|
-
`Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "..."}]}, one result per item.`,
|
|
3186
|
+
`Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "...", "lines": [{"id": "<line id>", "line": "...", "title": "..."}]}]}, one result per item.`,
|
|
2775
3187
|
`title: one line, at most ${DRAFT_TITLE_MAX} characters, no trailing period.`,
|
|
2776
3188
|
`summary: at most ${DRAFT_SUMMARY_SENTENCES} sentences, plain text.`,
|
|
3189
|
+
`lines: one per change line the item lists (its "lines"), by line id: line is one sentence, at most ${DRAFT_LINE_MAX} characters, no trailing period; title is the request title for that component change, at most ${DRAFT_TITLE_MAX} characters, no trailing period. Leave lines out when the item lists none.`,
|
|
3190
|
+
`Docs items (scope "docs", from Bridge Studio): follow the item's prompt and send {"id": "<item id>", "docs": {<only the sections it asks for>}} for a sections task (a section to check goes in only when something in it is no longer true for the code: leave accurate ones out, {"docs": {}} when all are), or {"id": "<item id>", "replacement": "..."} for a selection task. No title or summary. The designer reviews them on the Library page.`,
|
|
2777
3191
|
"The designer reviews the text in Figma before anything is sent."
|
|
2778
3192
|
];
|
|
2779
3193
|
var SESSION_RE = /^[a-z0-9-]{6,64}$/;
|
|
@@ -2793,7 +3207,7 @@ function validatePendingDrafts(x) {
|
|
|
2793
3207
|
if (!isDraftSessionId(p.sessionId)) out.push("The session id is missing or invalid.");
|
|
2794
3208
|
if (!str(p.createdAt) || Number.isNaN(Date.parse(p.createdAt))) out.push("createdAt must be an ISO date.");
|
|
2795
3209
|
if (p.status !== "pending" && p.status !== "drafted") out.push('status must be "pending" or "drafted".');
|
|
2796
|
-
if (p.source !== "figma") out.push('source must be "figma".');
|
|
3210
|
+
if (p.source !== "figma" && p.source !== "studio") out.push('source must be "figma" or "studio".');
|
|
2797
3211
|
if (!Array.isArray(p.items) || !p.items.length) out.push("A draft session needs at least one item.");
|
|
2798
3212
|
const ids = /* @__PURE__ */ new Set();
|
|
2799
3213
|
for (const [n, i] of (Array.isArray(p.items) ? p.items : []).entries()) {
|
|
@@ -2805,7 +3219,12 @@ function validatePendingDrafts(x) {
|
|
|
2805
3219
|
if (!str(i.id) || !i.id) out.push(`${at} has no id.`);
|
|
2806
3220
|
else if (ids.has(i.id)) out.push(`${at} repeats the id "${i.id}".`);
|
|
2807
3221
|
else ids.add(i.id);
|
|
2808
|
-
if (i.scope !== "request" && i.scope !== "push") out.push(`${at}: scope must be "request" or "
|
|
3222
|
+
if (i.scope !== "request" && i.scope !== "push" && i.scope !== "docs") out.push(`${at}: scope must be "request", "push" or "docs".`);
|
|
3223
|
+
if (i.scope === "docs") {
|
|
3224
|
+
const d = i.docs;
|
|
3225
|
+
if (!d || typeof d !== "object" || !str(d.slug) || !d.slug || !str(d.prompt) || !d.prompt || !optStr(d.path)) out.push(`${at}: a docs item needs docs {slug, task, prompt}.`);
|
|
3226
|
+
else out.push(...validateDocsTask(d.task).map((m) => `${at}: ${m}`));
|
|
3227
|
+
} else if (i.docs !== void 0) out.push(`${at}: only a docs item has docs.`);
|
|
2809
3228
|
if (!optStr(i.component)) out.push(`${at}: component must be text.`);
|
|
2810
3229
|
if (i.kind !== void 0 && !KINDS.has(i.kind)) out.push(`${at}: unknown kind "${String(i.kind)}".`);
|
|
2811
3230
|
if (!Array.isArray(i.changes) || i.changes.some((c) => !c || !str(c.field) || !optStr(c.before) || !optStr(c.after))) out.push(`${at}: changes must be a list of {field, before, after}.`);
|
|
@@ -2813,10 +3232,17 @@ function validatePendingDrafts(x) {
|
|
|
2813
3232
|
if (i.variables !== void 0 && i.scope !== "push") out.push(`${at}: only a push item has variables.`);
|
|
2814
3233
|
if (i.images !== void 0 && (!Array.isArray(i.images) || i.images.some((m) => !m || m.label !== "before" && m.label !== "after" || !str(m.url)))) out.push(`${at}: images must be a list of {label: before or after, url}.`);
|
|
2815
3234
|
if (!i.draft || !str(i.draft.title) || !str(i.draft.summary)) out.push(`${at}: draft must hold a title and a summary.`);
|
|
3235
|
+
if (i.lines !== void 0 && (!Array.isArray(i.lines) || i.lines.some((l) => !l || !str(l.id) || !l.id || !str(l.line) || !optStr(l.title) || !Array.isArray(l.changes)))) out.push(`${at}: lines must be a list of {id, changes, line, title}.`);
|
|
2816
3236
|
}
|
|
2817
3237
|
if (p.results !== void 0) {
|
|
2818
3238
|
if (!Array.isArray(p.results)) out.push("results must be a list.");
|
|
2819
|
-
else
|
|
3239
|
+
else
|
|
3240
|
+
for (const r of p.results) {
|
|
3241
|
+
if (!r || !str(r.id) || !str(r.title) || !str(r.summary)) out.push("Each result needs an id, a title and a summary.");
|
|
3242
|
+
else if (r.docs !== void 0 && (!r.docs || typeof r.docs !== "object" || Array.isArray(r.docs))) out.push("Result docs must be an object of sections.");
|
|
3243
|
+
else if (!optStr(r.replacement)) out.push("A result replacement must be text.");
|
|
3244
|
+
else if (r.lines !== void 0 && (!Array.isArray(r.lines) || r.lines.some((l) => !l || !str(l.id) || !str(l.line) || !optStr(l.title)))) out.push("Result lines must be a list of {id, line, title}.");
|
|
3245
|
+
}
|
|
2820
3246
|
}
|
|
2821
3247
|
return out;
|
|
2822
3248
|
}
|
|
@@ -2853,19 +3279,69 @@ function sanitizeDraftText(raw) {
|
|
|
2853
3279
|
const summary2 = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
|
|
2854
3280
|
return { title, summary: summary2 };
|
|
2855
3281
|
}
|
|
3282
|
+
function sanitizeDocsResult(item, r) {
|
|
3283
|
+
const task = item.docs.task;
|
|
3284
|
+
if (task.kind === "selection") {
|
|
3285
|
+
const raw = r.replacement ?? (r.docs && typeof r.docs === "object" ? r.docs[task.key] : void 0);
|
|
3286
|
+
const replacement = sanitizeDocsReplacement(raw);
|
|
3287
|
+
if (!replacement) throw new Error(`Item "${item.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item.id}", "replacement": "..."}.`);
|
|
3288
|
+
return { replacement };
|
|
3289
|
+
}
|
|
3290
|
+
try {
|
|
3291
|
+
const { sections: sections2, checked } = docsSectionsAnswer(task, r.docs ?? {});
|
|
3292
|
+
return { docs: sections2, checked };
|
|
3293
|
+
} catch {
|
|
3294
|
+
throw new Error(`Item "${item.id}" asks for the sections ${docsTaskKeys(task).join(", ")}: send {"id": "${item.id}", "docs": {${task.keys.map((k) => `"${k}": ...`).join(", ")}}} in the shapes of its prompt.`);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
function sanitizeResultLines(item, raw) {
|
|
3298
|
+
if (raw === void 0 || raw === null) return [];
|
|
3299
|
+
if (!Array.isArray(raw)) throw new Error(`lines for "${item.id}" must be a list of {"id", "line", "title"}.`);
|
|
3300
|
+
const known = new Set((item.lines ?? []).map((l) => l.id));
|
|
3301
|
+
const out = /* @__PURE__ */ new Map();
|
|
3302
|
+
for (const l of raw) {
|
|
3303
|
+
const id = typeof l?.id === "string" ? l.id : "";
|
|
3304
|
+
if (!known.has(id)) throw new Error(`Item "${item.id}" has no change line "${id}".${known.size ? ` Use the line ids from bridge_pending_drafts: ${[...known].join(", ")}.` : " It lists no lines: leave lines out."}`);
|
|
3305
|
+
const line = sanitizeDraftLine(l.line);
|
|
3306
|
+
const title = sanitizeDraftTitle(l.title);
|
|
3307
|
+
if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
|
|
3308
|
+
}
|
|
3309
|
+
return [...out.values()];
|
|
3310
|
+
}
|
|
3311
|
+
function sanitizeDraftTitle(raw) {
|
|
3312
|
+
if (typeof raw !== "string") return "";
|
|
3313
|
+
const t = clip(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_TITLE_MAX);
|
|
3314
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
3315
|
+
}
|
|
2856
3316
|
function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
|
|
2857
3317
|
const ids = new Set(p.items.map((i) => i.id));
|
|
2858
3318
|
const next = new Map((p.results ?? []).map((r) => [r.id, r]));
|
|
2859
3319
|
for (const r of results) {
|
|
2860
3320
|
if (!ids.has(r.id)) throw new Error(`Session ${p.sessionId} has no item "${r.id}". Use the ids from bridge_pending_drafts: ${[...ids].join(", ")}.`);
|
|
3321
|
+
const item = p.items.find((i) => i.id === r.id);
|
|
3322
|
+
if (item.scope === "docs") {
|
|
3323
|
+
next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(item, r), by: "claude", at: at.toISOString() });
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
2861
3326
|
const clean = sanitizeDraftText(r);
|
|
2862
|
-
|
|
2863
|
-
|
|
3327
|
+
const lines = sanitizeResultLines(item, r.lines);
|
|
3328
|
+
if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
|
|
3329
|
+
next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at.toISOString() });
|
|
2864
3330
|
}
|
|
2865
3331
|
const merged = p.items.map((i) => next.get(i.id)).filter((r) => !!r);
|
|
2866
3332
|
return { ...p, results: merged, status: merged.length === p.items.length ? "drafted" : "pending" };
|
|
2867
3333
|
}
|
|
2868
3334
|
function draftItemBrief(i) {
|
|
3335
|
+
if (i.scope === "docs" && i.docs) {
|
|
3336
|
+
const t = i.docs.task;
|
|
3337
|
+
const check = t.kind === "sections" ? (t.check ?? []).filter((k) => t.keys.includes(k)) : [];
|
|
3338
|
+
const write = t.kind === "sections" ? t.keys.filter((k) => !check.includes(k)) : [];
|
|
3339
|
+
const what = t.kind === "selection" ? `rewrite one passage of the ${t.key} section (${t.mode}), answer with "replacement"` : [
|
|
3340
|
+
write.length ? `${t.rewrite ? "rewrite" : "write"} the sections ${write.join(", ")}` : "",
|
|
3341
|
+
check.length ? `check the sections ${check.join(", ")} against the code and return one only when something in it is no longer true (leave accurate ones out)` : ""
|
|
3342
|
+
].filter(Boolean).join("; ") + ', answer with "docs"';
|
|
3343
|
+
return [`Item ${i.id}: component docs${i.component ? ` of ${i.component}` : ""} from Bridge Studio (${i.docs.path ?? i.docs.slug}): ${what}.`, "", i.docs.prompt].join("\n");
|
|
3344
|
+
}
|
|
2869
3345
|
const kind = i.kind ? REQUEST_KINDS.find((k) => k.value === i.kind)?.label : void 0;
|
|
2870
3346
|
const cell3 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2871
3347
|
const lines = [`Item ${i.id}: ${i.scope === "request" ? "a component change request (a GitHub issue developers implement)" : "the description of a pull request with design token (variable) edits from Figma"}.`];
|
|
@@ -2874,10 +3350,17 @@ function draftItemBrief(i) {
|
|
|
2874
3350
|
if (i.components?.length) lines.push(`Components with a change request: ${i.components.join(", ")}`);
|
|
2875
3351
|
if (i.changes.length) lines.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i.changes.map((c) => `| ${cell3(c.field)} | ${cell3(c.before) || "none"} | ${cell3(c.after) || "removed"} |`));
|
|
2876
3352
|
if (i.variables?.length) lines.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${cell3(v.path)} | ${cell3(v.before) || "none"} | ${cell3(v.after) || "removed"} |`));
|
|
2877
|
-
if (!i.changes.length && !i.variables?.length) lines.push("", "No change detected: rely on the notes and the images.");
|
|
3353
|
+
if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines.push("", "No change detected: rely on the notes and the images.");
|
|
2878
3354
|
if (i.draft.title || i.draft.summary) lines.push("", "Starting draft, written from the changes only:", `Title: ${i.draft.title}`, `Summary: ${i.draft.summary}`);
|
|
2879
3355
|
if (i.notes?.trim()) lines.push("", "The designer's notes (keep their intent):", i.notes.trim());
|
|
2880
|
-
if (i.
|
|
3356
|
+
if (i.lines?.length) {
|
|
3357
|
+
lines.push("", `Change lines to write (one sentence each, at most ${DRAFT_LINE_MAX} characters, from the before and after images), each with the title of its request (at most ${DRAFT_TITLE_MAX} characters):`);
|
|
3358
|
+
for (const l of i.lines) {
|
|
3359
|
+
lines.push(`- Line ${l.id}${l.component ? `, ${l.component}` : ""}${l.kind ? ` (${REQUEST_KINDS.find((k) => k.value === l.kind)?.label ?? l.kind})` : ""}. Starting line: ${l.line || "(none)"}${l.title ? `. Starting title: ${l.title}` : ""}`);
|
|
3360
|
+
for (const c of l.changes) lines.push(` - ${c.field}: ${c.before || "none"} -> ${c.after || "removed"}`);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
if (i.images?.length) lines.push("", `Images: ${i.images.map((m) => `${m.label}${m.line ? ` (line ${m.line})` : ""} ${m.url}`).join(", ")}`);
|
|
2881
3364
|
return lines.join("\n");
|
|
2882
3365
|
}
|
|
2883
3366
|
var isConflict = (err) => err instanceof GitHubError ? err.status === 409 || err.status === 422 && /sha/i.test(`${err.detail ?? ""} ${err.message}`) : err?.status === 409;
|
|
@@ -2920,9 +3403,10 @@ function writeDraftResults(client2, sessionId, results, opts = {}) {
|
|
|
2920
3403
|
message: `Bridge drafts: ${results.length === 1 ? "1 text" : `${results.length} texts`} drafted by Claude`
|
|
2921
3404
|
});
|
|
2922
3405
|
}
|
|
3406
|
+
var DRAFT_POLL_TIMEOUT_MS = 10 * 6e4;
|
|
2923
3407
|
|
|
2924
3408
|
// ../core/src/explorations-github.ts
|
|
2925
|
-
function explorationSummary(p,
|
|
3409
|
+
function explorationSummary(p, _owner, _repo, s) {
|
|
2926
3410
|
const e = parseExplorationBody(p.body);
|
|
2927
3411
|
const slug2 = p.headRef?.startsWith(EXPLORATION_PREFIX) ? p.headRef.slice(EXPLORATION_PREFIX.length) : e?.slug ?? explorationSlug(p.title);
|
|
2928
3412
|
return {
|
|
@@ -2937,8 +3421,8 @@ function explorationSummary(p, owner, repo, s) {
|
|
|
2937
3421
|
draft: p.draft,
|
|
2938
3422
|
author: e?.author ?? p.author,
|
|
2939
3423
|
updatedAt: p.updatedAt,
|
|
2940
|
-
|
|
2941
|
-
|
|
3424
|
+
headSha: p.headSha,
|
|
3425
|
+
previews: previewLinks(s.apps, { branch: p.headRef, pr: p.number, sha: p.headSha })
|
|
2942
3426
|
};
|
|
2943
3427
|
}
|
|
2944
3428
|
async function listExplorationsOnGitHub(gh, s, opts = {}) {
|
|
@@ -2964,7 +3448,7 @@ ${full.purpose.trim()}
|
|
|
2964
3448
|
Exploration: never merged as is. Promote it to review or close it.`,
|
|
2965
3449
|
files: [{ path: explorationNotePath(slug2), content: explorationNote(full) }]
|
|
2966
3450
|
});
|
|
2967
|
-
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps };
|
|
3451
|
+
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
|
|
2968
3452
|
const pr = await x.createPullRequest({ title: explorationPrTitle(full), head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
|
|
2969
3453
|
await x.addLabels(pr.number, [EXPLORATION_LABEL]);
|
|
2970
3454
|
await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
|
|
@@ -2974,10 +3458,16 @@ Exploration: never merged as is. Promote it to review or close it.`,
|
|
|
2974
3458
|
pr: pr.number,
|
|
2975
3459
|
url: pr.url,
|
|
2976
3460
|
commit: commit.sha,
|
|
2977
|
-
|
|
3461
|
+
tryIt: { state: "building" },
|
|
2978
3462
|
previews: previewLinks(s.apps, { branch, pr: pr.number, sha: commit.sha })
|
|
2979
3463
|
};
|
|
2980
3464
|
}
|
|
3465
|
+
async function explorationTestBuildOnGitHub(gh, e, config) {
|
|
3466
|
+
const [comments, head] = await Promise.all([gh.listIssueComments(e.number), gh.getBranchSha(e.branch).catch(() => void 0)]);
|
|
3467
|
+
const tryIt = explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
|
|
3468
|
+
const previews = await branchPreviewUrls(gh, e.number, config, { branch: e.branch, sha: head, comments }).catch(() => []);
|
|
3469
|
+
return { tryIt, previews, settled: tryIt.state !== "building" && previewsSettled(previews) };
|
|
3470
|
+
}
|
|
2981
3471
|
async function findExplorationOnGitHub(gh, ref) {
|
|
2982
3472
|
if (typeof ref === "number" || /^#?\d+$/.test(String(ref))) {
|
|
2983
3473
|
const p = await gh.getPullRequest(Number(String(ref).replace(/^#/, "")));
|
|
@@ -3001,6 +3491,7 @@ async function promoteExplorationOnGitHub(gh, ref, opts = {}) {
|
|
|
3001
3491
|
const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
|
|
3002
3492
|
if (plan.removeFiles.length) {
|
|
3003
3493
|
await x.commitToBranch({ branch: p.headRef, message: "chore(explore): promote to review, remove the exploration note", files: plan.removeFiles.map((path) => ({ path, content: null })) }).catch((e) => {
|
|
3494
|
+
if (e instanceof GitHubError && e.auth === "rejected") throw e;
|
|
3004
3495
|
out.warnings.push(`The exploration note stays on the branch (${e.message}). Delete ${plan.removeFiles.join(", ")} before merging.`);
|
|
3005
3496
|
});
|
|
3006
3497
|
}
|
|
@@ -4632,7 +5123,9 @@ async function detectConfig(files, read, extra = {}) {
|
|
|
4632
5123
|
branchPrefix: "bridge/",
|
|
4633
5124
|
labels: ["source: mcp"],
|
|
4634
5125
|
guardrails: [guidelines, "CLAUDE.md", ".github/", governance, contrast, CONFIG_FILE].filter(Boolean),
|
|
4635
|
-
owners: { design: [], tech: [] }
|
|
5126
|
+
owners: { design: [], tech: [] },
|
|
5127
|
+
// New configs start from the default roles: design owners approve design, tech owners approve tech and ship.
|
|
5128
|
+
rights: DEFAULT_RIGHTS
|
|
4636
5129
|
}
|
|
4637
5130
|
});
|
|
4638
5131
|
}
|
|
@@ -5187,6 +5680,7 @@ __export(ops_exports, {
|
|
|
5187
5680
|
normalizeTokenPath: () => normalizeTokenPath,
|
|
5188
5681
|
proposeTokenChange: () => proposeTokenChange,
|
|
5189
5682
|
reviewPr: () => reviewPr,
|
|
5683
|
+
reviewRoles: () => reviewRoles,
|
|
5190
5684
|
scaffoldComponentOp: () => scaffoldComponentOp,
|
|
5191
5685
|
searchTokens: () => searchTokens,
|
|
5192
5686
|
validateTokens: () => validateTokens,
|
|
@@ -5324,6 +5818,7 @@ __export(explorations_exports, {
|
|
|
5324
5818
|
closeExploration: () => closeExploration,
|
|
5325
5819
|
explorationContext: () => explorationContext,
|
|
5326
5820
|
explorationRules: () => explorationRules,
|
|
5821
|
+
installOf: () => installOf,
|
|
5327
5822
|
listExplorations: () => listExplorations,
|
|
5328
5823
|
promoteExploration: () => promoteExploration,
|
|
5329
5824
|
startExploration: () => startExploration,
|
|
@@ -5377,7 +5872,16 @@ async function startExploration(ctx, a) {
|
|
|
5377
5872
|
const work = `Work on it: \`git fetch origin && git switch ${branch}\`. Token edits: bridge_propose_token_change with "branch": "${branch}". Requests: bridge_create_request with "exploration": "${branch}", then bridge_get_request with "branch": "${branch}". It is never merged as is: promote it (\`bridge explore promote ${slug2}\`) or close it.`;
|
|
5378
5873
|
if (gh && ctx.canWriteRemote) {
|
|
5379
5874
|
const r = await startExplorationOnGitHub(gh, e, s);
|
|
5380
|
-
return {
|
|
5875
|
+
return {
|
|
5876
|
+
status: "started",
|
|
5877
|
+
...preview,
|
|
5878
|
+
pr: r.pr,
|
|
5879
|
+
url: r.url,
|
|
5880
|
+
commit: r.commit,
|
|
5881
|
+
install: installOf(r.tryIt),
|
|
5882
|
+
previews: r.previews,
|
|
5883
|
+
next: `${work} The install command and the preview links appear on the pull request a few minutes after each push: \`bridge explore list\` reads them.`
|
|
5884
|
+
};
|
|
5381
5885
|
}
|
|
5382
5886
|
if (ctx.mode === "local" && ctx.isGit) {
|
|
5383
5887
|
if (await branchExists(ctx.root, branch)) throw new Error(`Branch ${branch} already exists here. Pick another name, or continue it with \`git switch ${branch}\`.`);
|
|
@@ -5404,6 +5908,11 @@ Exploration: never merged as is. Promote it to review or close it.`,
|
|
|
5404
5908
|
}
|
|
5405
5909
|
throw new Error("Starting an exploration needs GITHUB_TOKEN (contents and pull requests: write), or a local git repo. Without them Bridge is read-only. Use dryRun to preview it.");
|
|
5406
5910
|
}
|
|
5911
|
+
function installOf(t) {
|
|
5912
|
+
if (t.state === "ready") return { state: "ready", command: t.command, note: `Snapshot of ${t.sha7 ?? "the latest commit"}, ${t.source === "release" ? "tarballs from the preview prerelease" : t.source === "pkg.pr.new" ? "pkg.pr.new" : "from the preview comment"}. Install it in a product branch that deploys to staging.` };
|
|
5913
|
+
if (t.state === "failed") return { state: "failed", command: null, note: "The test build did not finish for the latest commit. The Preview packages run on the pull request says why." };
|
|
5914
|
+
return { state: "building", command: null, note: 'The test build is being prepared, a few minutes after each push. The command then appears in the "Try this exploration in your product" comment.' };
|
|
5915
|
+
}
|
|
5407
5916
|
async function requestsByExploration(gh) {
|
|
5408
5917
|
const out = /* @__PURE__ */ new Map();
|
|
5409
5918
|
try {
|
|
@@ -5420,7 +5929,15 @@ async function listExplorations(ctx, a = {}) {
|
|
|
5420
5929
|
const { config } = await ctx.config();
|
|
5421
5930
|
const gh = await requireGitHub(ctx);
|
|
5422
5931
|
const [list, linked] = await Promise.all([listExplorationsOnGitHub(gh, settingsOf(config), { state: a.state ?? "open" }), requestsByExploration(gh)]);
|
|
5423
|
-
const explorations =
|
|
5932
|
+
const explorations = await Promise.all(
|
|
5933
|
+
list.map(async (x) => {
|
|
5934
|
+
const requests = linked.get(x.branch) ?? [];
|
|
5935
|
+
if (x.status !== "active") return { ...x, install: null, requests };
|
|
5936
|
+
const t = await explorationTestBuildOnGitHub(gh, { number: x.number, branch: x.branch }, config).catch(() => null);
|
|
5937
|
+
if (!t) return { ...x, install: installOf({ state: "building" }), requests };
|
|
5938
|
+
return { ...x, previews: t.previews.map((p) => ({ app: p.app, label: p.label, url: p.url, state: p.state })), install: installOf(t.tryIt), requests };
|
|
5939
|
+
})
|
|
5940
|
+
);
|
|
5424
5941
|
let localBranches = [];
|
|
5425
5942
|
if (ctx.mode === "local" && ctx.isGit) {
|
|
5426
5943
|
const refs = (await git(ctx.root, ["for-each-ref", "--format=%(refname:short)", `refs/heads/${EXPLORATION_PREFIX}`]).catch(() => "")).split("\n").filter(Boolean);
|
|
@@ -5962,6 +6479,7 @@ async function getReview(ctx, a) {
|
|
|
5962
6479
|
approvals: [...latest.entries()].filter(([, s]) => s === "APPROVED").map(([u]) => u),
|
|
5963
6480
|
changesRequested: [...latest.entries()].filter(([, s]) => s === "CHANGES_REQUESTED").map(([u]) => u)
|
|
5964
6481
|
};
|
|
6482
|
+
meta.roles = await reviewRoles(gh, config, meta.approvals);
|
|
5965
6483
|
}
|
|
5966
6484
|
const [before, after] = await Promise.all([setAt2(baseSrc, config), setAt2(headSrc, config)]);
|
|
5967
6485
|
const changes = core.diff(before, after);
|
|
@@ -5989,6 +6507,25 @@ async function getReview(ctx, a) {
|
|
|
5989
6507
|
impact: imp
|
|
5990
6508
|
};
|
|
5991
6509
|
}
|
|
6510
|
+
var sameLogin = (a, b) => a.trim().toLowerCase().replace(/^@/, "") === b.trim().toLowerCase().replace(/^@/, "");
|
|
6511
|
+
async function reviewRoles(gh, config, approvers) {
|
|
6512
|
+
const { owners, rights } = config.governance;
|
|
6513
|
+
const teams = await new TeamDirectory(gh).resolve(owners);
|
|
6514
|
+
const entries = approverEntries(owners, rights);
|
|
6515
|
+
const lists = expandOwners(entries, teams);
|
|
6516
|
+
const role = (k) => ({
|
|
6517
|
+
owners: entries[k].map((e) => entryLabel(e, teams)),
|
|
6518
|
+
approvedBy: approvers.filter((a) => lists[k].some((x) => sameLogin(x, a)))
|
|
6519
|
+
});
|
|
6520
|
+
const ship = shipPolicy(owners, rights, teams);
|
|
6521
|
+
const unreadable = unreadableTeams(teams);
|
|
6522
|
+
return {
|
|
6523
|
+
design: role("design"),
|
|
6524
|
+
tech: role("tech"),
|
|
6525
|
+
ship: ship ? whoCanShipSentence(ship) : "Anyone with write access can ship.",
|
|
6526
|
+
...unreadable.length ? { unreadableTeams: unreadable.map((t) => t.message) } : {}
|
|
6527
|
+
};
|
|
6528
|
+
}
|
|
5992
6529
|
async function reviewPr(ctx, a) {
|
|
5993
6530
|
const gh = await ctx.github();
|
|
5994
6531
|
if (!gh) throw new Error('No GitHub repo known: set "repo" in bridge.config.json.');
|
|
@@ -6025,7 +6562,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
6025
6562
|
if (ctx.mode !== "local" || !ctx.root) throw new Error("Visual impact needs a local checkout (start Bridge with --root): it builds Storybook and the app at both refs.");
|
|
6026
6563
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
6027
6564
|
const out = a.out ? resolve3(ctx.root, a.out) : join4(ctx.root, "visual-impact");
|
|
6028
|
-
const runImpact = run2 ?? (await import("./src-
|
|
6565
|
+
const runImpact = run2 ?? (await import("./src-XSBCZV4W.js")).runImpact;
|
|
6029
6566
|
const r = await runImpact({
|
|
6030
6567
|
root: ctx.root,
|
|
6031
6568
|
base: a.base ?? "main",
|
|
@@ -6620,7 +7157,7 @@ async function pendingDrafts(ctx, a) {
|
|
|
6620
7157
|
for (const img of item.images ?? []) {
|
|
6621
7158
|
if (images.length >= IMAGES_PER_CALL || !img.path) continue;
|
|
6622
7159
|
const f = await gh.getFileBase64(img.path, REQUESTS_BRANCH).catch(() => void 0);
|
|
6623
|
-
if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId: item.id, label: img.label, base64: f.base64 });
|
|
7160
|
+
if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId: item.id, label: img.label, ...img.line ? { line: img.line } : {}, base64: f.base64 });
|
|
6624
7161
|
}
|
|
6625
7162
|
}
|
|
6626
7163
|
}
|
|
@@ -6632,12 +7169,12 @@ async function pendingDrafts(ctx, a) {
|
|
|
6632
7169
|
sessions: sessions.map((s) => ({ sessionId: s.sessionId, createdAt: s.createdAt, status: s.status, items: s.items, briefs: s.items.map(draftItemBrief) })),
|
|
6633
7170
|
voice: DRAFT_VOICE_RULES,
|
|
6634
7171
|
contract: DRAFT_OUTPUT_CONTRACT,
|
|
6635
|
-
next: sessions.length ? `Write a title and a summary for each item, then call bridge_write_drafts with the sessionId and one result per item id. The designer reviews them in Figma.` : `Nothing is waiting. In the DS Bridge Figma plugin, click "Draft with my Claude" on a request or on the Push description, then say "${DRAFT_PHRASE}" again.`
|
|
7172
|
+
next: sessions.length ? sessions.some((s) => s.items.some((i) => i.scope === "docs")) ? `For each change item write a title, a summary and one line per change line; for each docs item follow its brief and send "docs" (the sections it asks for) or "replacement" (a selected passage). Then call bridge_write_drafts with the sessionId and one result per item id. The designer reviews them in Figma or on the Library page.` : `Write a title and a summary for each item, and one line per change line it lists, then call bridge_write_drafts with the sessionId and one result per item id. The designer reviews them in Figma.` : `Nothing is waiting. In the DS Bridge Figma plugin, click "Draft with my Claude" on a request or on the Push description, or pick "My Claude" when drafting docs on a Library page, then say "${DRAFT_PHRASE}" again.`
|
|
6636
7173
|
};
|
|
6637
7174
|
return { answer, images };
|
|
6638
7175
|
}
|
|
6639
7176
|
async function writeDrafts(ctx, a) {
|
|
6640
|
-
if (!a.results.length) throw new Error('Send at least one result: {"id", "title", "summary"} per item.');
|
|
7177
|
+
if (!a.results.length) throw new Error('Send at least one result: {"id", "title", "summary"} per change item, {"id", "docs"} or {"id", "replacement"} per docs item.');
|
|
6641
7178
|
const gh = await requireGitHub3(ctx);
|
|
6642
7179
|
if (!ctx.canWriteRemote) throw new Error("Writing drafts needs GITHUB_TOKEN (contents: read and write on this repository), the same token as the Figma plugin. Without it Bridge is read-only.");
|
|
6643
7180
|
const out = await writeDraftResults(gh, a.sessionId, a.results);
|
|
@@ -6647,12 +7184,12 @@ async function writeDrafts(ctx, a) {
|
|
|
6647
7184
|
status: out.status,
|
|
6648
7185
|
results: out.results ?? [],
|
|
6649
7186
|
...waiting.length ? { waiting } : {},
|
|
6650
|
-
next: waiting.length ? `Still waiting for ${waiting.join(", ")}: call bridge_write_drafts again with those ids.` : 'Done. The text shows up in the Figma plugin within a few seconds, marked "Drafted by Claude, review before sending". Tell the designer to review it there.'
|
|
7187
|
+
next: waiting.length ? `Still waiting for ${waiting.join(", ")}: call bridge_write_drafts again with those ids.` : out.source === "studio" ? 'Done. The docs show up on the Library page within a few seconds, marked "Drafted by Claude". Tell the designer to review them there before proposing.' : 'Done. The text shows up in the Figma plugin within a few seconds, marked "Drafted by Claude, review before sending". Tell the designer to review it there.'
|
|
6651
7188
|
};
|
|
6652
7189
|
}
|
|
6653
7190
|
|
|
6654
7191
|
// src/server.ts
|
|
6655
|
-
var VERSION = "0.
|
|
7192
|
+
var VERSION = true ? "0.3.0" : "dev";
|
|
6656
7193
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
6657
7194
|
var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
6658
7195
|
var run = (fn) => async (a) => {
|
|
@@ -6680,6 +7217,7 @@ function createServer(ctx) {
|
|
|
6680
7217
|
{
|
|
6681
7218
|
instructions: [
|
|
6682
7219
|
"Bridge exposes a design system repo (DTCG tokens, components, docs) to you.",
|
|
7220
|
+
"Every write goes through a branch and a pull request, never straight to the default branch: the library only changes through reviewed pull requests, and on a protected repo GitHub refuses direct pushes anyway.",
|
|
6683
7221
|
"Read before you write: bridge_get_guidelines for the repo rules, bridge_search_tokens / bridge_get_token before using or proposing a token.",
|
|
6684
7222
|
"Never hardcode a value in UI code when a token exists; never invent a token name (check it with bridge_get_token).",
|
|
6685
7223
|
"bridge_audit measures how a product codebase adopts the design system (hardcoded values, foreign UI components) and suggests the tokens and components to use.",
|
|
@@ -6688,7 +7226,7 @@ function createServer(ctx) {
|
|
|
6688
7226
|
"New brands: bridge_plan_brand first (dry run with ramps and the contrast gate for every theme), then bridge_propose_brand. A new brand is Tier 3.",
|
|
6689
7227
|
"To undo a merged token change use bridge_plan_revert first (it shows conflicts with later changes), then bridge_propose_revert; never revert by hand-editing files.",
|
|
6690
7228
|
'Component changes (new component, variant, prop, structure, visual, token binding) are requests: GitHub issues labeled component-change. bridge_list_requests finds them, bridge_get_request returns a brief to implement one in a pull request that says "Closes #n", bridge_create_request files a new one.',
|
|
6691
|
-
`Change texts from Figma: when the user says "${DRAFT_PHRASE}" (or asks you to draft their Bridge changes), call bridge_pending_drafts, look at the before and after images
|
|
7229
|
+
`Change texts from Figma and docs from Bridge Studio: when the user says "${DRAFT_PHRASE}" (or asks you to draft their Bridge changes or docs), call bridge_pending_drafts. For change items, look at the before and after images and write each title, summary and change line in the Bridge voice. For docs items (scope "docs"), follow the brief: write only the sections it asks for, grounded in the props, CSS module classes and source it quotes, never an invented prop or behavior, never anatomy. Then call bridge_write_drafts. The designer reviews them in the DS Bridge Figma plugin or on the Library page.`,
|
|
6692
7230
|
'Explorations try experimental variables and components in a product staging branch without touching main: an explore/<slug> branch with a draft pull request labeled exploration, previews and a snapshot package. bridge_start_exploration starts one, bridge_list_explorations lists them. To work on one, pass "branch": "explore/<slug>" to bridge_propose_token_change, bridge_draft_docs and bridge_get_request, and never open a pull request to main from it: a person promotes it to review or closes it.'
|
|
6693
7231
|
].join(" ")
|
|
6694
7232
|
}
|
|
@@ -6881,7 +7419,7 @@ function createServer(ctx) {
|
|
|
6881
7419
|
"bridge_get_review",
|
|
6882
7420
|
{
|
|
6883
7421
|
title: "Get review",
|
|
6884
|
-
description: "Review a token change: designer-readable diff table, contrast before -> after, new validation errors, downstream impact, computed risk tier (and a warning if labelled lower), and for PRs the CI status and
|
|
7422
|
+
description: "Review a token change: designer-readable diff table, contrast before -> after, new validation errors, downstream impact, computed risk tier (and a warning if labelled lower), and for PRs the CI status, approvals and roles: who may approve design, approve tech and ship, with GitHub teams named and their size, and any team that cannot be read with the fix. Pass `pr` (GitHub) or `branch` (local proposal branch).",
|
|
6885
7423
|
inputSchema: { pr: z.number().int().positive().optional(), branch: z.string().optional() },
|
|
6886
7424
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
6887
7425
|
},
|
|
@@ -7074,9 +7612,11 @@ function createServer(ctx) {
|
|
|
7074
7612
|
title: "Pending change texts from Figma",
|
|
7075
7613
|
description: [
|
|
7076
7614
|
`Change texts a designer asked their own Claude to write from the DS Bridge Figma plugin ("Draft with my Claude"), waiting on the bridge-requests branch. Call it when the user says "${DRAFT_PHRASE}".`,
|
|
7077
|
-
"Returns the pending sessions (newest first) with each item: scope (request: a component change request; push: a pull request description of token edits), component, kind, the change table, token edits, the designer's notes, the deterministic starting draft, a brief, and the before and after image links; the images themselves follow as image content when small enough.",
|
|
7615
|
+
"Returns the pending sessions (newest first) with each item: scope (request: a component change request; push: a pull request description of token edits; docs: component docs sections from Bridge Studio, with a brief holding the full prompt), component, kind, the change table, token edits, the designer's notes, the deterministic starting draft, the change lines to write (one per component change, with an id, its changes and a starting line), a brief, and the before and after image links; the images themselves follow as image content when small enough.",
|
|
7078
7616
|
"Look at the before and after images, then write what changes for developers and why it matters for people who use the component. Sentence case, start the title with a verb, no em dash, no invented numbers, names or reasons: if the reason is unknown, leave it out.",
|
|
7079
|
-
`
|
|
7617
|
+
`Write one short sentence per change line from the images: what changed for developers (the part, the variant, the values you can read), for example "Icon added after the label in primary sm: Pencil, 16px", and the title of the request it becomes, for example "Add an icon after the label".`,
|
|
7618
|
+
`Then call bridge_write_drafts with one result per item id: title at most ${DRAFT_TITLE_MAX} characters, summary at most 3 sentences, lines (one per change line id) with a line of at most ${DRAFT_LINE_MAX} characters and a title of at most ${DRAFT_TITLE_MAX}.`,
|
|
7619
|
+
`Docs items: read the brief. Sections to check (the code changed after they were written) go in the answer ONLY when something in them is no longer true for the code (a wrong or removed prop, a removed state, changed behavior), fixed with the writer's wording kept elsewhere; leave accurate ones out (a missing key means unchanged, "docs": {} when all are accurate). Otherwise write only the sections its task names (summary a string; whenToUse, whenNotToUse, accessibility, contentGuidelines lists of strings; dos and donts lists of {"title", "text"}) from the facts it quotes, never a prop, variant or keyboard behavior that is not in the code, never anatomy; answer with "docs": {sections}. A selection task rewrites one passage: answer with "replacement": "the passage". No title or summary for docs items.`
|
|
7080
7620
|
].join(" "),
|
|
7081
7621
|
inputSchema: {
|
|
7082
7622
|
sessionId: z.string().optional().describe("One session only. Default: every pending session."),
|
|
@@ -7091,7 +7631,7 @@ function createServer(ctx) {
|
|
|
7091
7631
|
content: [
|
|
7092
7632
|
{ type: "text", text: JSON.stringify(answer, null, 2) },
|
|
7093
7633
|
...images.flatMap((img) => [
|
|
7094
|
-
{ type: "text", text: `${img.label === "before" ? "Before (baseline)" : "After (now in Figma)"}: session ${img.sessionId}, item ${img.itemId}` },
|
|
7634
|
+
{ type: "text", text: `${img.label === "before" ? "Before (baseline)" : "After (now in Figma)"}: session ${img.sessionId}, item ${img.itemId}${img.line ? `, line ${img.line}` : ""}` },
|
|
7095
7635
|
{ type: "image", data: img.base64, mimeType: "image/png" }
|
|
7096
7636
|
])
|
|
7097
7637
|
]
|
|
@@ -7105,14 +7645,23 @@ function createServer(ctx) {
|
|
|
7105
7645
|
"bridge_write_drafts",
|
|
7106
7646
|
{
|
|
7107
7647
|
title: "Write change texts for Figma",
|
|
7108
|
-
description:
|
|
7648
|
+
description: `Write the titles, summaries and change lines (or, for docs items, the docs sections or the rewritten passage) you drafted for a session from bridge_pending_drafts. Docs results show up on the Library page marked "Drafted by Claude" for review, cleaned to the docs.json shapes (no em dash; only the sections the item asks for). The DS Bridge Figma plugin fills change texts in within seconds, marked "Drafted by Claude, review before sending"; it never overwrites text the designer typed meanwhile. Each change line becomes the main line of its component row and the first line of its request; its title becomes that request's title. Texts are cleaned to the contract (no em dash, titles at most ${DRAFT_TITLE_MAX} characters without a trailing period, summary at most 3 sentences, each line at most ${DRAFT_LINE_MAX} characters). Results without lines, or lines without a title, still work. Needs GITHUB_TOKEN with contents write. Writes only the session file on the bridge-requests branch, never main.`,
|
|
7109
7649
|
inputSchema: {
|
|
7110
7650
|
sessionId: z.string().min(1).describe("The sessionId from bridge_pending_drafts."),
|
|
7111
7651
|
results: z.array(
|
|
7112
7652
|
z.object({
|
|
7113
7653
|
id: z.string().min(1).describe("The item id from bridge_pending_drafts."),
|
|
7114
|
-
title: z.string().describe(`
|
|
7115
|
-
summary: z.string().describe("
|
|
7654
|
+
title: z.string().optional().describe(`Change items: one line, sentence case, starts with a verb, at most ${DRAFT_TITLE_MAX} characters, no trailing period. A request title never repeats the component name. Leave it out for docs items.`),
|
|
7655
|
+
summary: z.string().optional().describe("Change items: at most 3 sentences, what changes for developers and why it matters. No em dash, no invented numbers. Leave it out for docs items."),
|
|
7656
|
+
docs: z.record(z.string(), z.unknown()).optional().describe('Docs items with a sections task: only the sections the brief asks for (a section to check only when it is no longer true for the code; leave accurate ones out), in docs.json shapes: summary a string; whenToUse, whenNotToUse, accessibility, contentGuidelines lists of strings; dos and donts lists of {"title", "text"}.'),
|
|
7657
|
+
replacement: z.string().optional().describe("Docs items with a selection task: the rewritten passage only, plain text, Bridge voice, no em dash."),
|
|
7658
|
+
lines: z.array(
|
|
7659
|
+
z.object({
|
|
7660
|
+
id: z.string().min(1).describe(`A change line id from the item's "lines" in bridge_pending_drafts.`),
|
|
7661
|
+
line: z.string().describe(`One sentence, at most ${DRAFT_LINE_MAX} characters, no trailing period: what changed for developers, read from the before and after images ("Icon added after the label in primary sm: Pencil, 16px").`),
|
|
7662
|
+
title: z.string().optional().describe(`The title of the request this component change becomes: short, sentence case, starts with a verb, at most ${DRAFT_TITLE_MAX} characters, no trailing period, never the component name ("Add an icon after the label").`)
|
|
7663
|
+
})
|
|
7664
|
+
).optional().describe("One per change line the item lists. Leave it out when the item lists none.")
|
|
7116
7665
|
})
|
|
7117
7666
|
).min(1).describe("One result per item.")
|
|
7118
7667
|
},
|
|
@@ -7140,7 +7689,7 @@ function createServer(ctx) {
|
|
|
7140
7689
|
"bridge_list_explorations",
|
|
7141
7690
|
{
|
|
7142
7691
|
title: "List explorations",
|
|
7143
|
-
description: "Explorations of the repo (draft pull requests labeled exploration on explore/* branches): name, purpose, status (active: running; promoted: ready for review; closed), author, last update, preview links
|
|
7692
|
+
description: "Explorations of the repo (draft pull requests labeled exploration on explore/* branches): name, purpose, status (active: running; promoted: ready for review; closed), author, last update, for running ones the preview links Vercel reports (url null while it has not reported one yet) and the install command a product staging branch uses, read from the preview comment on the pull request (state building until the snapshot of the latest commit is published, never a guessed command), and the component requests implemented on each. Locally, also explore/* branches without a pull request.",
|
|
7144
7693
|
inputSchema: { state: z.enum(["open", "closed", "all"]).optional().describe("Pull request state. Default open: running and promoted ones.") },
|
|
7145
7694
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
7146
7695
|
},
|
|
@@ -7345,6 +7894,13 @@ function optionsFromArgv(argv, env = process.env) {
|
|
|
7345
7894
|
}
|
|
7346
7895
|
|
|
7347
7896
|
export {
|
|
7897
|
+
missingProse,
|
|
7898
|
+
formatDocsJson,
|
|
7899
|
+
DOCS_DRAFT_MODEL,
|
|
7900
|
+
parseDocsReply,
|
|
7901
|
+
mergeDocsDraft,
|
|
7902
|
+
anthropicRequest,
|
|
7903
|
+
anthropicReplyText,
|
|
7348
7904
|
detectConfig,
|
|
7349
7905
|
core,
|
|
7350
7906
|
LocalSource,
|