@bridge-ds/mcp 0.2.0 → 0.3.1

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.
@@ -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
- validateLayered
48
- } from "./chunk-FJL6UTZG.js";
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
- throw new GitHubError(res.status, friendlyMessage(res.status, detail), detail);
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
  }
@@ -973,6 +1039,25 @@ var GitHubClient = class _GitHubClient {
973
1039
  throw new GitHubError(422, `GitHub refused to mark #${n} ready for review${detail ? `: ${detail}` : "."} Use "Ready for review" on GitHub instead.`, detail);
974
1040
  }
975
1041
  }
1042
+ /**
1043
+ * Turns an open pull request back into a draft: the GraphQL mutation convertPullRequestToDraft (REST cannot set
1044
+ * `draft` either), same token. No-op when it is already a draft.
1045
+ */
1046
+ async convertToDraft(n) {
1047
+ this.assertToken("Turning a pull request back into a draft");
1048
+ const pr = await this.request("GET", `${this.repoPath}/pulls/${n}`);
1049
+ if (pr.draft === true) return;
1050
+ if (pr.state && pr.state !== "open") throw new GitHubError(422, `Pull request #${n} is ${pr.state}: only an open one can go back to draft.`);
1051
+ if (!pr.node_id) throw new GitHubError(422, `Pull request #${n} has no node id: convert it to draft on GitHub.`);
1052
+ const r = await this.request("POST", "/graphql", {
1053
+ query: "mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { number isDraft } } }",
1054
+ variables: { id: pr.node_id }
1055
+ });
1056
+ if (r?.errors?.length) {
1057
+ const detail = r.errors.map((e) => e.message).filter(Boolean).join(" ");
1058
+ throw new GitHubError(422, `GitHub refused to turn #${n} back into a draft${detail ? `: ${detail}` : "."} Use "Convert to draft" on GitHub instead.`, detail);
1059
+ }
1060
+ }
976
1061
  async createPullRequest(input) {
977
1062
  this.assertWritableBranch(input.head, input.base);
978
1063
  const r = await this.request(
@@ -1002,6 +1087,11 @@ var GitHubClient = class _GitHubClient {
1002
1087
  async addComment(issueNumber, body) {
1003
1088
  await this.request("POST", `${this.repoPath}/issues/${issueNumber}/comments`, { body });
1004
1089
  }
1090
+ /** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
1091
+ async listIssueComments(issueNumber) {
1092
+ const r = await this.request("GET", `${this.repoPath}/issues/${issueNumber}/comments?per_page=100`);
1093
+ return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at }));
1094
+ }
1005
1095
  // --- Issues (component change requests) ---------------------------------------------------
1006
1096
  async createIssue(input) {
1007
1097
  const r = await this.request("POST", `${this.repoPath}/issues`, {
@@ -1022,6 +1112,67 @@ var GitHubClient = class _GitHubClient {
1022
1112
  async getIssue(n) {
1023
1113
  return toIssue(await this.request("GET", `${this.repoPath}/issues/${n}`));
1024
1114
  }
1115
+ /** Edits an issue: its body, or closes it with a reason ("not_planned" for a declined request). */
1116
+ async updateIssue(n, patch) {
1117
+ this.assertToken("Editing an issue");
1118
+ return toIssue(await this.request("PATCH", `${this.repoPath}/issues/${n}`, patch));
1119
+ }
1120
+ /** Adds people to an issue's assignees (GitHub ignores logins that cannot be assigned: the result says who is). */
1121
+ async addAssignees(n, logins) {
1122
+ this.assertToken("Assigning an issue");
1123
+ const r = await this.request("POST", `${this.repoPath}/issues/${n}/assignees`, { assignees: logins });
1124
+ return (r.assignees ?? []).map((a) => a.login);
1125
+ }
1126
+ /** People with at least `permission` on the repository (reading the list needs push access). First 100. */
1127
+ async listCollaborators(permission = "push") {
1128
+ const r = await this.request("GET", `${this.repoPath}/collaborators?permission=${permission}&per_page=100`);
1129
+ return r.map((c) => ({ login: c.login, avatarUrl: c.avatar_url ?? "" }));
1130
+ }
1131
+ /** Whether a token is sent with each request (TeamReader). */
1132
+ get hasToken() {
1133
+ return Boolean(this.token);
1134
+ }
1135
+ /** Every page of a list endpoint (per_page 100), up to `maxPages`. */
1136
+ async paginate(path, maxPages = 20) {
1137
+ const out = [];
1138
+ const sep2 = path.includes("?") ? "&" : "?";
1139
+ for (let page = 1; page <= maxPages; page++) {
1140
+ const r = await this.request("GET", `${path}${sep2}per_page=100&page=${page}`);
1141
+ out.push(...r);
1142
+ if (r.length < 100) break;
1143
+ }
1144
+ return out;
1145
+ }
1146
+ /** Every member login of an organization team (needs read:org, or Members: Read on a fine-grained token). */
1147
+ async listTeamMembers(org, slug2) {
1148
+ const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug2)}/members`);
1149
+ return r.map((m) => m.login);
1150
+ }
1151
+ /** A user's membership of a team: "active", "pending", or null when GitHub says they are not a member (404). */
1152
+ async getTeamMembership(org, slug2, login) {
1153
+ try {
1154
+ const r = await this.request("GET", `/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug2)}/memberships/${encodeURIComponent(login)}`);
1155
+ return r.state === "active" ? "active" : "pending";
1156
+ } catch (err) {
1157
+ if (err instanceof GitHubError && err.status === 404) return null;
1158
+ throw err;
1159
+ }
1160
+ }
1161
+ /** "User" or "Organization" for a login, null when GitHub does not know it. */
1162
+ async getAccountType(login) {
1163
+ try {
1164
+ const r = await this.request("GET", `/users/${encodeURIComponent(login)}`);
1165
+ return r.type === "Organization" ? "Organization" : r.type === "User" ? "User" : null;
1166
+ } catch (err) {
1167
+ if (err instanceof GitHubError && err.status === 404) return null;
1168
+ throw err;
1169
+ }
1170
+ }
1171
+ /** The organization's teams this token can see. */
1172
+ async listOrgTeams(org) {
1173
+ const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams`);
1174
+ return r.map((t) => ({ slug: t.slug, name: t.name }));
1175
+ }
1025
1176
  /** Create or update a binary file (base64 content) on a branch, e.g. a Figma export attached to a request. */
