@sideboard-ai/core 0.1.160 → 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.
@@ -16382,13 +16382,13 @@ function formatStat(files) {
16382
16382
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
16383
16383
  }
16384
16384
  function emptyScopeStats() {
16385
- const z8 = emptyStat();
16385
+ const z9 = emptyStat();
16386
16386
  return {
16387
- commits: z8,
16388
- uncommitted: z8,
16389
- staged: z8,
16390
- unstaged: z8,
16391
- last_turn: z8
16387
+ commits: z9,
16388
+ uncommitted: z9,
16389
+ staged: z9,
16390
+ unstaged: z9,
16391
+ last_turn: z9
16392
16392
  };
16393
16393
  }
16394
16394
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -17525,17 +17525,17 @@ function formatIssueToolsDirective(opts) {
17525
17525
  ];
17526
17526
  if (opts.linear) {
17527
17527
  lines.push(
17528
- "- 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."
17528
+ "- 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."
17529
17529
  );
17530
17530
  }
17531
17531
  if (github) {
17532
17532
  lines.push(
17533
- "- GitHub: `github_get_issue` (comments), `github_comment`, `github_update_issue` (state open|closed), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
17533
+ "- 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`."
17534
17534
  );
17535
17535
  }
17536
17536
  if (opts.abletime) {
17537
17537
  lines.push(
17538
- "- AbleTime: `abletime_get_task` (comments), `abletime_comment`, `abletime_update_task` (state), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
17538
+ "- 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)."
17539
17539
  );
17540
17540
  }
17541
17541
  lines.push(
@@ -20763,7 +20763,7 @@ init_nested_electron_env();
20763
20763
  // src/mcp/server.ts
20764
20764
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
20765
20765
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
20766
- var import_zod7 = require("zod");
20766
+ var import_zod8 = require("zod");
20767
20767
  var import_node_crypto12 = require("crypto");
20768
20768
  var import_node_path50 = require("path");
20769
20769
  init_orchestrator();
@@ -21876,12 +21876,53 @@ ${link}` : link;
21876
21876
  // src/mcp/slack-tools.ts
21877
21877
  init_outbound_watch();
21878
21878
  init_workspaces();
21879
- function text(payload, isError = false) {
21879
+
21880
+ // src/mcp/issue-list.ts
21881
+ var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
21882
+ var MCP_ISSUE_LIST_MAX_LIMIT = 250;
21883
+ function clampMcpIssueLimit(limit) {
21884
+ if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
21885
+ return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
21886
+ }
21887
+ function applyIssueListWindow(items, limit) {
21888
+ const truncated = items.length > limit;
21880
21889
  return {
21881
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
21890
+ items: truncated ? items.slice(0, limit) : items,
21891
+ truncated
21892
+ };
21893
+ }
21894
+ function compactIssueRow(issue) {
21895
+ return {
21896
+ identifier: issue.identifier,
21897
+ title: issue.title,
21898
+ ...issue.assignee ? { assignee: issue.assignee } : {},
21899
+ ...issue.labels.length > 0 ? { labels: issue.labels } : {},
21900
+ ...issue.teamKey ? { team: issue.teamKey } : {},
21901
+ ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
21902
+ };
21903
+ }
21904
+ function formatMcpIssueList(input) {
21905
+ return {
21906
+ ...input.source ? { source: input.source } : {},
21907
+ ...input.viewer ? { viewer: input.viewer } : {},
21908
+ limit: input.limit,
21909
+ returned: input.issues.length,
21910
+ truncated: input.truncated,
21911
+ ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
21912
+ issues: input.issues.map(compactIssueRow)
21913
+ };
21914
+ }
21915
+ function mcpJson(payload, isError = false) {
21916
+ return {
21917
+ content: [{ type: "text", text: JSON.stringify(payload) }],
21882
21918
  ...isError ? { isError: true } : {}
21883
21919
  };
21884
21920
  }
21921
+
21922
+ // src/mcp/slack-tools.ts
21923
+ function text(payload, isError = false) {
21924
+ return mcpJson(payload, isError);
21925
+ }
21885
21926
  function fail(err) {
21886
21927
  return text(
21887
21928
  { error: err instanceof Error ? err.message : String(err) },
@@ -22150,49 +22191,331 @@ function registerSlackTools(server) {
22150
22191
  init_app_settings();
22151
22192
 
22152
22193
  // src/mcp/abletime-tools.ts
22194
+ var import_zod3 = require("zod");
22195
+ init_abletime();
22196
+
22197
+ // src/mcp/issue-payload.ts
22153
22198
  var import_zod2 = require("zod");
22154
22199
  init_abletime();
22155
22200
 
22156
- // src/mcp/issue-list.ts
22157
- var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
22158
- var MCP_ISSUE_LIST_MAX_LIMIT = 250;
22159
- function clampMcpIssueLimit(limit) {
22160
- if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
22161
- return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
22201
+ // src/mcp/smart-crush.ts
22202
+ var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
22203
+ var CRUSH_MIN_ITEMS = 5;
22204
+ var CRUSH_MIN_CHARS = 800;
22205
+ var CRUSH_COMMENT_MAX_CHARS = 16e3;
22206
+ var CRUSH_BODY_MIN_CHARS = 12e3;
22207
+ var CRUSH_BODY_MAX_CHARS = 8e3;
22208
+ var NEAR_DUP_JACCARD = 0.92;
22209
+ function looksLikeMustKeep(text7) {
22210
+ return MUST_KEEP_TEXT_RE.test(text7);
22211
+ }
22212
+ function normalizeText(text7) {
22213
+ return text7.replace(/\s+/g, " ").trim().toLowerCase();
22214
+ }
22215
+ function wordSet(text7) {
22216
+ return new Set(
22217
+ text7.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
22218
+ );
22162
22219
  }
22163
- function applyIssueListWindow(items, limit) {
22164
- const truncated = items.length > limit;
22220
+ function jaccard(a, b) {
22221
+ if (a.size === 0 && b.size === 0) return 1;
22222
+ let inter = 0;
22223
+ for (const w of a) {
22224
+ if (b.has(w)) inter += 1;
22225
+ }
22226
+ const union = a.size + b.size - inter;
22227
+ return union === 0 ? 0 : inter / union;
22228
+ }
22229
+ function importance(text7) {
22230
+ let score = text7.length;
22231
+ if (/```/.test(text7)) score += 400;
22232
+ if (/https?:\/\//.test(text7)) score += 50;
22233
+ if (looksLikeMustKeep(text7)) score += 1e4;
22234
+ return score;
22235
+ }
22236
+ function meanStd(values) {
22237
+ if (values.length === 0) return { mean: 0, std: 0 };
22238
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
22239
+ const variance = values.reduce((a, b) => a + (b - mean) * (b - mean), 0) / values.length;
22240
+ return { mean, std: Math.sqrt(variance) };
22241
+ }
22242
+ function dedupTextItems(items, getText) {
22243
+ const kept = [];
22244
+ const keptNorm = [];
22245
+ const keptWords = [];
22246
+ for (const item of items) {
22247
+ const text7 = getText(item);
22248
+ const norm = normalizeText(text7);
22249
+ if (!norm) {
22250
+ kept.push(item);
22251
+ keptNorm.push(norm);
22252
+ keptWords.push(wordSet(text7));
22253
+ continue;
22254
+ }
22255
+ const words = wordSet(text7);
22256
+ let dupAt = -1;
22257
+ for (let i = 0; i < kept.length; i++) {
22258
+ if (keptNorm[i] === norm) {
22259
+ dupAt = i;
22260
+ break;
22261
+ }
22262
+ if (keptWords[i].size >= 8 && words.size >= 8 && jaccard(keptWords[i], words) >= NEAR_DUP_JACCARD) {
22263
+ dupAt = i;
22264
+ break;
22265
+ }
22266
+ }
22267
+ if (dupAt < 0) {
22268
+ kept.push(item);
22269
+ keptNorm.push(norm);
22270
+ keptWords.push(words);
22271
+ continue;
22272
+ }
22273
+ if (text7.length > getText(kept[dupAt]).length) {
22274
+ kept[dupAt] = item;
22275
+ keptNorm[dupAt] = norm;
22276
+ keptWords[dupAt] = words;
22277
+ }
22278
+ }
22279
+ return { items: kept, dropped: items.length - kept.length };
22280
+ }
22281
+ function crushTextItems(items, getText, opts = {}) {
22282
+ const minItems = opts.minItems ?? CRUSH_MIN_ITEMS;
22283
+ const minChars = opts.minChars ?? CRUSH_MIN_CHARS;
22284
+ const maxChars = opts.maxChars ?? CRUSH_COMMENT_MAX_CHARS;
22285
+ const original = items;
22286
+ const totalChars = items.reduce((n, item) => n + getText(item).length, 0);
22287
+ if (items.length < minItems || totalChars < minChars) {
22288
+ return {
22289
+ items,
22290
+ truncated: false,
22291
+ kept: items.length,
22292
+ dropped: 0,
22293
+ reason: "passthrough"
22294
+ };
22295
+ }
22296
+ const deduped = dedupTextItems(items, getText);
22297
+ const unique = deduped.items;
22298
+ const uniqueChars = unique.reduce((n, item) => n + getText(item).length, 0);
22299
+ if (uniqueChars <= maxChars) {
22300
+ const changed = unique.length < original.length;
22301
+ return {
22302
+ items: unique,
22303
+ truncated: changed,
22304
+ kept: unique.length,
22305
+ dropped: original.length - unique.length,
22306
+ reason: changed ? "dedup" : "passthrough"
22307
+ };
22308
+ }
22309
+ const lengths = unique.map((item) => getText(item).length);
22310
+ const { mean, std } = meanStd(lengths);
22311
+ const outlierFloor = mean + 2 * std;
22312
+ const mustKeep = /* @__PURE__ */ new Set();
22313
+ unique.forEach((item, i) => {
22314
+ const text7 = getText(item);
22315
+ if (looksLikeMustKeep(text7) || text7.length > outlierFloor) mustKeep.add(i);
22316
+ });
22317
+ const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
22318
+ let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
22319
+ const pick = new Set(mustKeep);
22320
+ const canTake = (i) => {
22321
+ if (pick.has(i)) return false;
22322
+ const next = used + lengths[i];
22323
+ if (next > maxChars && pick.size > mustKeep.size) return false;
22324
+ used = next;
22325
+ pick.add(i);
22326
+ return true;
22327
+ };
22328
+ const k = remainingIdx.length;
22329
+ const headN = Math.max(1, Math.ceil(k * 0.3));
22330
+ const tailN = Math.max(1, Math.ceil(k * 0.15));
22331
+ for (const i of remainingIdx.slice(0, headN)) canTake(i);
22332
+ for (const i of remainingIdx.slice(-tailN).reverse()) canTake(i);
22333
+ const middle = remainingIdx.filter((i) => !pick.has(i)).sort((a, b) => importance(getText(unique[b])) - importance(getText(unique[a])));
22334
+ for (const i of middle) {
22335
+ if (used >= maxChars) break;
22336
+ canTake(i);
22337
+ }
22338
+ const crushed = unique.filter((_, i) => pick.has(i));
22339
+ const crushedChars = crushed.reduce((n, item) => n + getText(item).length, 0);
22340
+ if (crushed.length >= original.length && crushedChars >= totalChars) {
22341
+ return {
22342
+ items: original,
22343
+ truncated: false,
22344
+ kept: original.length,
22345
+ dropped: 0,
22346
+ reason: "passthrough"
22347
+ };
22348
+ }
22165
22349
  return {
22166
- items: truncated ? items.slice(0, limit) : items,
22167
- truncated
22350
+ items: crushed,
22351
+ truncated: crushed.length < original.length,
22352
+ kept: crushed.length,
22353
+ dropped: original.length - crushed.length,
22354
+ reason: "budget"
22168
22355
  };
22169
22356
  }
