@sideboard-ai/core 0.1.159 → 0.1.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/{abletime-LK7GV556.js → abletime-AGF54TNA.js} +2 -2
  2. package/dist/{abletime-HOQBDQC4.js → abletime-BMS53RIZ.js} +2 -2
  3. package/dist/{agents-67ZBH5OE.js → agents-6UOXBORU.js} +4 -4
  4. package/dist/{agents-5HY76BCR.js → agents-RD774UEV.js} +4 -4
  5. package/dist/{app-settings-BJ5GFAE5.js → app-settings-BRLZQA7N.js} +1 -1
  6. package/dist/{app-settings-YWJ4MVYM.js → app-settings-JJQDWPEK.js} +1 -1
  7. package/dist/{chunk-DWUOKF3W.js → chunk-7OWRFJK6.js} +4 -4
  8. package/dist/{chunk-FLG4A5FJ.js → chunk-CVZ4KXVP.js} +8 -3
  9. package/dist/{chunk-TGU3LSDG.js → chunk-EZANYS5D.js} +22 -22
  10. package/dist/{chunk-SIMFKJIB.js → chunk-HZROPNYL.js} +2 -2
  11. package/dist/{chunk-AGLPYXDT.js → chunk-ILXKJ4ZD.js} +1 -1
  12. package/dist/{chunk-MEZXWELT.js → chunk-IO5WGZRR.js} +1 -1
  13. package/dist/{chunk-UBMCEZRE.js → chunk-KJFFQAMA.js} +1 -1
  14. package/dist/{chunk-EQIJ5MHR.js → chunk-LTXS26K5.js} +8 -3
  15. package/dist/{chunk-OYOXLY6B.js → chunk-MIX4R72K.js} +23 -23
  16. package/dist/{chunk-O7RWWPMR.js → chunk-QANLIT2E.js} +2 -2
  17. package/dist/{chunk-V4R6L2ZL.js → chunk-RQNXLIXO.js} +4 -4
  18. package/dist/{chunk-KEDHHXKW.js → chunk-UUEBNUEG.js} +1 -1
  19. package/dist/{coordinator-prompt-CNI6AK65.js → coordinator-prompt-X3LXE5Q4.js} +3 -3
  20. package/dist/{coordinator-prompt-7NKY7QRK.js → coordinator-prompt-YYUXC7EM.js} +3 -3
  21. package/dist/{global-workspace-FKAJZHO5.js → global-workspace-E6S5XQZF.js} +3 -3
  22. package/dist/{global-workspace-IJYCUO4M.js → global-workspace-SPR2Y4YQ.js} +3 -3
  23. package/dist/index.cjs +756 -614
  24. package/dist/index.js +746 -612
  25. package/dist/mcp/run-stdio.cjs +756 -614
  26. package/dist/mcp/run-stdio.js +746 -612
  27. package/dist/{orchestrator-NSVP7DJH.js → orchestrator-6FZBSP5B.js} +5 -5
  28. package/dist/{orchestrator-KFLS5BTP.js → orchestrator-QREP2CFO.js} +5 -5
  29. package/dist/{workspaces-D2M3AM2M.js → workspaces-MZELP4BI.js} +3 -3
  30. package/dist/{workspaces-OFSDJ7PZ.js → workspaces-OK2SNNQF.js} +3 -3
  31. package/dist/{worktree-BQE5KLRY.js → worktree-BX53JZHO.js} +2 -2
  32. package/dist/{worktree-EA6AD6D6.js → worktree-IY7YCEGH.js} +2 -2
  33. package/package.json +1 -1
@@ -36,12 +36,12 @@ import {
36
36
  syncBoardPins,
37
37
  updateSchedule,
38
38
  waitForDetachedJob
39
- } from "../chunk-TGU3LSDG.js";
39
+ } from "../chunk-EZANYS5D.js";
40
40
  import {
41
41
  isInternalAgentStatusText,
42
42
  listModelsForAgent,
43
43
  sideboardMcpProfile
44
- } from "../chunk-DWUOKF3W.js";
44
+ } from "../chunk-7OWRFJK6.js";
45
45
  import "../chunk-LZ6QMEQC.js";
46
46
  import "../chunk-S42XV45P.js";
47
47
  import {
@@ -56,7 +56,7 @@ import {
56
56
  searchAbleTimeTasks,
57
57
  toAbleTimeIssueInfo,
58
58
  updateAbleTimeTask
59
- } from "../chunk-UBMCEZRE.js";
59
+ } from "../chunk-KJFFQAMA.js";
60
60
  import {
61
61
  httpFetch
62
62
  } from "../chunk-6GN5WPYZ.js";
@@ -66,7 +66,7 @@ import {
66
66
  GLOBAL_WORKSPACE_ID,
67
67
  isCloudCoordinatorThread,
68
68
  listWorkspaces
69
- } from "../chunk-SIMFKJIB.js";
69
+ } from "../chunk-HZROPNYL.js";
70
70
  import {
71
71
  canonicalizeRepoPath,
72
72
  formatGhLandError,
@@ -77,7 +77,7 @@ import {
77
77
  resolveListPrsOptions,
78
78
  resolveRepoRoot,
79
79
  warmGithubAgentAuth
80
- } from "../chunk-MEZXWELT.js";
80
+ } from "../chunk-IO5WGZRR.js";
81
81
  import "../chunk-5U4NZLBH.js";
82
82
  import "../chunk-LZQULFIT.js";
83
83
  import "../chunk-SYFVUF2C.js";
@@ -97,7 +97,7 @@ import {
97
97
  resolveViewerProfileForRepo,
98
98
  saveLinearOAuth,
99
99
  writeViewerContext
100
- } from "../chunk-EQIJ5MHR.js";
100
+ } from "../chunk-LTXS26K5.js";
101
101
  import "../chunk-NSTQ6QKD.js";
102
102
  import "../chunk-KGZBYWZ3.js";