1026
1177
  async putBase64File(input) {
1027
1178
  this.assertWritableBranch(input.branch);
@@ -1157,6 +1308,27 @@ var GitHubClient = class _GitHubClient {
1157
1308
  const pending = checks.some((c) => c.status !== "completed");
1158
1309
  return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
1159
1310
  }
1311
+ /** Commit statuses of a commit, newest first (Vercel reports each project as "Vercel – <project>"). */
1312
+ async listCommitStatuses(sha) {
1313
+ const r = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/statuses?per_page=100`);
1314
+ return r.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url ?? null, description: s.description ?? null }));
1315
+ }
1316
+ /**
1317
+ * GitHub deployments of a commit with their latest status (environment "Preview – <project>", environment_url the
1318
+ * preview). Latest per environment only, so it costs one read plus one per environment.
1319
+ */
1320
+ async listDeployments(sha) {
1321
+ const list = await this.request("GET", `${this.repoPath}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
1322
+ const latest = /* @__PURE__ */ new Map();
1323
+ for (const d of list) if (!latest.has(d.environment)) latest.set(d.environment, d);
1324
+ return Promise.all(
1325
+ [...latest.values()].map(async (d) => {
1326
+ const st = await this.request("GET", `${this.repoPath}/deployments/${d.id}/statuses?per_page=1`).catch(() => []);
1327
+ const s = st[0];
1328
+ return { id: d.id, environment: d.environment, state: s?.state ?? null, environmentUrl: s?.environment_url || null };
1329
+ })
1330
+ );
1331
+ }
1160
1332
  async getPullRequest(n) {
1161
1333
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
1162
1334
  return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null };
@@ -1183,7 +1355,44 @@ var GitHubClient = class _GitHubClient {
1183
1355
  "GET",
1184
1356
  `${this.repoPath}/commits/${encodeRef(ref)}/check-runs`
1185
1357
  );
1186
- return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url }));
1358
+ 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 } : {} }));
1359
+ }
1360
+ /** Rulesets that apply to the repository, without their rules (GET /rulesets; read access is enough). */
1361
+ async listRulesets() {
1362
+ const r = await this.request("GET", `${this.repoPath}/rulesets?includes_parents=true&per_page=100`);
1363
+ return r.map(withRulesetUrl);
1364
+ }
1365
+ /** One ruleset with its rules. */
1366
+ async getRuleset(id) {
1367
+ return withRulesetUrl(await this.request("GET", `${this.repoPath}/rulesets/${id}`));
1368
+ }
1369
+ /** Creates a repository ruleset. Needs admin on the repo (fine-grained token: Administration read and write). */
1370
+ async createRuleset(payload) {
1371
+ this.assertToken("Protecting the library");
1372
+ return withRulesetUrl(await this.request("POST", `${this.repoPath}/rulesets`, payload));
1373
+ }
1374
+ /** Replaces a repository ruleset (same permissions as createRuleset). */
1375
+ async updateRuleset(id, payload) {
1376
+ this.assertToken("Protecting the library");
1377
+ return withRulesetUrl(await this.request("PUT", `${this.repoPath}/rulesets/${id}`, payload));
1378
+ }
1379
+ /**
1380
+ * Classic branch protection of `branch`: the settings, null when the branch has none (404), undefined when this
1381
+ * token cannot read them (403: the endpoint needs admin). On a 403 the public branch summary still tells
1382
+ * whether the branch is protected at all: `protected: false` means there is nothing to read, so null.
1383
+ */
1384
+ async getBranchProtection(branch) {
1385
+ try {
1386
+ return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
1387
+ } catch (err) {
1388
+ if (!(err instanceof GitHubError)) throw err;
1389
+ if (err.status === 404) return null;
1390
+ if (err.status === 403 || err.status === 401) {
1391
+ const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
1392
+ return b?.protected === false ? null : void 0;
1393
+ }
1394
+ throw err;
1395
+ }
1187
1396
  }
