@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
package/dist/index.cjs CHANGED
@@ -1317,14 +1317,19 @@ function findProjectProfileKey(projects, repoPath) {
1317
1317
  const want = profileRepoKey(repoPath ?? "");
1318
1318
  if (!want) return void 0;
1319
1319
  if (projects[want]) return want;
1320
+ let best;
1321
+ let bestLen = 0;
1320
1322
  for (const key of Object.keys(projects)) {
1321
1323
  const stored = profileRepoKey(key);
1322
1324
  if (!stored) continue;
1323
- if (stored === want || want.startsWith(`${stored}/`) || stored.startsWith(`${want}/`)) {
1324
- return key;
1325
+ if (stored === want || want.startsWith(`${stored}/`)) {
1326
+ if (stored.length > bestLen) {
1327
+ best = key;
1328
+ bestLen = stored.length;
1329
+ }
1325
1330
  }
1326
1331
  }
1327
- return void 0;
1332
+ return best;
1328
1333
  }
1329
1334
  function normalizeAdvanced(raw) {
1330
1335
  if (!raw || typeof raw !== "object") return {};
@@ -18080,13 +18085,13 @@ function formatStat(files) {
18080
18085
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
18081
18086
  }
18082
18087
  function emptyScopeStats() {
18083
- const z8 = emptyStat();
18088
+ const z9 = emptyStat();
18084
18089
  return {
18085
- commits: z8,
18086
- uncommitted: z8,
18087
- staged: z8,
18088
- unstaged: z8,
18089
- last_turn: z8
18090
+ commits: z9,
18091
+ uncommitted: z9,
18092
+ staged: z9,
18093
+ unstaged: z9,
18094
+ last_turn: z9
18090
18095
  };
18091
18096
  }
18092
18097
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -19227,17 +19232,17 @@ function formatIssueToolsDirective(opts) {
19227
19232
  ];
19228
19233
  if (opts.linear) {
19229
19234
  lines.push(
19230
- "- Linear: `linear_get_issue` (comments), `linear_comment`, `linear_update_issue` (state), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
19235
+ "- Linear: `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
19231
19236
  );
19232
19237
  }
19233
19238
  if (github) {
19234
19239
  lines.push(
19235
- "- GitHub: `github_get_issue` (comments), `github_comment`, `github_update_issue` (state open|closed), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
19240
+ "- GitHub: `github_get_issue` (comments; pass `include=full` if crush.truncated), `github_comment`, `github_update_issue` (state open|closed), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
19236
19241
  );
19237
19242
  }
19238
19243
  if (opts.abletime) {
19239
19244
  lines.push(
19240
- "- AbleTime: `abletime_get_task` (comments), `abletime_comment`, `abletime_update_task` (state), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
19245
+ "- AbleTime: `abletime_get_task` (comments; pass `include=full` if crush.truncated), `abletime_comment`, `abletime_update_task` (state), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
19241
19246
  );
19242
19247
  }
19243
19248
  lines.push(
@@ -24713,7 +24718,7 @@ init_orphan_cleanup();
24713
24718
  // src/mcp/server.ts
24714
24719
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
24715
24720
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
24716
- var import_zod7 = require("zod");
24721
+ var import_zod8 = require("zod");
24717
24722
  var import_node_crypto13 = require("crypto");
24718
24723
  var import_node_path50 = require("path");
24719
24724
  init_orchestrator();
@@ -24994,12 +24999,53 @@ ${link}` : link;
24994
24999
  // src/mcp/slack-tools.ts
24995
25000
  init_outbound_watch();
24996
25001
  init_workspaces2();
24997
- function text(payload, isError = false) {
25002
+
25003
+ // src/mcp/issue-list.ts
25004
+ var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
25005
+ var MCP_ISSUE_LIST_MAX_LIMIT = 250;
25006
+ function clampMcpIssueLimit(limit) {
25007
+ if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
25008
+ return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
25009
+ }
25010
+ function applyIssueListWindow(items, limit) {
25011
+ const truncated = items.length > limit;
25012
+ return {
25013
+ items: truncated ? items.slice(0, limit) : items,
25014
+ truncated
25015
+ };
25016
+ }
25017
+ function compactIssueRow(issue) {
25018
+ return {
25019
+ identifier: issue.identifier,
25020
+ title: issue.title,
25021
+ ...issue.assignee ? { assignee: issue.assignee } : {},
25022
+ ...issue.labels.length > 0 ? { labels: issue.labels } : {},
25023
+ ...issue.teamKey ? { team: issue.teamKey } : {},
25024
+ ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
25025
+ };
25026
+ }
25027
+ function formatMcpIssueList(input) {
25028
+ return {
25029
+ ...input.source ? { source: input.source } : {},
25030
+ ...input.viewer ? { viewer: input.viewer } : {},
25031
+ limit: input.limit,
25032
+ returned: input.issues.length,
25033
+ truncated: input.truncated,
25034
+ ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
25035
+ issues: input.issues.map(compactIssueRow)
25036
+ };
25037
+ }
25038
+ function mcpJson(payload, isError = false) {
24998
25039
  return {
24999
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
25040
+ content: [{ type: "text", text: JSON.stringify(payload) }],
25000
25041
  ...isError ? { isError: true } : {}
25001
25042
  };
25002
25043
  }
25044
+
25045
+ // src/mcp/slack-tools.ts
25046
+ function text(payload, isError = false) {
25047
+ return mcpJson(payload, isError);
25048
+ }
25003
25049
  function fail(err) {
25004
25050
  return text(
25005
25051
  { error: err instanceof Error ? err.message : String(err) },
@@ -25268,49 +25314,331 @@ function registerSlackTools(server) {
25268
25314
  init_app_settings();
25269
25315
 
25270
25316
  // src/mcp/abletime-tools.ts
25317
+ var import_zod3 = require("zod");
25318
+ init_abletime();
25319
+
25320
+ // src/mcp/issue-payload.ts
25271
25321
  var import_zod2 = require("zod");
25272
25322
  init_abletime();
25273
25323
 
25274
- // src/mcp/issue-list.ts
25275
- var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
25276
- var MCP_ISSUE_LIST_MAX_LIMIT = 250;
25277
- function clampMcpIssueLimit(limit) {
25278
- if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
25279
- return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
25324
+ // src/mcp/smart-crush.ts
25325
+ var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
25326
+ var CRUSH_MIN_ITEMS = 5;
25327
+ var CRUSH_MIN_CHARS = 800;
25328
+ var CRUSH_COMMENT_MAX_CHARS = 16e3;
25329
+ var CRUSH_BODY_MIN_CHARS = 12e3;
25330
+ var CRUSH_BODY_MAX_CHARS = 8e3;
25331
+ var NEAR_DUP_JACCARD = 0.92;
25332
+ function looksLikeMustKeep(text7) {
25333
+ return MUST_KEEP_TEXT_RE.test(text7);
25334
+ }
25335
+ function normalizeText(text7) {
25336
+ return text7.replace(/\s+/g, " ").trim().toLowerCase();
25337
+ }
25338
+ function wordSet(text7) {
25339
+ return new Set(
25340
+ text7.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
25341
+ );
25280
25342
  }
25281
- function applyIssueListWindow(items, limit) {
25282
- const truncated = items.length > limit;
25343
+ function jaccard(a, b) {
25344
+ if (a.size === 0 && b.size === 0) return 1;
25345
+ let inter = 0;
25346
+ for (const w of a) {
25347
+ if (b.has(w)) inter += 1;
25348
+ }
25349
+ const union = a.size + b.size - inter;
25350
+ return union === 0 ? 0 : inter / union;
25351
+ }
25352
+ function importance(text7) {
25353
+ let score = text7.length;
25354
+ if (/```/.test(text7)) score += 400;
25355
+ if (/https?:\/\//.test(text7)) score += 50;
25356
+ if (looksLikeMustKeep(text7)) score += 1e4;
25357
+ return score;
25358
+ }
25359
+ function meanStd(values) {
25360
+ if (values.length === 0) return { mean: 0, std: 0 };
25361
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
25362
+ const variance = values.reduce((a, b) => a + (b - mean) * (b - mean), 0) / values.length;
25363
+ return { mean, std: Math.sqrt(variance) };
25364
+ }
25365
+ function dedupTextItems(items, getText) {
25366
+ const kept = [];
25367
+ const keptNorm = [];
25368
+ const keptWords = [];
25369
+ for (const item of items) {
25370
+ const text7 = getText(item);
25371
+ const norm = normalizeText(text7);
25372
+ if (!norm) {
25373
+ kept.push(item);
25374
+ keptNorm.push(norm);
25375
+ keptWords.push(wordSet(text7));
25376
+ continue;
25377
+ }
25378
+ const words = wordSet(text7);
25379
+ let dupAt = -1;
25380
+ for (let i = 0; i < kept.length; i++) {
25381
+ if (keptNorm[i] === norm) {
25382
+ dupAt = i;
25383
+ break;
25384
+ }
25385
+ if (keptWords[i].size >= 8 && words.size >= 8 && jaccard(keptWords[i], words) >= NEAR_DUP_JACCARD) {
25386
+ dupAt = i;
25387
+ break;
25388
+ }
25389
+ }
25390
+ if (dupAt < 0) {
25391
+ kept.push(item);
25392
+ keptNorm.push(norm);
25393
+ keptWords.push(words);
25394
+ continue;
25395
+ }
25396
+ if (text7.length > getText(kept[dupAt]).length) {
25397
+ kept[dupAt] = item;
25398
+ keptNorm[dupAt] = norm;
25399
+ keptWords[dupAt] = words;
25400
+ }
25401
+ }
25402
+ return { items: kept, dropped: items.length - kept.length };
25403
+ }
25404
+ function crushTextItems(items, getText, opts = {}) {
25405
+ const minItems = opts.minItems ?? CRUSH_MIN_ITEMS;
25406
+ const minChars = opts.minChars ?? CRUSH_MIN_CHARS;
25407
+ const maxChars = opts.maxChars ?? CRUSH_COMMENT_MAX_CHARS;
25408
+ const original = items;
25409
+ const totalChars = items.reduce((n, item) => n + getText(item).length, 0);
25410
+ if (items.length < minItems || totalChars < minChars) {
25411
+ return {
25412
+ items,
25413
+ truncated: false,
25414
+ kept: items.length,
25415
+ dropped: 0,
25416
+ reason: "passthrough"
25417
+ };
25418
+ }
25419
+ const deduped = dedupTextItems(items, getText);
25420
+ const unique = deduped.items;
25421
+ const uniqueChars = unique.reduce((n, item) => n + getText(item).length, 0);
25422
+ if (uniqueChars <= maxChars) {
25423
+ const changed = unique.length < original.length;
25424
+ return {
25425
+ items: unique,
25426
+ truncated: changed,
25427
+ kept: unique.length,
25428
+ dropped: original.length - unique.length,
25429
+ reason: changed ? "dedup" : "passthrough"
25430
+ };
25431
+ }
25432
+ const lengths = unique.map((item) => getText(item).length);
25433
+ const { mean, std } = meanStd(lengths);
25434
+ const outlierFloor = mean + 2 * std;
25435
+ const mustKeep = /* @__PURE__ */ new Set();
25436
+ unique.forEach((item, i) => {
25437
+ const text7 = getText(item);
25438
+ if (looksLikeMustKeep(text7) || text7.length > outlierFloor) mustKeep.add(i);
25439
+ });
25440
+ const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
25441
+ let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
25442
+ const pick = new Set(mustKeep);
25443
+ const canTake = (i) => {
25444
+ if (pick.has(i)) return false;
25445
+ const next = used + lengths[i];
25446
+ if (next > maxChars && pick.size > mustKeep.size) return false;
25447
+ used = next;
25448
+ pick.add(i);
25449
+ return true;
25450
+ };
25451
+ const k = remainingIdx.length;
25452
+ const headN = Math.max(1, Math.ceil(k * 0.3));
25453
+ const tailN = Math.max(1, Math.ceil(k * 0.15));
25454
+ for (const i of remainingIdx.slice(0, headN)) canTake(i);
25455
+ for (const i of remainingIdx.slice(-tailN).reverse()) canTake(i);
25456
+ const middle = remainingIdx.filter((i) => !pick.has(i)).sort((a, b) => importance(getText(unique[b])) - importance(getText(unique[a])));
25457
+ for (const i of middle) {
25458
+ if (used >= maxChars) break;
25459
+ canTake(i);
25460
+ }
25461
+ const crushed = unique.filter((_, i) => pick.has(i));
25462
+ const crushedChars = crushed.reduce((n, item) => n + getText(item).length, 0);
25463
+ if (crushed.length >= original.length && crushedChars >= totalChars) {
25464
+ return {
25465
+ items: original,
25466
+ truncated: false,
25467
+ kept: original.length,
25468
+ dropped: 0,
25469
+ reason: "passthrough"
25470
+ };
25471
+ }
25283
25472
  return {
25284
- items: truncated ? items.slice(0, limit) : items,
25285
- truncated
25473
+ items: crushed,
25474
+ truncated: crushed.length < original.length,
25475
+ kept: crushed.length,
25476
+ dropped: original.length - crushed.length,
25477
+ reason: "budget"
25286
25478
  };
25287
25479
  }
25288
- function compactIssueRow(issue) {
25289
- return {
25290
- identifier: issue.identifier,
25291
- title: issue.title,
25292
- ...issue.assignee ? { assignee: issue.assignee } : {},
25293
- ...issue.labels.length > 0 ? { labels: issue.labels } : {},
25294
- ...issue.teamKey ? { team: issue.teamKey } : {},
25295
- ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
25480
+ function splitBlocks(text7) {
25481
+ if (/\n\n/.test(text7)) return text7.split(/\n\n+/);
25482
+ return text7.split("\n");
25483
+ }
25484
+ function capOversizedBlock(block, maxChars) {
25485
+ if (block.length <= maxChars) return block;
25486
+ const head = Math.floor(maxChars * 0.3);
25487
+ const tail = Math.floor(maxChars * 0.15);
25488
+ return `${block.slice(0, head)}
25489
+
25490
+ [\u2026crushed ${block.length - head - tail} chars; pass include=full to read the rest\u2026]
25491
+
25492
+ ${block.slice(-tail)}`;
25493
+ }
25494
+ function crushMarkdown(text7, opts) {
25495
+ const original = text7 ?? "";
25496
+ const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
25497
+ const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
25498
+ if (original.length < minChars) {
25499
+ return { text: original, truncated: false, originalChars: original.length };
25500
+ }
25501
+ const rawBlocks = splitBlocks(original).filter((b) => b.length > 0);
25502
+ const blocks = rawBlocks.map((b) => capOversizedBlock(b, maxChars));
25503
+ const cappedABlock = blocks.some((b, i) => b !== rawBlocks[i]);
25504
+ if (blocks.length <= 1) {
25505
+ const cut = blocks[0] ?? original;
25506
+ if (cut.length >= original.length) {
25507
+ return { text: original, truncated: false, originalChars: original.length };
25508
+ }
25509
+ return { text: cut, truncated: true, originalChars: original.length };
25510
+ }
25511
+ const must = blocks.filter((b) => looksLikeMustKeep(b));
25512
+ const rest = blocks.filter((b) => !looksLikeMustKeep(b));
25513
+ const marker = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
25514
+ let used = must.reduce((n, b) => n + b.length, 0) + marker.length;
25515
+ const keptRest = [];
25516
+ const headN = Math.max(1, Math.ceil(rest.length * 0.3));
25517
+ const tailN = Math.max(1, Math.ceil(rest.length * 0.15));
25518
+ const head = rest.slice(0, headN);
25519
+ const tail = rest.slice(-tailN);
25520
+ const middle = rest.slice(headN, Math.max(headN, rest.length - tailN));
25521
+ const take = (block) => {
25522
+ if (used + block.length + 2 > maxChars && keptRest.length + must.length > 0) {
25523
+ return false;
25524
+ }
25525
+ used += block.length + 2;
25526
+ keptRest.push(block);
25527
+ return true;
25296
25528
  };
25529
+ for (const b of head) take(b);
25530
+ for (const b of tail) take(b);
25531
+ const ranked = [...middle].sort((a, b) => importance(b) - importance(a));
25532
+ for (const b of ranked) {
25533
+ if (used >= maxChars) break;
25534
+ take(b);
25535
+ }
25536
+ const keepSet = /* @__PURE__ */ new Set([...must, ...keptRest]);
25537
+ const ordered = blocks.filter((b) => keepSet.has(b));
25538
+ const omitted = blocks.length - ordered.length;
25539
+ if (omitted <= 0) {
25540
+ const joined = blocks.join("\n\n");
25541
+ if (cappedABlock && joined.length < original.length) {
25542
+ return { text: joined, truncated: true, originalChars: original.length };
25543
+ }
25544
+ return { text: original, truncated: false, originalChars: original.length };
25545
+ }
25546
+ const out = [];
25547
+ let inserted = false;
25548
+ for (const block of blocks) {
25549
+ if (keepSet.has(block)) {
25550
+ out.push(block);
25551
+ continue;
25552
+ }
25553
+ if (!inserted) {
25554
+ out.push(marker);
25555
+ inserted = true;
25556
+ }
25557
+ }
25558
+ const textOut = out.join("\n\n");
25559
+ if (textOut.length >= original.length) {
25560
+ return { text: original, truncated: false, originalChars: original.length };
25561
+ }
25562
+ return { text: textOut, truncated: true, originalChars: original.length };
25297
25563
  }
25298
- function formatMcpIssueList(input) {
25299
- return {
25300
- ...input.source ? { source: input.source } : {},
25301
- ...input.viewer ? { viewer: input.viewer } : {},
25302
- limit: input.limit,
25303
- returned: input.issues.length,
25304
- truncated: input.truncated,
25305
- ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
25306
- issues: input.issues.map(compactIssueRow)
25564
+
25565
+ // src/mcp/issue-payload.ts
25566
+ var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
25567
+ var mcpIssueIncludeSchema = import_zod2.z.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
25568
+ "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."
25569
+ );
25570
+ var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
25571
+ function emptyMeta() {
25572
+ return { truncated: true, hint: MCP_ISSUE_CRUSH_HINT };
25573
+ }
25574
+ function withComments(comments, meta) {
25575
+ const crushed = crushTextItems(comments, (c) => c.body);
25576
+ if (crushed.truncated) {
25577
+ meta.comments = {
25578
+ kept: crushed.kept,
25579
+ dropped: crushed.dropped,
25580
+ reason: crushed.reason
25581
+ };
25582
+ }
25583
+ return { comments: crushed.items, meta };
25584
+ }
25585
+ function withBody(text7, meta) {
25586
+ const crushed = crushMarkdown(text7);
25587
+ if (crushed.truncated) {
25588
+ meta.body = { originalChars: crushed.originalChars };
25589
+ return { value: crushed.text, meta };
25590
+ }
25591
+ return { value: text7, meta };
25592
+ }
25593
+ function attachCrush(payload, meta) {
25594
+ if (!meta.comments && !meta.body) return payload;
25595
+ return { ...payload, crush: meta };
25596
+ }
25597
+ function formatLinearIssuePayload(issue, include) {
25598
+ if (include === "full") return issue;
25599
+ const meta = emptyMeta();
25600
+ const comments = withComments(issue.comments, meta);
25601
+ const description = withBody(issue.description, comments.meta);
25602
+ const { team, ...rest } = issue;
25603
+ const next = {
25604
+ ...rest,
25605
+ ...team ? { team: { id: team.id, key: team.key, name: team.name } } : {},
25606
+ description: description.value,
25607
+ comments: comments.comments
25307
25608
  };
25609
+ return attachCrush(next, description.meta);
25308
25610
  }
25309
- function mcpJson(payload, isError = false) {
25310
- return {
25311
- content: [{ type: "text", text: JSON.stringify(payload) }],
25312
- ...isError ? { isError: true } : {}
25611
+ function formatGitHubIssuePayload(issue, include) {
25612
+ if (include === "full") return issue;
25613
+ const meta = emptyMeta();
25614
+ const comments = withComments(issue.comments, meta);
25615
+ const body = withBody(issue.body, comments.meta);
25616
+ const next = {
25617
+ ...issue,
25618
+ body: body.value,
25619
+ comments: comments.comments
25620
+ };
25621
+ return attachCrush(next, body.meta);
25622
+ }
25623
+ function formatAbleTimeTaskPayload(task, include) {
25624
+ const base = {
25625
+ ...toAbleTimeIssueInfo(task),
25626
+ description: task.description,
25627
+ state: task.state,
25628
+ comments: task.comments
25313
25629
  };
25630
+ if (include === "full") return base;
25631
+ const meta = emptyMeta();
25632
+ const comments = withComments(task.comments, meta);
25633
+ const description = withBody(task.description, comments.meta);
25634
+ return attachCrush(
25635
+ {
25636
+ ...base,
25637
+ description: description.value,
25638
+ comments: comments.comments
25639
+ },
25640
+ description.meta
25641
+ );
25314
25642
  }
25315
25643
 
25316
25644
  // src/mcp/abletime-tools.ts
@@ -25356,7 +25684,7 @@ function registerAbleTimeTools(server) {
25356
25684
  server.tool(
25357
25685
  "abletime_list_tasks",
25358
25686
  "List open AbleTime tasks (excludes done/archived). Optional projectId filter.",
25359
- { projectId: import_zod2.z.string().optional() },
25687
+ { projectId: import_zod3.z.string().optional() },
25360
25688
  async ({ projectId }) => {
25361
25689
  try {
25362
25690
  const tasks = await listAbleTimeTasks({ projectId });
@@ -25369,7 +25697,7 @@ function registerAbleTimeTools(server) {
25369
25697
  server.tool(
25370
25698
  "abletime_search_tasks",
25371
25699
  "Search AbleTime tasks by text, reference, or tag.",
25372
- { query: import_zod2.z.string() },
25700
+ { query: import_zod3.z.string() },
25373
25701
  async ({ query }) => {
25374
25702
  try {
25375
25703
  const tasks = await searchAbleTimeTasks(query);
@@ -25381,17 +25709,11 @@ function registerAbleTimeTools(server) {
25381
25709
  );
25382
25710
  server.tool(
25383
25711
  "abletime_get_task",
25384
- "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments.",
25385
- { id: import_zod2.z.string() },
25386
- async ({ id }) => {
25712
+ "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.",
25713
+ { id: import_zod3.z.string(), include: mcpIssueIncludeSchema },
25714
+ async ({ id, include }) => {
25387
25715
  try {
25388
- const task = await getAbleTimeTask(id);
25389
- return text2({
25390
- ...toAbleTimeIssueInfo(task),
25391
- description: task.description,
25392
- state: task.state,
25393
- comments: task.comments
25394
- });
25716
+ return text2(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
25395
25717
  } catch (err) {
25396
25718
  return fail2(err);
25397
25719
  }
@@ -25400,7 +25722,7 @@ function registerAbleTimeTools(server) {
25400
25722
  server.tool(
25401
25723
  "abletime_comment",
25402
25724
  "Add a markdown comment on an AbleTime task (id or CRM-232).",
25403
- { id: import_zod2.z.string(), body: import_zod2.z.string() },
25725
+ { id: import_zod3.z.string(), body: import_zod3.z.string() },
25404
25726
  async (args) => {
25405
25727
  try {
25406
25728
  return text2(await commentAbleTimeTask(args));
@@ -25413,10 +25735,10 @@ function registerAbleTimeTools(server) {
25413
25735
  "abletime_update_task",
25414
25736
  "Update an AbleTime task (id or CRM-232). Pass title, description, and/or state.",
25415
25737
  {
25416
- id: import_zod2.z.string(),
25417
- title: import_zod2.z.string().optional(),
25418
- description: import_zod2.z.string().optional(),
25419
- state: import_zod2.z.string().optional()
25738
+ id: import_zod3.z.string(),
25739
+ title: import_zod3.z.string().optional(),
25740
+ description: import_zod3.z.string().optional(),
25741
+ state: import_zod3.z.string().optional()
25420
25742
  },
25421
25743
  async (args) => {
25422
25744
  try {
@@ -25435,12 +25757,12 @@ function registerAbleTimeTools(server) {
25435
25757
  "abletime_create_task",
25436
25758
  "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).",
25437
25759
  {
25438
- title: import_zod2.z.string(),
25439
- description: import_zod2.z.string().optional(),
25440
- projectId: import_zod2.z.string().optional(),
25441
- categoryId: import_zod2.z.string().optional(),
25442
- state: import_zod2.z.enum(["backlog", "todo"]).optional(),
25443
- parent: import_zod2.z.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
25760
+ title: import_zod3.z.string(),
25761
+ description: import_zod3.z.string().optional(),
25762
+ projectId: import_zod3.z.string().optional(),
25763
+ categoryId: import_zod3.z.string().optional(),
25764
+ state: import_zod3.z.enum(["backlog", "todo"]).optional(),
25765
+ parent: import_zod3.z.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
25444
25766
  },
25445
25767
  async (args) => {
25446
25768
  try {
@@ -25454,10 +25776,10 @@ function registerAbleTimeTools(server) {
25454
25776
  "abletime_ensure_task",
25455
25777
  "Find or create an AbleTime task for this work, then create_thread with sourceType=ticket.",
25456
25778
  {
25457
- title: import_zod2.z.string(),
25458
- description: import_zod2.z.string().optional(),
25459
- projectId: import_zod2.z.string().optional(),
25460
- categoryId: import_zod2.z.string().optional()
25779
+ title: import_zod3.z.string(),
25780
+ description: import_zod3.z.string().optional(),
25781
+ projectId: import_zod3.z.string().optional(),
25782
+ categoryId: import_zod3.z.string().optional()
25461
25783
  },
25462
25784
  async (args) => {
25463
25785
  try {
@@ -25471,7 +25793,7 @@ function registerAbleTimeTools(server) {
25471
25793
  }
25472
25794
 
25473
25795
  // src/mcp/github-tools.ts
25474
- var import_zod3 = require("zod");
25796
+ var import_zod4 = require("zod");
25475
25797
  function text3(payload, isError = false) {
25476
25798
  return mcpJson(payload, isError);
25477
25799
  }
@@ -25481,15 +25803,15 @@ function fail3(err) {
25481
25803
  true
25482
25804
  );
25483
25805
  }
25484
- var repoPathSchema = import_zod3.z.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
25806
+ var repoPathSchema = import_zod4.z.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
25485
25807
  function registerGithubIssueTools(server) {
25486
25808
  server.tool(
25487
25809
  "github_get_issue",
25488
- "Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Uses Account gh.",
25489
- { id: import_zod3.z.string(), repoPath: repoPathSchema },
25490
- async ({ id, repoPath }) => {
25810
+ "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.",
25811
+ { id: import_zod4.z.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
25812
+ async ({ id, include, repoPath }) => {
25491
25813
  try {
25492
- return text3(await getGitHubIssue(id, { repoPath }));
25814
+ return text3(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
25493
25815
  } catch (err) {
25494
25816
  return fail3(err);
25495
25817
  }
@@ -25498,7 +25820,7 @@ function registerGithubIssueTools(server) {
25498
25820
  server.tool(
25499
25821
  "github_comment",
25500
25822
  "Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh.",
25501
- { id: import_zod3.z.string(), body: import_zod3.z.string(), repoPath: repoPathSchema },
25823
+ { id: import_zod4.z.string(), body: import_zod4.z.string(), repoPath: repoPathSchema },
25502
25824
  async ({ id, body, repoPath }) => {
25503
25825
  try {
25504
25826
  return text3(await commentGitHubIssue({ id, body }, { repoPath }));
@@ -25511,10 +25833,10 @@ function registerGithubIssueTools(server) {
25511
25833
  "github_update_issue",
25512
25834
  "Update a GitHub issue (#123). Pass title, body, and/or state (open|closed).",
25513
25835
  {
25514
- id: import_zod3.z.string(),
25515
- title: import_zod3.z.string().optional(),
25516
- body: import_zod3.z.string().optional(),
25517
- state: import_zod3.z.string().optional().describe("open or closed"),
25836
+ id: import_zod4.z.string(),
25837
+ title: import_zod4.z.string().optional(),
25838
+ body: import_zod4.z.string().optional(),
25839
+ state: import_zod4.z.string().optional().describe("open or closed"),
25518
25840
  repoPath: repoPathSchema
25519
25841
  },
25520
25842
  async (args) => {
@@ -25529,9 +25851,9 @@ function registerGithubIssueTools(server) {
25529
25851
  "github_create_issue",
25530
25852
  "Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
25531
25853
  {
25532
- title: import_zod3.z.string(),
25533
- body: import_zod3.z.string().optional(),
25534
- parent: import_zod3.z.string().optional().describe("Parent issue #123 or URL for a spin-off."),
25854
+ title: import_zod4.z.string(),
25855
+ body: import_zod4.z.string().optional(),
25856
+ parent: import_zod4.z.string().optional().describe("Parent issue #123 or URL for a spin-off."),
25535
25857
  repoPath: repoPathSchema
25536
25858
  },
25537
25859
  async (args) => {
@@ -25545,7 +25867,7 @@ function registerGithubIssueTools(server) {
25545
25867
  }
25546
25868
 
25547
25869
  // src/mcp/linear-tools.ts
25548
- var import_zod4 = require("zod");
25870
+ var import_zod5 = require("zod");
25549
25871
  function text4(payload, isError = false) {
25550
25872
  return mcpJson(payload, isError);
25551
25873
  }
@@ -25555,7 +25877,7 @@ function fail4(err) {
25555
25877
  true
25556
25878
  );
25557
25879
  }
25558
- var prioritySchema = import_zod4.z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
25880
+ var prioritySchema = import_zod5.z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
25559
25881
  function registerLinearTools(server) {
25560
25882
  server.tool(
25561
25883
  "linear_list_teams",
@@ -25573,9 +25895,9 @@ function registerLinearTools(server) {
25573
25895
  "linear_search_issues",
25574
25896
  "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.",
25575
25897
  {
25576
- query: import_zod4.z.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
25577
- assignee: import_zod4.z.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
25578
- limit: import_zod4.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
25898
+ query: import_zod5.z.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
25899
+ assignee: import_zod5.z.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
25900
+ limit: import_zod5.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
25579
25901
  },
25580
25902
  async ({ query, assignee, limit }) => {
25581
25903
  try {
@@ -25602,11 +25924,11 @@ function registerLinearTools(server) {
25602
25924
  );
25603
25925
  server.tool(
25604
25926
  "linear_get_issue",
25605
- "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children.",
25606
- { id: import_zod4.z.string() },
25607
- async ({ id }) => {
25927
+ "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.",
25928
+ { id: import_zod5.z.string(), include: mcpIssueIncludeSchema },
25929
+ async ({ id, include }) => {
25608
25930
  try {
25609
- return text4(await getLinearIssue(id));
25931
+ return text4(formatLinearIssuePayload(await getLinearIssue(id), include));
25610
25932
  } catch (err) {
25611
25933
  return fail4(err);
25612
25934
  }
@@ -25616,13 +25938,13 @@ function registerLinearTools(server) {
25616
25938
  "linear_create_issue",
25617
25939
  '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.',
25618
25940
  {
25619
- team: import_zod4.z.string(),
25620
- title: import_zod4.z.string(),
25621
- description: import_zod4.z.string().optional(),
25622
- state: import_zod4.z.string().optional(),
25623
- assignee: import_zod4.z.string().nullable().optional(),
25941
+ team: import_zod5.z.string(),
25942
+ title: import_zod5.z.string(),
25943
+ description: import_zod5.z.string().optional(),
25944
+ state: import_zod5.z.string().optional(),
25945
+ assignee: import_zod5.z.string().nullable().optional(),
25624
25946
  priority: prioritySchema,
25625
- parent: import_zod4.z.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
25947
+ parent: import_zod5.z.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
25626
25948
  },
25627
25949
  async (args) => {
25628
25950
  try {
@@ -25636,11 +25958,11 @@ function registerLinearTools(server) {
25636
25958
  "linear_update_issue",
25637
25959
  "Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority.",
25638
25960
  {
25639
- id: import_zod4.z.string(),
25640
- title: import_zod4.z.string().optional(),
25641
- description: import_zod4.z.string().optional(),
25642
- state: import_zod4.z.string().optional(),
25643
- assignee: import_zod4.z.string().nullable().optional(),
25961
+ id: import_zod5.z.string(),
25962
+ title: import_zod5.z.string().optional(),
25963
+ description: import_zod5.z.string().optional(),
25964
+ state: import_zod5.z.string().optional(),
25965
+ assignee: import_zod5.z.string().nullable().optional(),
25644
25966
  priority: prioritySchema
25645
25967
  },
25646
25968
  async (args) => {
@@ -25655,8 +25977,8 @@ function registerLinearTools(server) {
25655
25977
  "linear_comment",
25656
25978
  "Add a markdown comment on a Linear issue (uuid or ENG-123).",
25657
25979
  {
25658
- id: import_zod4.z.string(),
25659
- body: import_zod4.z.string()
25980
+ id: import_zod5.z.string(),
25981
+ body: import_zod5.z.string()
25660
25982
  },
25661
25983
  async (args) => {
25662
25984
  try {
@@ -25676,7 +25998,7 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
25676
25998
  }
25677
25999
 
25678
26000
  // src/mcp/viewer-context-tools.ts
25679
- var import_zod5 = require("zod");
26001
+ var import_zod6 = require("zod");
25680
26002
  init_worktree();
25681
26003
  init_app_settings();
25682
26004
  init_workspaces();
@@ -25686,11 +26008,14 @@ function text5(payload, isError = false) {
25686
26008
  function matchRegisteredWorkspace(repoPath) {
25687
26009
  const hint = (repoPath ?? "").trim().replace(/\/+$/, "");
25688
26010
  if (!hint) return null;
25689
- const match = listWorkspaces().find((w) => {
26011
+ let best = null;
26012
+ for (const w of listWorkspaces()) {
25690
26013
  const stored = w.path.replace(/\/+$/, "");
25691
- return stored === hint || hint.startsWith(`${stored}/`) || stored.startsWith(`${hint}/`);
25692
- });
25693
- return match?.path.replace(/\/+$/, "") ?? null;
26014
+ if (stored === hint || hint.startsWith(`${stored}/`)) {
26015
+ if (!best || stored.length > best.length) best = stored;
26016
+ }
26017
+ }
26018
+ return best;
25694
26019
  }
25695
26020
  async function resolveProjectRepoPath(repoPath) {
25696
26021
  const hint = repoPath?.trim() || process.cwd();
@@ -25708,7 +26033,7 @@ function registerViewerContextTools(server) {
25708
26033
  "get_viewer_context",
25709
26034
  "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).",
25710
26035
  {
25711
- repoPath: import_zod5.z.string().optional().describe(
26036
+ repoPath: import_zod6.z.string().optional().describe(
25712
26037
  "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
25713
26038
  )
25714
26039
  },
@@ -25728,10 +26053,10 @@ function registerViewerContextTools(server) {
25728
26053
  "update_viewer_context",
25729
26054
  "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.",
25730
26055
  {
25731
- scope: import_zod5.z.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
25732
- context: import_zod5.z.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
25733
- confirmed: import_zod5.z.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
25734
- repoPath: import_zod5.z.string().optional().describe(
26056
+ scope: import_zod6.z.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
26057
+ context: import_zod6.z.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
26058
+ confirmed: import_zod6.z.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
26059
+ repoPath: import_zod6.z.string().optional().describe(
25735
26060
  "Required for scope=project from orchestration (path from list_workspaces). Worktree turns may omit it (cwd)."
25736
26061
  )
25737
26062
  },
@@ -25812,14 +26137,11 @@ init_list_prs();
25812
26137
  init_app_settings();
25813
26138
 
25814
26139
  // src/mcp/schedule-tools.ts
25815
- var import_zod6 = require("zod");
26140
+ var import_zod7 = require("zod");
25816
26141
  init_schedules();
25817
26142
  init_schedule_runner();
25818
26143
  function text6(payload, isError = false) {
25819
- return {
25820
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
25821
- ...isError ? { isError: true } : {}
25822
- };
26144
+ return mcpJson(payload, isError);
25823
26145
  }
25824
26146
  function fail5(err) {
25825
26147
  return text6(
@@ -25857,17 +26179,17 @@ function registerScheduleTools(server) {
25857
26179
  "create_schedule",
25858
26180
  "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.",
25859
26181
  {
25860
- prompt: import_zod6.z.string().describe("User message / goal queued when the schedule fires"),
25861
- name: import_zod6.z.string().optional(),
25862
- at: import_zod6.z.string().optional().describe("ISO datetime for a one-shot"),
25863
- every: import_zod6.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
25864
- cron: import_zod6.z.string().optional().describe("5-field cron expression"),
25865
- tz: import_zod6.z.string().optional().describe("IANA timezone for cron (default: system)"),
25866
- threadId: import_zod6.z.string().optional().describe(
26182
+ prompt: import_zod7.z.string().describe("User message / goal queued when the schedule fires"),
26183
+ name: import_zod7.z.string().optional(),
26184
+ at: import_zod7.z.string().optional().describe("ISO datetime for a one-shot"),
26185
+ every: import_zod7.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
26186
+ cron: import_zod7.z.string().optional().describe("5-field cron expression"),
26187
+ tz: import_zod7.z.string().optional().describe("IANA timezone for cron (default: system)"),
26188
+ threadId: import_zod7.z.string().optional().describe(
25867
26189
  'Existing orchestration chat id, or "self" for this coordinator. Omit to create a new Global chat on fire.'
25868
26190
  ),
25869
- agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
25870
- model: import_zod6.z.string().optional()
26191
+ agent: import_zod7.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
26192
+ model: import_zod7.z.string().optional()
25871
26193
  },
25872
26194
  async (args) => {
25873
26195
  try {
@@ -25900,17 +26222,17 @@ function registerScheduleTools(server) {
25900
26222
  "update_schedule",
25901
26223
  "Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
25902
26224
  {
25903
- id: import_zod6.z.string(),
25904
- prompt: import_zod6.z.string().optional(),
25905
- name: import_zod6.z.string().optional(),
25906
- at: import_zod6.z.string().optional(),
25907
- every: import_zod6.z.string().optional(),
25908
- cron: import_zod6.z.string().optional(),
25909
- tz: import_zod6.z.string().optional(),
25910
- threadId: import_zod6.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
25911
- agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
25912
- model: import_zod6.z.string().optional(),
25913
- enabled: import_zod6.z.boolean().optional()
26225
+ id: import_zod7.z.string(),
26226
+ prompt: import_zod7.z.string().optional(),
26227
+ name: import_zod7.z.string().optional(),
26228
+ at: import_zod7.z.string().optional(),
26229
+ every: import_zod7.z.string().optional(),
26230
+ cron: import_zod7.z.string().optional(),
26231
+ tz: import_zod7.z.string().optional(),
26232
+ threadId: import_zod7.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
26233
+ agent: import_zod7.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
26234
+ model: import_zod7.z.string().optional(),
26235
+ enabled: import_zod7.z.boolean().optional()
25914
26236
  },
25915
26237
  async (args) => {
25916
26238
  try {
@@ -25948,7 +26270,7 @@ function registerScheduleTools(server) {
25948
26270
  server.tool(
25949
26271
  "delete_schedule",
25950
26272
  "Delete a local schedule. Pass id from list_schedules.",
25951
- { id: import_zod6.z.string() },
26273
+ { id: import_zod7.z.string() },
25952
26274
  async ({ id }) => {
25953
26275
  try {
25954
26276
  deleteSchedule(id);
@@ -25961,7 +26283,7 @@ function registerScheduleTools(server) {
25961
26283
  server.tool(
25962
26284
  "run_schedule",
25963
26285
  "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.",
25964
- { id: import_zod6.z.string() },
26286
+ { id: import_zod7.z.string() },
25965
26287
  async ({ id }) => {
25966
26288
  try {
25967
26289
  if (!getSchedule(id)) {
@@ -26455,13 +26777,13 @@ async function startMcpServer() {
26455
26777
  "list_board",
26456
26778
  "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.",
26457
26779
  {
26458
- query: import_zod7.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
26459
- repoPath: import_zod7.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
26460
- kind: import_zod7.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
26461
- column: import_zod7.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
26780
+ query: import_zod8.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
26781
+ repoPath: import_zod8.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
26782
+ kind: import_zod8.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
26783
+ column: import_zod8.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
26462
26784
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
26463
26785
  ),
26464
- limit: import_zod7.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
26786
+ limit: import_zod8.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
26465
26787
  },
26466
26788
  async ({ query, repoPath, kind, column, limit }) => {
26467
26789
  const workspaces = orch.listWorkspaces();
@@ -26476,30 +26798,19 @@ async function startMcpServer() {
26476
26798
  limit,
26477
26799
  workspaceName: (path2) => names.get(path2) ?? ""
26478
26800
  });
26479
- return {
26480
- content: [
26481
- {
26482
- type: "text",
26483
- text: JSON.stringify(
26484
- {
26485
- columns: snap.columns,
26486
- hidden: snap.hidden,
26487
- totals: snap.totals,
26488
- columnDefs: BOARD_COLUMN_DEFS,
26489
- hint: HOME_BOARD_AGENT_HINT
26490
- },
26491
- null,
26492
- 2
26493
- )
26494
- }
26495
- ]
26496
- };
26801
+ return mcpJson({
26802
+ columns: snap.columns,
26803
+ hidden: snap.hidden,
26804
+ totals: snap.totals,
26805
+ columnDefs: BOARD_COLUMN_DEFS,
26806
+ hint: HOME_BOARD_AGENT_HINT
26807
+ });
26497
26808
  }
26498
26809
  );
26499
26810
  server.tool(
26500
26811
  "get_thread",
26501
26812
  "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.",
26502
- { ref: import_zod7.z.string() },
26813
+ { ref: import_zod8.z.string() },
26503
26814
  async ({ ref }) => {
26504
26815
  const t = orch.getThread(ref);
26505
26816
  if (!t) {
@@ -26533,7 +26844,7 @@ async function startMcpServer() {
26533
26844
  usage: spend.usage,
26534
26845
  lastTurnUsage: spend.lastTurnUsage
26535
26846
  };
26536
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
26847
+ return mcpJson(summary);
26537
26848
  }
26538
26849
  );
26539
26850
  }
@@ -26541,17 +26852,17 @@ async function startMcpServer() {
26541
26852
  "present_artifact",
26542
26853
  "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.",
26543
26854
  {
26544
- title: import_zod7.z.string().describe("Short title shown in the artifact pane header"),
26545
- type: import_zod7.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
26855
+ title: import_zod8.z.string().describe("Short title shown in the artifact pane header"),
26856
+ type: import_zod8.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
26546
26857
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
26547
26858
  ),
26548
- content: import_zod7.z.string().describe(
26859
+ content: import_zod8.z.string().describe(
26549
26860
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
26550
26861
  ),
26551
- artifact_id: import_zod7.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
26552
- status: import_zod7.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
26553
- phase: import_zod7.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
26554
- mode: import_zod7.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
26862
+ artifact_id: import_zod8.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
26863
+ status: import_zod8.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
26864
+ phase: import_zod8.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
26865
+ mode: import_zod8.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
26555
26866
  },
26556
26867
  async ({ title, type, artifact_id, status, phase, mode }) => {
26557
26868
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26565,22 +26876,22 @@ async function startMcpServer() {
26565
26876
  mode: type === "log" ? mode ?? "append" : void 0,
26566
26877
  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."
26567
26878
  };
26568
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26879
+ return mcpJson(payload);
26569
26880
  }
26570
26881
  );
26571
26882
  server.tool(
26572
26883
  "ask_user",
26573
26884
  "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.",
26574
26885
  {
26575
- questions: import_zod7.z.array(
26576
- import_zod7.z.object({
26577
- question: import_zod7.z.string().describe("Full question text ending with ?"),
26578
- header: import_zod7.z.string().max(24).optional().describe("Short label shown above the question"),
26579
- multiSelect: import_zod7.z.boolean().optional().describe("Allow selecting multiple options"),
26580
- options: import_zod7.z.array(
26581
- import_zod7.z.object({
26582
- label: import_zod7.z.string(),
26583
- description: import_zod7.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
26886
+ questions: import_zod8.z.array(
26887
+ import_zod8.z.object({
26888
+ question: import_zod8.z.string().describe("Full question text ending with ?"),
26889
+ header: import_zod8.z.string().max(24).optional().describe("Short label shown above the question"),
26890
+ multiSelect: import_zod8.z.boolean().optional().describe("Allow selecting multiple options"),
26891
+ options: import_zod8.z.array(
26892
+ import_zod8.z.object({
26893
+ label: import_zod8.z.string(),
26894
+ description: import_zod8.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
26584
26895
  })
26585
26896
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
26586
26897
  })
@@ -26592,16 +26903,16 @@ async function startMcpServer() {
26592
26903
  questions,
26593
26904
  message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
26594
26905
  };
26595
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26906
+ return mcpJson(payload);
26596
26907
  }
26597
26908
  );
26598
26909
  server.tool(
26599
26910
  "present_plan",
26600
26911
  "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.",
26601
26912
  {
26602
- title: import_zod7.z.string().optional().describe("Short plan title (defaults to Plan)"),
26603
- content: import_zod7.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
26604
- thread_id: import_zod7.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
26913
+ title: import_zod8.z.string().optional().describe("Short plan title (defaults to Plan)"),
26914
+ content: import_zod8.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
26915
+ thread_id: import_zod8.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
26605
26916
  },
26606
26917
  async ({ title, content, thread_id }) => {
26607
26918
  const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
@@ -26617,22 +26928,22 @@ async function startMcpServer() {
26617
26928
  title: title?.trim() || "Plan",
26618
26929
  message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
26619
26930
  };
26620
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26931
+ return mcpJson(payload);
26621
26932
  }
26622
26933
  );
26623
26934
  server.tool(
26624
26935
  "present_schema",
26625
26936
  "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.",
26626
26937
  {
26627
- title: import_zod7.z.string().describe("Pane title"),
26628
- mode: import_zod7.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
26629
- datasource: import_zod7.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
26630
- resource_id: import_zod7.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
26631
- record_id: import_zod7.z.string().optional().describe("Record id when opening form mode"),
26632
- resource: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
26633
- record: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
26634
- records: import_zod7.z.array(import_zod7.z.record(import_zod7.z.unknown())).optional().describe("Inline records for table mode"),
26635
- pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
26938
+ title: import_zod8.z.string().describe("Pane title"),
26939
+ mode: import_zod8.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
26940
+ datasource: import_zod8.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
26941
+ resource_id: import_zod8.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
26942
+ record_id: import_zod8.z.string().optional().describe("Record id when opening form mode"),
26943
+ resource: import_zod8.z.record(import_zod8.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
26944
+ record: import_zod8.z.record(import_zod8.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
26945
+ records: import_zod8.z.array(import_zod8.z.record(import_zod8.z.unknown())).optional().describe("Inline records for table mode"),
26946
+ pane_id: import_zod8.z.string().optional().describe("Stable pane id across updates")
26636
26947
  },
26637
26948
  async (args) => {
26638
26949
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26646,17 +26957,17 @@ async function startMcpServer() {
26646
26957
  record_id: args.record_id,
26647
26958
  message: "Schema pane accepted. Sideboard desktop opens the CMS column beside chat."
26648
26959
  };
26649
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26960
+ return mcpJson(payload);
26650
26961
  }
26651
26962
  );
26652
26963
  server.tool(
26653
26964
  "present_files",
26654
26965
  "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.",
26655
26966
  {
26656
- title: import_zod7.z.string().optional().describe("Pane title (default: Files)"),
26657
- datasource: import_zod7.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
26658
- path: import_zod7.z.string().optional().describe("Initial folder path (e.g. public)"),
26659
- pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
26967
+ title: import_zod8.z.string().optional().describe("Pane title (default: Files)"),
26968
+ datasource: import_zod8.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
26969
+ path: import_zod8.z.string().optional().describe("Initial folder path (e.g. public)"),
26970
+ pane_id: import_zod8.z.string().optional().describe("Stable pane id across updates")
26660
26971
  },
26661
26972
  async (args) => {
26662
26973
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26668,21 +26979,21 @@ async function startMcpServer() {
26668
26979
  path: args.path,
26669
26980
  message: "Files pane accepted. Sideboard desktop opens the Files column beside chat."
26670
26981
  };
26671
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26982
+ return mcpJson(payload);
26672
26983
  }
26673
26984
  );
26674
26985
  server.tool(
26675
26986
  "wait_for_job",
26676
26987
  "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.",
26677
26988
  {
26678
- id: import_zod7.z.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
26679
- timeoutMs: import_zod7.z.number().optional()
26989
+ id: import_zod8.z.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
26990
+ timeoutMs: import_zod8.z.number().optional()
26680
26991
  },
26681
26992
  async ({ id, timeoutMs }) => {
26682
26993
  const result = await waitForDetachedJob(process.cwd(), id, {
26683
26994
  timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
26684
26995
  });
26685
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
26996
+ return mcpJson(result);
26686
26997
  }
26687
26998
  );
26688
26999
  if (!worktreeProfile) {
@@ -26697,56 +27008,44 @@ async function startMcpServer() {
26697
27008
  "set_caffeinate",
26698
27009
  "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.",
26699
27010
  {
26700
- enabled: import_zod7.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
27011
+ enabled: import_zod8.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
26701
27012
  },
26702
27013
  async ({ enabled }) => {
26703
27014
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
26704
27015
  const state = setCaffeinateHold2(enabled, { threadId });
26705
27016
  if (enabled && !state.held) {
26706
- return {
26707
- content: [
26708
- {
26709
- type: "text",
26710
- text: JSON.stringify({
26711
- ...state,
26712
- ok: false,
26713
- message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
26714
- })
26715
- }
26716
- ],
26717
- isError: true
26718
- };
26719
- }
26720
- return {
26721
- content: [
27017
+ return mcpJson(
26722
27018
  {
26723
- type: "text",
26724
- text: JSON.stringify({
26725
- ...getCaffeinateHold2(),
26726
- ok: true,
26727
- 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)."
26728
- })
26729
- }
26730
- ]
26731
- };
27019
+ ...state,
27020
+ ok: false,
27021
+ message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
27022
+ },
27023
+ true
27024
+ );
27025
+ }
27026
+ return mcpJson({
27027
+ ...getCaffeinateHold2(),
27028
+ ok: true,
27029
+ 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)."
27030
+ });
26732
27031
  }
26733
27032
  );
26734
27033
  server.tool(
26735
27034
  "create_thread",
26736
27035
  `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.`,
26737
27036
  {
26738
- sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]),
26739
- sourceRef: import_zod7.z.string(),
26740
- agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
26741
- model: import_zod7.z.string().nullable().optional().describe(
27037
+ sourceType: import_zod8.z.enum(["branch", "pr", "ticket"]),
27038
+ sourceRef: import_zod8.z.string(),
27039
+ agent: import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
27040
+ model: import_zod8.z.string().nullable().optional().describe(
26742
27041
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
26743
27042
  ),
26744
- repoPath: import_zod7.z.string(),
26745
- title: import_zod7.z.string().optional(),
26746
- cowboy: import_zod7.z.boolean().optional().describe(
27043
+ repoPath: import_zod8.z.string(),
27044
+ title: import_zod8.z.string().optional(),
27045
+ cowboy: import_zod8.z.boolean().optional().describe(
26747
27046
  "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."
26748
27047
  ),
26749
- parentThreadId: import_zod7.z.string().optional().describe(
27048
+ parentThreadId: import_zod8.z.string().optional().describe(
26750
27049
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
26751
27050
  )
26752
27051
  },
@@ -26762,10 +27061,10 @@ async function startMcpServer() {
26762
27061
  "start_board_card",
26763
27062
  "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.",
26764
27063
  {
26765
- kind: import_zod7.z.enum(["ticket", "pr", "branch"]),
26766
- ref: import_zod7.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
26767
- repoPath: import_zod7.z.string().describe("Workspace path from list_workspaces / list_board"),
26768
- title: import_zod7.z.string().optional()
27064
+ kind: import_zod8.z.enum(["ticket", "pr", "branch"]),
27065
+ ref: import_zod8.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
27066
+ repoPath: import_zod8.z.string().describe("Workspace path from list_workspaces / list_board"),
27067
+ title: import_zod8.z.string().optional()
26769
27068
  },
26770
27069
  async ({ kind, ref, repoPath, title }) => {
26771
27070
  const root = await resolveRepoRoot(repoPath);
@@ -26782,20 +27081,13 @@ async function startMcpServer() {
26782
27081
  }))
26783
27082
  );
26784
27083
  if (existing) {
26785
- return {
26786
- content: [
26787
- {
26788
- type: "text",
26789
- text: JSON.stringify({
26790
- alreadyStarted: true,
26791
- id: existing.id,
26792
- title: existing.title,
26793
- status: existing.status,
26794
- link: `sideboard://thread/${existing.id}`
26795
- })
26796
- }
26797
- ]
26798
- };
27084
+ return mcpJson({
27085
+ alreadyStarted: true,
27086
+ id: existing.id,
27087
+ title: existing.title,
27088
+ status: existing.status,
27089
+ link: `sideboard://thread/${existing.id}`
27090
+ });
26799
27091
  }
26800
27092
  const workspaces = orch.listWorkspaces();
26801
27093
  const loaded = await getHomeBoardInputs(workspaces);
@@ -26869,9 +27161,9 @@ async function startMcpServer() {
26869
27161
  "send_to_thread",
26870
27162
  '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.',
26871
27163
  {
26872
- ref: import_zod7.z.string(),
26873
- prompt: import_zod7.z.string(),
26874
- force_stop: import_zod7.z.boolean().optional()
27164
+ ref: import_zod8.z.string(),
27165
+ prompt: import_zod8.z.string(),
27166
+ force_stop: import_zod8.z.boolean().optional()
26875
27167
  },
26876
27168
  async ({ ref, prompt, force_stop }) => {
26877
27169
  if (force_stop) {
@@ -26881,79 +27173,58 @@ async function startMcpServer() {
26881
27173
  }
26882
27174
  }
26883
27175
  const thread = await orch.send(ref, prompt);
26884
- return {
26885
- content: [
26886
- {
26887
- type: "text",
26888
- text: JSON.stringify({
26889
- id: thread.id,
26890
- status: thread.status,
26891
- queueLength: thread.queue.length,
26892
- forceStopped: Boolean(force_stop)
26893
- })
26894
- }
26895
- ]
26896
- };
27176
+ return mcpJson({
27177
+ id: thread.id,
27178
+ status: thread.status,
27179
+ queueLength: thread.queue.length,
27180
+ forceStopped: Boolean(force_stop)
27181
+ });
26897
27182
  }
26898
27183
  );
26899
27184
  server.tool(
26900
27185
  "wait_for_turn",
26901
27186
  "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).",
26902
27187
  {
26903
- ref: import_zod7.z.string(),
26904
- timeoutMs: import_zod7.z.number().optional()
27188
+ ref: import_zod8.z.string(),
27189
+ timeoutMs: import_zod8.z.number().optional()
26905
27190
  },
26906
27191
  async ({ ref, timeoutMs }) => {
26907
27192
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
26908
27193
  resolveIfStillRunning: true
26909
27194
  });
26910
27195
  const result = orch.getTurnResult(thread.id);
26911
- return {
26912
- content: [
26913
- {
26914
- type: "text",
26915
- text: JSON.stringify({
26916
- id: thread.id,
26917
- status: result.status,
26918
- text: result.text,
26919
- lastError: result.lastError,
26920
- stillRunning: result.stillRunning,
26921
- progress: result.progress,
26922
- lastActivityAt: result.lastActivityAt,
26923
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
26924
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
26925
- })
26926
- }
26927
- ]
26928
- };
27196
+ return mcpJson({
27197
+ id: thread.id,
27198
+ status: result.status,
27199
+ text: result.text,
27200
+ lastError: result.lastError,
27201
+ stillRunning: result.stillRunning,
27202
+ progress: result.progress,
27203
+ lastActivityAt: result.lastActivityAt,
27204
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
27205
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
27206
+ });
26929
27207
  }
26930
27208
  );
26931
27209
  server.tool(
26932
27210
  "get_turn_result",
26933
27211
  "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).",
26934
- { ref: import_zod7.z.string() },
27212
+ { ref: import_zod8.z.string() },
26935
27213
  async ({ ref }) => {
26936
27214
  const result = orch.getTurnResult(ref);
26937
- return {
26938
- content: [
26939
- {
26940
- type: "text",
26941
- text: JSON.stringify({
26942
- ...result,
26943
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
26944
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
26945
- })
26946
- }
26947
- ]
26948
- };
27215
+ return mcpJson({
27216
+ ...result,
27217
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
27218
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
27219
+ });
26949
27220
  }
26950
27221
  );
26951
27222
  server.tool(
26952
27223
  "stop_thread",
26953
27224
  "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.",
26954
27225
  {
26955
- ref: import_zod7.z.string(),
26956
- force: import_zod7.z.boolean().optional()
27226
+ ref: import_zod8.z.string(),
27227
+ force: import_zod8.z.boolean().optional()
26957
27228
  },
26958
27229
  async ({ ref, force }) => {
26959
27230
  const t = orch.getThread(ref);
@@ -26966,24 +27237,17 @@ async function startMcpServer() {
26966
27237
  const clearQueue = force !== false;
26967
27238
  const hadQueued = t.queue.length > 0;
26968
27239
  const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
26969
- return {
26970
- content: [
26971
- {
26972
- type: "text",
26973
- text: JSON.stringify({
26974
- id: stopped.id,
26975
- status: stopped.status,
26976
- clearedQueue: clearQueue && hadQueued
26977
- })
26978
- }
26979
- ]
26980
- };
27240
+ return mcpJson({
27241
+ id: stopped.id,
27242
+ status: stopped.status,
27243
+ clearedQueue: clearQueue && hadQueued
27244
+ });
26981
27245
  }
26982
27246
  );
26983
27247
  server.tool(
26984
27248
  "archive_thread",
26985
27249
  "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.",
26986
- { ref: import_zod7.z.string() },
27250
+ { ref: import_zod8.z.string() },
26987
27251
  async ({ ref }) => {
26988
27252
  const t = orch.getThread(ref);
26989
27253
  if (!t) {
@@ -27000,38 +27264,24 @@ async function startMcpServer() {
27000
27264
  };
27001
27265
  }
27002
27266
  const archived = await orch.archive(ref);
27003
- return {
27004
- content: [
27005
- {
27006
- type: "text",
27007
- text: JSON.stringify({
27008
- id: archived.id,
27009
- status: archived.status
27010
- })
27011
- }
27012
- ]
27013
- };
27267
+ return mcpJson({
27268
+ id: archived.id,
27269
+ status: archived.status
27270
+ });
27014
27271
  }
27015
27272
  );
27016
27273
  server.tool(
27017
27274
  "restore_thread",
27018
27275
  "Restore an archived thread (recreates worktree from branch when needed)",
27019
- { ref: import_zod7.z.string() },
27276
+ { ref: import_zod8.z.string() },
27020
27277
  async ({ ref }) => {
27021
27278
  try {
27022
27279
  const restored = await orch.restore(ref);
27023
- return {
27024
- content: [
27025
- {
27026
- type: "text",
27027
- text: JSON.stringify({
27028
- id: restored.id,
27029
- status: restored.status,
27030
- worktreePath: restored.worktreePath
27031
- })
27032
- }
27033
- ]
27034
- };
27280
+ return mcpJson({
27281
+ id: restored.id,
27282
+ status: restored.status,
27283
+ worktreePath: restored.worktreePath
27284
+ });
27035
27285
  } catch (err) {
27036
27286
  const message = err instanceof Error ? err.message : String(err);
27037
27287
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27042,34 +27292,25 @@ async function startMcpServer() {
27042
27292
  "get_diff",
27043
27293
  "Compact diff summary (capped hunks, paginated)",
27044
27294
  {
27045
- ref: import_zod7.z.string(),
27046
- maxFiles: import_zod7.z.number().optional()
27295
+ ref: import_zod8.z.string(),
27296
+ maxFiles: import_zod8.z.number().optional()
27047
27297
  },
27048
27298
  async ({ ref, maxFiles }) => {
27049
27299
  const summary = await orch.diffSummary(ref);
27050
- return {
27051
- content: [
27052
- {
27053
- type: "text",
27054
- text: JSON.stringify({
27055
- ...summary,
27056
- files: summary.files.slice(0, maxFiles ?? 10)
27057
- })
27058
- }
27059
- ]
27060
- };
27300
+ return mcpJson({
27301
+ ...summary,
27302
+ files: summary.files.slice(0, maxFiles ?? 10)
27303
+ });
27061
27304
  }
27062
27305
  );
27063
27306
  server.tool(
27064
27307
  "get_pr_checks",
27065
27308
  "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.",
27066
- { ref: import_zod7.z.string().describe("Worktree thread id/ref") },
27309
+ { ref: import_zod8.z.string().describe("Worktree thread id/ref") },
27067
27310
  async ({ ref }) => {
27068
27311
  try {
27069
27312
  const checks = await orch.getPrChecks(ref);
27070
- return {
27071
- content: [{ type: "text", text: JSON.stringify(checks) }]
27072
- };
27313
+ return mcpJson(checks);
27073
27314
  } catch (err) {
27074
27315
  const message = err instanceof Error ? err.message : String(err);
27075
27316
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27079,25 +27320,18 @@ async function startMcpServer() {
27079
27320
  server.tool(
27080
27321
  "request_review",
27081
27322
  '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.',
27082
- { ref: import_zod7.z.string().describe("Worktree thread id/ref to review") },
27323
+ { ref: import_zod8.z.string().describe("Worktree thread id/ref to review") },
27083
27324
  async ({ ref }) => {
27084
27325
  try {
27085
27326
  const tab = await orch.requestReview(ref);
27086
27327
  const from = orch.getThread(ref);
27087
- return {
27088
- content: [
27089
- {
27090
- type: "text",
27091
- text: JSON.stringify({
27092
- id: tab.id,
27093
- title: tab.title,
27094
- status: tab.status,
27095
- fromThreadId: from?.id ?? ref,
27096
- link: `sideboard://thread/${tab.id}`
27097
- })
27098
- }
27099
- ]
27100
- };
27328
+ return mcpJson({
27329
+ id: tab.id,
27330
+ title: tab.title,
27331
+ status: tab.status,
27332
+ fromThreadId: from?.id ?? ref,
27333
+ link: `sideboard://thread/${tab.id}`
27334
+ });
27101
27335
  } catch (err) {
27102
27336
  const message = err instanceof Error ? err.message : String(err);
27103
27337
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27108,53 +27342,37 @@ async function startMcpServer() {
27108
27342
  "ask_git",
27109
27343
  "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.",
27110
27344
  {
27111
- ref: import_zod7.z.string().describe("Worktree thread id/ref"),
27112
- action: import_zod7.z.enum(AGENT_GIT_ACTIONS).describe(
27345
+ ref: import_zod8.z.string().describe("Worktree thread id/ref"),
27346
+ action: import_zod8.z.enum(AGENT_GIT_ACTIONS).describe(
27113
27347
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
27114
27348
  )
27115
27349
  },
27116
27350
  async ({ ref, action }) => {
27117
27351
  try {
27118
27352
  const thread = await orch.askGit(ref, action);
27119
- return {
27120
- content: [
27121
- {
27122
- type: "text",
27123
- text: JSON.stringify({
27124
- id: thread.id,
27125
- status: thread.status,
27126
- queueLength: thread.queue.length,
27127
- action,
27128
- link: `sideboard://thread/${thread.id}`
27129
- })
27130
- }
27131
- ]
27132
- };
27353
+ return mcpJson({
27354
+ id: thread.id,
27355
+ status: thread.status,
27356
+ queueLength: thread.queue.length,
27357
+ action,
27358
+ link: `sideboard://thread/${thread.id}`
27359
+ });
27133
27360
  } catch (err) {
27134
27361
  const raw = err instanceof Error ? err.message : String(err);
27135
27362
  const message = formatGhLandError(raw);
27136
- return {
27137
- content: [
27138
- {
27139
- type: "text",
27140
- text: JSON.stringify(
27141
- {
27142
- error: true,
27143
- action,
27144
- lastError: message,
27145
- 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
27146
- },
27147
- null,
27148
- 2
27149
- )
27150
- }
27151
- ],
27152
- isError: true
27153
- };
27363
+ return mcpJson(
27364
+ {
27365
+ error: true,
27366
+ action,
27367
+ lastError: message,
27368
+ 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
27369
+ },
27370
+ true
27371
+ );
27154
27372
  }
27155
27373
  }
27156
27374
  );
27157
- const agentEnum = import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
27375
+ const agentEnum = import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
27158
27376
  server.tool(
27159
27377
  "list_models",
27160
27378
  "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.",
@@ -27164,9 +27382,7 @@ async function startMcpServer() {
27164
27382
  async ({ agent }) => {
27165
27383
  try {
27166
27384
  const catalogs = await listModelsForAgent(agent);
27167
- return {
27168
- content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
27169
- };
27385
+ return mcpJson(catalogs);
27170
27386
  } catch (err) {
27171
27387
  const message = err instanceof Error ? err.message : String(err);
27172
27388
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27177,11 +27393,11 @@ async function startMcpServer() {
27177
27393
  "fork_worktree",
27178
27394
  "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.",
27179
27395
  {
27180
- ref: import_zod7.z.string().describe("Worktree thread id/ref to fork"),
27181
- through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27396
+ ref: import_zod8.z.string().describe("Worktree thread id/ref to fork"),
27397
+ through_index: import_zod8.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27182
27398
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
27183
- model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27184
- title: import_zod7.z.string().optional()
27399
+ model: import_zod8.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27400
+ title: import_zod8.z.string().optional()
27185
27401
  },
27186
27402
  async ({ ref, through_index, agent, model, title }) => {
27187
27403
  try {
@@ -27194,24 +27410,17 @@ async function startMcpServer() {
27194
27410
  model,
27195
27411
  title
27196
27412
  });
27197
- return {
27198
- content: [
27199
- {
27200
- type: "text",
27201
- text: JSON.stringify({
27202
- id: thread.id,
27203
- title: thread.title,
27204
- status: thread.status,
27205
- agent: thread.agent,
27206
- model: thread.model,
27207
- branchName: thread.branchName,
27208
- worktreePath: thread.worktreePath,
27209
- fromThreadId: source?.id ?? ref,
27210
- link: `sideboard://thread/${thread.id}`
27211
- })
27212
- }
27213
- ]
27214
- };
27413
+ return mcpJson({
27414
+ id: thread.id,
27415
+ title: thread.title,
27416
+ status: thread.status,
27417
+ agent: thread.agent,
27418
+ model: thread.model,
27419
+ branchName: thread.branchName,
27420
+ worktreePath: thread.worktreePath,
27421
+ fromThreadId: source?.id ?? ref,
27422
+ link: `sideboard://thread/${thread.id}`
27423
+ });
27215
27424
  } catch (err) {
27216
27425
  const message = err instanceof Error ? err.message : String(err);
27217
27426
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27222,11 +27431,11 @@ async function startMcpServer() {
27222
27431
  "fork_chat",
27223
27432
  "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.",
27224
27433
  {
27225
- ref: import_zod7.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
27226
- through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27434
+ ref: import_zod8.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
27435
+ through_index: import_zod8.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27227
27436
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
27228
- model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27229
- title: import_zod7.z.string().optional()
27437
+ model: import_zod8.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27438
+ title: import_zod8.z.string().optional()
27230
27439
  },
27231
27440
  async ({ ref, through_index, agent, model, title }) => {
27232
27441
  try {
@@ -27244,24 +27453,17 @@ async function startMcpServer() {
27244
27453
  model,
27245
27454
  title
27246
27455
  });
27247
- return {
27248
- content: [
27249
- {
27250
- type: "text",
27251
- text: JSON.stringify({
27252
- id: tab.id,
27253
- title: tab.title,
27254
- status: tab.status,
27255
- agent: tab.agent,
27256
- model: tab.model,
27257
- sourceType: tab.sourceType,
27258
- worktreePath: tab.worktreePath,
27259
- fromThreadId: source.id,
27260
- link: `sideboard://thread/${tab.id}`
27261
- })
27262
- }
27263
- ]
27264
- };
27456
+ return mcpJson({
27457
+ id: tab.id,
27458
+ title: tab.title,
27459
+ status: tab.status,
27460
+ agent: tab.agent,
27461
+ model: tab.model,
27462
+ sourceType: tab.sourceType,
27463
+ worktreePath: tab.worktreePath,
27464
+ fromThreadId: source.id,
27465
+ link: `sideboard://thread/${tab.id}`
27466
+ });
27265
27467
  } catch (err) {
27266
27468
  const message = err instanceof Error ? err.message : String(err);
27267
27469
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27272,49 +27474,35 @@ async function startMcpServer() {
27272
27474
  "run_dev_script",
27273
27475
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
27274
27476
  {
27275
- ref: import_zod7.z.string(),
27276
- name: import_zod7.z.string().optional()
27477
+ ref: import_zod8.z.string(),
27478
+ name: import_zod8.z.string().optional()
27277
27479
  },
27278
27480
  async ({ ref, name }) => {
27279
27481
  const result = await orch.startDev(ref, name);
27280
- return {
27281
- content: [
27282
- {
27283
- type: "text",
27284
- text: JSON.stringify({
27285
- port: result.port,
27286
- scriptName: result.scriptName,
27287
- ports: result.ports,
27288
- url: `http://localhost:${result.port}`
27289
- })
27290
- }
27291
- ]
27292
- };
27482
+ return mcpJson({
27483
+ port: result.port,
27484
+ scriptName: result.scriptName,
27485
+ ports: result.ports,
27486
+ url: `http://localhost:${result.port}`
27487
+ });
27293
27488
  }
27294
27489
  );
27295
27490
  server.tool(
27296
27491
  "list_run_scripts",
27297
27492
  "List named run scripts available for a thread",
27298
- { ref: import_zod7.z.string() },
27493
+ { ref: import_zod8.z.string() },
27299
27494
  async ({ ref }) => {
27300
27495
  const scripts = orch.listThreadRunScripts(ref);
27301
27496
  const active = orch.getActiveRuns(ref);
27302
- return {
27303
- content: [
27304
- {
27305
- type: "text",
27306
- text: JSON.stringify({ scripts, active }, null, 2)
27307
- }
27308
- ]
27309
- };
27497
+ return mcpJson({ scripts, active });
27310
27498
  }
27311
27499
  );
27312
27500
  server.tool(
27313
27501
  "stop_dev_script",
27314
27502
  "Stop a running script for a thread (all scripts if name omitted)",
27315
27503
  {
27316
- ref: import_zod7.z.string(),
27317
- name: import_zod7.z.string().optional()
27504
+ ref: import_zod8.z.string(),
27505
+ name: import_zod8.z.string().optional()
27318
27506
  },
27319
27507
  async ({ ref, name }) => {
27320
27508
  orch.stopDev(ref, name);
@@ -27324,27 +27512,25 @@ async function startMcpServer() {
27324
27512
  server.tool(
27325
27513
  "run_setup",
27326
27514
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
27327
- { ref: import_zod7.z.string() },
27515
+ { ref: import_zod8.z.string() },
27328
27516
  async ({ ref }) => {
27329
27517
  const result = await orch.runSetup(ref);
27330
- return {
27331
- content: [{ type: "text", text: JSON.stringify(result) }]
27332
- };
27518
+ return mcpJson(result);
27333
27519
  }
27334
27520
  );
27335
27521
  server.tool(
27336
27522
  "add_workspace",
27337
27523
  "Register a git repo as a Sideboard workspace",
27338
- { repoPath: import_zod7.z.string() },
27524
+ { repoPath: import_zod8.z.string() },
27339
27525
  async ({ repoPath }) => {
27340
27526
  const ws = await orch.addWorkspace(repoPath);
27341
- return { content: [{ type: "text", text: JSON.stringify(ws) }] };
27527
+ return mcpJson(ws);
27342
27528
  }
27343
27529
  );
27344
27530
  server.tool(
27345
27531
  "remove_workspace",
27346
27532
  "Unregister a Sideboard workspace (does not archive threads)",
27347
- { repoPath: import_zod7.z.string() },
27533
+ { repoPath: import_zod8.z.string() },
27348
27534
  async ({ repoPath }) => {
27349
27535
  orch.removeWorkspace(repoPath);
27350
27536
  return { content: [{ type: "text", text: "ok" }] };
@@ -27354,40 +27540,31 @@ async function startMcpServer() {
27354
27540
  "fanout",
27355
27541
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
27356
27542
  {
27357
- prompt: import_zod7.z.string(),
27358
- agents: import_zod7.z.array(import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
27359
- repoPath: import_zod7.z.string(),
27360
- sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]).optional(),
27361
- sourceRef: import_zod7.z.string().optional(),
27362
- title: import_zod7.z.string().optional()
27543
+ prompt: import_zod8.z.string(),
27544
+ agents: import_zod8.z.array(import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
27545
+ repoPath: import_zod8.z.string(),
27546
+ sourceType: import_zod8.z.enum(["branch", "pr", "ticket"]).optional(),
27547
+ sourceRef: import_zod8.z.string().optional(),
27548
+ title: import_zod8.z.string().optional()
27363
27549
  },
27364
27550
  async (args) => {
27365
27551
  const threads = await orch.bestOfN(args);
27366
- return {
27367
- content: [
27368
- {
27369
- type: "text",
27370
- text: JSON.stringify(
27371
- threads.map((t) => ({
27372
- id: t.id,
27373
- agent: t.agent,
27374
- branchName: t.branchName,
27375
- worktreePath: t.worktreePath
27376
- })),
27377
- null,
27378
- 2
27379
- )
27380
- }
27381
- ]
27382
- };
27552
+ return mcpJson(
27553
+ threads.map((t) => ({
27554
+ id: t.id,
27555
+ agent: t.agent,
27556
+ branchName: t.branchName,
27557
+ worktreePath: t.worktreePath
27558
+ }))
27559
+ );
27383
27560
  }
27384
27561
  );
27385
27562
  server.tool(
27386
27563
  "list_branches",
27387
27564
  "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).",
27388
27565
  {
27389
- repoPath: import_zod7.z.string(),
27390
- unmergedOnly: import_zod7.z.boolean().optional()
27566
+ repoPath: import_zod8.z.string(),
27567
+ unmergedOnly: import_zod8.z.boolean().optional()
27391
27568
  },
27392
27569
  async ({ repoPath, unmergedOnly }) => {
27393
27570
  const root = await resolveRepoRoot(repoPath);
@@ -27408,19 +27585,19 @@ async function startMcpServer() {
27408
27585
  "list_prs",
27409
27586
  '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.',
27410
27587
  {
27411
- repoPath: import_zod7.z.string(),
27412
- query: import_zod7.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
27413
- queue: import_zod7.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
27588
+ repoPath: import_zod8.z.string(),
27589
+ query: import_zod8.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
27590
+ queue: import_zod8.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
27414
27591
  'review = unclaimed eng-review inbox (use for "get me N tickets to review"). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes.'
27415
27592
  ),
27416
- state: import_zod7.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
27417
- label: import_zod7.z.string().optional().describe(
27593
+ state: import_zod8.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
27594
+ label: import_zod8.z.string().optional().describe(
27418
27595
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
27419
27596
  ),
27420
- reviewer: import_zod7.z.string().optional().describe(
27597
+ reviewer: import_zod8.z.string().optional().describe(
27421
27598
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
27422
27599
  ),
27423
- limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27600
+ limit: import_zod8.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27424
27601
  },
27425
27602
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
27426
27603
  const root = await resolveRepoRoot(repoPath);
@@ -27452,89 +27629,65 @@ async function startMcpServer() {
27452
27629
  server.tool(
27453
27630
  "get_pr_stack",
27454
27631
  "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).",
27455
- { ref: import_zod7.z.string() },
27632
+ { ref: import_zod8.z.string() },
27456
27633
  async ({ ref }) => {
27457
27634
  const stack = await orch.getPrStack(ref);
27458
- return {
27459
- content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
27460
- };
27635
+ return mcpJson(stack);
27461
27636
  }
27462
27637
  );
27463
27638
  server.tool(
27464
27639
  "open_pr_stack_layers",
27465
27640
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
27466
27641
  {
27467
- ref: import_zod7.z.string(),
27468
- layer: import_zod7.z.number().int().positive().optional()
27642
+ ref: import_zod8.z.string(),
27643
+ layer: import_zod8.z.number().int().positive().optional()
27469
27644
  },
27470
27645
  async ({ ref, layer }) => {
27471
27646
  const result = await orch.openPrStackLayers(ref, { layer });
27472
- return {
27473
- content: [
27474
- {
27475
- type: "text",
27476
- text: JSON.stringify(
27477
- {
27478
- stackNumber: result.stack.stackNumber,
27479
- trunk: result.stack.trunk,
27480
- threads: result.threads.map((t) => ({
27481
- id: t.id,
27482
- title: t.title,
27483
- branchName: t.branchName,
27484
- stackLayer: t.stackLayer,
27485
- worktreePath: t.worktreePath,
27486
- prUrl: t.prUrl,
27487
- link: `sideboard://thread/${t.id}`
27488
- }))
27489
- },
27490
- null,
27491
- 2
27492
- )
27493
- }
27494
- ]
27495
- };
27647
+ return mcpJson({
27648
+ stackNumber: result.stack.stackNumber,
27649
+ trunk: result.stack.trunk,
27650
+ threads: result.threads.map((t) => ({
27651
+ id: t.id,
27652
+ title: t.title,
27653
+ branchName: t.branchName,
27654
+ stackLayer: t.stackLayer,
27655
+ worktreePath: t.worktreePath,
27656
+ prUrl: t.prUrl,
27657
+ link: `sideboard://thread/${t.id}`
27658
+ }))
27659
+ });
27496
27660
  }
27497
27661
  );
27498
27662
  server.tool(
27499
27663
  "add_stack_layer",
27500
27664
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
27501
27665
  {
27502
- ref: import_zod7.z.string(),
27503
- branchName: import_zod7.z.string(),
27504
- title: import_zod7.z.string().optional()
27666
+ ref: import_zod8.z.string(),
27667
+ branchName: import_zod8.z.string(),
27668
+ title: import_zod8.z.string().optional()
27505
27669
  },
27506
27670
  async ({ ref, branchName, title }) => {
27507
27671
  const result = await orch.addStackLayer(ref, branchName, { title });
27508
- return {
27509
- content: [
27510
- {
27511
- type: "text",
27512
- text: JSON.stringify(
27513
- {
27514
- id: result.thread.id,
27515
- title: result.thread.title,
27516
- branchName: result.thread.branchName,
27517
- stackLayer: result.thread.stackLayer,
27518
- worktreePath: result.thread.worktreePath,
27519
- link: `sideboard://thread/${result.thread.id}`
27520
- },
27521
- null,
27522
- 2
27523
- )
27524
- }
27525
- ]
27526
- };
27672
+ return mcpJson({
27673
+ id: result.thread.id,
27674
+ title: result.thread.title,
27675
+ branchName: result.thread.branchName,
27676
+ stackLayer: result.thread.stackLayer,
27677
+ worktreePath: result.thread.worktreePath,
27678
+ link: `sideboard://thread/${result.thread.id}`
27679
+ });
27527
27680
  }
27528
27681
  );
27529
27682
  server.tool(
27530
27683
  "create_pr_stack",
27531
27684
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
27532
27685
  {
27533
- repoPath: import_zod7.z.string(),
27534
- branches: import_zod7.z.array(import_zod7.z.string()).min(1),
27535
- agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
27536
- base: import_zod7.z.string().optional(),
27537
- title: import_zod7.z.string().optional()
27686
+ repoPath: import_zod8.z.string(),
27687
+ branches: import_zod8.z.array(import_zod8.z.string()).min(1),
27688
+ agent: import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
27689
+ base: import_zod8.z.string().optional(),
27690
+ title: import_zod8.z.string().optional()
27538
27691
  },
27539
27692
  async (args) => {
27540
27693
  const result = await orch.createPrStack({
@@ -27544,39 +27697,28 @@ async function startMcpServer() {
27544
27697
  base: args.base,
27545
27698
  title: args.title
27546
27699
  });
27547
- return {
27548
- content: [
27549
- {
27550
- type: "text",
27551
- text: JSON.stringify(
27552
- {
27553
- stackNumber: result.stack.stackNumber,
27554
- trunk: result.stack.trunk,
27555
- threads: result.threads.map((t) => ({
27556
- id: t.id,
27557
- title: t.title,
27558
- branchName: t.branchName,
27559
- stackLayer: t.stackLayer,
27560
- worktreePath: t.worktreePath,
27561
- link: `sideboard://thread/${t.id}`
27562
- }))
27563
- },
27564
- null,
27565
- 2
27566
- )
27567
- }
27568
- ]
27569
- };
27700
+ return mcpJson({
27701
+ stackNumber: result.stack.stackNumber,
27702
+ trunk: result.stack.trunk,
27703
+ threads: result.threads.map((t) => ({
27704
+ id: t.id,
27705
+ title: t.title,
27706
+ branchName: t.branchName,
27707
+ stackLayer: t.stackLayer,
27708
+ worktreePath: t.worktreePath,
27709
+ link: `sideboard://thread/${t.id}`
27710
+ }))
27711
+ });
27570
27712
  }
27571
27713
  );
27572
27714
  server.tool(
27573
27715
  "list_issues",
27574
27716
  "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.",
27575
27717
  {
27576
- repoPath: import_zod7.z.string(),
27577
- query: import_zod7.z.string().optional().describe("Search title, identifier, or description"),
27578
- assignee: import_zod7.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
27579
- limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27718
+ repoPath: import_zod8.z.string(),
27719
+ query: import_zod8.z.string().optional().describe("Search title, identifier, or description"),
27720
+ assignee: import_zod8.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
27721
+ limit: import_zod8.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27580
27722
  },
27581
27723
  async ({ repoPath, query, assignee, limit }) => {
27582
27724
  const root = await resolveRepoRoot(repoPath);