103
103
  import {
@@ -107,7 +107,7 @@ import {
107
107
  // src/mcp/server.ts
108
108
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
109
109
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
110
- import { z as z7 } from "zod";
110
+ import { z as z8 } from "zod";
111
111
  import { randomUUID as randomUUID2 } from "crypto";
112
112
  import { basename } from "path";
113
113
 
@@ -1189,13 +1189,52 @@ ${githubUrl.trim()}` : githubUrl.trim();
1189
1189
  ${link}` : link;
1190
1190
  }
1191
1191
 
1192
- // src/mcp/slack-tools.ts
1193
- function text(payload, isError = false) {
1192
+ // src/mcp/issue-list.ts
1193
+ var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
1194
+ var MCP_ISSUE_LIST_MAX_LIMIT = 250;
1195
+ function clampMcpIssueLimit(limit) {
1196
+ if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
1197
+ return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
1198
+ }
1199
+ function applyIssueListWindow(items, limit) {
1200
+ const truncated = items.length > limit;
1201
+ return {
1202
+ items: truncated ? items.slice(0, limit) : items,
1203
+ truncated
1204
+ };
1205
+ }
1206
+ function compactIssueRow(issue) {
1207
+ return {
1208
+ identifier: issue.identifier,
1209
+ title: issue.title,
1210
+ ...issue.assignee ? { assignee: issue.assignee } : {},
1211
+ ...issue.labels.length > 0 ? { labels: issue.labels } : {},
1212
+ ...issue.teamKey ? { team: issue.teamKey } : {},
1213
+ ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
1214
+ };
1215
+ }
1216
+ function formatMcpIssueList(input) {
1217
+ return {
1218
+ ...input.source ? { source: input.source } : {},
1219
+ ...input.viewer ? { viewer: input.viewer } : {},
1220
+ limit: input.limit,
1221
+ returned: input.issues.length,
1222
+ truncated: input.truncated,
1223
+ ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
1224
+ issues: input.issues.map(compactIssueRow)
1225
+ };
1226
+ }
1227
+ function mcpJson(payload, isError = false) {
1194
1228
  return {
1195
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
1229
+ content: [{ type: "text", text: JSON.stringify(payload) }],
1196
1230
  ...isError ? { isError: true } : {}
1197
1231
  };
1198
1232
  }
1233
+
1234
+ // src/mcp/slack-tools.ts
1235
+ function text(payload, isError = false) {
1236
+ return mcpJson(payload, isError);
1237
+ }
1199
1238
  function fail(err) {
1200
1239
  return text(
1201
1240
  { error: err instanceof Error ? err.message : String(err) },
@@ -1461,48 +1500,329 @@ function registerSlackTools(server) {
1461
1500
  }
1462
1501
 
1463
1502
  // src/mcp/abletime-tools.ts
1503
+ import { z as z3 } from "zod";
1504
+
1505
+ // src/mcp/issue-payload.ts
1464
1506
  import { z as z2 } from "zod";
1465
1507
 
1466
- // src/mcp/issue-list.ts
1467
- var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
1468
- var MCP_ISSUE_LIST_MAX_LIMIT = 250;
1469
- function clampMcpIssueLimit(limit) {
1470
- if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
1471
- return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
1508
+ // src/mcp/smart-crush.ts
1509
+ var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
1510
+ var CRUSH_MIN_ITEMS = 5;
1511
+ var CRUSH_MIN_CHARS = 800;
1512
+ var CRUSH_COMMENT_MAX_CHARS = 16e3;
1513
+ var CRUSH_BODY_MIN_CHARS = 12e3;
1514
+ var CRUSH_BODY_MAX_CHARS = 8e3;
1515
+ var NEAR_DUP_JACCARD = 0.92;
1516
+ function looksLikeMustKeep(text7) {
1517
+ return MUST_KEEP_TEXT_RE.test(text7);
1518
+ }
1519
+ function normalizeText(text7) {
1520
+ return text7.replace(/\s+/g, " ").trim().toLowerCase();
1521
+ }
1522
+ function wordSet(text7) {
1523
+ return new Set(
1524
+ text7.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
1525
+ );
1472
1526
  }
1473
- function applyIssueListWindow(items, limit) {
1474
- const truncated = items.length > limit;
1475
- return {
1476
- items: truncated ? items.slice(0, limit) : items,
1477
- truncated
1527
+ function jaccard(a, b) {
1528
+ if (a.size === 0 && b.size === 0) return 1;
1529
+ let inter = 0;
1530
+ for (const w of a) {
1531
+ if (b.has(w)) inter += 1;
1532
+ }
1533
+ const union = a.size + b.size - inter;
1534
+ return union === 0 ? 0 : inter / union;
1535
+ }
1536
+ function importance(text7) {
1537
+ let score = text7.length;
1538
+ if (/```/.test(text7)) score += 400;
1539
+ if (/https?:\/\//.test(text7)) score += 50;
1540
+ if (looksLikeMustKeep(text7)) score += 1e4;
1541
+ return score;
1542
+ }
1543
+ function meanStd(values) {
1544
+ if (values.length === 0) return { mean: 0, std: 0 };
1545
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
1546
+ const variance = values.reduce((a, b) => a + (b - mean) * (b - mean), 0) / values.length;
1547
+ return { mean, std: Math.sqrt(variance) };
1548
+ }
1549
+ function dedupTextItems(items, getText) {
1550
+ const kept = [];
1551
+ const keptNorm = [];
1552
+ const keptWords = [];
1553
+ for (const item of items) {
1554
+ const text7 = getText(item);
1555
+ const norm = normalizeText(text7);
1556
+ if (!norm) {
1557
+ kept.push(item);
1558
+ keptNorm.push(norm);
1559
+ keptWords.push(wordSet(text7));
1560
+ continue;
1561
+ }
1562
+ const words = wordSet(text7);
1563
+ let dupAt = -1;
1564
+ for (let i = 0; i < kept.length; i++) {
1565
+ if (keptNorm[i] === norm) {
1566
+ dupAt = i;
1567
+ break;
1568
+ }
1569
+ if (keptWords[i].size >= 8 && words.size >= 8 && jaccard(keptWords[i], words) >= NEAR_DUP_JACCARD) {
1570
+ dupAt = i;
1571
+ break;
1572
+ }
1573
+ }
1574
+ if (dupAt < 0) {
1575
+ kept.push(item);
1576
+ keptNorm.push(norm);
1577
+ keptWords.push(words);
1578
+ continue;
1579
+ }
1580
+ if (text7.length > getText(kept[dupAt]).length) {
1581
+ kept[dupAt] = item;
1582
+ keptNorm[dupAt] = norm;
1583
+ keptWords[dupAt] = words;
1584
+ }
1585
+ }
1586
+ return { items: kept, dropped: items.length - kept.length };
1587
+ }
1588
+ function crushTextItems(items, getText, opts = {}) {
1589
+ const minItems = opts.minItems ?? CRUSH_MIN_ITEMS;
1590
+ const minChars = opts.minChars ?? CRUSH_MIN_CHARS;
1591
+ const maxChars = opts.maxChars ?? CRUSH_COMMENT_MAX_CHARS;
1592
+ const original = items;
1593
+ const totalChars = items.reduce((n, item) => n + getText(item).length, 0);
1594
+ if (items.length < minItems || totalChars < minChars) {
1595
+ return {
1596
+ items,
1597
+ truncated: false,
1598
+ kept: items.length,
1599
+ dropped: 0,
1600
+ reason: "passthrough"
1601
+ };
1602
+ }
1603
+ const deduped = dedupTextItems(items, getText);
1604
+ const unique = deduped.items;
1605
+ const uniqueChars = unique.reduce((n, item) => n + getText(item).length, 0);
1606
+ if (uniqueChars <= maxChars) {
1607
+ const changed = unique.length < original.length;
1608
+ return {
1609
+ items: unique,
1610
+ truncated: changed,
1611
+ kept: unique.length,
1612
+ dropped: original.length - unique.length,
1613
+ reason: changed ? "dedup" : "passthrough"
1614
+ };
1615
+ }
1616
+ const lengths = unique.map((item) => getText(item).length);
1617
+ const { mean, std } = meanStd(lengths);
1618
+ const outlierFloor = mean + 2 * std;
1619
+ const mustKeep = /* @__PURE__ */ new Set();
1620
+ unique.forEach((item, i) => {
1621
+ const text7 = getText(item);
1622
+ if (looksLikeMustKeep(text7) || text7.length > outlierFloor) mustKeep.add(i);
1623
+ });
1624
+ const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
1625
+ let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
1626
+ const pick = new Set(mustKeep);
1627
+ const canTake = (i) => {
1628
+ if (pick.has(i)) return false;
1629
+ const next = used + lengths[i];
1630
+ if (next > maxChars && pick.size > mustKeep.size) return false;
1631
+ used = next;
1632
+ pick.add(i);
1633
+ return true;
1478
1634
  };
1479
- }
1480
- function compactIssueRow(issue) {
1635
+ const k = remainingIdx.length;
1636
+ const headN = Math.max(1, Math.ceil(k * 0.3));
1637
+ const tailN = Math.max(1, Math.ceil(k * 0.15));
1638
+ for (const i of remainingIdx.slice(0, headN)) canTake(i);
1639
+ for (const i of remainingIdx.slice(-tailN).reverse()) canTake(i);
1640
+ const middle = remainingIdx.filter((i) => !pick.has(i)).sort((a, b) => importance(getText(unique[b])) - importance(getText(unique[a])));
1641
+ for (const i of middle) {
1642
+ if (used >= maxChars) break;
1643
+ canTake(i);
1644
+ }
1645
+ const crushed = unique.filter((_, i) => pick.has(i));
1646
+ const crushedChars = crushed.reduce((n, item) => n + getText(item).length, 0);
1647
+ if (crushed.length >= original.length && crushedChars >= totalChars) {
1648
+ return {
1649
+ items: original,
1650
+ truncated: false,
1651
+ kept: original.length,
1652
+ dropped: 0,
1653
+ reason: "passthrough"
1654
+ };
1655
+ }
1481
1656
  return {
1482
- identifier: issue.identifier,
1483
- title: issue.title,
1484
- ...issue.assignee ? { assignee: issue.assignee } : {},
1485
- ...issue.labels.length > 0 ? { labels: issue.labels } : {},
1486
- ...issue.teamKey ? { team: issue.teamKey } : {},
1487
- ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
1657
+ items: crushed,
1658
+ truncated: crushed.length < original.length,
1659
+ kept: crushed.length,
1660
+ dropped: original.length - crushed.length,
1661
+ reason: "budget"
1488
1662
  };
1489
1663
  }
1490
- function formatMcpIssueList(input) {
1491
- return {
1492
- ...input.source ? { source: input.source } : {},
1493
- ...input.viewer ? { viewer: input.viewer } : {},
1494
- limit: input.limit,
1495
- returned: input.issues.length,
1496
- truncated: input.truncated,
1497
- ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
1498
- issues: input.issues.map(compactIssueRow)
1664
+ function splitBlocks(text7) {
1665
+ if (/\n\n/.test(text7)) return text7.split(/\n\n+/);
1666
+ return text7.split("\n");
1667
+ }
1668
+ function capOversizedBlock(block, maxChars) {
1669
+ if (block.length <= maxChars) return block;
1670
+ const head = Math.floor(maxChars * 0.3);
1671
+ const tail = Math.floor(maxChars * 0.15);
1672
+ return `${block.slice(0, head)}
1673
+
1674
+ [\u2026crushed ${block.length - head - tail} chars; pass include=full to read the rest\u2026]
1675
+
1676
+ ${block.slice(-tail)}`;
1677
+ }
1678
+ function crushMarkdown(text7, opts) {
1679
+ const original = text7 ?? "";
1680
+ const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
1681
+ const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
1682
+ if (original.length < minChars) {
1683
+ return { text: original, truncated: false, originalChars: original.length };
1684
+ }
1685
+ const rawBlocks = splitBlocks(original).filter((b) => b.length > 0);
1686
+ const blocks = rawBlocks.map((b) => capOversizedBlock(b, maxChars));
1687
+ const cappedABlock = blocks.some((b, i) => b !== rawBlocks[i]);
1688
+ if (blocks.length <= 1) {
1689
+ const cut = blocks[0] ?? original;
1690
+ if (cut.length >= original.length) {
1691
+ return { text: original, truncated: false, originalChars: original.length };
1692
+ }
1693
+ return { text: cut, truncated: true, originalChars: original.length };
1694
+ }
1695
+ const must = blocks.filter((b) => looksLikeMustKeep(b));
1696
+ const rest = blocks.filter((b) => !looksLikeMustKeep(b));
1697
+ const marker = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
1698
+ let used = must.reduce((n, b) => n + b.length, 0) + marker.length;
1699
+ const keptRest = [];
1700
+ const headN = Math.max(1, Math.ceil(rest.length * 0.3));
1701
+ const tailN = Math.max(1, Math.ceil(rest.length * 0.15));
1702
+ const head = rest.slice(0, headN);
1703
+ const tail = rest.slice(-tailN);
1704
+ const middle = rest.slice(headN, Math.max(headN, rest.length - tailN));
1705
+ const take = (block) => {
1706
+ if (used + block.length + 2 > maxChars && keptRest.length + must.length > 0) {
1707
+ return false;
1708
+ }
1709
+ used += block.length + 2;
1710
+ keptRest.push(block);
1711
+ return true;
1499
1712
  };
1713
+ for (const b of head) take(b);
1714
+ for (const b of tail) take(b);
1715
+ const ranked = [...middle].sort((a, b) => importance(b) - importance(a));
1716
+ for (const b of ranked) {
1717
+ if (used >= maxChars) break;
1718
+ take(b);
1719
+ }
1720
+ const keepSet = /* @__PURE__ */ new Set([...must, ...keptRest]);
1721
+ const ordered = blocks.filter((b) => keepSet.has(b));
1722
+ const omitted = blocks.length - ordered.length;
1723
+ if (omitted <= 0) {
1724
+ const joined = blocks.join("\n\n");
1725
+ if (cappedABlock && joined.length < original.length) {
1726
+ return { text: joined, truncated: true, originalChars: original.length };
1727
+ }
1728
+ return { text: original, truncated: false, originalChars: original.length };
1729
+ }
1730
+ const out = [];
1731
+ let inserted = false;
1732
+ for (const block of blocks) {
1733
+ if (keepSet.has(block)) {
1734
+ out.push(block);
1735
+ continue;
1736
+ }
1737
+ if (!inserted) {
1738
+ out.push(marker);
1739
+ inserted = true;
1740
+ }
1741
+ }
1742
+ const textOut = out.join("\n\n");
1743
+ if (textOut.length >= original.length) {
1744
+ return { text: original, truncated: false, originalChars: original.length };
1745
+ }
1746
+ return { text: textOut, truncated: true, originalChars: original.length };
1500
1747
  }
1501
- function mcpJson(payload, isError = false) {
1502
- return {
1503
- content: [{ type: "text", text: JSON.stringify(payload) }],
1504
- ...isError ? { isError: true } : {}
1748
+
1749
+ // src/mcp/issue-payload.ts
1750
+ var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
1751
+ var mcpIssueIncludeSchema = z2.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
1752
+ "default applies SmartCrusher-style crush (dedup + budget; small unique tickets pass through). full returns the uncompressed vendor payload. Re-call with include=full if crush.truncated."
1753
+ );
1754
+ var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
1755
+ function emptyMeta() {
1756
+ return { truncated: true, hint: MCP_ISSUE_CRUSH_HINT };
1757
+ }
1758
+ function withComments(comments, meta) {
1759
+ const crushed = crushTextItems(comments, (c) => c.body);
1760
+ if (crushed.truncated) {
1761
+ meta.comments = {
1762
+ kept: crushed.kept,
1763
+ dropped: crushed.dropped,
1764
+ reason: crushed.reason
1765
+ };
1766
+ }
1767
+ return { comments: crushed.items, meta };
1768
+ }
1769
+ function withBody(text7, meta) {
1770
+ const crushed = crushMarkdown(text7);
1771
+ if (crushed.truncated) {
1772
+ meta.body = { originalChars: crushed.originalChars };
1773
+ return { value: crushed.text, meta };
1774
+ }
1775
+ return { value: text7, meta };
1776
+ }
1777
+ function attachCrush(payload, meta) {
1778
+ if (!meta.comments && !meta.body) return payload;
1779
+ return { ...payload, crush: meta };
1780
+ }
1781
+ function formatLinearIssuePayload(issue, include) {
1782
+ if (include === "full") return issue;
1783
+ const meta = emptyMeta();
1784
+ const comments = withComments(issue.comments, meta);
1785
+ const description = withBody(issue.description, comments.meta);
1786
+ const { team, ...rest } = issue;
1787
+ const next = {
1788
+ ...rest,
1789
+ ...team ? { team: { id: team.id, key: team.key, name: team.name } } : {},
1790
+ description: description.value,
1791
+ comments: comments.comments
1792
+ };
1793
+ return attachCrush(next, description.meta);
1794
+ }
1795
+ function formatGitHubIssuePayload(issue, include) {
1796
+ if (include === "full") return issue;
1797
+ const meta = emptyMeta();
1798
+ const comments = withComments(issue.comments, meta);
1799
+ const body = withBody(issue.body, comments.meta);
1800
+ const next = {
1801
+ ...issue,
1802
+ body: body.value,
1803
+ comments: comments.comments
1505
1804
  };
1805
+ return attachCrush(next, body.meta);
1806
+ }
1807
+ function formatAbleTimeTaskPayload(task, include) {
1808
+ const base = {
1809
+ ...toAbleTimeIssueInfo(task),
1810
+ description: task.description,
1811
+ state: task.state,
1812
+ comments: task.comments
1813
+ };
1814
+ if (include === "full") return base;
1815
+ const meta = emptyMeta();
1816
+ const comments = withComments(task.comments, meta);
1817
+ const description = withBody(task.description, comments.meta);
1818
+ return attachCrush(
1819
+ {
1820
+ ...base,
1821
+ description: description.value,
1822
+ comments: comments.comments
1823
+ },
1824
+ description.meta
1825
+ );
1506
1826
  }
1507
1827
 
1508
1828
  // src/mcp/abletime-tools.ts
@@ -1548,7 +1868,7 @@ function registerAbleTimeTools(server) {
1548
1868
  server.tool(
1549
1869
  "abletime_list_tasks",
1550
1870
  "List open AbleTime tasks (excludes done/archived). Optional projectId filter.",
1551
- { projectId: z2.string().optional() },
1871
+ { projectId: z3.string().optional() },
1552
1872
  async ({ projectId }) => {
1553
1873
  try {
1554
1874
  const tasks = await listAbleTimeTasks({ projectId });
@@ -1561,7 +1881,7 @@ function registerAbleTimeTools(server) {
1561
1881
  server.tool(
1562
1882
  "abletime_search_tasks",
1563
1883
  "Search AbleTime tasks by text, reference, or tag.",
1564
- { query: z2.string() },
1884
+ { query: z3.string() },
1565
1885
  async ({ query }) => {
1566
1886
  try {
1567
1887
  const tasks = await searchAbleTimeTasks(query);
@@ -1573,17 +1893,11 @@ function registerAbleTimeTools(server) {
1573
1893
  );
1574
1894
  server.tool(
1575
1895
  "abletime_get_task",
1576
- "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments.",
1577
- { id: z2.string() },
1578
- async ({ id }) => {
1896
+ "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload.",
1897
+ { id: z3.string(), include: mcpIssueIncludeSchema },
1898
+ async ({ id, include }) => {
1579
1899
  try {
1580
- const task = await getAbleTimeTask(id);
1581
- return text2({
1582
- ...toAbleTimeIssueInfo(task),
1583
- description: task.description,
1584
- state: task.state,
1585
- comments: task.comments
1586
- });
1900
+ return text2(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
1587
1901
  } catch (err) {
1588
1902
  return fail2(err);
1589
1903
  }
@@ -1592,7 +1906,7 @@ function registerAbleTimeTools(server) {
1592
1906
  server.tool(
1593
1907
  "abletime_comment",
1594
1908
  "Add a markdown comment on an AbleTime task (id or CRM-232).",
1595
- { id: z2.string(), body: z2.string() },
1909
+ { id: z3.string(), body: z3.string() },
1596
1910
  async (args) => {
1597
1911
  try {
1598
1912
  return text2(await commentAbleTimeTask(args));
@@ -1605,10 +1919,10 @@ function registerAbleTimeTools(server) {
1605
1919
  "abletime_update_task",
1606
1920
  "Update an AbleTime task (id or CRM-232). Pass title, description, and/or state.",
1607
1921
  {
1608
- id: z2.string(),
1609
- title: z2.string().optional(),
1610
- description: z2.string().optional(),
1611
- state: z2.string().optional()
1922
+ id: z3.string(),
1923
+ title: z3.string().optional(),
1924
+ description: z3.string().optional(),
1925
+ state: z3.string().optional()
1612
1926
  },
1613
1927
  async (args) => {
1614
1928
  try {
@@ -1627,12 +1941,12 @@ function registerAbleTimeTools(server) {
1627
1941
  "abletime_create_task",
1628
1942
  "Create an AbleTime task in a project. Call abletime_list_projects if you do not have a project id. Pass parent (CRM-232) for a spin-off. New tasks start in todo (or backlog).",
1629
1943
  {
1630
- title: z2.string(),
1631
- description: z2.string().optional(),
1632
- projectId: z2.string().optional(),
1633
- categoryId: z2.string().optional(),
1634
- state: z2.enum(["backlog", "todo"]).optional(),
1635
- parent: z2.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
1944
+ title: z3.string(),
1945
+ description: z3.string().optional(),
1946
+ projectId: z3.string().optional(),
1947
+ categoryId: z3.string().optional(),
1948
+ state: z3.enum(["backlog", "todo"]).optional(),
1949
+ parent: z3.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
1636
1950
  },
1637
1951
  async (args) => {
1638
1952
  try {
@@ -1646,10 +1960,10 @@ function registerAbleTimeTools(server) {
1646
1960
  "abletime_ensure_task",
1647
1961
  "Find or create an AbleTime task for this work, then create_thread with sourceType=ticket.",
1648
1962
  {
1649
- title: z2.string(),
1650
- description: z2.string().optional(),
1651
- projectId: z2.string().optional(),
1652
- categoryId: z2.string().optional()
1963
+ title: z3.string(),
1964
+ description: z3.string().optional(),
1965
+ projectId: z3.string().optional(),
1966
+ categoryId: z3.string().optional()
1653
1967
  },
1654
1968
  async (args) => {
1655
1969
  try {
@@ -1663,7 +1977,7 @@ function registerAbleTimeTools(server) {
1663
1977
  }
1664
1978
 
1665
1979
  // src/mcp/github-tools.ts
1666
- import { z as z3 } from "zod";
1980
+ import { z as z4 } from "zod";
1667
1981
 
1668
1982
  // src/integrations/github-issues.ts
1669
1983
  function requireGhOk(result, label) {
@@ -1847,15 +2161,15 @@ function fail3(err) {
1847
2161
  true
1848
2162
  );
1849
2163
  }
1850
- var repoPathSchema = z3.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
2164
+ var repoPathSchema = z4.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
1851
2165
  function registerGithubIssueTools(server) {
1852
2166
  server.tool(
1853
2167
  "github_get_issue",
1854
- "Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Uses Account gh.",
1855
- { id: z3.string(), repoPath: repoPathSchema },
1856
- async ({ id, repoPath }) => {
2168
+ "Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. Uses Account gh.",
2169
+ { id: z4.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
2170
+ async ({ id, include, repoPath }) => {
1857
2171
  try {
1858
- return text3(await getGitHubIssue(id, { repoPath }));
2172
+ return text3(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
1859
2173
  } catch (err) {
1860
2174
  return fail3(err);
1861
2175
  }
@@ -1864,7 +2178,7 @@ function registerGithubIssueTools(server) {
1864
2178
  server.tool(
1865
2179
  "github_comment",
1866
2180
  "Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh.",
1867
- { id: z3.string(), body: z3.string(), repoPath: repoPathSchema },
2181
+ { id: z4.string(), body: z4.string(), repoPath: repoPathSchema },
1868
2182
  async ({ id, body, repoPath }) => {
1869
2183
  try {
1870
2184
  return text3(await commentGitHubIssue({ id, body }, { repoPath }));
@@ -1877,10 +2191,10 @@ function registerGithubIssueTools(server) {
1877
2191
  "github_update_issue",
1878
2192
  "Update a GitHub issue (#123). Pass title, body, and/or state (open|closed).",
1879
2193
  {
1880
- id: z3.string(),
1881
- title: z3.string().optional(),
1882
- body: z3.string().optional(),
1883
- state: z3.string().optional().describe("open or closed"),
2194
+ id: z4.string(),
2195
+ title: z4.string().optional(),
2196
+ body: z4.string().optional(),
2197
+ state: z4.string().optional().describe("open or closed"),
1884
2198
  repoPath: repoPathSchema
1885
2199
  },
1886
2200
  async (args) => {
@@ -1895,9 +2209,9 @@ function registerGithubIssueTools(server) {
1895
2209
  "github_create_issue",
1896
2210
  "Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
1897
2211
  {
1898
- title: z3.string(),
1899
- body: z3.string().optional(),
1900
- parent: z3.string().optional().describe("Parent issue #123 or URL for a spin-off."),
2212
+ title: z4.string(),
2213
+ body: z4.string().optional(),
2214
+ parent: z4.string().optional().describe("Parent issue #123 or URL for a spin-off."),
1901
2215
  repoPath: repoPathSchema
1902
2216
  },
1903
2217
  async (args) => {
@@ -1911,7 +2225,7 @@ function registerGithubIssueTools(server) {
1911
2225
  }
1912
2226
 
1913
2227
  // src/mcp/linear-tools.ts
1914
- import { z as z4 } from "zod";
2228
+ import { z as z5 } from "zod";
1915
2229
  function text4(payload, isError = false) {
1916
2230
  return mcpJson(payload, isError);
1917
2231
  }
@@ -1921,7 +2235,7 @@ function fail4(err) {
1921
2235
  true
1922
2236
  );
1923
2237
  }
1924
- var prioritySchema = z4.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
2238
+ var prioritySchema = z5.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
1925
2239
  function registerLinearTools(server) {
1926
2240
  server.tool(
1927
2241
  "linear_list_teams",
@@ -1939,9 +2253,9 @@ function registerLinearTools(server) {
1939
2253
  "linear_search_issues",
1940
2254
  "Search or list open Linear issues. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all (default with query), or a user id/name.",
1941
2255
  {
1942
- query: z4.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
1943
- assignee: z4.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
1944
- limit: z4.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
2256
+ query: z5.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
2257
+ assignee: z5.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
2258
+ limit: z5.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
1945
2259
  },
1946
2260
  async ({ query, assignee, limit }) => {
1947
2261
  try {
@@ -1968,11 +2282,11 @@ function registerLinearTools(server) {
1968
2282
  );
1969
2283
  server.tool(
1970
2284
  "linear_get_issue",
1971
- "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children.",
1972
- { id: z4.string() },
1973
- async ({ id }) => {
2285
+ "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload.",
2286
+ { id: z5.string(), include: mcpIssueIncludeSchema },
2287
+ async ({ id, include }) => {
1974
2288
  try {
1975
- return text4(await getLinearIssue(id));
2289
+ return text4(formatLinearIssuePayload(await getLinearIssue(id), include));
1976
2290
  } catch (err) {
1977
2291
  return fail4(err);
1978
2292
  }
@@ -1982,13 +2296,13 @@ function registerLinearTools(server) {
1982
2296
  "linear_create_issue",
1983
2297
  'Create a Linear issue. Call linear_list_teams first. team is id/key/name; state is name/type/id; assignee is "me" or a user id. Pass parent (ENG-123 or uuid) to nest a spin-off under the current ticket.',
1984
2298
  {
1985
- team: z4.string(),
1986
- title: z4.string(),
1987
- description: z4.string().optional(),
1988
- state: z4.string().optional(),
1989
- assignee: z4.string().nullable().optional(),
2299
+ team: z5.string(),
2300
+ title: z5.string(),
2301
+ description: z5.string().optional(),
2302
+ state: z5.string().optional(),
2303
+ assignee: z5.string().nullable().optional(),
1990
2304
  priority: prioritySchema,
1991
- parent: z4.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
2305
+ parent: z5.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
1992
2306
  },
1993
2307
  async (args) => {
1994
2308
  try {
@@ -2002,11 +2316,11 @@ function registerLinearTools(server) {
2002
2316
  "linear_update_issue",
2003
2317
  "Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority.",
2004
2318
  {
2005
- id: z4.string(),
2006
- title: z4.string().optional(),
2007
- description: z4.string().optional(),
2008
- state: z4.string().optional(),
2009
- assignee: z4.string().nullable().optional(),
2319
+ id: z5.string(),
2320
+ title: z5.string().optional(),
2321
+ description: z5.string().optional(),
2322
+ state: z5.string().optional(),
2323
+ assignee: z5.string().nullable().optional(),
2010
2324
  priority: prioritySchema
2011
2325
  },
2012
2326
  async (args) => {
@@ -2021,8 +2335,8 @@ function registerLinearTools(server) {
2021
2335
  "linear_comment",
2022
2336
  "Add a markdown comment on a Linear issue (uuid or ENG-123).",
2023
2337
  {
2024
- id: z4.string(),
2025
- body: z4.string()
2338
+ id: z5.string(),
2339
+ body: z5.string()
2026
2340
  },
2027
2341
  async (args) => {
2028
2342
  try {
@@ -2042,18 +2356,21 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
2042
2356
  }
2043
2357
 
2044
2358
  // src/mcp/viewer-context-tools.ts
2045
- import { z as z5 } from "zod";
2359
+ import { z as z6 } from "zod";
2046
2360
  function text5(payload, isError = false) {
2047
2361
  return mcpJson(payload, isError);
2048
2362
  }
2049
2363
  function matchRegisteredWorkspace(repoPath) {
2050
2364
  const hint = (repoPath ?? "").trim().replace(/\/+$/, "");
2051
2365
  if (!hint) return null;
2052
- const match = listWorkspaces().find((w) => {
2366
+ let best = null;
2367
+ for (const w of listWorkspaces()) {
2053
2368
  const stored = w.path.replace(/\/+$/, "");
2054
- return stored === hint || hint.startsWith(`${stored}/`) || stored.startsWith(`${hint}/`);
2055
- });
2056
- return match?.path.replace(/\/+$/, "") ?? null;
2369
+ if (stored === hint || hint.startsWith(`${stored}/`)) {
2370
+ if (!best || stored.length > best.length) best = stored;
2371
+ }
2372
+ }
2373
+ return best;
2057
2374
  }
2058
2375
  async function resolveProjectRepoPath(repoPath) {
2059
2376
  const hint = repoPath?.trim() || process.cwd();
@@ -2071,7 +2388,7 @@ function registerViewerContextTools(server) {
2071
2388
  "get_viewer_context",
2072
2389
  "Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_workspaces for project context. Worktree turns may omit repoPath (uses cwd).",
2073
2390
  {
2074
- repoPath: z5.string().optional().describe(
2391
+ repoPath: z6.string().optional().describe(
2075
2392
  "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
2076
2393
  )
2077
2394
  },
@@ -2091,10 +2408,10 @@ function registerViewerContextTools(server) {
2091
2408
  "update_viewer_context",
2092
2409
  "Replace account or project user context (Settings \u2192 Agents / Projects). Never call until the user confirmed. Show the proposed text, ask_user (Save this context / Do not save), wait, then pass confirmed=true. Empty context clears that field.",
2093
2410
  {
2094
- scope: z5.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
2095
- context: z5.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
2096
- confirmed: z5.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
2097
- repoPath: z5.string().optional().describe(
2411
+ scope: z6.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
2412
+ context: z6.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
2413
+ confirmed: z6.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
2414
+ repoPath: z6.string().optional().describe(
2098
2415
  "Required for scope=project from orchestration (path from list_workspaces). Worktree turns may omit it (cwd)."
2099
2416
  )
2100
2417
  },
@@ -2171,12 +2488,9 @@ function formatMcpPrList(input) {
2171
2488
  }
2172
2489
 
2173
2490
  // src/mcp/schedule-tools.ts
2174
- import { z as z6 } from "zod";
2491
+ import { z as z7 } from "zod";
2175
2492
  function text6(payload, isError = false) {
2176
- return {
2177
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
2178
- ...isError ? { isError: true } : {}
2179
- };
2493
+ return mcpJson(payload, isError);
2180
2494
  }
2181
2495
  function fail5(err) {
2182
2496
  return text6(
@@ -2214,17 +2528,17 @@ function registerScheduleTools(server) {
2214
2528
  "create_schedule",
2215
2529
  "Create a local schedule that, when due, sends a prompt to an orchestration chat (threadId) or starts a new Global orchestration chat (omit threadId). Pass threadId=self to continue this coordinator. Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without threadId open a new chat each run. Overnight/unattended runs need Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or set_caffeinate. Sideboard.app must be running for the job to fire.",
2216
2530
  {
2217
- prompt: z6.string().describe("User message / goal queued when the schedule fires"),
2218
- name: z6.string().optional(),
2219
- at: z6.string().optional().describe("ISO datetime for a one-shot"),
2220
- every: z6.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
2221
- cron: z6.string().optional().describe("5-field cron expression"),
2222
- tz: z6.string().optional().describe("IANA timezone for cron (default: system)"),
2223
- threadId: z6.string().optional().describe(
2531
+ prompt: z7.string().describe("User message / goal queued when the schedule fires"),
2532
+ name: z7.string().optional(),
2533
+ at: z7.string().optional().describe("ISO datetime for a one-shot"),
2534
+ every: z7.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
2535
+ cron: z7.string().optional().describe("5-field cron expression"),
2536
+ tz: z7.string().optional().describe("IANA timezone for cron (default: system)"),
2537
+ threadId: z7.string().optional().describe(
2224
2538
  'Existing orchestration chat id, or "self" for this coordinator. Omit to create a new Global chat on fire.'
2225
2539
  ),
2226
- agent: z6.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
2227
- model: z6.string().optional()
2540
+ agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
2541
+ model: z7.string().optional()
2228
2542
  },
2229
2543
  async (args) => {
2230
2544
  try {
@@ -2257,17 +2571,17 @@ function registerScheduleTools(server) {
2257
2571
  "update_schedule",
2258
2572
  "Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
2259
2573
  {
2260
- id: z6.string(),
2261
- prompt: z6.string().optional(),
2262
- name: z6.string().optional(),
2263
- at: z6.string().optional(),
2264
- every: z6.string().optional(),
2265
- cron: z6.string().optional(),
2266
- tz: z6.string().optional(),
2267
- threadId: z6.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
2268
- agent: z6.enum(["claude", "cursor", "codex", "opencode"]).optional(),
2269
- model: z6.string().optional(),
2270
- enabled: z6.boolean().optional()
2574
+ id: z7.string(),
2575
+ prompt: z7.string().optional(),
2576
+ name: z7.string().optional(),
2577
+ at: z7.string().optional(),
2578
+ every: z7.string().optional(),
2579
+ cron: z7.string().optional(),
2580
+ tz: z7.string().optional(),
2581
+ threadId: z7.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
2582
+ agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional(),
2583
+ model: z7.string().optional(),
2584
+ enabled: z7.boolean().optional()
2271
2585
  },
2272
2586
  async (args) => {
2273
2587
  try {
@@ -2305,7 +2619,7 @@ function registerScheduleTools(server) {
2305
2619
  server.tool(
2306
2620
  "delete_schedule",
2307
2621
  "Delete a local schedule. Pass id from list_schedules.",
2308
- { id: z6.string() },
2622
+ { id: z7.string() },
2309
2623
  async ({ id }) => {
2310
2624
  try {
2311
2625
  deleteSchedule(id);
@@ -2318,7 +2632,7 @@ function registerScheduleTools(server) {
2318
2632
  server.tool(
2319
2633
  "run_schedule",
2320
2634
  "Fire a schedule now (does not wait for nextRunAt). Queues the prompt or starts a new Global chat. If Sideboard.app is running, the desktop drains the turn.",
2321
- { id: z6.string() },
2635
+ { id: z7.string() },
2322
2636
  async ({ id }) => {
2323
2637
  try {
2324
2638
  if (!getSchedule(id)) {
@@ -2666,7 +2980,7 @@ async function startMcpServer() {
2666
2980
  );
2667
2981
  }
2668
2982
  try {
2669
- const { maxConcurrentAgents } = await import("../app-settings-BJ5GFAE5.js");
2983
+ const { maxConcurrentAgents } = await import("../app-settings-BRLZQA7N.js");
2670
2984
  orch.setMaxConcurrent(maxConcurrentAgents());
2671
2985
  } catch {
2672
2986
  }
@@ -2739,13 +3053,13 @@ async function startMcpServer() {
2739
3053
  "list_board",
2740
3054
  "Home Kanban of worktrees (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chat tabs nest as inner cards. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), column, limit (default 40). create_thread adds a worktree (and a Home card), or returns the live one if that ticket/PR/named branch is already checked out.",
2741
3055
  {
2742
- query: z7.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
2743
- repoPath: z7.string().optional().describe("Limit to one workspace path from list_workspaces"),
2744
- kind: z7.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
2745
- column: z7.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
3056
+ query: z8.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
3057
+ repoPath: z8.string().optional().describe("Limit to one workspace path from list_workspaces"),
3058
+ kind: z8.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
3059
+ column: z8.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
2746
3060
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
2747
3061
  ),
2748
- limit: z7.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
3062
+ limit: z8.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
2749
3063
  },
2750
3064
  async ({ query, repoPath, kind, column, limit }) => {
2751
3065
  const workspaces = orch.listWorkspaces();
@@ -2760,30 +3074,19 @@ async function startMcpServer() {
2760
3074
  limit,
2761
3075
  workspaceName: (path) => names.get(path) ?? ""
2762
3076
  });
2763
- return {
2764
- content: [
2765
- {
2766
- type: "text",
2767
- text: JSON.stringify(
2768
- {
2769
- columns: snap.columns,
2770
- hidden: snap.hidden,
2771
- totals: snap.totals,
2772
- columnDefs: BOARD_COLUMN_DEFS,
2773
- hint: HOME_BOARD_AGENT_HINT
2774
- },
2775
- null,
2776
- 2
2777
- )
2778
- }
2779
- ]
2780
- };
3077
+ return mcpJson({
3078
+ columns: snap.columns,
3079
+ hidden: snap.hidden,
3080
+ totals: snap.totals,
3081
+ columnDefs: BOARD_COLUMN_DEFS,
3082
+ hint: HOME_BOARD_AGENT_HINT
3083
+ });
2781
3084
  }
2782
3085
  );
2783
3086
  server.tool(
2784
3087
  "get_thread",
2785
3088
  "Get a compact thread summary by id/ref. Includes last message preview, parentThreadId, and child worktree threads (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (thread billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
2786
- { ref: z7.string() },
3089
+ { ref: z8.string() },
2787
3090
  async ({ ref }) => {
2788
3091
  const t = orch.getThread(ref);
2789
3092
  if (!t) {
@@ -2817,7 +3120,7 @@ async function startMcpServer() {
2817
3120
  usage: spend.usage,
2818
3121
  lastTurnUsage: spend.lastTurnUsage
2819
3122
  };
2820
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
3123
+ return mcpJson(summary);
2821
3124
  }
2822
3125
  );
2823
3126
  }
@@ -2825,17 +3128,17 @@ async function startMcpServer() {
2825
3128
  "present_artifact",
2826
3129
  "Show a document or live log in Sideboard\u2019s side column. For html/svg/markdown/react, pass the FULL document and do not also fence that same body in chat. For type=log, pass only NEW lines (same artifact_id appends). Prefer type=log for long-running job output \u2014 do not resend HTML. type=react is a single default-export component (JSX/TSX); only react/react-dom imports.",
2827
3130
  {
2828
- title: z7.string().describe("Short title shown in the artifact pane header"),
2829
- type: z7.enum(["html", "svg", "markdown", "react", "log"]).describe(
3131
+ title: z8.string().describe("Short title shown in the artifact pane header"),
3132
+ type: z8.enum(["html", "svg", "markdown", "react", "log"]).describe(
2830
3133
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
2831
3134
  ),
2832
- content: z7.string().describe(
3135
+ content: z8.string().describe(
2833
3136
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
2834
3137
  ),
2835
- artifact_id: z7.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
2836
- status: z7.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
2837
- phase: z7.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
2838
- mode: z7.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
3138
+ artifact_id: z8.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
3139
+ status: z8.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
3140
+ phase: z8.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
3141
+ mode: z8.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
2839
3142
  },
2840
3143
  async ({ title, type, artifact_id, status, phase, mode }) => {
2841
3144
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -2849,22 +3152,22 @@ async function startMcpServer() {
2849
3152
  mode: type === "log" ? mode ?? "append" : void 0,
2850
3153
  message: type === "log" ? "Log accepted. Same artifact_id appends; send only new lines next time." : "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
2851
3154
  };
2852
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
3155
+ return mcpJson(payload);
2853
3156
  }
2854
3157
  );
2855
3158
  server.tool(
2856
3159
  "ask_user",
2857
3160
  "Ask the user a clarifying multiple-choice question in Sideboard\u2019s composer. Call only when work is blocked on choosing among a few concrete options (approach fork, which API, auth vs cookies). Do not call for greetings, check-ins, \u201Chello\u201D, open-ended how-can-I-help, or to invent a menu of possible next tasks \u2014 reply in chat instead. If one option is the obvious default, proceed without asking. Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. After calling, stop and wait for answers. Not for \u201Cis the plan ready?\u201D.",
2858
3161
  {
2859
- questions: z7.array(
2860
- z7.object({
2861
- question: z7.string().describe("Full question text ending with ?"),
2862
- header: z7.string().max(24).optional().describe("Short label shown above the question"),
2863
- multiSelect: z7.boolean().optional().describe("Allow selecting multiple options"),
2864
- options: z7.array(
2865
- z7.object({
2866
- label: z7.string(),
2867
- description: z7.string().optional().describe("What this option means / when to choose it (strongly preferred)")
3162
+ questions: z8.array(
3163
+ z8.object({
3164
+ question: z8.string().describe("Full question text ending with ?"),
3165
+ header: z8.string().max(24).optional().describe("Short label shown above the question"),
3166
+ multiSelect: z8.boolean().optional().describe("Allow selecting multiple options"),
3167
+ options: z8.array(
3168
+ z8.object({
3169
+ label: z8.string(),
3170
+ description: z8.string().optional().describe("What this option means / when to choose it (strongly preferred)")
2868
3171
  })
2869
3172
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
2870
3173
  })
@@ -2876,16 +3179,16 @@ async function startMcpServer() {
2876
3179
  questions,
2877
3180
  message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
2878
3181
  };
2879
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
3182
+ return mcpJson(payload);
2880
3183
  }
2881
3184
  );
2882
3185
  server.tool(
2883
3186
  "present_plan",
2884
3187
  "Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
2885
3188
  {
2886
- title: z7.string().optional().describe("Short plan title (defaults to Plan)"),
2887
- content: z7.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
2888
- thread_id: z7.string().optional().describe("Sideboard thread id when cwd is not the worktree")
3189
+ title: z8.string().optional().describe("Short plan title (defaults to Plan)"),
3190
+ content: z8.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
3191
+ thread_id: z8.string().optional().describe("Sideboard thread id when cwd is not the worktree")
2889
3192
  },
2890
3193
  async ({ title, content, thread_id }) => {
2891
3194
  const { writePlanFile } = await import("../plan-file-I24VJVP2.js");
@@ -2901,22 +3204,22 @@ async function startMcpServer() {
2901
3204
  title: title?.trim() || "Plan",
2902
3205
  message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
2903
3206
  };
2904
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
3207
+ return mcpJson(payload);
2905
3208
  }
2906
3209
  );
2907
3210
  server.tool(
2908
3211
  "present_schema",
2909
3212
  "Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
2910
3213
  {
2911
- title: z7.string().describe("Pane title"),
2912
- mode: z7.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
2913
- datasource: z7.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
2914
- resource_id: z7.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
2915
- record_id: z7.string().optional().describe("Record id when opening form mode"),
2916
- resource: z7.record(z7.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
2917
- record: z7.record(z7.unknown()).optional().describe("Inline record { id, data, published_at? }"),
2918
- records: z7.array(z7.record(z7.unknown())).optional().describe("Inline records for table mode"),
2919
- pane_id: z7.string().optional().describe("Stable pane id across updates")
3214
+ title: z8.string().describe("Pane title"),
3215
+ mode: z8.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
3216
+ datasource: z8.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
3217
+ resource_id: z8.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
3218
+ record_id: z8.string().optional().describe("Record id when opening form mode"),
3219
+ resource: z8.record(z8.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
3220
+ record: z8.record(z8.unknown()).optional().describe("Inline record { id, data, published_at? }"),
3221
+ records: z8.array(z8.record(z8.unknown())).optional().describe("Inline records for table mode"),
3222
+ pane_id: z8.string().optional().describe("Stable pane id across updates")
2920
3223
  },
2921
3224
  async (args) => {
2922
3225
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -2930,17 +3233,17 @@ async function startMcpServer() {
2930
3233
  record_id: args.record_id,
2931
3234
  message: "Schema pane accepted. Sideboard desktop opens the CMS column beside chat."
2932
3235
  };
2933
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
3236
+ return mcpJson(payload);
2934
3237
  }
2935
3238
  );
2936
3239
  server.tool(
2937
3240
  "present_files",
2938
3241
  "Open Sideboard\u2019s Files column (CMS-style file manager: browse, upload, pick). Use datasource=brightsy when the user is logged into Brightsy account storage; datasource=memory for a session-local demo store. Prefer this over claiming a file manager UI is unavailable. Pair with present_schema when editing records that need media.",
2939
3242
  {
2940
- title: z7.string().optional().describe("Pane title (default: Files)"),
2941
- datasource: z7.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
2942
- path: z7.string().optional().describe("Initial folder path (e.g. public)"),
2943
- pane_id: z7.string().optional().describe("Stable pane id across updates")
3243
+ title: z8.string().optional().describe("Pane title (default: Files)"),
3244
+ datasource: z8.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
3245
+ path: z8.string().optional().describe("Initial folder path (e.g. public)"),
3246
+ pane_id: z8.string().optional().describe("Stable pane id across updates")
2944
3247
  },
2945
3248
  async (args) => {
2946
3249
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -2952,21 +3255,21 @@ async function startMcpServer() {
2952
3255
  path: args.path,
2953
3256
  message: "Files pane accepted. Sideboard desktop opens the Files column beside chat."
2954
3257
  };
2955
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
3258
+ return mcpJson(payload);
2956
3259
  }
2957
3260
  );
2958
3261
  server.tool(
2959
3262
  "wait_for_job",
2960
3263
  "Wait on a detached long job (tests, pack, deploy) started with detached-job.js. MCP clients kill tools around 60s, so this returns within 45s. stillRunning is the source of truth \u2014 if true, present_artifact type=log with content=delta (same artifact_id) and call wait_for_job again. Do not end the turn or tell the user you will let them know later. If false, ok/failed is the result.",
2961
3264
  {
2962
- id: z7.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
2963
- timeoutMs: z7.number().optional()
3265
+ id: z8.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
3266
+ timeoutMs: z8.number().optional()
2964
3267
  },
2965
3268
  async ({ id, timeoutMs }) => {
2966
3269
  const result = await waitForDetachedJob(process.cwd(), id, {
2967
3270
  timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
2968
3271
  });
2969
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
3272
+ return mcpJson(result);
2970
3273
  }
2971
3274
  );
2972
3275
  if (!worktreeProfile) {
@@ -2974,63 +3277,51 @@ async function startMcpServer() {
2974
3277
  registerConnectedIssueVendorTools(server);
2975
3278
  registerScheduleTools(server);
2976
3279
  const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-DBKMRZZO.js");
2977
- const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-BJ5GFAE5.js");
3280
+ const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-BRLZQA7N.js");
2978
3281
  const accountDefaults = resolveThreadDefaults();
2979
3282
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
2980
3283
  server.tool(
2981
3284
  "set_caffeinate",
2982
3285
  "Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. Closing or archiving this orchestration chat also releases the hold. macOS only.",
2983
3286
  {
2984
- enabled: z7.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
3287
+ enabled: z8.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
2985
3288
  },
2986
3289
  async ({ enabled }) => {
2987
3290
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
2988
3291
  const state = setCaffeinateHold(enabled, { threadId });
2989
3292
  if (enabled && !state.held) {
2990
- return {
2991
- content: [
2992
- {
2993
- type: "text",
2994
- text: JSON.stringify({
2995
- ...state,
2996
- ok: false,
2997
- message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
2998
- })
2999
- }
3000
- ],
3001
- isError: true
3002
- };
3003
- }
3004
- return {
3005
- content: [
3293
+ return mcpJson(
3006
3294
  {
3007
- type: "text",
3008
- text: JSON.stringify({
3009
- ...getCaffeinateHold(),
3010
- ok: true,
3011
- message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false, the user says they are done, or this orchestration chat is closed." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
3012
- })
3013
- }
3014
- ]
3015
- };
3295
+ ...state,
3296
+ ok: false,
3297
+ message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
3298
+ },
3299
+ true
3300
+ );
3301
+ }
3302
+ return mcpJson({
3303
+ ...getCaffeinateHold(),
3304
+ ok: true,
3305
+ message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false, the user says they are done, or this orchestration chat is closed." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
3306
+ });
3016
3307
  }
3017
3308
  );
3018
3309
  server.tool(
3019
3310
  "create_thread",
3020
3311
  `Create a worktree thread (chat) from branch, pr, or ticket. A ticket, PR, or named branch may have only one live worktree \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. Creating from the default branch still opens a new isolated worktree. Pass repoPath from list_workspaces. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_thread to chat.`,
3021
3312
  {
3022
- sourceType: z7.enum(["branch", "pr", "ticket"]),
3023
- sourceRef: z7.string(),
3024
- agent: z7.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
3025
- model: z7.string().nullable().optional().describe(
3313
+ sourceType: z8.enum(["branch", "pr", "ticket"]),
3314
+ sourceRef: z8.string(),
3315
+ agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
3316
+ model: z8.string().nullable().optional().describe(
3026
3317
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
3027
3318
  ),
3028
- repoPath: z7.string(),
3029
- title: z7.string().optional(),
3030
- cowboy: z7.boolean().optional().describe(
3319
+ repoPath: z8.string(),
3320
+ title: z8.string().optional(),
3321
+ cowboy: z8.boolean().optional().describe(
3031
3322
  "If true, work in the project folder on the default branch (no thread/* worktree). Requires Settings \u2192 Advanced \u2192 Cowboy mode. Land is commit+push to that branch. Archive does not delete the folder."
3032
3323
  ),
3033
- parentThreadId: z7.string().optional().describe(
3324
+ parentThreadId: z8.string().optional().describe(
3034
3325
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
3035
3326
  )
3036
3327
  },
@@ -3046,10 +3337,10 @@ async function startMcpServer() {
3046
3337
  "start_board_card",
3047
3338
  "Same as create_thread for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second worktree when one already matches \u2014 returns that thread (alreadyStarted). Then send_to_thread.",
3048
3339
  {
3049
- kind: z7.enum(["ticket", "pr", "branch"]),
3050
- ref: z7.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
3051
- repoPath: z7.string().describe("Workspace path from list_workspaces / list_board"),
3052
- title: z7.string().optional()
3340
+ kind: z8.enum(["ticket", "pr", "branch"]),
3341
+ ref: z8.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
3342
+ repoPath: z8.string().describe("Workspace path from list_workspaces / list_board"),
3343
+ title: z8.string().optional()
3053
3344
  },
3054
3345
  async ({ kind, ref, repoPath, title }) => {
3055
3346
  const root = await resolveRepoRoot(repoPath);
@@ -3066,20 +3357,13 @@ async function startMcpServer() {
3066
3357
  }))
3067
3358
  );
3068
3359
  if (existing) {
3069
- return {
3070
- content: [
3071
- {
3072
- type: "text",
3073
- text: JSON.stringify({
3074
- alreadyStarted: true,
3075
- id: existing.id,
3076
- title: existing.title,
3077
- status: existing.status,
3078
- link: `sideboard://thread/${existing.id}`
3079
- })
3080
- }
3081
- ]
3082
- };
3360
+ return mcpJson({
3361
+ alreadyStarted: true,
3362
+ id: existing.id,
3363
+ title: existing.title,
3364
+ status: existing.status,
3365
+ link: `sideboard://thread/${existing.id}`
3366
+ });
3083
3367
  }
3084
3368
  const workspaces = orch.listWorkspaces();
3085
3369
  const loaded = await getHomeBoardInputs(workspaces);
@@ -3153,9 +3437,9 @@ async function startMcpServer() {
3153
3437
  "send_to_thread",
3154
3438
  'Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not force_stop to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that stops the child mid-thought. Call wait_for_turn again instead.',
3155
3439
  {
3156
- ref: z7.string(),
3157
- prompt: z7.string(),
3158
- force_stop: z7.boolean().optional()
3440
+ ref: z8.string(),
3441
+ prompt: z8.string(),
3442
+ force_stop: z8.boolean().optional()
3159
3443
  },
3160
3444
  async ({ ref, prompt, force_stop }) => {
3161
3445
  if (force_stop) {
@@ -3165,79 +3449,58 @@ async function startMcpServer() {
3165
3449
  }
3166
3450
  }
3167
3451
  const thread = await orch.send(ref, prompt);
3168
- return {
3169
- content: [
3170
- {
3171
- type: "text",
3172
- text: JSON.stringify({
3173
- id: thread.id,
3174
- status: thread.status,
3175
- queueLength: thread.queue.length,
3176
- forceStopped: Boolean(force_stop)
3177
- })
3178
- }
3179
- ]
3180
- };
3452
+ return mcpJson({
3453
+ id: thread.id,
3454
+ status: thread.status,
3455
+ queueLength: thread.queue.length,
3456
+ forceStopped: Boolean(force_stop)
3457
+ });
3181
3458
  }
3182
3459
  );
3183
3460
  server.tool(
3184
3461
  "wait_for_turn",
3185
3462
  "Wait until the thread finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. stillRunning is the source of truth \u2014 if false, the child is not working (do not say it is waiting for a gate). If stillRunning is true, progress is tools/thinking (or \u201Cqueued, waiting for a concurrency slot\u201D if it has not started). Call wait_for_turn again. Do not send a check-in prompt, force_stop, or assume a hang. On status error, lastError/text is the failure. On status stopped or broken, the child did not finish \u2014 resume with send_to_thread or tell the user; do not treat that as success. When finished, usage is the last agent turn\u2019s tokens + costUsd (when the provider reported cost).",
3186
3463
  {
3187
- ref: z7.string(),
3188
- timeoutMs: z7.number().optional()
3464
+ ref: z8.string(),
3465
+ timeoutMs: z8.number().optional()
3189
3466
  },
3190
3467
  async ({ ref, timeoutMs }) => {
3191
3468
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
3192
3469
  resolveIfStillRunning: true
3193
3470
  });
3194
3471
  const result = orch.getTurnResult(thread.id);
3195
- return {
3196
- content: [
3197
- {
3198
- type: "text",
3199
- text: JSON.stringify({
3200
- id: thread.id,
3201
- status: result.status,
3202
- text: result.text,
3203
- lastError: result.lastError,
3204
- stillRunning: result.stillRunning,
3205
- progress: result.progress,
3206
- lastActivityAt: result.lastActivityAt,
3207
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
3208
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
3209
- })
3210
- }
3211
- ]
3212
- };
3472
+ return mcpJson({
3473
+ id: thread.id,
3474
+ status: result.status,
3475
+ text: result.text,
3476
+ lastError: result.lastError,
3477
+ stillRunning: result.stillRunning,
3478
+ progress: result.progress,
3479
+ lastActivityAt: result.lastActivityAt,
3480
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
3481
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
3482
+ });
3213
3483
  }
3214
3484
  );
3215
3485
  server.tool(
3216
3486
  "get_turn_result",
3217
3487
  "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript. Includes usage for the last agent turn (tokens + costUsd when reported).",
3218
- { ref: z7.string() },
3488
+ { ref: z8.string() },
3219
3489
  async ({ ref }) => {
3220
3490
  const result = orch.getTurnResult(ref);
3221
- return {
3222
- content: [
3223
- {
3224
- type: "text",
3225
- text: JSON.stringify({
3226
- ...result,
3227
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
3228
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
3229
- })
3230
- }
3231
- ]
3232
- };
3491
+ return mcpJson({
3492
+ ...result,
3493
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
3494
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
3495
+ });
3233
3496
  }
3234
3497
  );
3235
3498
  server.tool(
3236
3499
  "stop_thread",
3237
3500
  "Force-stop a thread: kill any in-flight agent turn AND clear queued prompts so drainQueue cannot continue. Does not archive the worktree. Optional force defaults to true.",
3238
3501
  {
3239
- ref: z7.string(),
3240
- force: z7.boolean().optional()
3502
+ ref: z8.string(),
3503
+ force: z8.boolean().optional()
3241
3504
  },
3242
3505
  async ({ ref, force }) => {
3243
3506
  const t = orch.getThread(ref);
@@ -3250,24 +3513,17 @@ async function startMcpServer() {
3250
3513
  const clearQueue = force !== false;
3251
3514
  const hadQueued = t.queue.length > 0;
3252
3515
  const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
3253
- return {
3254
- content: [
3255
- {
3256
- type: "text",
3257
- text: JSON.stringify({
3258
- id: stopped.id,
3259
- status: stopped.status,
3260
- clearedQueue: clearQueue && hadQueued
3261
- })
3262
- }
3263
- ]
3264
- };
3516
+ return mcpJson({
3517
+ id: stopped.id,
3518
+ status: stopped.status,
3519
+ clearedQueue: clearQueue && hadQueued
3520
+ });
3265
3521
  }
3266
3522
  );
3267
3523
  server.tool(
3268
3524
  "archive_thread",
3269
3525
  "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, and open PRs by asking the worktree agent (ask_git). Merge only when the user explicitly asked.",
3270
- { ref: z7.string() },
3526
+ { ref: z8.string() },
3271
3527
  async ({ ref }) => {
3272
3528
  const t = orch.getThread(ref);
3273
3529
  if (!t) {
@@ -3284,38 +3540,24 @@ async function startMcpServer() {
3284
3540
  };
3285
3541
  }
3286
3542
  const archived = await orch.archive(ref);
3287
- return {
3288
- content: [
3289
- {
3290
- type: "text",
3291
- text: JSON.stringify({
3292
- id: archived.id,
3293
- status: archived.status
3294
- })
3295
- }
3296
- ]
3297
- };
3543
+ return mcpJson({
3544
+ id: archived.id,
3545
+ status: archived.status
3546
+ });
3298
3547
  }
3299
3548
  );
3300
3549
  server.tool(
3301
3550
  "restore_thread",
3302
3551
  "Restore an archived thread (recreates worktree from branch when needed)",
3303
- { ref: z7.string() },
3552
+ { ref: z8.string() },
3304
3553
  async ({ ref }) => {
3305
3554
  try {
3306
3555
  const restored = await orch.restore(ref);
3307
- return {
3308
- content: [
3309
- {
3310
- type: "text",
3311
- text: JSON.stringify({
3312
- id: restored.id,
3313
- status: restored.status,
3314
- worktreePath: restored.worktreePath
3315
- })
3316
- }
3317
- ]
3318
- };
3556
+ return mcpJson({
3557
+ id: restored.id,
3558
+ status: restored.status,
3559
+ worktreePath: restored.worktreePath
3560
+ });
3319
3561
  } catch (err) {
3320
3562
  const message = err instanceof Error ? err.message : String(err);
3321
3563
  return { content: [{ type: "text", text: message }], isError: true };
@@ -3326,34 +3568,25 @@ async function startMcpServer() {
3326
3568
  "get_diff",
3327
3569
  "Compact diff summary (capped hunks, paginated)",
3328
3570
  {
3329
- ref: z7.string(),
3330
- maxFiles: z7.number().optional()
3571
+ ref: z8.string(),
3572
+ maxFiles: z8.number().optional()
3331
3573
  },
3332
3574
  async ({ ref, maxFiles }) => {
3333
3575
  const summary = await orch.diffSummary(ref);
3334
- return {
3335
- content: [
3336
- {
3337
- type: "text",
3338
- text: JSON.stringify({
3339
- ...summary,
3340
- files: summary.files.slice(0, maxFiles ?? 10)
3341
- })
3342
- }
3343
- ]
3344
- };
3576
+ return mcpJson({
3577
+ ...summary,
3578
+ files: summary.files.slice(0, maxFiles ?? 10)
3579
+ });
3345
3580
  }
3346
3581
  );
3347
3582
  server.tool(
3348
3583
  "get_pr_checks",
3349
3584
  "Snapshot of GitHub PR checks for a worktree thread (`gh pr checks` plus merge/review gates). null = no PR. If the user gave a goal (Greptile 5/5, CI green), the worktree agent watches with `gh pr checks --watch` via /long-running \u2014 this tool is a snapshot, not a waiter. Coordinators: do not run gh from the orchestration cwd.",
3350
- { ref: z7.string().describe("Worktree thread id/ref") },
3585
+ { ref: z8.string().describe("Worktree thread id/ref") },
3351
3586
  async ({ ref }) => {
3352
3587
  try {
3353
3588
  const checks = await orch.getPrChecks(ref);
3354
- return {
3355
- content: [{ type: "text", text: JSON.stringify(checks) }]
3356
- };
3589
+ return mcpJson(checks);
3357
3590
  } catch (err) {
3358
3591
  const message = err instanceof Error ? err.message : String(err);
3359
3592
  return { content: [{ type: "text", text: message }], isError: true };
@@ -3363,25 +3596,18 @@ async function startMcpServer() {
3363
3596
  server.tool(
3364
3597
  "request_review",
3365
3598
  'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .claude/skills/review/SKILL.md when present (else copies .sideboard/review.md into .context/review.md, or seeds that file from the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
3366
- { ref: z7.string().describe("Worktree thread id/ref to review") },
3599
+ { ref: z8.string().describe("Worktree thread id/ref to review") },
3367
3600
  async ({ ref }) => {
3368
3601
  try {
3369
3602
  const tab = await orch.requestReview(ref);
3370
3603
  const from = orch.getThread(ref);
3371
- return {
3372
- content: [
3373
- {
3374
- type: "text",
3375
- text: JSON.stringify({
3376
- id: tab.id,
3377
- title: tab.title,
3378
- status: tab.status,
3379
- fromThreadId: from?.id ?? ref,
3380
- link: `sideboard://thread/${tab.id}`
3381
- })
3382
- }
3383
- ]
3384
- };
3604
+ return mcpJson({
3605
+ id: tab.id,
3606
+ title: tab.title,
3607
+ status: tab.status,
3608
+ fromThreadId: from?.id ?? ref,
3609
+ link: `sideboard://thread/${tab.id}`
3610
+ });
3385
3611
  } catch (err) {
3386
3612
  const message = err instanceof Error ? err.message : String(err);
3387
3613
  return { content: [{ type: "text", text: message }], isError: true };
@@ -3392,53 +3618,37 @@ async function startMcpServer() {
3392
3618
  "ask_git",
3393
3619
  "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` even when origin is SSH / Settings \u2192 Git is SSH). When dirty, queues the worktree agent to commit; then wait_for_turn (loop while stillRunning). Do not start a checks loop on a plain push \u2014 only if the user gave a goal (Greptile 5/5, CI green). Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd. If this tool errors that the GraphQL/PR body is too long, the branch is already pushed \u2014 have the worktree agent retry `gh pr create --body-file` with a short description (GitHub limit 65,536 characters). Do not invent SSH/auth failures from that error.",
3394
3620
  {
3395
- ref: z7.string().describe("Worktree thread id/ref"),
3396
- action: z7.enum(AGENT_GIT_ACTIONS).describe(
3621
+ ref: z8.string().describe("Worktree thread id/ref"),
3622
+ action: z8.enum(AGENT_GIT_ACTIONS).describe(
3397
3623
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
3398
3624
  )
3399
3625
  },
3400
3626
  async ({ ref, action }) => {
3401
3627
  try {
3402
3628
  const thread = await orch.askGit(ref, action);
3403
- return {
3404
- content: [
3405
- {
3406
- type: "text",
3407
- text: JSON.stringify({
3408
- id: thread.id,
3409
- status: thread.status,
3410
- queueLength: thread.queue.length,
3411
- action,
3412
- link: `sideboard://thread/${thread.id}`
3413
- })
3414
- }
3415
- ]
3416
- };
3629
+ return mcpJson({
3630
+ id: thread.id,
3631
+ status: thread.status,
3632
+ queueLength: thread.queue.length,
3633
+ action,
3634
+ link: `sideboard://thread/${thread.id}`
3635
+ });
3417
3636
  } catch (err) {
3418
3637
  const raw = err instanceof Error ? err.message : String(err);
3419
3638
  const message = formatGhLandError(raw);
3420
- return {
3421
- content: [
3422
- {
3423
- type: "text",
3424
- text: JSON.stringify(
3425
- {
3426
- error: true,
3427
- action,
3428
- lastError: message,
3429
- hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_thread so the worktree agent runs gh pr create --draft -R <origin> --body-file <short.md>. Keep the description short." : void 0
3430
- },
3431
- null,
3432
- 2
3433
- )
3434
- }
3435
- ],
3436
- isError: true
3437
- };
3639
+ return mcpJson(
3640
+ {
3641
+ error: true,
3642
+ action,
3643
+ lastError: message,
3644
+ hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_thread so the worktree agent runs gh pr create --draft -R <origin> --body-file <short.md>. Keep the description short." : void 0
3645
+ },
3646
+ true
3647
+ );
3438
3648
  }
3439
3649
  }
3440
3650
  );
3441
- const agentEnum = z7.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
3651
+ const agentEnum = z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
3442
3652
  server.tool(
3443
3653
  "list_models",
3444
3654
  "List models for an agent. Prefer Auto: do not call this unless you have a reason to pin a specific model (user request, cost/latency, capability). Omit agent to list all.",
@@ -3448,9 +3658,7 @@ async function startMcpServer() {
3448
3658
  async ({ agent }) => {
3449
3659
  try {
3450
3660
  const catalogs = await listModelsForAgent(agent);
3451
- return {
3452
- content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
3453
- };
3661
+ return mcpJson(catalogs);
3454
3662
  } catch (err) {
3455
3663
  const message = err instanceof Error ? err.message : String(err);
3456
3664
  return { content: [{ type: "text", text: message }], isError: true };
@@ -3461,11 +3669,11 @@ async function startMcpServer() {
3461
3669
  "fork_worktree",
3462
3670
  "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id.",
3463
3671
  {
3464
- ref: z7.string().describe("Worktree thread id/ref to fork"),
3465
- through_index: z7.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
3672
+ ref: z8.string().describe("Worktree thread id/ref to fork"),
3673
+ through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
3466
3674
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
3467
- model: z7.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
3468
- title: z7.string().optional()
3675
+ model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
3676
+ title: z8.string().optional()
3469
3677
  },
3470
3678
  async ({ ref, through_index, agent, model, title }) => {
3471
3679
  try {
@@ -3478,24 +3686,17 @@ async function startMcpServer() {
3478
3686
  model,
3479
3687
  title
3480
3688
  });
3481
- return {
3482
- content: [
3483
- {
3484
- type: "text",
3485
- text: JSON.stringify({
3486
- id: thread.id,
3487
- title: thread.title,
3488
- status: thread.status,
3489
- agent: thread.agent,
3490
- model: thread.model,
3491
- branchName: thread.branchName,
3492
- worktreePath: thread.worktreePath,
3493
- fromThreadId: source?.id ?? ref,
3494
- link: `sideboard://thread/${thread.id}`
3495
- })
3496
- }
3497
- ]
3498
- };
3689
+ return mcpJson({
3690
+ id: thread.id,
3691
+ title: thread.title,
3692
+ status: thread.status,
3693
+ agent: thread.agent,
3694
+ model: thread.model,
3695
+ branchName: thread.branchName,
3696
+ worktreePath: thread.worktreePath,
3697
+ fromThreadId: source?.id ?? ref,
3698
+ link: `sideboard://thread/${thread.id}`
3699
+ });
3499
3700
  } catch (err) {
3500
3701
  const message = err instanceof Error ? err.message : String(err);
3501
3702
  return { content: [{ type: "text", text: message }], isError: true };
@@ -3506,11 +3707,11 @@ async function startMcpServer() {
3506
3707
  "fork_chat",
3507
3708
  "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
3508
3709
  {
3509
- ref: z7.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
3510
- through_index: z7.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
3710
+ ref: z8.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
3711
+ through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
3511
3712
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
3512
- model: z7.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
3513
- title: z7.string().optional()
3713
+ model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
3714
+ title: z8.string().optional()
3514
3715
  },
3515
3716
  async ({ ref, through_index, agent, model, title }) => {
3516
3717
  try {
@@ -3528,24 +3729,17 @@ async function startMcpServer() {
3528
3729
  model,
3529
3730
  title
3530
3731
  });
3531
- return {
3532
- content: [
3533
- {
3534
- type: "text",
3535
- text: JSON.stringify({
3536
- id: tab.id,
3537
- title: tab.title,
3538
- status: tab.status,
3539
- agent: tab.agent,
3540
- model: tab.model,
3541
- sourceType: tab.sourceType,
3542
- worktreePath: tab.worktreePath,
3543
- fromThreadId: source.id,
3544
- link: `sideboard://thread/${tab.id}`
3545
- })
3546
- }
3547
- ]
3548
- };
3732
+ return mcpJson({
3733
+ id: tab.id,
3734
+ title: tab.title,
3735
+ status: tab.status,
3736
+ agent: tab.agent,
3737
+ model: tab.model,
3738
+ sourceType: tab.sourceType,
3739
+ worktreePath: tab.worktreePath,
3740
+ fromThreadId: source.id,
3741
+ link: `sideboard://thread/${tab.id}`
3742
+ });
3549
3743
  } catch (err) {
3550
3744
  const message = err instanceof Error ? err.message : String(err);
3551
3745
  return { content: [{ type: "text", text: message }], isError: true };
@@ -3556,49 +3750,35 @@ async function startMcpServer() {
3556
3750
  "run_dev_script",
3557
3751
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
3558
3752
  {
3559
- ref: z7.string(),
3560
- name: z7.string().optional()
3753
+ ref: z8.string(),
3754
+ name: z8.string().optional()
3561
3755
  },
3562
3756
  async ({ ref, name }) => {
3563
3757
  const result = await orch.startDev(ref, name);
3564
- return {
3565
- content: [
3566
- {
3567
- type: "text",
3568
- text: JSON.stringify({
3569
- port: result.port,
3570
- scriptName: result.scriptName,
3571
- ports: result.ports,
3572
- url: `http://localhost:${result.port}`
3573
- })
3574
- }
3575
- ]
3576
- };
3758
+ return mcpJson({
3759
+ port: result.port,
3760
+ scriptName: result.scriptName,
3761
+ ports: result.ports,
3762
+ url: `http://localhost:${result.port}`
3763
+ });
3577
3764
  }
3578
3765
  );
3579
3766
  server.tool(
3580
3767
  "list_run_scripts",
3581
3768
  "List named run scripts available for a thread",
3582
- { ref: z7.string() },
3769
+ { ref: z8.string() },
3583
3770
  async ({ ref }) => {
3584
3771
  const scripts = orch.listThreadRunScripts(ref);
3585
3772
  const active = orch.getActiveRuns(ref);
3586
- return {
3587
- content: [
3588
- {
3589
- type: "text",
3590
- text: JSON.stringify({ scripts, active }, null, 2)
3591
- }
3592
- ]
3593
- };
3773
+ return mcpJson({ scripts, active });
3594
3774
  }
3595
3775
  );
3596
3776
  server.tool(
3597
3777
  "stop_dev_script",
3598
3778
  "Stop a running script for a thread (all scripts if name omitted)",
3599
3779
  {
3600
- ref: z7.string(),
3601
- name: z7.string().optional()
3780
+ ref: z8.string(),
3781
+ name: z8.string().optional()
3602
3782
  },
3603
3783
  async ({ ref, name }) => {
3604
3784
  orch.stopDev(ref, name);
@@ -3608,27 +3788,25 @@ async function startMcpServer() {
3608
3788
  server.tool(
3609
3789
  "run_setup",
3610
3790
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
3611
- { ref: z7.string() },
3791
+ { ref: z8.string() },
3612
3792
  async ({ ref }) => {
3613
3793
  const result = await orch.runSetup(ref);
3614
- return {
3615
- content: [{ type: "text", text: JSON.stringify(result) }]
3616
- };
3794
+ return mcpJson(result);
3617
3795
  }
3618
3796
  );
3619
3797
  server.tool(
3620
3798
  "add_workspace",
3621
3799
  "Register a git repo as a Sideboard workspace",
3622
- { repoPath: z7.string() },
3800
+ { repoPath: z8.string() },
3623
3801
  async ({ repoPath }) => {
3624
3802
  const ws = await orch.addWorkspace(repoPath);
3625
- return { content: [{ type: "text", text: JSON.stringify(ws) }] };
3803
+ return mcpJson(ws);
3626
3804
  }
3627
3805
  );
3628
3806
  server.tool(
3629
3807
  "remove_workspace",
3630
3808
  "Unregister a Sideboard workspace (does not archive threads)",
3631
- { repoPath: z7.string() },
3809
+ { repoPath: z8.string() },
3632
3810
  async ({ repoPath }) => {
3633
3811
  orch.removeWorkspace(repoPath);
3634
3812
  return { content: [{ type: "text", text: "ok" }] };
@@ -3638,40 +3816,31 @@ async function startMcpServer() {
3638
3816
  "fanout",
3639
3817
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
3640
3818
  {
3641
- prompt: z7.string(),
3642
- agents: z7.array(z7.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
3643
- repoPath: z7.string(),
3644
- sourceType: z7.enum(["branch", "pr", "ticket"]).optional(),
3645
- sourceRef: z7.string().optional(),
3646
- title: z7.string().optional()
3819
+ prompt: z8.string(),
3820
+ agents: z8.array(z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
3821
+ repoPath: z8.string(),
3822
+ sourceType: z8.enum(["branch", "pr", "ticket"]).optional(),
3823
+ sourceRef: z8.string().optional(),
3824
+ title: z8.string().optional()
3647
3825
  },
3648
3826
  async (args) => {
3649
3827
  const threads = await orch.bestOfN(args);
3650
- return {
3651
- content: [
3652
- {
3653
- type: "text",
3654
- text: JSON.stringify(
3655
- threads.map((t) => ({
3656
- id: t.id,
3657
- agent: t.agent,
3658
- branchName: t.branchName,
3659
- worktreePath: t.worktreePath
3660
- })),
3661
- null,
3662
- 2
3663
- )
3664
- }
3665
- ]
3666
- };
3828
+ return mcpJson(
3829
+ threads.map((t) => ({
3830
+ id: t.id,
3831
+ agent: t.agent,
3832
+ branchName: t.branchName,
3833
+ worktreePath: t.worktreePath
3834
+ }))
3835
+ );
3667
3836
  }
3668
3837
  );
3669
3838
  server.tool(
3670
3839
  "list_branches",
3671
3840
  "List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
3672
3841
  {
3673
- repoPath: z7.string(),
3674
- unmergedOnly: z7.boolean().optional()
3842
+ repoPath: z8.string(),
3843
+ unmergedOnly: z8.boolean().optional()
3675
3844
  },
3676
3845
  async ({ repoPath, unmergedOnly }) => {
3677
3846
  const root = await resolveRepoRoot(repoPath);
@@ -3692,19 +3861,19 @@ async function startMcpServer() {
3692
3861
  "list_prs",
3693
3862
  'List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_workspaces. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled eng-review with no individual user reviewer. Prefer teams that match Settings \u2192 Agents / Projects roles for this repo. A team like engineering-team is not a claim (you are on that team). Also: state (open|closed|merged|all|review), label, reviewer (me|unassigned|login), query, limit (default 40, max 250). Then create_thread with sourceType=pr.',
3694
3863
  {
3695
- repoPath: z7.string(),
3696
- query: z7.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
3697
- queue: z7.enum(["review", "mine", "approved", "changes"]).optional().describe(
3864
+ repoPath: z8.string(),
3865
+ query: z8.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
3866
+ queue: z8.enum(["review", "mine", "approved", "changes"]).optional().describe(
3698
3867
  'review = unclaimed eng-review inbox (use for "get me N tickets to review"). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes.'
3699
3868
  ),
3700
- state: z7.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
3701
- label: z7.string().optional().describe(
3869
+ state: z8.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
3870
+ label: z8.string().optional().describe(
3702
3871
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
3703
3872
  ),
3704
- reviewer: z7.string().optional().describe(
3873
+ reviewer: z8.string().optional().describe(
3705
3874
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
3706
3875
  ),
3707
- limit: z7.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
3876
+ limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
3708
3877
  },
3709
3878
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
3710
3879
  const root = await resolveRepoRoot(repoPath);
@@ -3736,89 +3905,65 @@ async function startMcpServer() {
3736
3905
  server.tool(
3737
3906
  "get_pr_stack",
3738
3907
  "Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before ask_git merge on stacked PRs (and only merge when the user explicitly asked).",
3739
- { ref: z7.string() },
3908
+ { ref: z8.string() },
3740
3909
  async ({ ref }) => {
3741
3910
  const stack = await orch.getPrStack(ref);
3742
- return {
3743
- content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
3744
- };
3911
+ return mcpJson(stack);
3745
3912
  }
3746
3913
  );
3747
3914
  server.tool(
3748
3915
  "open_pr_stack_layers",
3749
3916
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
3750
3917
  {
3751
- ref: z7.string(),
3752
- layer: z7.number().int().positive().optional()
3918
+ ref: z8.string(),
3919
+ layer: z8.number().int().positive().optional()
3753
3920
  },
3754
3921
  async ({ ref, layer }) => {
3755
3922
  const result = await orch.openPrStackLayers(ref, { layer });
3756
- return {
3757
- content: [
3758
- {
3759
- type: "text",
3760
- text: JSON.stringify(
3761
- {
3762
- stackNumber: result.stack.stackNumber,
3763
- trunk: result.stack.trunk,
3764
- threads: result.threads.map((t) => ({
3765
- id: t.id,
3766
- title: t.title,
3767
- branchName: t.branchName,
3768
- stackLayer: t.stackLayer,
3769
- worktreePath: t.worktreePath,
3770
- prUrl: t.prUrl,
3771
- link: `sideboard://thread/${t.id}`
3772
- }))
3773
- },
3774
- null,
3775
- 2
3776
- )
3777
- }
3778
- ]
3779
- };
3923
+ return mcpJson({
3924
+ stackNumber: result.stack.stackNumber,
3925
+ trunk: result.stack.trunk,
3926
+ threads: result.threads.map((t) => ({
3927
+ id: t.id,
3928
+ title: t.title,
3929
+ branchName: t.branchName,
3930
+ stackLayer: t.stackLayer,
3931
+ worktreePath: t.worktreePath,
3932
+ prUrl: t.prUrl,
3933
+ link: `sideboard://thread/${t.id}`
3934
+ }))
3935
+ });
3780
3936
  }
3781
3937
  );
3782
3938
  server.tool(
3783
3939
  "add_stack_layer",
3784
3940
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
3785
3941
  {
3786
- ref: z7.string(),
3787
- branchName: z7.string(),
3788
- title: z7.string().optional()
3942
+ ref: z8.string(),
3943
+ branchName: z8.string(),
3944
+ title: z8.string().optional()
3789
3945
  },
3790
3946
  async ({ ref, branchName, title }) => {
3791
3947
  const result = await orch.addStackLayer(ref, branchName, { title });
3792
- return {
3793
- content: [
3794
- {
3795
- type: "text",
3796
- text: JSON.stringify(
3797
- {
3798
- id: result.thread.id,
3799
- title: result.thread.title,
3800
- branchName: result.thread.branchName,
3801
- stackLayer: result.thread.stackLayer,
3802
- worktreePath: result.thread.worktreePath,
3803
- link: `sideboard://thread/${result.thread.id}`
3804
- },
3805
- null,
3806
- 2
3807
- )
3808
- }
3809
- ]
3810
- };
3948
+ return mcpJson({
3949
+ id: result.thread.id,
3950
+ title: result.thread.title,
3951
+ branchName: result.thread.branchName,
3952
+ stackLayer: result.thread.stackLayer,
3953
+ worktreePath: result.thread.worktreePath,
3954
+ link: `sideboard://thread/${result.thread.id}`
3955
+ });
3811
3956
  }
3812
3957
  );
3813
3958
  server.tool(
3814
3959
  "create_pr_stack",
3815
3960
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
3816
3961
  {
3817
- repoPath: z7.string(),
3818
- branches: z7.array(z7.string()).min(1),
3819
- agent: z7.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
3820
- base: z7.string().optional(),
3821
- title: z7.string().optional()
3962
+ repoPath: z8.string(),
3963
+ branches: z8.array(z8.string()).min(1),
3964
+ agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
3965
+ base: z8.string().optional(),
3966
+ title: z8.string().optional()
3822
3967
  },
3823
3968
  async (args) => {
3824
3969
  const result = await orch.createPrStack({
@@ -3828,39 +3973,28 @@ async function startMcpServer() {
3828
3973
  base: args.base,
3829
3974
  title: args.title
3830
3975
  });
3831
- return {
3832
- content: [
3833
- {
3834
- type: "text",
3835
- text: JSON.stringify(
3836
- {
3837
- stackNumber: result.stack.stackNumber,
3838
- trunk: result.stack.trunk,
3839
- threads: result.threads.map((t) => ({
3840
- id: t.id,
3841
- title: t.title,
3842
- branchName: t.branchName,
3843
- stackLayer: t.stackLayer,
3844
- worktreePath: t.worktreePath,
3845
- link: `sideboard://thread/${t.id}`
3846
- }))
3847
- },
3848
- null,
3849
- 2
3850
- )
3851
- }
3852
- ]
3853
- };
3976
+ return mcpJson({
3977
+ stackNumber: result.stack.stackNumber,
3978
+ trunk: result.stack.trunk,
3979
+ threads: result.threads.map((t) => ({
3980
+ id: t.id,
3981
+ title: t.title,
3982
+ branchName: t.branchName,
3983
+ stackLayer: t.stackLayer,
3984
+ worktreePath: t.worktreePath,
3985
+ link: `sideboard://thread/${t.id}`
3986
+ }))
3987
+ });
3854
3988
  }
3855
3989
  );
3856
3990
  server.tool(
3857
3991
  "list_issues",
3858
3992
  "List or search issues (Linear, AbleTime, or GitHub; falls back to GitHub). Use Settings \u2192 Agents / Projects notes and roles to pick tickets relevant to the viewer (query + assignee=me or unassigned as the notes say). Default 40; pass query and/or limit (max 250) when truncated. assignee: me (Linear default), unassigned, all, or a user id. Then create_thread with sourceType=ticket.",
3859
3993
  {
3860
- repoPath: z7.string(),
3861
- query: z7.string().optional().describe("Search title, identifier, or description"),
3862
- assignee: z7.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
3863
- limit: z7.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
3994
+ repoPath: z8.string(),
3995
+ query: z8.string().optional().describe("Search title, identifier, or description"),
3996
+ assignee: z8.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
3997
+ limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
3864
3998
  },
3865
3999
  async ({ repoPath, query, assignee, limit }) => {
3866
4000
  const root = await resolveRepoRoot(repoPath);