1188
1397
  /** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
1189
1398
  async createCheckRun(input) {
@@ -1225,7 +1434,9 @@ function toSummary(r) {
1225
1434
  ...typeof r.body === "string" ? { body: r.body } : {}
1226
1435
  };
1227
1436
  }
1228
- function friendlyMessage(status, detail) {
1437
+ function friendlyMessage(status, detail, info = {}) {
1438
+ if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
1439
+ if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
1229
1440
  switch (status) {
1230
1441
  case 401:
1231
1442
  return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
@@ -1245,6 +1456,10 @@ function friendlyMessage(status, detail) {
1245
1456
  return `GitHub error ${status}${detail ? `: ${detail}` : ""}`;
1246
1457
  }
1247
1458
  }
1459
+ function withRulesetUrl(r) {
1460
+ const href = r._links?.html?.href;
1461
+ return href && !r.html_url ? { ...r, html_url: href } : r;
1462
+ }
1248
1463
  function toIssue(r) {
1249
1464
  return {
1250
1465
  number: r.number,
@@ -1850,6 +2065,38 @@ async function loadGitHubHistory(client2, config, opts = {}) {
1850
2065
  return buildReport({ source: "github", label: `${client2.fullName}@${ref}`, ref, entries, releases, ...currentVersion ? { currentVersion } : {}, truncated: listed >= limit });
1851
2066
  }
1852
2067
 
2068
+ // ../core/src/previews-github.ts
2069
+ async function branchPreviewUrls(gh, ref, config, opts = {}) {
2070
+ const apps = config?.previews?.apps ?? {};
2071
+ if (!Object.values(apps).some(Boolean)) return [];
2072
+ let pr;
2073
+ let branch = opts.branch;
2074
+ let sha = opts.sha;
2075
+ if (typeof ref === "number") {
2076
+ pr = ref;
2077
+ if (!branch || !sha) {
2078
+ const p = await gh.getPullRequest(ref);
2079
+ branch ??= p.headRef;
2080
+ sha ??= p.headSha;
2081
+ }
2082
+ } else {
2083
+ sha = ref;
2084
+ if (!branch || !opts.comments) {
2085
+ const open = (await gh.listPullRequestsForCommit(ref).catch(() => [])).find((p) => p.state === "open");
2086
+ if (open) {
2087
+ pr = open.number;
2088
+ branch ??= open.headRef;
2089
+ }
2090
+ }
2091
+ }
2092
+ const [comments, statuses, deployments] = await Promise.all([
2093
+ opts.comments ? Promise.resolve(opts.comments) : pr !== void 0 ? gh.listIssueComments(pr).catch(() => []) : Promise.resolve([]),
2094
+ sha ? gh.listCommitStatuses(sha).catch(() => []) : Promise.resolve([]),
2095
+ sha ? gh.listDeployments(sha).catch(() => []) : Promise.resolve([])
2096
+ ]);
2097
+ return resolveBranchPreviews(apps, { branch: branch ?? "", ...pr !== void 0 ? { pr } : {}, ...sha ? { sha } : {} }, { comments, statuses, deployments });
2098
+ }
2099
+
1853
2100
  // ../core/src/ramps.js
1854
2101
  var STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
1855
2102
  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 +2625,7 @@ function composeNewBrandBody(plan, opts) {
2378
2625
  }
2379
2626
 
2380
2627
  // ../core/src/docs-draft.ts
2628
+ var DOCS_KEYS = ["summary", "preview", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines", "anatomy"];
2381
2629
  var PROSE_KEYS = ["summary", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines"];
2382
2630
  var ACRONYMS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "xs"]);
2383
2631
  function anatomyLabel(className) {
@@ -2430,6 +2678,7 @@ function prefillDocs(input) {
2430
2678
  contentGuidelines: existing?.contentGuidelines ?? [],
2431
2679
  anatomy,
2432
2680
  ...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
2681
+ ...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
2433
2682
  ...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
2434
2683
  };
2435
2684
  return {
@@ -2444,7 +2693,17 @@ function prefillDocs(input) {
2444
2693
  draft
2445
2694
  };
2446
2695
  }
2447
- function docsPrompt(prefill, conventions = {}) {
2696
+ var SECTION_SHAPE = {
2697
+ summary: '"summary": "one or two sentences"',
2698
+ whenToUse: '"whenToUse": ["..."]',
2699
+ whenNotToUse: '"whenNotToUse": ["..."]',
2700
+ dos: '"dos": [{ "title": "...", "text": "..." }]',
2701
+ donts: '"donts": [{ "title": "...", "text": "..." }]',
2702
+ accessibility: '"accessibility": ["..."]',
2703
+ contentGuidelines: '"contentGuidelines": ["..."]'
2704
+ };
2705
+ var docsSectionShapes = (keys) => `{ ${keys.map((k) => SECTION_SHAPE[k]).join(", ")} }`;
2706
+ function docsPrompt(prefill, conventions = {}, options = {}) {
2448
2707
  const facts = [
2449
2708
  `Component: ${prefill.component}`,
2450
2709
  prefill.propsTable ? `Props (parsed from TypeScript):
@@ -2459,15 +2718,33 @@ ${prefill.propsTable}` : "Props: none of its own.",
2459
2718
  const block = (title, text) => text?.trim() ? `<${title}>
2460
2719
  ${text.trim()}
2461
2720
  </${title}>` : "";
2721
+ const keys = options.keys;
2722
+ const check = keys ? (options.check ?? []).filter((k) => keys.includes(k)) : [];
2723
+ const write = keys ? keys.filter((k) => !check.includes(k)) : [];
2724
+ const task = keys ? [
2725
+ ...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
2726
+ ...check.length ? [
2727
+ `Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
2728
+ "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.",
2729
+ "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."
2730
+ ] : [],
2731
+ ...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."] : []
2732
+ ] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
2733
+ const shape = keys ? [
2734
+ `- ${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.`,
2735
+ `- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
2736
+ ] : [
2737
+ `- 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.`,
2738
+ "- Answer with the full docs.json as one JSON object, nothing else."
2739
+ ];
2462
2740
  return [
2463
- `Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`,
2741
+ ...task,
2464
2742
  "",
2465
2743
  "Rules:",
2466
2744
  "- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
2467
2745
  "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
2468
2746
  '- 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
- `- 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.`,
2470
- "- Answer with the full docs.json as one JSON object, nothing else.",
2747
+ ...shape,
2471
2748
  "",
2472
2749
  "Facts computed from the code:",
2473
2750
  facts.join("\n\n"),
@@ -2476,11 +2753,179 @@ ${text.trim()}
2476
2753
  ${JSON.stringify(prefill.draft, null, 2)}`,
2477
2754
  "",
2478
2755
  block("source", conventions.source),
2756
+ block("styles", conventions.styles),
2479
2757
  block("agents-md", conventions.agents),
2480
2758
  block("voice", conventions.voice),
2481
2759
  block("example-docs-json", conventions.example)
2482
2760
  ].filter((x, i, all) => x !== "" || all[i - 1] !== "").join("\n");
2483
2761
  }
2762
+ function markClaudeDraft(d, sections2) {
2763
+ if (!sections2) {
2764
+ const { draftedSections: _all, ...rest } = d;
2765
+ return { ...rest, draftedBy: "claude", needsReview: true };
2766
+ }
2767
+ const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
2768
+ const union = PROSE_KEYS.filter((k) => waiting.includes(k) || sections2.includes(k));
2769
+ return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
2770
+ }
2771
+ function formatDocsJson(d) {
2772
+ const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k) => k !== "summary"), "draftedBy", "draftedSections", "needsReview"];
2773
+ const out = {};
2774
+ for (const k of order) if (d[k] !== void 0) out[k] = d[k];
2775
+ for (const [k, v] of Object.entries(d)) if (!(k in out) && v !== void 0) out[k] = v;
2776
+ return `${JSON.stringify(out, null, 2)}
2777
+ `;
2778
+ }
2779
+
2780
+ // ../core/src/change-drafts.ts
2781
+ function cleanDraftText(text) {
2782
+ return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
2783
+ }
2784
+ var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2785
+ var DRAFT_LINE_MAX = 120;
2786
+ function clipText(text, max) {
2787
+ const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
2788
+ if (t.length <= max) return t;
2789
+ const cut = t.slice(0, max + 1);
2790
+ const at = cut.lastIndexOf(" ");
2791
+ return (at > max * 0.5 ? cut.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
2792
+ }
2793
+ function sanitizeDraftLine(raw) {
2794
+ if (typeof raw !== "string") return "";
2795
+ return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
2796
+ }
2797
+
2798
+ // ../core/src/docs-ai.ts
2799
+ var DOCS_DRAFT_MODEL = "claude-sonnet-5-5";
2800
+ var ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
2801
+ var DOCS_SELECTION_MODES = {
2802
+ clearer: "Make it clearer: plain words, one idea per sentence, the most useful fact first.",
2803
+ shorter: "Make it shorter: keep every fact, drop filler, aim for half the words or fewer.",
2804
+ voice: "Put it in the Bridge voice: sentence case, verbs first, calm and precise, no marketing words, no em dash."
2805
+ };
2806
+ var isDocsSelectionMode = (m) => typeof m === "string" && m in DOCS_SELECTION_MODES;
2807
+ var isProseKey = (k) => typeof k === "string" && PROSE_KEYS.includes(k);
2808
+ function validateDocsTask(x) {
2809
+ if (!x || typeof x !== "object") return ["The docs task is missing."];
2810
+ const t = x;
2811
+ if (t.kind === "sections") {
2812
+ const keys = t.keys;
2813
+ if (!Array.isArray(keys) || !keys.length || !keys.every(isProseKey)) return [`The docs task lists no sections: use ${PROSE_KEYS.join(", ")}.`];
2814
+ if (t.check !== void 0 && (!Array.isArray(t.check) || !t.check.every(isProseKey))) return ["check must list prose sections."];
2815
+ return [];
2816
+ }
2817
+ if (t.kind === "selection") {
2818
+ const out = [];
2819
+ if (!isProseKey(t.key)) out.push(`The selection needs a section: ${PROSE_KEYS.join(", ")}.`);
2820
+ if (typeof t.text !== "string" || !t.text.trim()) out.push("The selection has no text.");
2821
+ if (!isDocsSelectionMode(t.mode)) out.push("The selection mode must be clearer, shorter or voice.");
2822
+ return out;
2823
+ }
2824
+ return ['The docs task kind must be "sections" or "selection".'];
2825
+ }
2826
+ var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
2827
+ function parseDocsReply(text) {
2828
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
2829
+ const body = fenced ? fenced[1] : text;
2830
+ const start = body.indexOf("{");
2831
+ const end = body.lastIndexOf("}");
2832
+ if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
2833
+ const json = JSON.parse(body.slice(start, end + 1));
2834
+ if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("The reply is not a docs.json object.");
2835
+ return json;
2836
+ }
2837
+ var cleanDocsText = (s) => cleanDraftText(s.replace(/[\r\n]+/g, " "));
2838
+ var isExample = (x) => {
2839
+ if (typeof x === "string") return true;
2840
+ if (!x || typeof x !== "object" || Array.isArray(x)) return false;
2841
+ const n = x;
2842
+ if (typeof n.c !== "string" || !n.c) return false;
2843
+ if (n.p !== void 0 && (!n.p || typeof n.p !== "object" || Array.isArray(n.p))) return false;
2844
+ return n.children === void 0 || Array.isArray(n.children) && n.children.every(isExample);
2845
+ };
2846
+ var textList = (x) => {
2847
+ if (typeof x === "string") x = [x];
2848
+ if (!Array.isArray(x)) return void 0;
2849
+ const out = x.filter((s) => typeof s === "string").map(cleanDocsText).filter(Boolean);
2850
+ return out.length ? out : void 0;
2851
+ };
2852
+ var guidanceList = (x, examples) => {
2853
+ if (!Array.isArray(x)) return void 0;
2854
+ const out = [];
2855
+ for (const g of x) {
2856
+ if (!g || typeof g !== "object") continue;
2857
+ const r = g;
2858
+ const title = typeof r.title === "string" ? cleanDocsText(r.title) : "";
2859
+ const text = typeof r.text === "string" ? cleanDocsText(r.text) : "";
2860
+ if (!title || !text) continue;
2861
+ out.push({ title, text, ...examples && r.example !== void 0 && isExample(r.example) ? { example: r.example } : {} });
2862
+ }
2863
+ return out.length ? out : void 0;
2864
+ };
2865
+ function sanitizeDocsSections(raw, keys = PROSE_KEYS, opts = {}) {
2866
+ const out = {};
2867
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
2868
+ const r = raw;
2869
+ for (const k of keys) {
2870
+ const v = r[k];
2871
+ if (k === "summary") {
2872
+ const s = typeof v === "string" ? cleanDocsText(v) : "";
2873
+ if (s) out.summary = s;
2874
+ } else if (k === "dos" || k === "donts") {
2875
+ const g = guidanceList(v, opts.examples ?? false);
2876
+ if (g) out[k] = g;
2877
+ } else {
2878
+ const l = textList(v);
2879
+ if (l) out[k] = l;
2880
+ }
2881
+ }
2882
+ return out;
2883
+ }
2884
+ function sanitizeDocsReplacement(raw) {
2885
+ if (typeof raw !== "string") return "";
2886
+ return cleanDocsText(raw.trim().replace(/^["'“]+|["'”]+$/g, ""));
2887
+ }
2888
+ function docsSectionsAnswer(task, raw) {
2889
+ const check = (task.check ?? []).filter((k) => task.keys.includes(k));
2890
+ const sections2 = check.length ? sanitizeDocsSections(raw, task.keys) : docsSectionsResult(task, raw);
2891
+ return { sections: sections2, checked: check.filter((k) => sections2[k] === void 0) };
2892
+ }
2893
+ function docsSectionsResult(task, raw) {
2894
+ const sections2 = sanitizeDocsSections(raw, task.keys);
2895
+ if (!Object.keys(sections2).length) throw new Error(`Claude's answer holds none of the sections asked for (${task.keys.join(", ")}). Try again.`);
2896
+ return sections2;
2897
+ }
2898
+ function mergeDocsDraft(prefillDraft, reply) {
2899
+ const empty = PROSE_KEYS.filter((k) => {
2900
+ const v = prefillDraft[k];
2901
+ return v === void 0 || (typeof v === "string" ? !v.trim() : Array.isArray(v) && !v.length);
2902
+ });
2903
+ const sections2 = sanitizeDocsSections(reply, empty, { examples: true });
2904
+ const merged = { ...prefillDraft, ...sections2 };
2905
+ if (reply.preview && !prefillDraft.preview && isExample(reply.preview)) merged.preview = reply.preview;
2906
+ merged.anatomy = prefillDraft.anatomy;
2907
+ const drafted = PROSE_KEYS.filter((k) => sections2[k] !== void 0);
2908
+ return markClaudeDraft(merged, drafted.length && drafted.length < PROSE_KEYS.length ? drafted : void 0);
2909
+ }
2910
+ function anthropicRequest(input) {
2911
+ const headers = { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
2912
+ if (input.fallback) headers["anthropic-beta"] = "server-side-fallback-2026-07-01";
2913
+ if (input.browser) headers["anthropic-dangerous-direct-browser-access"] = "true";
2914
+ const body = {
2915
+ model: input.model || DOCS_DRAFT_MODEL,
2916
+ max_tokens: input.maxTokens ?? 16e3,
2917
+ ...input.fallback ? { fallbacks: "default" } : {},
2918
+ ...input.system ? { system: input.system } : {},
2919
+ messages: [{ role: "user", content: input.prompt }]
2920
+ };
2921
+ return { url: ANTHROPIC_MESSAGES_URL, init: { method: "POST", headers, body: JSON.stringify(body) } };
2922
+ }
2923
+ function anthropicReplyText(msg) {
2924
+ const m = msg ?? {};
2925
+ if (m.stop_reason === "refusal") throw new Error("Claude declined to draft this one.");
2926
+ if (m.stop_reason === "max_tokens") throw new Error("The reply was cut off (max_tokens).");
2927
+ return (m.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
2928
+ }
2484
2929
 
2485
2930
  // ../core/src/explorations.ts
2486
2931
  var EXPLORATION_LABEL = "exploration";
@@ -2535,16 +2980,6 @@ function explorationNote(e) {
2535
2980
  ""
2536
2981
  ].join("\n");
2537
2982
  }
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
2983
  function explorationPrTitle(e) {
2549
2984
  const n = e.name.trim().replace(/\.$/, "");
2550
2985
  return `Exploration: ${n.charAt(0).toLowerCase()}${n.slice(1)}`;
@@ -2570,20 +3005,9 @@ function explorationPrBody(e, ctx) {
2570
3005
  "### Try it in a product staging branch",
2571
3006
  ""
2572
3007
  ];
2573
- if (ctx.pr) {
2574
- const install = explorationInstall({ owner: ctx.owner, repo: ctx.repo, pr: ctx.pr, packages: ctx.packages });
2575
- lines.push(
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
- }
3008
+ lines.push(
3009
+ '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.'
3010
+ );
2587
3011
  lines.push("", "### Previews", "");
2588
3012
  const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
2589
3013
  if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url} |`));
@@ -2595,7 +3019,7 @@ function explorationPrBody(e, ctx) {
2595
3019
  `- **Promote:** in Bridge Studio (Changes, Explorations, Promote to review) or \`bridge explore promote ${slug2}\`. Bridge marks the pull request ready for review, removes the \`${EXPLORATION_LABEL}\` label and the exploration note, and adds the tier labels. Then it goes through the usual review.`,
2596
3020
  "- **Close:** close this pull request. The branch stays for reference; the preview prerelease is removed.",
2597
3021
  "",
2598
- `<!-- ${MARK} ${JSON.stringify({ name: e.name.trim(), purpose: e.purpose.trim(), slug: slug2, ...e.useCase ? { useCase: e.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e.author ? { author: e.author } : {}, ...e.startedAt ? { startedAt: e.startedAt } : {} }).replace(/-->/g, "--\\u003e")} -->`
3022
+ `<!-- ${MARK} ${JSON.stringify({ name: e.name.trim(), purpose: e.purpose.trim(), slug: slug2, ...e.useCase ? { useCase: e.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e.author ? { author: e.author } : {}, ...e.startedAt ? { startedAt: e.startedAt } : {}, ...e.source ? { source: e.source } : {}, ...e.agent ? { agent: e.agent } : {} }).replace(/-->/g, "--\\u003e")} -->`
2599
3023
  );
2600
3024
  return lines.join("\n");
2601
3025
  }
@@ -2652,6 +3076,13 @@ function promotionPlan(input) {
2652
3076
  markReady: true
2653
3077
  };
2654
3078
  }
3079
+ function explorationTryIt(input) {
3080
+ const c = readPreviewComment(input.comment);
3081
+ if (!c) return { state: "building" };
3082
+ if (input.headSha && c.sha7 && !input.headSha.startsWith(c.sha7)) return { state: "building" };
3083
+ if (!c.command || c.source === "none") return { state: "failed" };
3084
+ return { state: "ready", command: c.command, source: c.source, sha7: c.sha7 };
3085
+ }
2655
3086
  function explorationRules(branch) {
2656
3087
  return [
2657
3088
  `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
@@ -2664,7 +3095,8 @@ function explorationRules(branch) {
2664
3095
  var REQUEST_KINDS = [
2665
3096
  { value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
2666
3097
  { value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
2667
- { value: "prop-change", label: "Prop change", hint: "Add, rename or change a prop or its default." },
3098
+ { value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
3099
+ { value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
2668
3100
  { value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
2669
3101
  { value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
2670
3102
  { value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
@@ -2686,6 +3118,7 @@ function composeRequestIssue(r, opts = {}) {
2686
3118
  "",
2687
3119
  r.summary.trim(),
2688
3120
  "",
3121
+ ...r.line?.trim() ? [`**What changes:** ${r.line.trim()}`, ""] : [],
2689
3122
  ...rows.length ? [...rows, ""] : [],
2690
3123
  ...r.images?.length ? [...r.images.map((u, i) => `![${r.component} design ${i + 1}](${u})`), ""] : [],
2691
3124
  ...figma ? [`**Design:** ${figma}`, ""] : [],
@@ -2733,6 +3166,7 @@ function requestBrief(r, ctx) {
2733
3166
  `Component: ${r.component}`,
2734
3167
  `Kind: ${kindLabel(r.kind)}`,
2735
3168
  `Request: ${r.title}`,
3169
+ ...r.line?.trim() ? [`What changes: ${r.line.trim()}`] : [],
2736
3170
  "",
2737
3171
  r.summary.trim(),
2738
3172
  "",
@@ -2750,11 +3184,6 @@ function requestBrief(r, ctx) {
2750
3184
  var cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
2751
3185
  var sourceLabel = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
2752
3186
 
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
3187
  // ../core/src/draft-exchange.ts
2759
3188
  var DRAFT_EXCHANGE_VERSION = 1;
2760
3189
  var DRAFTS_DIR = ".bridge/drafts";
@@ -2768,12 +3197,16 @@ var DRAFT_VOICE_RULES = [
2768
3197
  "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
3198
  "Keep the designer's intent from their notes.",
2770
3199
  "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."
3200
+ "A request title never repeats the component name: the issue title already starts with it.",
3201
+ `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.`,
3202
+ `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
3203
  ];
2773
3204
  var DRAFT_OUTPUT_CONTRACT = [
2774
- `Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "..."}]}, one result per item.`,
3205
+ `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
3206
  `title: one line, at most ${DRAFT_TITLE_MAX} characters, no trailing period.`,
2776
3207
  `summary: at most ${DRAFT_SUMMARY_SENTENCES} sentences, plain text.`,
3208
+ `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.`,
3209
+ `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
3210
  "The designer reviews the text in Figma before anything is sent."
2778
3211
  ];
2779
3212
  var SESSION_RE = /^[a-z0-9-]{6,64}$/;
@@ -2793,7 +3226,7 @@ function validatePendingDrafts(x) {
2793
3226
  if (!isDraftSessionId(p.sessionId)) out.push("The session id is missing or invalid.");
2794
3227
  if (!str(p.createdAt) || Number.isNaN(Date.parse(p.createdAt))) out.push("createdAt must be an ISO date.");
2795
3228
  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".');
3229
+ if (p.source !== "figma" && p.source !== "studio") out.push('source must be "figma" or "studio".');
2797
3230
  if (!Array.isArray(p.items) || !p.items.length) out.push("A draft session needs at least one item.");
2798
3231
  const ids = /* @__PURE__ */ new Set();
