@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.
@@ -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
  }
@@ -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
- function docsPrompt(prefill, conventions = {}) {
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
- `Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`,
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
- `- 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.",
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
- 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
- }
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: "Prop change", hint: "Add, rename or change a prop or its default." },
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) => `![${r.component} design ${i + 1}](${u})`), ""] : [],
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 "push".`);
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 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.");
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
- 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() });
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.images?.length) lines.push("", `Images: ${i.images.map((m) => `${m.label} ${m.url}`).join(", ")}`);
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, owner, repo, s) {
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
- 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 })
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
- install: explorationInstall({ owner: gh.owner, repo: gh.name, pr: pr.number, packages: s.packages, sha: commit.sha }),
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 { status: "started", ...preview, pr: r.pr, url: r.url, commit: r.commit, install: r.install, previews: r.previews, next: work };
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 = list.map((x) => ({ ...x, requests: linked.get(x.branch) ?? [] }));
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-ABJPOTKE.js")).runImpact;
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.1.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, write each title and summary in the Bridge voice, then call bridge_write_drafts. The designer reviews them in the DS Bridge Figma plugin.`,
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 approvals. Pass `pr` (GitHub) or `branch` (local proposal branch).",
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
- `Then call bridge_write_drafts with one result per item id: title at most ${DRAFT_TITLE_MAX} characters, summary at most 3 sentences.`
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: '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.',
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(`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.")
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, 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.",
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,