22170
- function compactIssueRow(issue) {
22171
- return {
22172
- identifier: issue.identifier,
22173
- title: issue.title,
22174
- ...issue.assignee ? { assignee: issue.assignee } : {},
22175
- ...issue.labels.length > 0 ? { labels: issue.labels } : {},
22176
- ...issue.teamKey ? { team: issue.teamKey } : {},
22177
- ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
22357
+ function splitBlocks(text7) {
22358
+ if (/\n\n/.test(text7)) return text7.split(/\n\n+/);
22359
+ return text7.split("\n");
22360
+ }
22361
+ function capOversizedBlock(block, maxChars) {
22362
+ if (block.length <= maxChars) return block;
22363
+ const head = Math.floor(maxChars * 0.3);
22364
+ const tail = Math.floor(maxChars * 0.15);
22365
+ return `${block.slice(0, head)}
22366
+
22367
+ [\u2026crushed ${block.length - head - tail} chars; pass include=full to read the rest\u2026]
22368
+
22369
+ ${block.slice(-tail)}`;
22370
+ }
22371
+ function crushMarkdown(text7, opts) {
22372
+ const original = text7 ?? "";
22373
+ const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
22374
+ const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
22375
+ if (original.length < minChars) {
22376
+ return { text: original, truncated: false, originalChars: original.length };
22377
+ }
22378
+ const rawBlocks = splitBlocks(original).filter((b) => b.length > 0);
22379
+ const blocks = rawBlocks.map((b) => capOversizedBlock(b, maxChars));
22380
+ const cappedABlock = blocks.some((b, i) => b !== rawBlocks[i]);
22381
+ if (blocks.length <= 1) {
22382
+ const cut = blocks[0] ?? original;
22383
+ if (cut.length >= original.length) {
22384
+ return { text: original, truncated: false, originalChars: original.length };
22385
+ }
22386
+ return { text: cut, truncated: true, originalChars: original.length };
22387
+ }
22388
+ const must = blocks.filter((b) => looksLikeMustKeep(b));
22389
+ const rest = blocks.filter((b) => !looksLikeMustKeep(b));
22390
+ const marker = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
22391
+ let used = must.reduce((n, b) => n + b.length, 0) + marker.length;
22392
+ const keptRest = [];
22393
+ const headN = Math.max(1, Math.ceil(rest.length * 0.3));
22394
+ const tailN = Math.max(1, Math.ceil(rest.length * 0.15));
22395
+ const head = rest.slice(0, headN);
22396
+ const tail = rest.slice(-tailN);
22397
+ const middle = rest.slice(headN, Math.max(headN, rest.length - tailN));
22398
+ const take = (block) => {
22399
+ if (used + block.length + 2 > maxChars && keptRest.length + must.length > 0) {
22400
+ return false;
22401
+ }
22402
+ used += block.length + 2;
22403
+ keptRest.push(block);
22404
+ return true;
22178
22405
  };
22406
+ for (const b of head) take(b);
22407
+ for (const b of tail) take(b);
22408
+ const ranked = [...middle].sort((a, b) => importance(b) - importance(a));
22409
+ for (const b of ranked) {
22410
+ if (used >= maxChars) break;
22411
+ take(b);
22412
+ }
22413
+ const keepSet = /* @__PURE__ */ new Set([...must, ...keptRest]);
22414
+ const ordered = blocks.filter((b) => keepSet.has(b));
22415
+ const omitted = blocks.length - ordered.length;
22416
+ if (omitted <= 0) {
22417
+ const joined = blocks.join("\n\n");
22418
+ if (cappedABlock && joined.length < original.length) {
22419
+ return { text: joined, truncated: true, originalChars: original.length };
22420
+ }
22421
+ return { text: original, truncated: false, originalChars: original.length };
22422
+ }
22423
+ const out = [];
22424
+ let inserted = false;
22425
+ for (const block of blocks) {
22426
+ if (keepSet.has(block)) {
22427
+ out.push(block);
22428
+ continue;
22429
+ }
22430
+ if (!inserted) {
22431
+ out.push(marker);
22432
+ inserted = true;
22433
+ }
22434
+ }
22435
+ const textOut = out.join("\n\n");
22436
+ if (textOut.length >= original.length) {
22437
+ return { text: original, truncated: false, originalChars: original.length };
22438
+ }
22439
+ return { text: textOut, truncated: true, originalChars: original.length };
22179
22440
  }
22180
- function formatMcpIssueList(input) {
22181
- return {
22182
- ...input.source ? { source: input.source } : {},
22183
- ...input.viewer ? { viewer: input.viewer } : {},
22184
- limit: input.limit,
22185
- returned: input.issues.length,
22186
- truncated: input.truncated,
22187
- ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
22188
- issues: input.issues.map(compactIssueRow)
22441
+
22442
+ // src/mcp/issue-payload.ts
22443
+ var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
22444
+ var mcpIssueIncludeSchema = import_zod2.z.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
22445
+ "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."
22446
+ );
22447
+ var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
22448
+ function emptyMeta() {
22449
+ return { truncated: true, hint: MCP_ISSUE_CRUSH_HINT };
22450
+ }
22451
+ function withComments(comments, meta) {
22452
+ const crushed = crushTextItems(comments, (c) => c.body);
22453
+ if (crushed.truncated) {
22454
+ meta.comments = {
22455
+ kept: crushed.kept,
22456
+ dropped: crushed.dropped,
22457
+ reason: crushed.reason
22458
+ };
22459
+ }
22460
+ return { comments: crushed.items, meta };
22461
+ }
22462
+ function withBody(text7, meta) {
22463
+ const crushed = crushMarkdown(text7);
22464
+ if (crushed.truncated) {
22465
+ meta.body = { originalChars: crushed.originalChars };
22466
+ return { value: crushed.text, meta };
22467
+ }
22468
+ return { value: text7, meta };
22469
+ }
22470
+ function attachCrush(payload, meta) {
22471
+ if (!meta.comments && !meta.body) return payload;
22472
+ return { ...payload, crush: meta };
22473
+ }
22474
+ function formatLinearIssuePayload(issue, include) {
22475
+ if (include === "full") return issue;
22476
+ const meta = emptyMeta();
22477
+ const comments = withComments(issue.comments, meta);
22478
+ const description = withBody(issue.description, comments.meta);
22479
+ const { team, ...rest } = issue;
22480
+ const next = {
22481
+ ...rest,
22482
+ ...team ? { team: { id: team.id, key: team.key, name: team.name } } : {},
22483
+ description: description.value,
22484
+ comments: comments.comments
22189
22485
  };
22486
+ return attachCrush(next, description.meta);
22190
22487
  }
22191
- function mcpJson(payload, isError = false) {
22192
- return {
22193
- content: [{ type: "text", text: JSON.stringify(payload) }],
22194
- ...isError ? { isError: true } : {}
22488
+ function formatGitHubIssuePayload(issue, include) {
22489
+ if (include === "full") return issue;
22490
+ const meta = emptyMeta();
22491
+ const comments = withComments(issue.comments, meta);
22492
+ const body = withBody(issue.body, comments.meta);
22493
+ const next = {
22494
+ ...issue,
22495
+ body: body.value,
22496
+ comments: comments.comments
22497
+ };
22498
+ return attachCrush(next, body.meta);
22499
+ }
22500
+ function formatAbleTimeTaskPayload(task, include) {
22501
+ const base = {
22502
+ ...toAbleTimeIssueInfo(task),
22503
+ description: task.description,
22504
+ state: task.state,
22505
+ comments: task.comments
22195
22506
  };
22507
+ if (include === "full") return base;
22508
+ const meta = emptyMeta();
22509
+ const comments = withComments(task.comments, meta);
22510
+ const description = withBody(task.description, comments.meta);
22511
+ return attachCrush(
22512
+ {
22513
+ ...base,
22514
+ description: description.value,
22515
+ comments: comments.comments
22516
+ },
22517
+ description.meta
22518
+ );
22196
22519
  }
22197
22520
 
22198
22521
  // src/mcp/abletime-tools.ts
@@ -22238,7 +22561,7 @@ function registerAbleTimeTools(server) {
22238
22561
  server.tool(
22239
22562
  "abletime_list_tasks",
22240
22563
  "List open AbleTime tasks (excludes done/archived). Optional projectId filter.",
22241
- { projectId: import_zod2.z.string().optional() },
22564
+ { projectId: import_zod3.z.string().optional() },
22242
22565
  async ({ projectId }) => {
22243
22566
  try {
22244
22567
  const tasks = await listAbleTimeTasks({ projectId });
@@ -22251,7 +22574,7 @@ function registerAbleTimeTools(server) {
22251
22574
  server.tool(
22252
22575
  "abletime_search_tasks",
22253
22576
  "Search AbleTime tasks by text, reference, or tag.",
22254
- { query: import_zod2.z.string() },
22577
+ { query: import_zod3.z.string() },
22255
22578
  async ({ query }) => {
22256
22579
  try {
22257
22580
  const tasks = await searchAbleTimeTasks(query);
@@ -22263,17 +22586,11 @@ function registerAbleTimeTools(server) {
22263
22586
  );
22264
22587
  server.tool(
22265
22588
  "abletime_get_task",
22266
- "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments.",
22267
- { id: import_zod2.z.string() },
22268
- async ({ id }) => {
22589
+ "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.",
22590
+ { id: import_zod3.z.string(), include: mcpIssueIncludeSchema },
22591
+ async ({ id, include }) => {
22269
22592
  try {
22270
- const task = await getAbleTimeTask(id);
22271
- return text2({
22272
- ...toAbleTimeIssueInfo(task),
22273
- description: task.description,
22274
- state: task.state,
22275
- comments: task.comments
22276
- });
22593
+ return text2(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
22277
22594
  } catch (err) {
22278
22595
  return fail2(err);
22279
22596
  }
@@ -22282,7 +22599,7 @@ function registerAbleTimeTools(server) {
22282
22599
  server.tool(
22283
22600
  "abletime_comment",
22284
22601
  "Add a markdown comment on an AbleTime task (id or CRM-232).",
22285
- { id: import_zod2.z.string(), body: import_zod2.z.string() },
22602
+ { id: import_zod3.z.string(), body: import_zod3.z.string() },
22286
22603
  async (args) => {
22287
22604
  try {
22288
22605
  return text2(await commentAbleTimeTask(args));
@@ -22295,10 +22612,10 @@ function registerAbleTimeTools(server) {
22295
22612
  "abletime_update_task",
22296
22613
  "Update an AbleTime task (id or CRM-232). Pass title, description, and/or state.",
22297
22614
  {
22298
- id: import_zod2.z.string(),
22299
- title: import_zod2.z.string().optional(),
22300
- description: import_zod2.z.string().optional(),
22301
- state: import_zod2.z.string().optional()
22615
+ id: import_zod3.z.string(),
22616
+ title: import_zod3.z.string().optional(),
22617
+ description: import_zod3.z.string().optional(),
22618
+ state: import_zod3.z.string().optional()
22302
22619
  },
22303
22620
  async (args) => {
22304
22621
  try {
@@ -22317,12 +22634,12 @@ function registerAbleTimeTools(server) {
22317
22634
  "abletime_create_task",
22318
22635
  "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).",
22319
22636
  {
22320
- title: import_zod2.z.string(),
22321
- description: import_zod2.z.string().optional(),
22322
- projectId: import_zod2.z.string().optional(),
22323
- categoryId: import_zod2.z.string().optional(),
22324
- state: import_zod2.z.enum(["backlog", "todo"]).optional(),
22325
- parent: import_zod2.z.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
22637
+ title: import_zod3.z.string(),
22638
+ description: import_zod3.z.string().optional(),
22639
+ projectId: import_zod3.z.string().optional(),
22640
+ categoryId: import_zod3.z.string().optional(),
22641
+ state: import_zod3.z.enum(["backlog", "todo"]).optional(),
22642
+ parent: import_zod3.z.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
22326
22643
  },
22327
22644
  async (args) => {
22328
22645
  try {
@@ -22336,10 +22653,10 @@ function registerAbleTimeTools(server) {
22336
22653
  "abletime_ensure_task",
22337
22654
  "Find or create an AbleTime task for this work, then create_thread with sourceType=ticket.",
22338
22655
  {
22339
- title: import_zod2.z.string(),
22340
- description: import_zod2.z.string().optional(),
22341
- projectId: import_zod2.z.string().optional(),
22342
- categoryId: import_zod2.z.string().optional()
22656
+ title: import_zod3.z.string(),
22657
+ description: import_zod3.z.string().optional(),
22658
+ projectId: import_zod3.z.string().optional(),
22659
+ categoryId: import_zod3.z.string().optional()
22343
22660
  },
22344
22661
  async (args) => {
22345
22662
  try {
@@ -22353,7 +22670,7 @@ function registerAbleTimeTools(server) {
22353
22670
  }
22354
22671
 
22355
22672
  // src/mcp/github-tools.ts
22356
- var import_zod3 = require("zod");
22673
+ var import_zod4 = require("zod");
22357
22674
 
22358
22675
  // src/integrations/github-issues.ts
22359
22676
  init_worktree();
@@ -22539,15 +22856,15 @@ function fail3(err) {
22539
22856
  true
22540
22857
  );
22541
22858
  }
22542
- var repoPathSchema = import_zod3.z.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
22859
+ var repoPathSchema = import_zod4.z.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
22543
22860
  function registerGithubIssueTools(server) {
22544
22861
  server.tool(
22545
22862
  "github_get_issue",
22546
- "Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Uses Account gh.",
22547
- { id: import_zod3.z.string(), repoPath: repoPathSchema },
22548
- async ({ id, repoPath }) => {
22863
+ "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.",
22864
+ { id: import_zod4.z.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
22865
+ async ({ id, include, repoPath }) => {
22549
22866
  try {
22550
- return text3(await getGitHubIssue(id, { repoPath }));
22867
+ return text3(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
22551
22868
  } catch (err) {
22552
22869
  return fail3(err);
22553
22870
  }
@@ -22556,7 +22873,7 @@ function registerGithubIssueTools(server) {
22556
22873
  server.tool(
22557
22874
  "github_comment",
22558
22875
  "Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh.",
22559
- { id: import_zod3.z.string(), body: import_zod3.z.string(), repoPath: repoPathSchema },
22876
+ { id: import_zod4.z.string(), body: import_zod4.z.string(), repoPath: repoPathSchema },
22560
22877
  async ({ id, body, repoPath }) => {
22561
22878
  try {
22562
22879
  return text3(await commentGitHubIssue({ id, body }, { repoPath }));
@@ -22569,10 +22886,10 @@ function registerGithubIssueTools(server) {
22569
22886
  "github_update_issue",
22570
22887
  "Update a GitHub issue (#123). Pass title, body, and/or state (open|closed).",
22571
22888
  {
22572
- id: import_zod3.z.string(),
22573
- title: import_zod3.z.string().optional(),
22574
- body: import_zod3.z.string().optional(),
22575
- state: import_zod3.z.string().optional().describe("open or closed"),
22889
+ id: import_zod4.z.string(),
22890
+ title: import_zod4.z.string().optional(),
22891
+ body: import_zod4.z.string().optional(),
22892
+ state: import_zod4.z.string().optional().describe("open or closed"),
22576
22893
  repoPath: repoPathSchema
22577
22894
  },
22578
22895
  async (args) => {
@@ -22587,9 +22904,9 @@ function registerGithubIssueTools(server) {
22587
22904
  "github_create_issue",
22588
22905
  "Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
22589
22906
  {
22590
- title: import_zod3.z.string(),
22591
- body: import_zod3.z.string().optional(),
22592
- parent: import_zod3.z.string().optional().describe("Parent issue #123 or URL for a spin-off."),
22907
+ title: import_zod4.z.string(),
22908
+ body: import_zod4.z.string().optional(),
22909
+ parent: import_zod4.z.string().optional().describe("Parent issue #123 or URL for a spin-off."),
22593
22910
  repoPath: repoPathSchema
22594
22911
  },
22595
22912
  async (args) => {
@@ -22603,7 +22920,7 @@ function registerGithubIssueTools(server) {
22603
22920
  }
22604
22921
 
22605
22922
  // src/mcp/linear-tools.ts
22606
- var import_zod4 = require("zod");
22923
+ var import_zod5 = require("zod");
22607
22924
  function text4(payload, isError = false) {
22608
22925
  return mcpJson(payload, isError);
22609
22926
  }
@@ -22613,7 +22930,7 @@ function fail4(err) {
22613
22930
  true
22614
22931
  );
22615
22932
  }
22616
- var prioritySchema = import_zod4.z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
22933
+ var prioritySchema = import_zod5.z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
22617
22934
  function registerLinearTools(server) {
22618
22935
  server.tool(
22619
22936
  "linear_list_teams",
@@ -22631,9 +22948,9 @@ function registerLinearTools(server) {
22631
22948
  "linear_search_issues",
22632
22949
  "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.",
22633
22950
  {
22634
- query: import_zod4.z.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
22635
- 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."),
22636
- limit: import_zod4.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
22951
+ query: import_zod5.z.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
22952
+ 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."),
22953
+ limit: import_zod5.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
22637
22954
  },
22638
22955
  async ({ query, assignee, limit }) => {
22639
22956
  try {
@@ -22660,11 +22977,11 @@ function registerLinearTools(server) {
22660
22977
  );
22661
22978
  server.tool(
22662
22979
  "linear_get_issue",
22663
- "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children.",
22664
- { id: import_zod4.z.string() },
22665
- async ({ id }) => {
22980
+ "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.",
22981
+ { id: import_zod5.z.string(), include: mcpIssueIncludeSchema },
22982
+ async ({ id, include }) => {
22666
22983
  try {
22667
- return text4(await getLinearIssue(id));
22984
+ return text4(formatLinearIssuePayload(await getLinearIssue(id), include));
22668
22985
  } catch (err) {
22669
22986
  return fail4(err);
22670
22987
  }
@@ -22674,13 +22991,13 @@ function registerLinearTools(server) {
22674
22991
  "linear_create_issue",
22675
22992
  '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.',
22676
22993
  {
22677
- team: import_zod4.z.string(),
22678
- title: import_zod4.z.string(),
22679
- description: import_zod4.z.string().optional(),
22680
- state: import_zod4.z.string().optional(),
22681
- assignee: import_zod4.z.string().nullable().optional(),
22994
+ team: import_zod5.z.string(),
22995
+ title: import_zod5.z.string(),
22996
+ description: import_zod5.z.string().optional(),
22997
+ state: import_zod5.z.string().optional(),
22998
+ assignee: import_zod5.z.string().nullable().optional(),
22682
22999
  priority: prioritySchema,
22683
- parent: import_zod4.z.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
23000
+ parent: import_zod5.z.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
22684
23001
  },
22685
23002
  async (args) => {
22686
23003
  try {
@@ -22694,11 +23011,11 @@ function registerLinearTools(server) {
22694
23011
  "linear_update_issue",
22695
23012
  "Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority.",
22696
23013
  {
22697
- id: import_zod4.z.string(),
22698
- title: import_zod4.z.string().optional(),
22699
- description: import_zod4.z.string().optional(),
22700
- state: import_zod4.z.string().optional(),
22701
- assignee: import_zod4.z.string().nullable().optional(),
23014
+ id: import_zod5.z.string(),
23015
+ title: import_zod5.z.string().optional(),
23016
+ description: import_zod5.z.string().optional(),
23017
+ state: import_zod5.z.string().optional(),
23018
+ assignee: import_zod5.z.string().nullable().optional(),
22702
23019
  priority: prioritySchema
22703
23020
  },
22704
23021
  async (args) => {
@@ -22713,8 +23030,8 @@ function registerLinearTools(server) {
22713
23030
  "linear_comment",
22714
23031
  "Add a markdown comment on a Linear issue (uuid or ENG-123).",
22715
23032
  {
22716
- id: import_zod4.z.string(),
22717
- body: import_zod4.z.string()
23033
+ id: import_zod5.z.string(),
23034
+ body: import_zod5.z.string()
22718
23035
  },
22719
23036
  async (args) => {
22720
23037
  try {
@@ -22734,7 +23051,7 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
22734
23051
  }
22735
23052
 
22736
23053
  // src/mcp/viewer-context-tools.ts
22737
- var import_zod5 = require("zod");
23054
+ var import_zod6 = require("zod");
22738
23055
  init_worktree();
22739
23056
  init_app_settings();
22740
23057
  init_workspaces2();
@@ -22769,7 +23086,7 @@ function registerViewerContextTools(server) {
22769
23086
  "get_viewer_context",
22770
23087
  "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).",
22771
23088
  {
22772
- repoPath: import_zod5.z.string().optional().describe(
23089
+ repoPath: import_zod6.z.string().optional().describe(
22773
23090
  "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
22774
23091
  )
22775
23092
  },
@@ -22789,10 +23106,10 @@ function registerViewerContextTools(server) {
22789
23106
  "update_viewer_context",
22790
23107
  "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.",
22791
23108
  {
22792
- scope: import_zod5.z.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
22793
- context: import_zod5.z.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
22794
- confirmed: import_zod5.z.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
22795
- repoPath: import_zod5.z.string().optional().describe(
23109
+ scope: import_zod6.z.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
23110
+ context: import_zod6.z.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
23111
+ confirmed: import_zod6.z.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
23112
+ repoPath: import_zod6.z.string().optional().describe(
22796
23113
  "Required for scope=project from orchestration (path from list_workspaces). Worktree turns may omit it (cwd)."
22797
23114
  )
22798
23115
  },
@@ -22873,14 +23190,11 @@ init_list_prs();
22873
23190
  init_app_settings();
22874
23191
 
22875
23192
  // src/mcp/schedule-tools.ts
22876
- var import_zod6 = require("zod");
23193
+ var import_zod7 = require("zod");
22877
23194
  init_schedules();
22878
23195
  init_schedule_runner();
22879
23196
  function text6(payload, isError = false) {
22880
- return {
22881
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
22882
- ...isError ? { isError: true } : {}
22883
- };
23197
+ return mcpJson(payload, isError);
22884
23198
  }
22885
23199
  function fail5(err) {
22886
23200
  return text6(
@@ -22918,17 +23232,17 @@ function registerScheduleTools(server) {
22918
23232
  "create_schedule",
22919
23233
  "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.",
22920
23234
  {
22921
- prompt: import_zod6.z.string().describe("User message / goal queued when the schedule fires"),
22922
- name: import_zod6.z.string().optional(),
22923
- at: import_zod6.z.string().optional().describe("ISO datetime for a one-shot"),
22924
- every: import_zod6.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
22925
- cron: import_zod6.z.string().optional().describe("5-field cron expression"),
22926
- tz: import_zod6.z.string().optional().describe("IANA timezone for cron (default: system)"),
22927
- threadId: import_zod6.z.string().optional().describe(
23235
+ prompt: import_zod7.z.string().describe("User message / goal queued when the schedule fires"),
23236
+ name: import_zod7.z.string().optional(),
23237
+ at: import_zod7.z.string().optional().describe("ISO datetime for a one-shot"),
23238
+ every: import_zod7.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
23239
+ cron: import_zod7.z.string().optional().describe("5-field cron expression"),
23240
+ tz: import_zod7.z.string().optional().describe("IANA timezone for cron (default: system)"),
23241
+ threadId: import_zod7.z.string().optional().describe(
22928
23242
  'Existing orchestration chat id, or "self" for this coordinator. Omit to create a new Global chat on fire.'
22929
23243
  ),
22930
- agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
22931
- model: import_zod6.z.string().optional()
23244
+ agent: import_zod7.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
23245
+ model: import_zod7.z.string().optional()
22932
23246
  },
22933
23247
  async (args) => {
22934
23248
  try {
@@ -22961,17 +23275,17 @@ function registerScheduleTools(server) {
22961
23275
  "update_schedule",
22962
23276
  "Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
22963
23277
  {
22964
- id: import_zod6.z.string(),
22965
- prompt: import_zod6.z.string().optional(),
22966
- name: import_zod6.z.string().optional(),
22967
- at: import_zod6.z.string().optional(),
22968
- every: import_zod6.z.string().optional(),
22969
- cron: import_zod6.z.string().optional(),
22970
- tz: import_zod6.z.string().optional(),
22971
- threadId: import_zod6.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
22972
- agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
22973
- model: import_zod6.z.string().optional(),
22974
- enabled: import_zod6.z.boolean().optional()
23278
+ id: import_zod7.z.string(),
23279
+ prompt: import_zod7.z.string().optional(),
23280
+ name: import_zod7.z.string().optional(),
23281
+ at: import_zod7.z.string().optional(),
23282
+ every: import_zod7.z.string().optional(),
23283
+ cron: import_zod7.z.string().optional(),
23284
+ tz: import_zod7.z.string().optional(),
23285
+ threadId: import_zod7.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
23286
+ agent: import_zod7.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
23287
+ model: import_zod7.z.string().optional(),
23288
+ enabled: import_zod7.z.boolean().optional()
22975
23289
  },
22976
23290
  async (args) => {
22977
23291
  try {
@@ -23009,7 +23323,7 @@ function registerScheduleTools(server) {
23009
23323
  server.tool(
23010
23324
  "delete_schedule",
23011
23325
  "Delete a local schedule. Pass id from list_schedules.",
23012
- { id: import_zod6.z.string() },
23326
+ { id: import_zod7.z.string() },
23013
23327
  async ({ id }) => {
23014
23328
  try {
23015
23329
  deleteSchedule(id);
@@ -23022,7 +23336,7 @@ function registerScheduleTools(server) {
23022
23336
  server.tool(
23023
23337
  "run_schedule",
23024
23338
  "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.",
23025
- { id: import_zod6.z.string() },
23339
+ { id: import_zod7.z.string() },
23026
23340
  async ({ id }) => {
23027
23341
  try {
23028
23342
  if (!getSchedule(id)) {
@@ -23454,13 +23768,13 @@ async function startMcpServer() {
23454
23768
  "list_board",
23455
23769
  "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.",
23456
23770
  {
23457
- query: import_zod7.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
23458
- repoPath: import_zod7.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
23459
- kind: import_zod7.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
23460
- column: import_zod7.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
23771
+ query: import_zod8.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
23772
+ repoPath: import_zod8.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
23773
+ kind: import_zod8.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
23774
+ column: import_zod8.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
23461
23775
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
23462
23776
  ),
23463
- limit: import_zod7.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
23777
+ limit: import_zod8.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
23464
23778
  },
23465
23779
  async ({ query, repoPath, kind, column, limit }) => {
23466
23780
  const workspaces = orch.listWorkspaces();
@@ -23475,30 +23789,19 @@ async function startMcpServer() {
23475
23789
  limit,
23476
23790
  workspaceName: (path) => names.get(path) ?? ""
23477
23791
  });
23478
- return {
23479
- content: [
23480
- {
23481
- type: "text",
23482
- text: JSON.stringify(
23483
- {
23484
- columns: snap.columns,
23485
- hidden: snap.hidden,
23486
- totals: snap.totals,
23487
- columnDefs: BOARD_COLUMN_DEFS,
23488
- hint: HOME_BOARD_AGENT_HINT
23489
- },
23490
- null,
23491
- 2
23492
- )
23493
- }
23494
- ]
23495
- };
23792
+ return mcpJson({
23793
+ columns: snap.columns,
23794
+ hidden: snap.hidden,
23795
+ totals: snap.totals,
23796
+ columnDefs: BOARD_COLUMN_DEFS,
23797
+ hint: HOME_BOARD_AGENT_HINT
23798
+ });
23496
23799
  }
23497
23800
  );
23498
23801
  server.tool(
23499
23802
  "get_thread",
23500
23803
  "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.",
23501
- { ref: import_zod7.z.string() },
23804
+ { ref: import_zod8.z.string() },
23502
23805
  async ({ ref }) => {
23503
23806
  const t = orch.getThread(ref);
23504
23807
  if (!t) {
@@ -23532,7 +23835,7 @@ async function startMcpServer() {
23532
23835
  usage: spend.usage,
23533
23836
  lastTurnUsage: spend.lastTurnUsage
23534
23837
  };
23535
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
23838
+ return mcpJson(summary);
23536
23839
  }
23537
23840
  );
23538
23841
  }
@@ -23540,17 +23843,17 @@ async function startMcpServer() {
23540
23843
  "present_artifact",
23541
23844
  "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.",
23542
23845
  {
23543
- title: import_zod7.z.string().describe("Short title shown in the artifact pane header"),
23544
- type: import_zod7.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
23846
+ title: import_zod8.z.string().describe("Short title shown in the artifact pane header"),
23847
+ type: import_zod8.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
23545
23848
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
23546
23849
  ),
23547
- content: import_zod7.z.string().describe(
23850
+ content: import_zod8.z.string().describe(
23548
23851
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
23549
23852
  ),
23550
- artifact_id: import_zod7.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
23551
- status: import_zod7.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
23552
- phase: import_zod7.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
23553
- mode: import_zod7.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
23853
+ artifact_id: import_zod8.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
23854
+ status: import_zod8.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
23855
+ phase: import_zod8.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
23856
+ mode: import_zod8.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
23554
23857
  },
23555
23858
  async ({ title, type, artifact_id, status, phase, mode }) => {
23556
23859
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -23564,22 +23867,22 @@ async function startMcpServer() {
23564
23867
  mode: type === "log" ? mode ?? "append" : void 0,
23565
23868
  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."
23566
23869
  };
23567
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
23870
+ return mcpJson(payload);
23568
23871
  }
23569
23872
  );
23570
23873
  server.tool(
23571
23874
  "ask_user",
23572
23875
  "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.",
23573
23876
  {
23574
- questions: import_zod7.z.array(
23575
- import_zod7.z.object({
23576
- question: import_zod7.z.string().describe("Full question text ending with ?"),
23577
- header: import_zod7.z.string().max(24).optional().describe("Short label shown above the question"),
23578
- multiSelect: import_zod7.z.boolean().optional().describe("Allow selecting multiple options"),
23579
- options: import_zod7.z.array(
23580
- import_zod7.z.object({
23581
- label: import_zod7.z.string(),
23582
- description: import_zod7.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
23877
+ questions: import_zod8.z.array(
23878
+ import_zod8.z.object({
23879
+ question: import_zod8.z.string().describe("Full question text ending with ?"),
23880
+ header: import_zod8.z.string().max(24).optional().describe("Short label shown above the question"),
23881
+ multiSelect: import_zod8.z.boolean().optional().describe("Allow selecting multiple options"),
23882
+ options: import_zod8.z.array(
23883
+ import_zod8.z.object({
23884
+ label: import_zod8.z.string(),
23885
+ description: import_zod8.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
23583
23886
  })
23584
23887
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
23585
23888
  })
@@ -23591,16 +23894,16 @@ async function startMcpServer() {
23591
23894
  questions,
23592
23895
  message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
23593
23896
  };
23594
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
23897
+ return mcpJson(payload);
23595
23898
  }
23596
23899
  );
23597
23900
  server.tool(
23598
23901
  "present_plan",
23599
23902
  "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.",
23600
23903
  {
23601
- title: import_zod7.z.string().optional().describe("Short plan title (defaults to Plan)"),
23602
- content: import_zod7.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
23603
- thread_id: import_zod7.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
23904
+ title: import_zod8.z.string().optional().describe("Short plan title (defaults to Plan)"),
23905
+ content: import_zod8.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
23906
+ thread_id: import_zod8.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
23604
23907
  },
23605
23908
  async ({ title, content, thread_id }) => {
23606
23909
  const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
@@ -23616,22 +23919,22 @@ async function startMcpServer() {
23616
23919
  title: title?.trim() || "Plan",
23617
23920
  message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
23618
23921
  };
23619
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
23922
+ return mcpJson(payload);
23620
23923
  }
23621
23924
  );
23622
23925
  server.tool(
23623
23926
  "present_schema",
23624
23927
  "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.",
23625
23928
  {
23626
- title: import_zod7.z.string().describe("Pane title"),
23627
- mode: import_zod7.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
23628
- datasource: import_zod7.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
23629
- resource_id: import_zod7.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
23630
- record_id: import_zod7.z.string().optional().describe("Record id when opening form mode"),
23631
- resource: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
23632
- record: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
23633
- records: import_zod7.z.array(import_zod7.z.record(import_zod7.z.unknown())).optional().describe("Inline records for table mode"),
23634
- pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
23929
+ title: import_zod8.z.string().describe("Pane title"),
23930
+ mode: import_zod8.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
23931
+ datasource: import_zod8.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
23932
+ resource_id: import_zod8.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
23933
+ record_id: import_zod8.z.string().optional().describe("Record id when opening form mode"),
23934
+ resource: import_zod8.z.record(import_zod8.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
23935
+ record: import_zod8.z.record(import_zod8.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
23936
+ records: import_zod8.z.array(import_zod8.z.record(import_zod8.z.unknown())).optional().describe("Inline records for table mode"),
23937
+ pane_id: import_zod8.z.string().optional().describe("Stable pane id across updates")
23635
23938
  },
23636
23939
  async (args) => {
23637
23940
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -23645,17 +23948,17 @@ async function startMcpServer() {
23645
23948
  record_id: args.record_id,
23646
23949
  message: "Schema pane accepted. Sideboard desktop opens the CMS column beside chat."
23647
23950
  };
23648
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
23951
+ return mcpJson(payload);
23649
23952
  }
23650
23953
  );
23651
23954
  server.tool(
23652
23955
  "present_files",
23653
23956
  "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.",
23654
23957
  {
23655
- title: import_zod7.z.string().optional().describe("Pane title (default: Files)"),
23656
- datasource: import_zod7.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
23657
- path: import_zod7.z.string().optional().describe("Initial folder path (e.g. public)"),
23658
- pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
23958
+ title: import_zod8.z.string().optional().describe("Pane title (default: Files)"),
23959
+ datasource: import_zod8.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
23960
+ path: import_zod8.z.string().optional().describe("Initial folder path (e.g. public)"),
23961
+ pane_id: import_zod8.z.string().optional().describe("Stable pane id across updates")
23659
23962
  },
23660
23963
  async (args) => {
23661
23964
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -23667,21 +23970,21 @@ async function startMcpServer() {
23667
23970
  path: args.path,
23668
23971
  message: "Files pane accepted. Sideboard desktop opens the Files column beside chat."
23669
23972
  };
23670
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
23973
+ return mcpJson(payload);
23671
23974
  }
23672
23975
  );
23673
23976
  server.tool(
23674
23977
  "wait_for_job",
23675
23978
  "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.",
23676
23979
  {
23677
- id: import_zod7.z.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
23678
- timeoutMs: import_zod7.z.number().optional()
23980
+ id: import_zod8.z.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
23981
+ timeoutMs: import_zod8.z.number().optional()
23679
23982
  },
23680
23983
  async ({ id, timeoutMs }) => {
23681
23984
  const result = await waitForDetachedJob(process.cwd(), id, {
23682
23985
  timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
23683
23986
  });
23684
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
23987
+ return mcpJson(result);
23685
23988
  }
23686
23989
  );
23687
23990
  if (!worktreeProfile) {
@@ -23696,56 +23999,44 @@ async function startMcpServer() {
23696
23999
  "set_caffeinate",
23697
24000
  "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.",
23698
24001
  {
23699
- enabled: import_zod7.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
24002
+ enabled: import_zod8.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
23700
24003
  },
23701
24004
  async ({ enabled }) => {
23702
24005
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
23703
24006
  const state = setCaffeinateHold2(enabled, { threadId });
23704
24007
  if (enabled && !state.held) {
23705
- return {
23706
- content: [
23707
- {
23708
- type: "text",
23709
- text: JSON.stringify({
23710
- ...state,
23711
- ok: false,
23712
- message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
23713
- })
23714
- }
23715
- ],
23716
- isError: true
23717
- };
23718
- }
23719
- return {
23720
- content: [
24008
+ return mcpJson(
23721
24009
  {
23722
- type: "text",
23723
- text: JSON.stringify({
23724
- ...getCaffeinateHold2(),
23725
- ok: true,
23726
- 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)."
23727
- })
23728
- }
23729
- ]
23730
- };
24010
+ ...state,
24011
+ ok: false,
24012
+ message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
24013
+ },
24014
+ true
24015
+ );
24016
+ }
24017
+ return mcpJson({
24018
+ ...getCaffeinateHold2(),
24019
+ ok: true,
24020
+ 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)."
24021
+ });
23731
24022
  }
23732
24023
  );
23733
24024
  server.tool(
23734
24025
  "create_thread",
23735
24026
  `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.`,
23736
24027
  {
23737
- sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]),
23738
- sourceRef: import_zod7.z.string(),
23739
- agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
23740
- model: import_zod7.z.string().nullable().optional().describe(
24028
+ sourceType: import_zod8.z.enum(["branch", "pr", "ticket"]),
24029
+ sourceRef: import_zod8.z.string(),
24030
+ agent: import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
24031
+ model: import_zod8.z.string().nullable().optional().describe(
23741
24032
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
23742
24033
  ),
23743
- repoPath: import_zod7.z.string(),
23744
- title: import_zod7.z.string().optional(),
23745
- cowboy: import_zod7.z.boolean().optional().describe(
24034
+ repoPath: import_zod8.z.string(),
24035
+ title: import_zod8.z.string().optional(),
24036
+ cowboy: import_zod8.z.boolean().optional().describe(
23746
24037
  "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."
23747
24038
  ),
23748
- parentThreadId: import_zod7.z.string().optional().describe(
24039
+ parentThreadId: import_zod8.z.string().optional().describe(
23749
24040
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
23750
24041
  )
23751
24042
  },
@@ -23761,10 +24052,10 @@ async function startMcpServer() {
23761
24052
  "start_board_card",
23762
24053
  "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.",
23763
24054
  {
23764
- kind: import_zod7.z.enum(["ticket", "pr", "branch"]),
23765
- ref: import_zod7.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
23766
- repoPath: import_zod7.z.string().describe("Workspace path from list_workspaces / list_board"),
23767
- title: import_zod7.z.string().optional()
24055
+ kind: import_zod8.z.enum(["ticket", "pr", "branch"]),
24056
+ ref: import_zod8.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
24057
+ repoPath: import_zod8.z.string().describe("Workspace path from list_workspaces / list_board"),
24058
+ title: import_zod8.z.string().optional()
23768
24059
  },
23769
24060
  async ({ kind, ref, repoPath, title }) => {
23770
24061
  const root = await resolveRepoRoot(repoPath);
@@ -23781,20 +24072,13 @@ async function startMcpServer() {
23781
24072
  }))
23782
24073
  );
23783
24074
  if (existing) {
23784
- return {
23785
- content: [
23786
- {
23787
- type: "text",
23788
- text: JSON.stringify({
23789
- alreadyStarted: true,
23790
- id: existing.id,
23791
- title: existing.title,
23792
- status: existing.status,
23793
- link: `sideboard://thread/${existing.id}`
23794
- })
23795
- }
23796
- ]
23797
- };
24075
+ return mcpJson({
24076
+ alreadyStarted: true,
24077
+ id: existing.id,
24078
+ title: existing.title,
24079
+ status: existing.status,
24080
+ link: `sideboard://thread/${existing.id}`
24081
+ });
23798
24082
  }
23799
24083
  const workspaces = orch.listWorkspaces();
23800
24084
  const loaded = await getHomeBoardInputs(workspaces);
@@ -23868,9 +24152,9 @@ async function startMcpServer() {
23868
24152
  "send_to_thread",
23869
24153
  '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.',
23870
24154
  {
23871
- ref: import_zod7.z.string(),
23872
- prompt: import_zod7.z.string(),
23873
- force_stop: import_zod7.z.boolean().optional()
24155
+ ref: import_zod8.z.string(),
24156
+ prompt: import_zod8.z.string(),
24157
+ force_stop: import_zod8.z.boolean().optional()
23874
24158
  },
23875
24159
  async ({ ref, prompt, force_stop }) => {
23876
24160
  if (force_stop) {
@@ -23880,79 +24164,58 @@ async function startMcpServer() {
23880
24164
  }
23881
24165
  }
23882
24166
  const thread = await orch.send(ref, prompt);
23883
- return {
23884
- content: [
23885
- {
23886
- type: "text",
23887
- text: JSON.stringify({
23888
- id: thread.id,
23889
- status: thread.status,
23890
- queueLength: thread.queue.length,
23891
- forceStopped: Boolean(force_stop)
23892
- })
23893
- }
23894
- ]
23895
- };
24167
+ return mcpJson({
24168
+ id: thread.id,
24169
+ status: thread.status,
24170
+ queueLength: thread.queue.length,
24171
+ forceStopped: Boolean(force_stop)
24172
+ });
23896
24173
  }
23897
24174
  );
23898
24175
  server.tool(
23899
24176
  "wait_for_turn",
23900
24177
  "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).",
23901
24178
  {
23902
- ref: import_zod7.z.string(),
23903
- timeoutMs: import_zod7.z.number().optional()
24179
+ ref: import_zod8.z.string(),
24180
+ timeoutMs: import_zod8.z.number().optional()
23904
24181
  },
23905
24182
  async ({ ref, timeoutMs }) => {
23906
24183
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
23907
24184
  resolveIfStillRunning: true
23908
24185
  });
23909
24186
  const result = orch.getTurnResult(thread.id);
23910
- return {
23911
- content: [
23912
- {
23913
- type: "text",
23914
- text: JSON.stringify({
23915
- id: thread.id,
23916
- status: result.status,
23917
- text: result.text,
23918
- lastError: result.lastError,
23919
- stillRunning: result.stillRunning,
23920
- progress: result.progress,
23921
- lastActivityAt: result.lastActivityAt,
23922
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
23923
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
23924
- })
23925
- }
23926
- ]
23927
- };
24187
+ return mcpJson({
24188
+ id: thread.id,
24189
+ status: result.status,
24190
+ text: result.text,
24191
+ lastError: result.lastError,
24192
+ stillRunning: result.stillRunning,
24193
+ progress: result.progress,
24194
+ lastActivityAt: result.lastActivityAt,
24195
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
24196
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
24197
+ });
23928
24198
  }
23929
24199
  );
23930
24200
  server.tool(
23931
24201
  "get_turn_result",
23932
24202
  "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).",
23933
- { ref: import_zod7.z.string() },
24203
+ { ref: import_zod8.z.string() },
23934
24204
  async ({ ref }) => {
23935
24205
  const result = orch.getTurnResult(ref);
23936
- return {
23937
- content: [
23938
- {
23939
- type: "text",
23940
- text: JSON.stringify({
23941
- ...result,
23942
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
23943
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
23944
- })
23945
- }
23946
- ]
23947
- };
24206
+ return mcpJson({
24207
+ ...result,
24208
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
24209
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
24210
+ });
23948
24211
  }
23949
24212
  );
23950
24213
  server.tool(
23951
24214
  "stop_thread",
23952
24215
  "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.",
23953
24216
  {
23954
- ref: import_zod7.z.string(),
23955
- force: import_zod7.z.boolean().optional()
24217
+ ref: import_zod8.z.string(),
24218
+ force: import_zod8.z.boolean().optional()
23956
24219
  },
23957
24220
  async ({ ref, force }) => {
23958
24221
  const t = orch.getThread(ref);
@@ -23965,24 +24228,17 @@ async function startMcpServer() {
23965
24228
  const clearQueue = force !== false;
23966
24229
  const hadQueued = t.queue.length > 0;
23967
24230
  const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
23968
- return {
23969
- content: [
23970
- {
23971
- type: "text",
23972
- text: JSON.stringify({
23973
- id: stopped.id,
23974
- status: stopped.status,
23975
- clearedQueue: clearQueue && hadQueued
23976
- })
23977
- }
23978
- ]
23979
- };
24231
+ return mcpJson({
24232
+ id: stopped.id,
24233
+ status: stopped.status,
24234
+ clearedQueue: clearQueue && hadQueued
24235
+ });
23980
24236
  }
23981
24237
  );
23982
24238
  server.tool(
23983
24239
  "archive_thread",
23984
24240
  "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.",
23985
- { ref: import_zod7.z.string() },
24241
+ { ref: import_zod8.z.string() },
23986
24242
  async ({ ref }) => {
23987
24243
  const t = orch.getThread(ref);
23988
24244
  if (!t) {
@@ -23999,38 +24255,24 @@ async function startMcpServer() {
23999
24255
  };
24000
24256
  }
24001
24257
  const archived = await orch.archive(ref);
24002
- return {
24003
- content: [
24004
- {
24005
- type: "text",
24006
- text: JSON.stringify({
24007
- id: archived.id,
24008
- status: archived.status
24009
- })
24010
- }
24011
- ]
24012
- };
24258
+ return mcpJson({
24259
+ id: archived.id,
24260
+ status: archived.status
24261
+ });
24013
24262
  }
24014
24263
  );
24015
24264
  server.tool(
24016
24265
  "restore_thread",
24017
24266
  "Restore an archived thread (recreates worktree from branch when needed)",
24018
- { ref: import_zod7.z.string() },
24267
+ { ref: import_zod8.z.string() },
24019
24268
  async ({ ref }) => {
24020
24269
  try {
24021
24270
  const restored = await orch.restore(ref);
24022
- return {
24023
- content: [
24024
- {
24025
- type: "text",
24026
- text: JSON.stringify({
24027
- id: restored.id,
24028
- status: restored.status,
24029
- worktreePath: restored.worktreePath
24030
- })
24031
- }
24032
- ]
24033
- };
24271
+ return mcpJson({
24272
+ id: restored.id,
24273
+ status: restored.status,
24274
+ worktreePath: restored.worktreePath
24275
+ });
24034
24276
  } catch (err) {
24035
24277
  const message = err instanceof Error ? err.message : String(err);
24036
24278
  return { content: [{ type: "text", text: message }], isError: true };
@@ -24041,34 +24283,25 @@ async function startMcpServer() {
24041
24283
  "get_diff",
24042
24284
  "Compact diff summary (capped hunks, paginated)",
24043
24285
  {
24044
- ref: import_zod7.z.string(),
24045
- maxFiles: import_zod7.z.number().optional()
24286
+ ref: import_zod8.z.string(),
24287
+ maxFiles: import_zod8.z.number().optional()
24046
24288
  },
24047
24289
  async ({ ref, maxFiles }) => {
24048
24290
  const summary = await orch.diffSummary(ref);
24049
- return {
24050
- content: [
24051
- {
24052
- type: "text",
24053
- text: JSON.stringify({
24054
- ...summary,
24055
- files: summary.files.slice(0, maxFiles ?? 10)
24056
- })
24057
- }
24058
- ]
24059
- };
24291
+ return mcpJson({
24292
+ ...summary,
24293
+ files: summary.files.slice(0, maxFiles ?? 10)
24294
+ });
24060
24295
  }
24061
24296
  );
24062
24297
  server.tool(
24063
24298
  "get_pr_checks",
24064
24299
  "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.",
24065
- { ref: import_zod7.z.string().describe("Worktree thread id/ref") },
24300
+ { ref: import_zod8.z.string().describe("Worktree thread id/ref") },
24066
24301
  async ({ ref }) => {
24067
24302
  try {
24068
24303
  const checks = await orch.getPrChecks(ref);
24069
- return {
24070
- content: [{ type: "text", text: JSON.stringify(checks) }]
24071
- };
24304
+ return mcpJson(checks);
24072
24305
  } catch (err) {
24073
24306
  const message = err instanceof Error ? err.message : String(err);
24074
24307
  return { content: [{ type: "text", text: message }], isError: true };
@@ -24078,25 +24311,18 @@ async function startMcpServer() {
24078
24311
  server.tool(
24079
24312
  "request_review",
24080
24313
  '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.',
24081
- { ref: import_zod7.z.string().describe("Worktree thread id/ref to review") },
24314
+ { ref: import_zod8.z.string().describe("Worktree thread id/ref to review") },
24082
24315
  async ({ ref }) => {
24083
24316
  try {
24084
24317
  const tab = await orch.requestReview(ref);
24085
24318
  const from = orch.getThread(ref);
24086
- return {
24087
- content: [
24088
- {
24089
- type: "text",
24090
- text: JSON.stringify({
24091
- id: tab.id,
24092
- title: tab.title,
24093
- status: tab.status,
24094
- fromThreadId: from?.id ?? ref,
24095
- link: `sideboard://thread/${tab.id}`
24096
- })
24097
- }
24098
- ]
24099
- };
24319
+ return mcpJson({
24320
+ id: tab.id,
24321
+ title: tab.title,
24322
+ status: tab.status,
24323
+ fromThreadId: from?.id ?? ref,
24324
+ link: `sideboard://thread/${tab.id}`
24325
+ });
24100
24326
  } catch (err) {
24101
24327
  const message = err instanceof Error ? err.message : String(err);
24102
24328
  return { content: [{ type: "text", text: message }], isError: true };
@@ -24107,53 +24333,37 @@ async function startMcpServer() {
24107
24333
  "ask_git",
24108
24334
  "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.",
24109
24335
  {
24110
- ref: import_zod7.z.string().describe("Worktree thread id/ref"),
24111
- action: import_zod7.z.enum(AGENT_GIT_ACTIONS).describe(
24336
+ ref: import_zod8.z.string().describe("Worktree thread id/ref"),
24337
+ action: import_zod8.z.enum(AGENT_GIT_ACTIONS).describe(
24112
24338
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
24113
24339
  )
24114
24340
  },
24115
24341
  async ({ ref, action }) => {
24116
24342
  try {
24117
24343
  const thread = await orch.askGit(ref, action);
24118
- return {
24119
- content: [
24120
- {
24121
- type: "text",
24122
- text: JSON.stringify({
24123
- id: thread.id,
24124
- status: thread.status,
24125
- queueLength: thread.queue.length,
24126
- action,
24127
- link: `sideboard://thread/${thread.id}`
24128
- })
24129
- }
24130
- ]
24131
- };
24344
+ return mcpJson({
24345
+ id: thread.id,
24346
+ status: thread.status,
24347
+ queueLength: thread.queue.length,
24348
+ action,
24349
+ link: `sideboard://thread/${thread.id}`
24350
+ });
24132
24351
  } catch (err) {
24133
24352
  const raw = err instanceof Error ? err.message : String(err);
24134
24353
  const message = formatGhLandError(raw);
24135
- return {
24136
- content: [
24137
- {
24138
- type: "text",
24139
- text: JSON.stringify(
24140
- {
24141
- error: true,
24142
- action,
24143
- lastError: message,
24144
- 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
24145
- },
24146
- null,
24147
- 2
24148
- )
24149
- }
24150
- ],
24151
- isError: true
24152
- };
24354
+ return mcpJson(
24355
+ {
24356
+ error: true,
24357
+ action,
24358
+ lastError: message,
24359
+ 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
24360
+ },
24361
+ true
24362
+ );
24153
24363
  }
24154
24364
  }
24155
24365
  );
24156
- const agentEnum = import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
24366
+ const agentEnum = import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
24157
24367
  server.tool(
24158
24368
  "list_models",
24159
24369
  "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.",
@@ -24163,9 +24373,7 @@ async function startMcpServer() {
24163
24373
  async ({ agent }) => {
24164
24374
  try {
24165
24375
  const catalogs = await listModelsForAgent(agent);
24166
- return {
24167
- content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
24168
- };
24376
+ return mcpJson(catalogs);
24169
24377
  } catch (err) {
24170
24378
  const message = err instanceof Error ? err.message : String(err);
24171
24379
  return { content: [{ type: "text", text: message }], isError: true };
@@ -24176,11 +24384,11 @@ async function startMcpServer() {
24176
24384
  "fork_worktree",
24177
24385
  "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.",
24178
24386
  {
24179
- ref: import_zod7.z.string().describe("Worktree thread id/ref to fork"),
24180
- through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
24387
+ ref: import_zod8.z.string().describe("Worktree thread id/ref to fork"),
24388
+ through_index: import_zod8.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
24181
24389
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
24182
- model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
24183
- title: import_zod7.z.string().optional()
24390
+ model: import_zod8.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
24391
+ title: import_zod8.z.string().optional()
24184
24392
  },
24185
24393
  async ({ ref, through_index, agent, model, title }) => {
24186
24394
  try {
@@ -24193,24 +24401,17 @@ async function startMcpServer() {
24193
24401
  model,
24194
24402
  title
24195
24403
  });
24196
- return {
24197
- content: [
24198
- {
24199
- type: "text",
24200
- text: JSON.stringify({
24201
- id: thread.id,
24202
- title: thread.title,
24203
- status: thread.status,
24204
- agent: thread.agent,
24205
- model: thread.model,
24206
- branchName: thread.branchName,
24207
- worktreePath: thread.worktreePath,
24208
- fromThreadId: source?.id ?? ref,
24209
- link: `sideboard://thread/${thread.id}`
24210
- })
24211
- }
24212
- ]
24213
- };
24404
+ return mcpJson({
24405
+ id: thread.id,
24406
+ title: thread.title,
24407
+ status: thread.status,
24408
+ agent: thread.agent,
24409
+ model: thread.model,
24410
+ branchName: thread.branchName,
24411
+ worktreePath: thread.worktreePath,
24412
+ fromThreadId: source?.id ?? ref,
24413
+ link: `sideboard://thread/${thread.id}`
24414
+ });
24214
24415
  } catch (err) {
24215
24416
  const message = err instanceof Error ? err.message : String(err);
24216
24417
  return { content: [{ type: "text", text: message }], isError: true };
@@ -24221,11 +24422,11 @@ async function startMcpServer() {
24221
24422
  "fork_chat",
24222
24423
  "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.",
24223
24424
  {
24224
- ref: import_zod7.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
24225
- through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
24425
+ ref: import_zod8.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
24426
+ through_index: import_zod8.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
24226
24427
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
24227
- model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
24228
- title: import_zod7.z.string().optional()
24428
+ model: import_zod8.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
24429
+ title: import_zod8.z.string().optional()
24229
24430
  },
24230
24431
  async ({ ref, through_index, agent, model, title }) => {
24231
24432
  try {
@@ -24243,24 +24444,17 @@ async function startMcpServer() {
24243
24444
  model,
24244
24445
  title
24245
24446
  });
24246
- return {
24247
- content: [
24248
- {
24249
- type: "text",
24250
- text: JSON.stringify({
24251
- id: tab.id,
24252
- title: tab.title,
24253
- status: tab.status,
24254
- agent: tab.agent,
24255
- model: tab.model,
24256
- sourceType: tab.sourceType,
24257
- worktreePath: tab.worktreePath,
24258
- fromThreadId: source.id,
24259
- link: `sideboard://thread/${tab.id}`
24260
- })
24261
- }
24262
- ]
24263
- };
24447
+ return mcpJson({
24448
+ id: tab.id,
24449
+ title: tab.title,
24450
+ status: tab.status,
24451
+ agent: tab.agent,
24452
+ model: tab.model,
24453
+ sourceType: tab.sourceType,
24454
+ worktreePath: tab.worktreePath,
24455
+ fromThreadId: source.id,
24456
+ link: `sideboard://thread/${tab.id}`
24457
+ });
24264
24458
  } catch (err) {
24265
24459
  const message = err instanceof Error ? err.message : String(err);
24266
24460
  return { content: [{ type: "text", text: message }], isError: true };
@@ -24271,49 +24465,35 @@ async function startMcpServer() {
24271
24465
  "run_dev_script",
24272
24466
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
24273
24467
  {
24274
- ref: import_zod7.z.string(),
24275
- name: import_zod7.z.string().optional()
24468
+ ref: import_zod8.z.string(),
24469
+ name: import_zod8.z.string().optional()
24276
24470
  },
24277
24471
  async ({ ref, name }) => {
24278
24472
  const result = await orch.startDev(ref, name);
24279
- return {
24280
- content: [
24281
- {
24282
- type: "text",
24283
- text: JSON.stringify({
24284
- port: result.port,
24285
- scriptName: result.scriptName,
24286
- ports: result.ports,
24287
- url: `http://localhost:${result.port}`
24288
- })
24289
- }
24290
- ]
24291
- };
24473
+ return mcpJson({
24474
+ port: result.port,
24475
+ scriptName: result.scriptName,
24476
+ ports: result.ports,
24477
+ url: `http://localhost:${result.port}`
24478
+ });
24292
24479
  }
24293
24480
  );
24294
24481
  server.tool(
24295
24482
  "list_run_scripts",
24296
24483
  "List named run scripts available for a thread",
24297
- { ref: import_zod7.z.string() },
24484
+ { ref: import_zod8.z.string() },
24298
24485
  async ({ ref }) => {
24299
24486
  const scripts = orch.listThreadRunScripts(ref);
24300
24487
  const active = orch.getActiveRuns(ref);
24301
- return {
24302
- content: [
24303
- {
24304
- type: "text",
24305
- text: JSON.stringify({ scripts, active }, null, 2)
24306
- }
24307
- ]
24308
- };
24488
+ return mcpJson({ scripts, active });
24309
24489
  }
24310
24490
  );
24311
24491
  server.tool(
24312
24492
  "stop_dev_script",
24313
24493
  "Stop a running script for a thread (all scripts if name omitted)",
24314
24494
  {
24315
- ref: import_zod7.z.string(),
24316
- name: import_zod7.z.string().optional()
24495
+ ref: import_zod8.z.string(),
24496
+ name: import_zod8.z.string().optional()
24317
24497
  },
24318
24498
  async ({ ref, name }) => {
24319
24499
  orch.stopDev(ref, name);
@@ -24323,27 +24503,25 @@ async function startMcpServer() {
24323
24503
  server.tool(
24324
24504
  "run_setup",
24325
24505
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
24326
- { ref: import_zod7.z.string() },
24506
+ { ref: import_zod8.z.string() },
24327
24507
  async ({ ref }) => {
24328
24508
  const result = await orch.runSetup(ref);
24329
- return {
24330
- content: [{ type: "text", text: JSON.stringify(result) }]
24331
- };
24509
+ return mcpJson(result);
24332
24510
  }
24333
24511
  );
24334
24512
  server.tool(
24335
24513
  "add_workspace",
24336
24514
  "Register a git repo as a Sideboard workspace",
24337
- { repoPath: import_zod7.z.string() },
24515
+ { repoPath: import_zod8.z.string() },
24338
24516
  async ({ repoPath }) => {
24339
24517
  const ws = await orch.addWorkspace(repoPath);
24340
- return { content: [{ type: "text", text: JSON.stringify(ws) }] };
24518
+ return mcpJson(ws);
24341
24519
  }
24342
24520
  );
24343
24521
  server.tool(
24344
24522
  "remove_workspace",
24345
24523
  "Unregister a Sideboard workspace (does not archive threads)",
24346
- { repoPath: import_zod7.z.string() },
24524
+ { repoPath: import_zod8.z.string() },
24347
24525
  async ({ repoPath }) => {
24348
24526
  orch.removeWorkspace(repoPath);
24349
24527
  return { content: [{ type: "text", text: "ok" }] };
@@ -24353,40 +24531,31 @@ async function startMcpServer() {
24353
24531
  "fanout",
24354
24532
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
24355
24533
  {
24356
- prompt: import_zod7.z.string(),
24357
- agents: import_zod7.z.array(import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
24358
- repoPath: import_zod7.z.string(),
24359
- sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]).optional(),
24360
- sourceRef: import_zod7.z.string().optional(),
24361
- title: import_zod7.z.string().optional()
24534
+ prompt: import_zod8.z.string(),
24535
+ agents: import_zod8.z.array(import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
24536
+ repoPath: import_zod8.z.string(),
24537
+ sourceType: import_zod8.z.enum(["branch", "pr", "ticket"]).optional(),
24538
+ sourceRef: import_zod8.z.string().optional(),
24539
+ title: import_zod8.z.string().optional()
24362
24540
  },
24363
24541
  async (args) => {
24364
24542
  const threads = await orch.bestOfN(args);
24365
- return {
24366
- content: [
24367
- {
24368
- type: "text",
24369
- text: JSON.stringify(
24370
- threads.map((t) => ({
24371
- id: t.id,
24372
- agent: t.agent,
24373
- branchName: t.branchName,
24374
- worktreePath: t.worktreePath
24375
- })),
24376
- null,
24377
- 2
24378
- )
24379
- }
24380
- ]
24381
- };
24543
+ return mcpJson(
24544
+ threads.map((t) => ({
24545
+ id: t.id,
24546
+ agent: t.agent,
24547
+ branchName: t.branchName,
24548
+ worktreePath: t.worktreePath
24549
+ }))
24550
+ );
24382
24551
  }
24383
24552
  );
24384
24553
  server.tool(
24385
24554
  "list_branches",
24386
24555
  "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).",
24387
24556
  {
24388
- repoPath: import_zod7.z.string(),
24389
- unmergedOnly: import_zod7.z.boolean().optional()
24557
+ repoPath: import_zod8.z.string(),
24558
+ unmergedOnly: import_zod8.z.boolean().optional()
24390
24559
  },
24391
24560
  async ({ repoPath, unmergedOnly }) => {
24392
24561
  const root = await resolveRepoRoot(repoPath);
@@ -24407,19 +24576,19 @@ async function startMcpServer() {
24407
24576
  "list_prs",
24408
24577
  '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.',
24409
24578
  {
24410
- repoPath: import_zod7.z.string(),
24411
- query: import_zod7.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
24412
- queue: import_zod7.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
24579
+ repoPath: import_zod8.z.string(),
24580
+ query: import_zod8.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
24581
+ queue: import_zod8.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
24413
24582
  'review = unclaimed eng-review inbox (use for "get me N tickets to review"). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes.'
24414
24583
  ),
24415
- state: import_zod7.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
24416
- label: import_zod7.z.string().optional().describe(
24584
+ state: import_zod8.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
24585
+ label: import_zod8.z.string().optional().describe(
24417
24586
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
24418
24587
  ),
24419
- reviewer: import_zod7.z.string().optional().describe(
24588
+ reviewer: import_zod8.z.string().optional().describe(
24420
24589
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
24421
24590
  ),
24422
- limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
24591
+ limit: import_zod8.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
24423
24592
  },
24424
24593
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
24425
24594
  const root = await resolveRepoRoot(repoPath);
@@ -24451,89 +24620,65 @@ async function startMcpServer() {
24451
24620
  server.tool(
24452
24621
  "get_pr_stack",
24453
24622
  "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).",
24454
- { ref: import_zod7.z.string() },
24623
+ { ref: import_zod8.z.string() },
24455
24624
  async ({ ref }) => {
24456
24625
  const stack = await orch.getPrStack(ref);
24457
- return {
24458
- content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
24459
- };
24626
+ return mcpJson(stack);
24460
24627
  }
24461
24628
  );
24462
24629
  server.tool(
24463
24630
  "open_pr_stack_layers",
24464
24631
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
24465
24632
  {
24466
- ref: import_zod7.z.string(),
24467
- layer: import_zod7.z.number().int().positive().optional()
24633
+ ref: import_zod8.z.string(),
24634
+ layer: import_zod8.z.number().int().positive().optional()
24468
24635
  },
24469
24636
  async ({ ref, layer }) => {
24470
24637
  const result = await orch.openPrStackLayers(ref, { layer });
24471
- return {
24472
- content: [
24473
- {
24474
- type: "text",
24475
- text: JSON.stringify(
24476
- {
24477
- stackNumber: result.stack.stackNumber,
24478
- trunk: result.stack.trunk,
24479
- threads: result.threads.map((t) => ({
24480
- id: t.id,
24481
- title: t.title,
24482
- branchName: t.branchName,
24483
- stackLayer: t.stackLayer,
24484
- worktreePath: t.worktreePath,
24485
- prUrl: t.prUrl,
24486
- link: `sideboard://thread/${t.id}`
24487
- }))
24488
- },
24489
- null,
24490
- 2
24491
- )
24492
- }
24493
- ]
24494
- };
24638
+ return mcpJson({
24639
+ stackNumber: result.stack.stackNumber,
24640
+ trunk: result.stack.trunk,
24641
+ threads: result.threads.map((t) => ({
24642
+ id: t.id,
24643
+ title: t.title,
24644
+ branchName: t.branchName,
24645
+ stackLayer: t.stackLayer,
24646
+ worktreePath: t.worktreePath,
24647
+ prUrl: t.prUrl,
24648
+ link: `sideboard://thread/${t.id}`
24649
+ }))
24650
+ });
24495
24651
  }
24496
24652
  );
24497
24653
  server.tool(
24498
24654
  "add_stack_layer",
24499
24655
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
24500
24656
  {
24501
- ref: import_zod7.z.string(),
24502
- branchName: import_zod7.z.string(),
24503
- title: import_zod7.z.string().optional()
24657
+ ref: import_zod8.z.string(),
24658
+ branchName: import_zod8.z.string(),
24659
+ title: import_zod8.z.string().optional()
24504
24660
  },
24505
24661
  async ({ ref, branchName, title }) => {
24506
24662
  const result = await orch.addStackLayer(ref, branchName, { title });
24507
- return {
24508
- content: [
24509
- {
24510
- type: "text",
24511
- text: JSON.stringify(
24512
- {
24513
- id: result.thread.id,
24514
- title: result.thread.title,
24515
- branchName: result.thread.branchName,
24516
- stackLayer: result.thread.stackLayer,
24517
- worktreePath: result.thread.worktreePath,
24518
- link: `sideboard://thread/${result.thread.id}`
24519
- },
24520
- null,
24521
- 2
24522
- )
24523
- }
24524
- ]
24525
- };
24663
+ return mcpJson({
24664
+ id: result.thread.id,
24665
+ title: result.thread.title,
24666
+ branchName: result.thread.branchName,
24667
+ stackLayer: result.thread.stackLayer,
24668
+ worktreePath: result.thread.worktreePath,
24669
+ link: `sideboard://thread/${result.thread.id}`
24670
+ });
24526
24671
  }
24527
24672
  );
24528
24673
  server.tool(
24529
24674
  "create_pr_stack",
24530
24675
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
24531
24676
  {
24532
- repoPath: import_zod7.z.string(),
24533
- branches: import_zod7.z.array(import_zod7.z.string()).min(1),
24534
- agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
24535
- base: import_zod7.z.string().optional(),
24536
- title: import_zod7.z.string().optional()
24677
+ repoPath: import_zod8.z.string(),
24678
+ branches: import_zod8.z.array(import_zod8.z.string()).min(1),
24679
+ agent: import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
24680
+ base: import_zod8.z.string().optional(),
24681
+ title: import_zod8.z.string().optional()
24537
24682
  },
24538
24683
  async (args) => {
24539
24684
  const result = await orch.createPrStack({
@@ -24543,39 +24688,28 @@ async function startMcpServer() {
24543
24688
  base: args.base,
24544
24689
  title: args.title
24545
24690
  });
24546
- return {
24547
- content: [
24548
- {
24549
- type: "text",
24550
- text: JSON.stringify(
24551
- {
24552
- stackNumber: result.stack.stackNumber,
24553
- trunk: result.stack.trunk,
24554
- threads: result.threads.map((t) => ({
24555
- id: t.id,
24556
- title: t.title,
24557
- branchName: t.branchName,
24558
- stackLayer: t.stackLayer,
24559
- worktreePath: t.worktreePath,
24560
- link: `sideboard://thread/${t.id}`
24561
- }))
24562
- },
24563
- null,
24564
- 2
24565
- )
24566
- }
24567
- ]
24568
- };
24691
+ return mcpJson({
24692
+ stackNumber: result.stack.stackNumber,
24693
+ trunk: result.stack.trunk,
24694
+ threads: result.threads.map((t) => ({
24695
+ id: t.id,
24696
+ title: t.title,
24697
+ branchName: t.branchName,
24698
+ stackLayer: t.stackLayer,
24699
+ worktreePath: t.worktreePath,
24700
+ link: `sideboard://thread/${t.id}`
24701
+ }))
24702
+ });
24569
24703
  }
24570
24704
  );
24571
24705
  server.tool(
24572
24706
  "list_issues",
24573
24707
  "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.",
24574
24708
  {
24575
- repoPath: import_zod7.z.string(),
24576
- query: import_zod7.z.string().optional().describe("Search title, identifier, or description"),
24577
- assignee: import_zod7.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
24578
- limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
24709
+ repoPath: import_zod8.z.string(),
24710
+ query: import_zod8.z.string().optional().describe("Search title, identifier, or description"),
24711
+ assignee: import_zod8.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
24712
+ limit: import_zod8.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
24579
24713
  },
24580
24714
  async ({ repoPath, query, assignee, limit }) => {
24581
24715
  const root = await resolveRepoRoot(repoPath);