2799
3232
  for (const [n, i] of (Array.isArray(p.items) ? p.items : []).entries()) {
@@ -2805,7 +3238,12 @@ function validatePendingDrafts(x) {
2805
3238
  if (!str(i.id) || !i.id) out.push(`${at} has no id.`);
2806
3239
  else if (ids.has(i.id)) out.push(`${at} repeats the id "${i.id}".`);
2807
3240
  else ids.add(i.id);
2808
- if (i.scope !== "request" && i.scope !== "push") out.push(`${at}: scope must be "request" or "push".`);
3241
+ if (i.scope !== "request" && i.scope !== "push" && i.scope !== "docs") out.push(`${at}: scope must be "request", "push" or "docs".`);
3242
+ if (i.scope === "docs") {
3243
+ const d = i.docs;
3244
+ 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}.`);
3245
+ else out.push(...validateDocsTask(d.task).map((m) => `${at}: ${m}`));
3246
+ } else if (i.docs !== void 0) out.push(`${at}: only a docs item has docs.`);
2809
3247
  if (!optStr(i.component)) out.push(`${at}: component must be text.`);
2810
3248
  if (i.kind !== void 0 && !KINDS.has(i.kind)) out.push(`${at}: unknown kind "${String(i.kind)}".`);
2811
3249
  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 +3251,17 @@ function validatePendingDrafts(x) {
2813
3251
  if (i.variables !== void 0 && i.scope !== "push") out.push(`${at}: only a push item has variables.`);
2814
3252
  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
3253
  if (!i.draft || !str(i.draft.title) || !str(i.draft.summary)) out.push(`${at}: draft must hold a title and a summary.`);
3254
+ 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
3255
  }
2817
3256
  if (p.results !== void 0) {
2818
3257
  if (!Array.isArray(p.results)) out.push("results must be a list.");
2819
- else for (const r of p.results) if (!r || !str(r.id) || !str(r.title) || !str(r.summary)) out.push("Each result needs an id, a title and a summary.");
3258
+ else
3259
+ for (const r of p.results) {
3260
+ if (!r || !str(r.id) || !str(r.title) || !str(r.summary)) out.push("Each result needs an id, a title and a summary.");
3261
+ 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.");
3262
+ else if (!optStr(r.replacement)) out.push("A result replacement must be text.");
3263
+ 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}.");
3264
+ }
2820
3265
  }
2821
3266
  return out;
2822
3267
  }
@@ -2853,19 +3298,69 @@ function sanitizeDraftText(raw) {
2853
3298
  const summary2 = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
2854
3299
  return { title, summary: summary2 };
2855
3300
  }
3301
+ function sanitizeDocsResult(item, r) {
3302
+ const task = item.docs.task;
3303
+ if (task.kind === "selection") {
3304
+ const raw = r.replacement ?? (r.docs && typeof r.docs === "object" ? r.docs[task.key] : void 0);
3305
+ const replacement = sanitizeDocsReplacement(raw);
3306
+ if (!replacement) throw new Error(`Item "${item.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item.id}", "replacement": "..."}.`);
3307
+ return { replacement };
3308
+ }
3309
+ try {
3310
+ const { sections: sections2, checked } = docsSectionsAnswer(task, r.docs ?? {});
3311
+ return { docs: sections2, checked };
3312
+ } catch {
3313
+ 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.`);
3314
+ }
3315
+ }
3316
+ function sanitizeResultLines(item, raw) {
3317
+ if (raw === void 0 || raw === null) return [];
3318
+ if (!Array.isArray(raw)) throw new Error(`lines for "${item.id}" must be a list of {"id", "line", "title"}.`);
3319
+ const known = new Set((item.lines ?? []).map((l) => l.id));
3320
+ const out = /* @__PURE__ */ new Map();
3321
+ for (const l of raw) {
3322
+ const id = typeof l?.id === "string" ? l.id : "";
3323
+ 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."}`);
3324
+ const line = sanitizeDraftLine(l.line);
3325
+ const title = sanitizeDraftTitle(l.title);
3326
+ if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
3327
+ }
3328
+ return [...out.values()];
3329
+ }
3330
+ function sanitizeDraftTitle(raw) {
3331
+ if (typeof raw !== "string") return "";
3332
+ const t = clip(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_TITLE_MAX);
3333
+ return t.charAt(0).toUpperCase() + t.slice(1);
3334
+ }
2856
3335
  function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
2857
3336
  const ids = new Set(p.items.map((i) => i.id));
2858
3337
  const next = new Map((p.results ?? []).map((r) => [r.id, r]));
2859
3338
  for (const r of results) {
2860
3339
  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(", ")}.`);
3340
+ const item = p.items.find((i) => i.id === r.id);
3341
+ if (item.scope === "docs") {
3342
+ next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(item, r), by: "claude", at: at.toISOString() });
3343
+ continue;
3344
+ }
2861
3345
  const clean = sanitizeDraftText(r);
2862
- if (!clean.title && !clean.summary) throw new Error(`The result for "${r.id}" has no title and no summary.`);
2863
- next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, by: "claude", at: at.toISOString() });
3346
+ const lines = sanitizeResultLines(item, r.lines);
3347
+ if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
3348
+ next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at.toISOString() });
2864
3349
  }
2865
3350
  const merged = p.items.map((i) => next.get(i.id)).filter((r) => !!r);
2866
3351
  return { ...p, results: merged, status: merged.length === p.items.length ? "drafted" : "pending" };
2867
3352
  }
2868
3353
  function draftItemBrief(i) {
3354
+ if (i.scope === "docs" && i.docs) {
3355
+ const t = i.docs.task;
3356
+ const check = t.kind === "sections" ? (t.check ?? []).filter((k) => t.keys.includes(k)) : [];
3357
+ const write = t.kind === "sections" ? t.keys.filter((k) => !check.includes(k)) : [];
3358
+ const what = t.kind === "selection" ? `rewrite one passage of the ${t.key} section (${t.mode}), answer with "replacement"` : [
3359
+ write.length ? `${t.rewrite ? "rewrite" : "write"} the sections ${write.join(", ")}` : "",
3360
+ 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)` : ""
3361
+ ].filter(Boolean).join("; ") + ', answer with "docs"';
3362
+ 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");
3363
+ }
2869
3364
  const kind = i.kind ? REQUEST_KINDS.find((k) => k.value === i.kind)?.label : void 0;
2870
3365
  const cell3 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
2871
3366
  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 +3369,17 @@ function draftItemBrief(i) {
2874
3369
  if (i.components?.length) lines.push(`Components with a change request: ${i.components.join(", ")}`);
2875
3370
  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
3371
  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.");
3372
+ if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines.push("", "No change detected: rely on the notes and the images.");
2878
3373
  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
3374
  if (i.notes?.trim()) lines.push("", "The designer's notes (keep their intent):", i.notes.trim());
2880
- if (i.images?.length) lines.push("", `Images: ${i.images.map((m) => `${m.label} ${m.url}`).join(", ")}`);
3375
+ if (i.lines?.length) {
3376
+ 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):`);
3377
+ for (const l of i.lines) {
3378
+ 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}` : ""}`);
3379
+ for (const c of l.changes) lines.push(` - ${c.field}: ${c.before || "none"} -> ${c.after || "removed"}`);
3380
+ }
3381
+ }
3382
+ if (i.images?.length) lines.push("", `Images: ${i.images.map((m) => `${m.label}${m.line ? ` (line ${m.line})` : ""} ${m.url}`).join(", ")}`);
2881
3383
  return lines.join("\n");
2882
3384
  }
2883
3385
  var isConflict = (err) => err instanceof GitHubError ? err.status === 409 || err.status === 422 && /sha/i.test(`${err.detail ?? ""} ${err.message}`) : err?.status === 409;
@@ -2920,9 +3422,10 @@ function writeDraftResults(client2, sessionId, results, opts = {}) {
2920
3422
  message: `Bridge drafts: ${results.length === 1 ? "1 text" : `${results.length} texts`} drafted by Claude`
2921
3423
  });
2922
3424
  }
3425
+ var DRAFT_POLL_TIMEOUT_MS = 10 * 6e4;
2923
3426
 
2924
3427
  // ../core/src/explorations-github.ts
2925
- function explorationSummary(p, owner, repo, s) {
3428
+ function explorationSummary(p, _owner, _repo, s) {
2926
3429
  const e = parseExplorationBody(p.body);
2927
3430
  const slug2 = p.headRef?.startsWith(EXPLORATION_PREFIX) ? p.headRef.slice(EXPLORATION_PREFIX.length) : e?.slug ?? explorationSlug(p.title);
2928
3431
  return {
@@ -2936,9 +3439,11 @@ function explorationSummary(p, owner, repo, s) {
2936
3439
  status: explorationStatus({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body }),
2937
3440
  draft: p.draft,
2938
3441
  author: e?.author ?? p.author,
3442
+ ...e?.source ? { source: e.source } : {},
3443
+ ...e?.agent ? { agent: e.agent } : {},
2939
3444
  updatedAt: p.updatedAt,
2940
- previews: previewLinks(s.apps, { branch: p.headRef, pr: p.number, sha: p.headSha }),
2941
- install: explorationInstall({ owner, repo, pr: p.number, packages: s.packages, sha: p.headSha })
3445
+ headSha: p.headSha,
3446
+ previews: previewLinks(s.apps, { branch: p.headRef, pr: p.number, sha: p.headSha })
2942
3447
  };
2943
3448
  }
2944
3449
  async function listExplorationsOnGitHub(gh, s, opts = {}) {
@@ -2964,7 +3469,7 @@ ${full.purpose.trim()}
2964
3469
  Exploration: never merged as is. Promote it to review or close it.`,
2965
3470
  files: [{ path: explorationNotePath(slug2), content: explorationNote(full) }]
2966
3471
  });
2967
- const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps };
3472
+ const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
2968
3473
  const pr = await x.createPullRequest({ title: explorationPrTitle(full), head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
2969
3474
  await x.addLabels(pr.number, [EXPLORATION_LABEL]);
2970
3475
  await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
@@ -2974,10 +3479,16 @@ Exploration: never merged as is. Promote it to review or close it.`,
2974
3479
  pr: pr.number,
2975
3480
  url: pr.url,
2976
3481
  commit: commit.sha,
2977
- install: explorationInstall({ owner: gh.owner, repo: gh.name, pr: pr.number, packages: s.packages, sha: commit.sha }),
3482
+ tryIt: { state: "building" },
2978
3483
  previews: previewLinks(s.apps, { branch, pr: pr.number, sha: commit.sha })
2979
3484
  };
2980
3485
  }
3486
+ async function explorationTestBuildOnGitHub(gh, e, config) {
3487
+ const [comments, head] = await Promise.all([gh.listIssueComments(e.number), gh.getBranchSha(e.branch).catch(() => void 0)]);
3488
+ const tryIt = explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
3489
+ const previews = await branchPreviewUrls(gh, e.number, config, { branch: e.branch, sha: head, comments }).catch(() => []);
3490
+ return { tryIt, previews, settled: tryIt.state !== "building" && previewsSettled(previews) };
3491
+ }
2981
3492
  async function findExplorationOnGitHub(gh, ref) {
2982
3493
  if (typeof ref === "number" || /^#?\d+$/.test(String(ref))) {
2983
3494
  const p = await gh.getPullRequest(Number(String(ref).replace(/^#/, "")));
@@ -3001,6 +3512,7 @@ async function promoteExplorationOnGitHub(gh, ref, opts = {}) {
3001
3512
  const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
3002
3513
  if (plan.removeFiles.length) {
3003
3514
  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) => {
3515
+ if (e instanceof GitHubError && e.auth === "rejected") throw e;
3004
3516
  out.warnings.push(`The exploration note stays on the branch (${e.message}). Delete ${plan.removeFiles.join(", ")} before merging.`);
3005
3517
  });
3006
3518
  }
@@ -4632,7 +5144,9 @@ async function detectConfig(files, read, extra = {}) {
4632
5144
  branchPrefix: "bridge/",
4633
5145
  labels: ["source: mcp"],
4634
5146
  guardrails: [guidelines, "CLAUDE.md", ".github/", governance, contrast, CONFIG_FILE].filter(Boolean),
4635
- owners: { design: [], tech: [] }
5147
+ owners: { design: [], tech: [] },
5148
+ // New configs start from the default roles: design owners approve design, tech owners approve tech and ship.
5149
+ rights: DEFAULT_RIGHTS
4636
5150
  }
4637
5151
  });
4638
5152
  }
@@ -5187,6 +5701,7 @@ __export(ops_exports, {
5187
5701
  normalizeTokenPath: () => normalizeTokenPath,
5188
5702
  proposeTokenChange: () => proposeTokenChange,
5189
5703
  reviewPr: () => reviewPr,
5704
+ reviewRoles: () => reviewRoles,
5190
5705
  scaffoldComponentOp: () => scaffoldComponentOp,
5191
5706
  searchTokens: () => searchTokens,
5192
5707
  validateTokens: () => validateTokens,
@@ -5324,6 +5839,7 @@ __export(explorations_exports, {
5324
5839
  closeExploration: () => closeExploration,
5325
5840
  explorationContext: () => explorationContext,
5326
5841
  explorationRules: () => explorationRules,
5842
+ installOf: () => installOf,
5327
5843
  listExplorations: () => listExplorations,
5328
5844
  promoteExploration: () => promoteExploration,
5329
5845
  startExploration: () => startExploration,
@@ -5361,6 +5877,8 @@ async function startExploration(ctx, a) {
5361
5877
  ...a.useCase ? { useCase: a.useCase } : {},
5362
5878
  ...a.experimental?.length ? { experimental: a.experimental } : {},
5363
5879
  ...a.author ? { author: a.author } : {},
5880
+ ...a.source ? { source: a.source } : {},
5881
+ ...a.agent ? { agent: a.agent } : {},
5364
5882
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
5365
5883
  };
5366
5884
  const gh = await ctx.github().catch(() => null);
@@ -5377,7 +5895,16 @@ async function startExploration(ctx, a) {
5377
5895
  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
5896
  if (gh && ctx.canWriteRemote) {
5379
5897
  const r = await startExplorationOnGitHub(gh, e, s);
5380
- return { status: "started", ...preview, pr: r.pr, url: r.url, commit: r.commit, install: r.install, previews: r.previews, next: work };
5898
+ return {
5899
+ status: "started",
5900
+ ...preview,
5901
+ pr: r.pr,
5902
+ url: r.url,
5903
+ commit: r.commit,
5904
+ install: installOf(r.tryIt),
5905
+ previews: r.previews,
5906
+ 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.`
5907
+ };
5381
5908
  }
5382
5909
  if (ctx.mode === "local" && ctx.isGit) {
5383
5910
  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 +5931,11 @@ Exploration: never merged as is. Promote it to review or close it.`,
5404
5931
  }
5405
5932
  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
5933
  }
5934
+ function installOf(t) {
5935
+ 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.` };
5936
+ 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." };
5937
+ 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.' };
5938
+ }
5407
5939
  async function requestsByExploration(gh) {
5408
5940
  const out = /* @__PURE__ */ new Map();
5409
5941
  try {
@@ -5420,7 +5952,15 @@ async function listExplorations(ctx, a = {}) {
5420
5952
  const { config } = await ctx.config();
5421
5953
  const gh = await requireGitHub(ctx);
5422
5954
  const [list, linked] = await Promise.all([listExplorationsOnGitHub(gh, settingsOf(config), { state: a.state ?? "open" }), requestsByExploration(gh)]);
5423
- const explorations = list.map((x) => ({ ...x, requests: linked.get(x.branch) ?? [] }));
5955
+ const explorations = await Promise.all(
5956
+ list.map(async (x) => {
5957
+ const requests = linked.get(x.branch) ?? [];
5958
+ if (x.status !== "active") return { ...x, install: null, requests };
5959
+ const t = await explorationTestBuildOnGitHub(gh, { number: x.number, branch: x.branch }, config).catch(() => null);
5960
+ if (!t) return { ...x, install: installOf({ state: "building" }), requests };
5961
+ return { ...x, previews: t.previews.map((p) => ({ app: p.app, label: p.label, url: p.url, state: p.state })), install: installOf(t.tryIt), requests };
5962
+ })
5963
+ );
5424
5964
  let localBranches = [];
5425
5965
  if (ctx.mode === "local" && ctx.isGit) {
5426
5966
  const refs = (await git(ctx.root, ["for-each-ref", "--format=%(refname:short)", `refs/heads/${EXPLORATION_PREFIX}`]).catch(() => "")).split("\n").filter(Boolean);
@@ -5962,6 +6502,7 @@ async function getReview(ctx, a) {
5962
6502
  approvals: [...latest.entries()].filter(([, s]) => s === "APPROVED").map(([u]) => u),
5963
6503
  changesRequested: [...latest.entries()].filter(([, s]) => s === "CHANGES_REQUESTED").map(([u]) => u)
5964
6504
  };
6505
+ meta.roles = await reviewRoles(gh, config, meta.approvals);
5965
6506
  }
5966
6507
  const [before, after] = await Promise.all([setAt2(baseSrc, config), setAt2(headSrc, config)]);
5967
6508
  const changes = core.diff(before, after);
@@ -5989,6 +6530,25 @@ async function getReview(ctx, a) {
5989
6530
  impact: imp
5990
6531
  };
5991
6532
  }
6533
+ var sameLogin = (a, b) => a.trim().toLowerCase().replace(/^@/, "") === b.trim().toLowerCase().replace(/^@/, "");
6534
+ async function reviewRoles(gh, config, approvers) {
6535
+ const { owners, rights } = config.governance;
6536
+ const teams = await new TeamDirectory(gh).resolve(owners);
6537
+ const entries = approverEntries(owners, rights);
6538
+ const lists = expandOwners(entries, teams);
6539
+ const role = (k) => ({
6540
+ owners: entries[k].map((e) => entryLabel(e, teams)),
6541
+ approvedBy: approvers.filter((a) => lists[k].some((x) => sameLogin(x, a)))
6542
+ });
6543
+ const ship = shipPolicy(owners, rights, teams);
6544
+ const unreadable = unreadableTeams(teams);
6545
+ return {
6546
+ design: role("design"),
6547
+ tech: role("tech"),
6548
+ ship: ship ? whoCanShipSentence(ship) : "Anyone with write access can ship.",
6549
+ ...unreadable.length ? { unreadableTeams: unreadable.map((t) => t.message) } : {}
6550
+ };
6551
+ }
5992
6552
  async function reviewPr(ctx, a) {
5993
6553
  const gh = await ctx.github();
5994
6554
  if (!gh) throw new Error('No GitHub repo known: set "repo" in bridge.config.json.');
@@ -6025,7 +6585,7 @@ async function visualImpact(ctx, a, run2) {
6025
6585
  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
6586
  if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
6027
6587
  const out = a.out ? resolve3(ctx.root, a.out) : join4(ctx.root, "visual-impact");
6028
- const runImpact = run2 ?? (await import("./src-ABJPOTKE.js")).runImpact;
6588
+ const runImpact = run2 ?? (await import("./src-XSBCZV4W.js")).runImpact;
6029
6589
  const r = await runImpact({
6030
6590
  root: ctx.root,
6031
6591
  base: a.base ?? "main",
@@ -6620,7 +7180,7 @@ async function pendingDrafts(ctx, a) {
6620
7180
  for (const img of item.images ?? []) {
6621
7181
  if (images.length >= IMAGES_PER_CALL || !img.path) continue;
6622
7182
  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 });
7183
+ 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
7184
  }
6625
7185
  }
6626
7186
  }
@@ -6632,12 +7192,12 @@ async function pendingDrafts(ctx, a) {
6632
7192
  sessions: sessions.map((s) => ({ sessionId: s.sessionId, createdAt: s.createdAt, status: s.status, items: s.items, briefs: s.items.map(draftItemBrief) })),
6633
7193
  voice: DRAFT_VOICE_RULES,
6634
7194
  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.`
7195
+ 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
7196
  };
6637
7197
  return { answer, images };
6638
7198
  }
6639
7199
  async function writeDrafts(ctx, a) {
6640
- if (!a.results.length) throw new Error('Send at least one result: {"id", "title", "summary"} per item.');
7200
+ 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
7201
  const gh = await requireGitHub3(ctx);
6642
7202
  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
7203
  const out = await writeDraftResults(gh, a.sessionId, a.results);
@@ -6647,12 +7207,12 @@ async function writeDrafts(ctx, a) {
6647
7207
  status: out.status,
6648
7208
  results: out.results ?? [],
6649
7209
  ...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.'
7210
+ 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
7211
  };
6652
7212
  }
6653
7213
 
6654
7214
  // src/server.ts
6655
- var VERSION = "0.1.0";
7215
+ var VERSION = true ? "0.3.1" : "dev";
6656
7216
  var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
6657
7217
  var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
6658
7218
  var run = (fn) => async (a) => {
@@ -6680,6 +7240,7 @@ function createServer(ctx) {
6680
7240
  {
6681
7241
  instructions: [
6682
7242
  "Bridge exposes a design system repo (DTCG tokens, components, docs) to you.",
7243
+ "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
7244
  "Read before you write: bridge_get_guidelines for the repo rules, bridge_search_tokens / bridge_get_token before using or proposing a token.",
6684
7245
  "Never hardcode a value in UI code when a token exists; never invent a token name (check it with bridge_get_token).",
6685
7246
  "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 +7249,7 @@ function createServer(ctx) {
6688
7249
  "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
7250
  "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
7251
  '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, write each title and summary in the Bridge voice, then call bridge_write_drafts. The designer reviews them in the DS Bridge Figma plugin.`,
7252
+ `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
7253
  '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
7254
  ].join(" ")
6694
7255
  }
@@ -6881,7 +7442,7 @@ function createServer(ctx) {
6881
7442
  "bridge_get_review",
6882
7443
  {
6883
7444
  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 approvals. Pass `pr` (GitHub) or `branch` (local proposal branch).",
7445
+ 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
7446
  inputSchema: { pr: z.number().int().positive().optional(), branch: z.string().optional() },
6886
7447
  annotations: { readOnlyHint: true, openWorldHint: true }
6887
7448
  },
@@ -7074,9 +7635,11 @@ function createServer(ctx) {
7074
7635
  title: "Pending change texts from Figma",
7075
7636
  description: [
7076
7637
  `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.",
7638
+ "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
7639
  "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
- `Then call bridge_write_drafts with one result per item id: title at most ${DRAFT_TITLE_MAX} characters, summary at most 3 sentences.`
7640
+ `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".`,
7641
+ `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}.`,
7642
+ `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
7643
  ].join(" "),
7081
7644
  inputSchema: {
7082
7645
  sessionId: z.string().optional().describe("One session only. Default: every pending session."),
@@ -7091,7 +7654,7 @@ function createServer(ctx) {
7091
7654
  content: [
7092
7655
  { type: "text", text: JSON.stringify(answer, null, 2) },
7093
7656
  ...images.flatMap((img) => [
7094
- { type: "text", text: `${img.label === "before" ? "Before (baseline)" : "After (now in Figma)"}: session ${img.sessionId}, item ${img.itemId}` },
7657
+ { type: "text", text: `${img.label === "before" ? "Before (baseline)" : "After (now in Figma)"}: session ${img.sessionId}, item ${img.itemId}${img.line ? `, line ${img.line}` : ""}` },
7095
7658
  { type: "image", data: img.base64, mimeType: "image/png" }
7096
7659
  ])
7097
7660
  ]
@@ -7105,14 +7668,23 @@ function createServer(ctx) {
7105
7668
  "bridge_write_drafts",
7106
7669
  {
7107
7670
  title: "Write change texts for Figma",
7108
- description: 'Write the titles and summaries you drafted for a session from bridge_pending_drafts. The DS Bridge Figma plugin fills them in within seconds, marked "Drafted by Claude, review before sending"; it never overwrites text the designer typed meanwhile. Texts are cleaned to the contract (no em dash, title at most 70 characters without a trailing period, summary at most 3 sentences). Needs GITHUB_TOKEN with contents write. Writes only the session file on the bridge-requests branch, never main.',
7671
+ 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
7672
  inputSchema: {
7110
7673
  sessionId: z.string().min(1).describe("The sessionId from bridge_pending_drafts."),
7111
7674
  results: z.array(
7112
7675
  z.object({
7113
7676
  id: z.string().min(1).describe("The item id from bridge_pending_drafts."),
7114
- title: z.string().describe(`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.`),
7115
- summary: z.string().describe("At most 3 sentences: what changes for developers and why it matters. No em dash, no invented numbers.")
7677
+ 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.`),
7678
+ 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."),
7679
+ 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"}.'),
7680
+ replacement: z.string().optional().describe("Docs items with a selection task: the rewritten passage only, plain text, Bridge voice, no em dash."),
7681
+ lines: z.array(
7682
+ z.object({
7683
+ id: z.string().min(1).describe(`A change line id from the item's "lines" in bridge_pending_drafts.`),
7684
+ 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").`),
7685
+ 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").`)
7686
+ })
7687
+ ).optional().describe("One per change line the item lists. Leave it out when the item lists none.")
7116
7688
  })
7117
7689
  ).min(1).describe("One result per item.")
7118
7690
  },
@@ -7134,13 +7706,13 @@ function createServer(ctx) {
7134
7706
  },
7135
7707
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
7136
7708
  },
7137
- run((a) => startExploration(ctx, a))
7709
+ run((a) => startExploration(ctx, { ...a, source: "mcp" }))
7138
7710
  );
7139
7711
  server.registerTool(
7140
7712
  "bridge_list_explorations",
7141
7713
  {
7142
7714
  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, the snapshot install command for a product staging branch (and the tarball fallback), and the component requests implemented on each. Locally, also explore/* branches without a pull request.",
7715
+ 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
7716
  inputSchema: { state: z.enum(["open", "closed", "all"]).optional().describe("Pull request state. Default open: running and promoted ones.") },
7145
7717
  annotations: { readOnlyHint: true, openWorldHint: true }
7146
7718
  },
@@ -7345,6 +7917,13 @@ function optionsFromArgv(argv, env = process.env) {
7345
7917
  }
7346
7918
 
7347
7919
  export {
7920
+ missingProse,
7921
+ formatDocsJson,
7922
+ DOCS_DRAFT_MODEL,
7923
+ parseDocsReply,
7924
+ mergeDocsDraft,
7925
+ anthropicRequest,
7926
+ anthropicReplyText,
7348
7927
  detectConfig,
7349
7928
  core,
7350
7929
  LocalSource,