@sideboard-ai/core 0.1.160 → 0.1.162

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.
package/dist/index.cjs CHANGED
@@ -10215,6 +10215,17 @@ function partsToAssistantText(parts) {
10215
10215
  (p) => p.type === "text" && !messagePartParentId(p)
10216
10216
  ).map((p) => p.text).join("").trim();
10217
10217
  }
10218
+ function lastAssistantMessageText(parts) {
10219
+ if (!parts?.length) return "";
10220
+ for (let i = parts.length - 1; i >= 0; i--) {
10221
+ const p = parts[i];
10222
+ if (!p || p.type !== "text" || messagePartParentId(p)) continue;
10223
+ const text7 = p.text.trim();
10224
+ if (!text7 || isInternalAgentStatusText(text7)) continue;
10225
+ return text7;
10226
+ }
10227
+ return "";
10228
+ }
10218
10229
  function stripBrightsyNdjsonNoise(text7) {
10219
10230
  if (!text7 || !text7.includes('"type"')) return text7;
10220
10231
  let out = text7;
@@ -16094,6 +16105,7 @@ var GLOBAL_WORKSPACE_ID2, BOARD_COLUMN_DEFS, HOME_BOARD_CACHE_TTL_MS, DEFAULT_WO
16094
16105
  var init_home_board = __esm({
16095
16106
  "src/board/home-board.ts"() {
16096
16107
  "use strict";
16108
+ init_message_parts();
16097
16109
  init_worktree_labels();
16098
16110
  GLOBAL_WORKSPACE_ID2 = "__global__";
16099
16111
  BOARD_COLUMN_DEFS = [
@@ -16894,6 +16906,23 @@ var init_create = __esm({
16894
16906
  }
16895
16907
  });
16896
16908
 
16909
+ // src/threads/implied-first-prompt.ts
16910
+ function resolveCreateFirstPrompt(input) {
16911
+ const typed = input.prompt?.trim();
16912
+ if (typed) return typed;
16913
+ if (input.sourceType !== "ticket") return void 0;
16914
+ if (input.parentThreadId) return void 0;
16915
+ if (input.reused && input.hasUserMessages) return void 0;
16916
+ return IMPLIED_TICKET_RESOLVE_PROMPT;
16917
+ }
16918
+ var IMPLIED_TICKET_RESOLVE_PROMPT;
16919
+ var init_implied_first_prompt = __esm({
16920
+ "src/threads/implied-first-prompt.ts"() {
16921
+ "use strict";
16922
+ IMPLIED_TICKET_RESOLVE_PROMPT = "Resolve this issue.";
16923
+ }
16924
+ });
16925
+
16897
16926
  // src/threads/cowboy.ts
16898
16927
  function isCowboyThread(thread) {
16899
16928
  return Boolean(thread?.cowboy);
@@ -18085,13 +18114,13 @@ function formatStat(files) {
18085
18114
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
18086
18115
  }
18087
18116
  function emptyScopeStats() {
18088
- const z8 = emptyStat();
18117
+ const z9 = emptyStat();
18089
18118
  return {
18090
- commits: z8,
18091
- uncommitted: z8,
18092
- staged: z8,
18093
- unstaged: z8,
18094
- last_turn: z8
18119
+ commits: z9,
18120
+ uncommitted: z9,
18121
+ staged: z9,
18122
+ unstaged: z9,
18123
+ last_turn: z9
18095
18124
  };
18096
18125
  }
18097
18126
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -19232,17 +19261,17 @@ function formatIssueToolsDirective(opts) {
19232
19261
  ];
19233
19262
  if (opts.linear) {
19234
19263
  lines.push(
19235
- "- 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."
19264
+ "- 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."
19236
19265
  );
19237
19266
  }
19238
19267
  if (github) {
19239
19268
  lines.push(
19240
- "- GitHub: `github_get_issue` (comments), `github_comment`, `github_update_issue` (state open|closed), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
19269
+ "- 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`."
19241
19270
  );
19242
19271
  }
19243
19272
  if (opts.abletime) {
19244
19273
  lines.push(
19245
- "- AbleTime: `abletime_get_task` (comments), `abletime_comment`, `abletime_update_task` (state), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
19274
+ "- 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)."
19246
19275
  );
19247
19276
  }
19248
19277
  lines.push(
@@ -20075,6 +20104,7 @@ var init_orchestrator = __esm({
20075
20104
  init_child_halt();
20076
20105
  init_wait_for_job();
20077
20106
  init_create();
20107
+ init_implied_first_prompt();
20078
20108
  init_cowboy();
20079
20109
  init_orchestrator_capable();
20080
20110
  init_chat_tabs();
@@ -20481,10 +20511,18 @@ var init_orchestrator = __esm({
20481
20511
  listThreads({ includeArchived: false }).map((t) => t.id)
20482
20512
  );
20483
20513
  const thread = await createThread(input);
20484
- if (prior.has(thread.id)) {
20485
- if (input.prompt?.trim()) {
20514
+ const reused = prior.has(thread.id);
20515
+ const prompt = resolveCreateFirstPrompt({
20516
+ sourceType: input.sourceType,
20517
+ prompt: input.prompt,
20518
+ parentThreadId: input.parentThreadId ?? thread.parentThreadId,
20519
+ reused,
20520
+ hasUserMessages: thread.messages.some((m) => m.role === "user")
20521
+ });
20522
+ if (reused) {
20523
+ if (prompt) {
20486
20524
  try {
20487
- await this.send(thread.id, input.prompt.trim());
20525
+ await this.send(thread.id, prompt);
20488
20526
  } catch (err) {
20489
20527
  const message = err instanceof Error ? err.message : String(err);
20490
20528
  updateThread(thread.id, {
@@ -20495,7 +20533,7 @@ var init_orchestrator = __esm({
20495
20533
  return thread;
20496
20534
  }
20497
20535
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
20498
- void this.finishCreateThread(thread.id, input.prompt?.trim() || void 0);
20536
+ void this.finishCreateThread(thread.id, prompt);
20499
20537
  return thread;
20500
20538
  }
20501
20539
  async finishCreateThread(threadId, prompt) {
@@ -22339,6 +22377,7 @@ __export(index_exports, {
22339
22377
  GLOBAL_WORKSPACE_ID: () => GLOBAL_WORKSPACE_ID,
22340
22378
  HARNESS_ENV_KEYS: () => HARNESS_ENV_KEYS,
22341
22379
  HOME_BOARD_CACHE_TTL_MS: () => HOME_BOARD_CACHE_TTL_MS,
22380
+ IMPLIED_TICKET_RESOLVE_PROMPT: () => IMPLIED_TICKET_RESOLVE_PROMPT,
22342
22381
  ISSUE_SOURCE_LABELS: () => ISSUE_SOURCE_LABELS,
22343
22382
  LEGACY_ATTACHMENTS_DIR: () => LEGACY_ATTACHMENTS_DIR,
22344
22383
  LEGACY_DETACHED_JOBS_DIR: () => LEGACY_DETACHED_JOBS_DIR,
@@ -22741,6 +22780,7 @@ __export(index_exports, {
22741
22780
  issueMatchesAssignee: () => issueMatchesAssignee,
22742
22781
  issueSourceLabel: () => issueSourceLabel,
22743
22782
  issueTicketFromThread: () => issueTicketFromThread,
22783
+ lastAssistantMessageText: () => lastAssistantMessageText,
22744
22784
  lastRequestOccupancy: () => lastRequestOccupancy,
22745
22785
  latestPendingPlanQuestions: () => latestPendingPlanQuestions,
22746
22786
  linearAuthorizationHeader: () => linearAuthorizationHeader,
@@ -22891,6 +22931,7 @@ __export(index_exports, {
22891
22931
  resolveCodexGitWritableRoots: () => resolveCodexGitWritableRoots,
22892
22932
  resolveCommandBinarySync: () => resolveCommandBinarySync,
22893
22933
  resolveConductorCursorAgentId: () => resolveConductorCursorAgentId,
22934
+ resolveCreateFirstPrompt: () => resolveCreateFirstPrompt,
22894
22935
  resolveCursorModelId: () => resolveCursorModelId,
22895
22936
  resolveDefaultBranch: () => resolveDefaultBranch,
22896
22937
  resolveDetachedJobScript: () => resolveDetachedJobScript,
@@ -24579,6 +24620,7 @@ init_context_compact();
24579
24620
  init_summarize();
24580
24621
  init_land();
24581
24622
  init_create();
24623
+ init_implied_first_prompt();
24582
24624
  init_cowboy();
24583
24625
  init_chat_tabs();
24584
24626
  init_fork_worktree();
@@ -24718,7 +24760,7 @@ init_orphan_cleanup();
24718
24760
  // src/mcp/server.ts
24719
24761
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
24720
24762
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
24721
- var import_zod7 = require("zod");
24763
+ var import_zod8 = require("zod");
24722
24764
  var import_node_crypto13 = require("crypto");
24723
24765
  var import_node_path50 = require("path");
24724
24766
  init_orchestrator();
@@ -24999,12 +25041,53 @@ ${link}` : link;
24999
25041
  // src/mcp/slack-tools.ts
25000
25042
  init_outbound_watch();
25001
25043
  init_workspaces2();
25002
- function text(payload, isError = false) {
25044
+
25045
+ // src/mcp/issue-list.ts
25046
+ var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
25047
+ var MCP_ISSUE_LIST_MAX_LIMIT = 250;
25048
+ function clampMcpIssueLimit(limit) {
25049
+ if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
25050
+ return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
25051
+ }
25052
+ function applyIssueListWindow(items, limit) {
25053
+ const truncated = items.length > limit;
25054
+ return {
25055
+ items: truncated ? items.slice(0, limit) : items,
25056
+ truncated
25057
+ };
25058
+ }
25059
+ function compactIssueRow(issue) {
25060
+ return {
25061
+ identifier: issue.identifier,
25062
+ title: issue.title,
25063
+ ...issue.assignee ? { assignee: issue.assignee } : {},
25064
+ ...issue.labels.length > 0 ? { labels: issue.labels } : {},
25065
+ ...issue.teamKey ? { team: issue.teamKey } : {},
25066
+ ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
25067
+ };
25068
+ }
25069
+ function formatMcpIssueList(input) {
25003
25070
  return {
25004
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
25071
+ ...input.source ? { source: input.source } : {},
25072
+ ...input.viewer ? { viewer: input.viewer } : {},
25073
+ limit: input.limit,
25074
+ returned: input.issues.length,
25075
+ truncated: input.truncated,
25076
+ ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
25077
+ issues: input.issues.map(compactIssueRow)
25078
+ };
25079
+ }
25080
+ function mcpJson(payload, isError = false) {
25081
+ return {
25082
+ content: [{ type: "text", text: JSON.stringify(payload) }],
25005
25083
  ...isError ? { isError: true } : {}
25006
25084
  };
25007
25085
  }
25086
+
25087
+ // src/mcp/slack-tools.ts
25088
+ function text(payload, isError = false) {
25089
+ return mcpJson(payload, isError);
25090
+ }
25008
25091
  function fail(err) {
25009
25092
  return text(
25010
25093
  { error: err instanceof Error ? err.message : String(err) },
@@ -25273,49 +25356,331 @@ function registerSlackTools(server) {
25273
25356
  init_app_settings();
25274
25357
 
25275
25358
  // src/mcp/abletime-tools.ts
25359
+ var import_zod3 = require("zod");
25360
+ init_abletime();
25361
+
25362
+ // src/mcp/issue-payload.ts
25276
25363
  var import_zod2 = require("zod");
25277
25364
  init_abletime();
25278
25365
 
25279
- // src/mcp/issue-list.ts
25280
- var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
25281
- var MCP_ISSUE_LIST_MAX_LIMIT = 250;
25282
- function clampMcpIssueLimit(limit) {
25283
- if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
25284
- return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
25366
+ // src/mcp/smart-crush.ts
25367
+ var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
25368
+ var CRUSH_MIN_ITEMS = 5;
25369
+ var CRUSH_MIN_CHARS = 800;
25370
+ var CRUSH_COMMENT_MAX_CHARS = 16e3;
25371
+ var CRUSH_BODY_MIN_CHARS = 12e3;
25372
+ var CRUSH_BODY_MAX_CHARS = 8e3;
25373
+ var NEAR_DUP_JACCARD = 0.92;
25374
+ function looksLikeMustKeep(text7) {
25375
+ return MUST_KEEP_TEXT_RE.test(text7);
25376
+ }
25377
+ function normalizeText(text7) {
25378
+ return text7.replace(/\s+/g, " ").trim().toLowerCase();
25379
+ }
25380
+ function wordSet(text7) {
25381
+ return new Set(
25382
+ text7.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
25383
+ );
25285
25384
  }
25286
- function applyIssueListWindow(items, limit) {
25287
- const truncated = items.length > limit;
25385
+ function jaccard(a, b) {
25386
+ if (a.size === 0 && b.size === 0) return 1;
25387
+ let inter = 0;
25388
+ for (const w of a) {
25389
+ if (b.has(w)) inter += 1;
25390
+ }
25391
+ const union = a.size + b.size - inter;
25392
+ return union === 0 ? 0 : inter / union;
25393
+ }
25394
+ function importance(text7) {
25395
+ let score = text7.length;
25396
+ if (/```/.test(text7)) score += 400;
25397
+ if (/https?:\/\//.test(text7)) score += 50;
25398
+ if (looksLikeMustKeep(text7)) score += 1e4;
25399
+ return score;
25400
+ }
25401
+ function meanStd(values) {
25402
+ if (values.length === 0) return { mean: 0, std: 0 };
25403
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
25404
+ const variance = values.reduce((a, b) => a + (b - mean) * (b - mean), 0) / values.length;
25405
+ return { mean, std: Math.sqrt(variance) };
25406
+ }
25407
+ function dedupTextItems(items, getText) {
25408
+ const kept = [];
25409
+ const keptNorm = [];
25410
+ const keptWords = [];
25411
+ for (const item of items) {
25412
+ const text7 = getText(item);
25413
+ const norm = normalizeText(text7);
25414
+ if (!norm) {
25415
+ kept.push(item);
25416
+ keptNorm.push(norm);
25417
+ keptWords.push(wordSet(text7));
25418
+ continue;
25419
+ }
25420
+ const words = wordSet(text7);
25421
+ let dupAt = -1;
25422
+ for (let i = 0; i < kept.length; i++) {
25423
+ if (keptNorm[i] === norm) {
25424
+ dupAt = i;
25425
+ break;
25426
+ }
25427
+ if (keptWords[i].size >= 8 && words.size >= 8 && jaccard(keptWords[i], words) >= NEAR_DUP_JACCARD) {
25428
+ dupAt = i;
25429
+ break;
25430
+ }
25431
+ }
25432
+ if (dupAt < 0) {
25433
+ kept.push(item);
25434
+ keptNorm.push(norm);
25435
+ keptWords.push(words);
25436
+ continue;
25437
+ }
25438
+ if (text7.length > getText(kept[dupAt]).length) {
25439
+ kept[dupAt] = item;
25440
+ keptNorm[dupAt] = norm;
25441
+ keptWords[dupAt] = words;
25442
+ }
25443
+ }
25444
+ return { items: kept, dropped: items.length - kept.length };
25445
+ }
25446
+ function crushTextItems(items, getText, opts = {}) {
25447
+ const minItems = opts.minItems ?? CRUSH_MIN_ITEMS;
25448
+ const minChars = opts.minChars ?? CRUSH_MIN_CHARS;
25449
+ const maxChars = opts.maxChars ?? CRUSH_COMMENT_MAX_CHARS;
25450
+ const original = items;
25451
+ const totalChars = items.reduce((n, item) => n + getText(item).length, 0);
25452
+ if (items.length < minItems || totalChars < minChars) {
25453
+ return {
25454
+ items,
25455
+ truncated: false,
25456
+ kept: items.length,
25457
+ dropped: 0,
25458
+ reason: "passthrough"
25459
+ };
25460
+ }
25461
+ const deduped = dedupTextItems(items, getText);
25462
+ const unique = deduped.items;
25463
+ const uniqueChars = unique.reduce((n, item) => n + getText(item).length, 0);
25464
+ if (uniqueChars <= maxChars) {
25465
+ const changed = unique.length < original.length;
25466
+ return {
25467
+ items: unique,
25468
+ truncated: changed,
25469
+ kept: unique.length,
25470
+ dropped: original.length - unique.length,
25471
+ reason: changed ? "dedup" : "passthrough"
25472
+ };
25473
+ }
25474
+ const lengths = unique.map((item) => getText(item).length);
25475
+ const { mean, std } = meanStd(lengths);
25476
+ const outlierFloor = mean + 2 * std;
25477
+ const mustKeep = /* @__PURE__ */ new Set();
25478
+ unique.forEach((item, i) => {
25479
+ const text7 = getText(item);
25480
+ if (looksLikeMustKeep(text7) || text7.length > outlierFloor) mustKeep.add(i);
25481
+ });
25482
+ const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
25483
+ let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
25484
+ const pick = new Set(mustKeep);
25485
+ const canTake = (i) => {
25486
+ if (pick.has(i)) return false;
25487
+ const next = used + lengths[i];
25488
+ if (next > maxChars && pick.size > mustKeep.size) return false;
25489
+ used = next;
25490
+ pick.add(i);
25491
+ return true;
25492
+ };
25493
+ const k = remainingIdx.length;
25494
+ const headN = Math.max(1, Math.ceil(k * 0.3));
25495
+ const tailN = Math.max(1, Math.ceil(k * 0.15));
25496
+ for (const i of remainingIdx.slice(0, headN)) canTake(i);
25497
+ for (const i of remainingIdx.slice(-tailN).reverse()) canTake(i);
25498
+ const middle = remainingIdx.filter((i) => !pick.has(i)).sort((a, b) => importance(getText(unique[b])) - importance(getText(unique[a])));
25499
+ for (const i of middle) {
25500
+ if (used >= maxChars) break;
25501
+ canTake(i);
25502
+ }
25503
+ const crushed = unique.filter((_, i) => pick.has(i));
25504
+ const crushedChars = crushed.reduce((n, item) => n + getText(item).length, 0);
25505
+ if (crushed.length >= original.length && crushedChars >= totalChars) {
25506
+ return {
25507
+ items: original,
25508
+ truncated: false,
25509
+ kept: original.length,
25510
+ dropped: 0,
25511
+ reason: "passthrough"
25512
+ };
25513
+ }
25288
25514
  return {
25289
- items: truncated ? items.slice(0, limit) : items,
25290
- truncated
25515
+ items: crushed,
25516
+ truncated: crushed.length < original.length,
25517
+ kept: crushed.length,
25518
+ dropped: original.length - crushed.length,
25519
+ reason: "budget"
25291
25520
  };
25292
25521
  }
25293
- function compactIssueRow(issue) {
25294
- return {
25295
- identifier: issue.identifier,
25296
- title: issue.title,
25297
- ...issue.assignee ? { assignee: issue.assignee } : {},
25298
- ...issue.labels.length > 0 ? { labels: issue.labels } : {},
25299
- ...issue.teamKey ? { team: issue.teamKey } : {},
25300
- ...issue.cycle?.name ? { cycle: issue.cycle.name } : {}
25522
+ function splitBlocks(text7) {
25523
+ if (/\n\n/.test(text7)) return text7.split(/\n\n+/);
25524
+ return text7.split("\n");
25525
+ }
25526
+ function capOversizedBlock(block, maxChars) {
25527
+ if (block.length <= maxChars) return block;
25528
+ const head = Math.floor(maxChars * 0.3);
25529
+ const tail = Math.floor(maxChars * 0.15);
25530
+ return `${block.slice(0, head)}
25531
+
25532
+ [\u2026crushed ${block.length - head - tail} chars; pass include=full to read the rest\u2026]
25533
+
25534
+ ${block.slice(-tail)}`;
25535
+ }
25536
+ function crushMarkdown(text7, opts) {
25537
+ const original = text7 ?? "";
25538
+ const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
25539
+ const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
25540
+ if (original.length < minChars) {
25541
+ return { text: original, truncated: false, originalChars: original.length };
25542
+ }
25543
+ const rawBlocks = splitBlocks(original).filter((b) => b.length > 0);
25544
+ const blocks = rawBlocks.map((b) => capOversizedBlock(b, maxChars));
25545
+ const cappedABlock = blocks.some((b, i) => b !== rawBlocks[i]);
25546
+ if (blocks.length <= 1) {
25547
+ const cut = blocks[0] ?? original;
25548
+ if (cut.length >= original.length) {
25549
+ return { text: original, truncated: false, originalChars: original.length };
25550
+ }
25551
+ return { text: cut, truncated: true, originalChars: original.length };
25552
+ }
25553
+ const must = blocks.filter((b) => looksLikeMustKeep(b));
25554
+ const rest = blocks.filter((b) => !looksLikeMustKeep(b));
25555
+ const marker = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
25556
+ let used = must.reduce((n, b) => n + b.length, 0) + marker.length;
25557
+ const keptRest = [];
25558
+ const headN = Math.max(1, Math.ceil(rest.length * 0.3));
25559
+ const tailN = Math.max(1, Math.ceil(rest.length * 0.15));
25560
+ const head = rest.slice(0, headN);
25561
+ const tail = rest.slice(-tailN);
25562
+ const middle = rest.slice(headN, Math.max(headN, rest.length - tailN));
25563
+ const take = (block) => {
25564
+ if (used + block.length + 2 > maxChars && keptRest.length + must.length > 0) {
25565
+ return false;
25566
+ }
25567
+ used += block.length + 2;
25568
+ keptRest.push(block);
25569
+ return true;
25301
25570
  };
25571
+ for (const b of head) take(b);
25572
+ for (const b of tail) take(b);
25573
+ const ranked = [...middle].sort((a, b) => importance(b) - importance(a));
25574
+ for (const b of ranked) {
25575
+ if (used >= maxChars) break;
25576
+ take(b);
25577
+ }
25578
+ const keepSet = /* @__PURE__ */ new Set([...must, ...keptRest]);
25579
+ const ordered = blocks.filter((b) => keepSet.has(b));
25580
+ const omitted = blocks.length - ordered.length;
25581
+ if (omitted <= 0) {
25582
+ const joined = blocks.join("\n\n");
25583
+ if (cappedABlock && joined.length < original.length) {
25584
+ return { text: joined, truncated: true, originalChars: original.length };
25585
+ }
25586
+ return { text: original, truncated: false, originalChars: original.length };
25587
+ }
25588
+ const out = [];
25589
+ let inserted = false;
25590
+ for (const block of blocks) {
25591
+ if (keepSet.has(block)) {
25592
+ out.push(block);
25593
+ continue;
25594
+ }
25595
+ if (!inserted) {
25596
+ out.push(marker);
25597
+ inserted = true;
25598
+ }
25599
+ }
25600
+ const textOut = out.join("\n\n");
25601
+ if (textOut.length >= original.length) {
25602
+ return { text: original, truncated: false, originalChars: original.length };
25603
+ }
25604
+ return { text: textOut, truncated: true, originalChars: original.length };
25302
25605
  }
25303
- function formatMcpIssueList(input) {
25304
- return {
25305
- ...input.source ? { source: input.source } : {},
25306
- ...input.viewer ? { viewer: input.viewer } : {},
25307
- limit: input.limit,
25308
- returned: input.issues.length,
25309
- truncated: input.truncated,
25310
- ...input.truncated ? { hint: "More issues match. Pass a higher limit (max 250) or a tighter query." } : {},
25311
- issues: input.issues.map(compactIssueRow)
25606
+
25607
+ // src/mcp/issue-payload.ts
25608
+ var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
25609
+ var mcpIssueIncludeSchema = import_zod2.z.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
25610
+ "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."
25611
+ );
25612
+ var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
25613
+ function emptyMeta() {
25614
+ return { truncated: true, hint: MCP_ISSUE_CRUSH_HINT };
25615
+ }
25616
+ function withComments(comments, meta) {
25617
+ const crushed = crushTextItems(comments, (c) => c.body);
25618
+ if (crushed.truncated) {
25619
+ meta.comments = {
25620
+ kept: crushed.kept,
25621
+ dropped: crushed.dropped,
25622
+ reason: crushed.reason
25623
+ };
25624
+ }
25625
+ return { comments: crushed.items, meta };
25626
+ }
25627
+ function withBody(text7, meta) {
25628
+ const crushed = crushMarkdown(text7);
25629
+ if (crushed.truncated) {
25630
+ meta.body = { originalChars: crushed.originalChars };
25631
+ return { value: crushed.text, meta };
25632
+ }
25633
+ return { value: text7, meta };
25634
+ }
25635
+ function attachCrush(payload, meta) {
25636
+ if (!meta.comments && !meta.body) return payload;
25637
+ return { ...payload, crush: meta };
25638
+ }
25639
+ function formatLinearIssuePayload(issue, include) {
25640
+ if (include === "full") return issue;
25641
+ const meta = emptyMeta();
25642
+ const comments = withComments(issue.comments, meta);
25643
+ const description = withBody(issue.description, comments.meta);
25644
+ const { team, ...rest } = issue;
25645
+ const next = {
25646
+ ...rest,
25647
+ ...team ? { team: { id: team.id, key: team.key, name: team.name } } : {},
25648
+ description: description.value,
25649
+ comments: comments.comments
25312
25650
  };
25651
+ return attachCrush(next, description.meta);
25313
25652
  }
25314
- function mcpJson(payload, isError = false) {
25315
- return {
25316
- content: [{ type: "text", text: JSON.stringify(payload) }],
25317
- ...isError ? { isError: true } : {}
25653
+ function formatGitHubIssuePayload(issue, include) {
25654
+ if (include === "full") return issue;
25655
+ const meta = emptyMeta();
25656
+ const comments = withComments(issue.comments, meta);
25657
+ const body = withBody(issue.body, comments.meta);
25658
+ const next = {
25659
+ ...issue,
25660
+ body: body.value,
25661
+ comments: comments.comments
25318
25662
  };
25663
+ return attachCrush(next, body.meta);
25664
+ }
25665
+ function formatAbleTimeTaskPayload(task, include) {
25666
+ const base = {
25667
+ ...toAbleTimeIssueInfo(task),
25668
+ description: task.description,
25669
+ state: task.state,
25670
+ comments: task.comments
25671
+ };
25672
+ if (include === "full") return base;
25673
+ const meta = emptyMeta();
25674
+ const comments = withComments(task.comments, meta);
25675
+ const description = withBody(task.description, comments.meta);
25676
+ return attachCrush(
25677
+ {
25678
+ ...base,
25679
+ description: description.value,
25680
+ comments: comments.comments
25681
+ },
25682
+ description.meta
25683
+ );
25319
25684
  }
25320
25685
 
25321
25686
  // src/mcp/abletime-tools.ts
@@ -25361,7 +25726,7 @@ function registerAbleTimeTools(server) {
25361
25726
  server.tool(
25362
25727
  "abletime_list_tasks",
25363
25728
  "List open AbleTime tasks (excludes done/archived). Optional projectId filter.",
25364
- { projectId: import_zod2.z.string().optional() },
25729
+ { projectId: import_zod3.z.string().optional() },
25365
25730
  async ({ projectId }) => {
25366
25731
  try {
25367
25732
  const tasks = await listAbleTimeTasks({ projectId });
@@ -25374,7 +25739,7 @@ function registerAbleTimeTools(server) {
25374
25739
  server.tool(
25375
25740
  "abletime_search_tasks",
25376
25741
  "Search AbleTime tasks by text, reference, or tag.",
25377
- { query: import_zod2.z.string() },
25742
+ { query: import_zod3.z.string() },
25378
25743
  async ({ query }) => {
25379
25744
  try {
25380
25745
  const tasks = await searchAbleTimeTasks(query);
@@ -25386,17 +25751,11 @@ function registerAbleTimeTools(server) {
25386
25751
  );
25387
25752
  server.tool(
25388
25753
  "abletime_get_task",
25389
- "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments.",
25390
- { id: import_zod2.z.string() },
25391
- async ({ id }) => {
25754
+ "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.",
25755
+ { id: import_zod3.z.string(), include: mcpIssueIncludeSchema },
25756
+ async ({ id, include }) => {
25392
25757
  try {
25393
- const task = await getAbleTimeTask(id);
25394
- return text2({
25395
- ...toAbleTimeIssueInfo(task),
25396
- description: task.description,
25397
- state: task.state,
25398
- comments: task.comments
25399
- });
25758
+ return text2(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
25400
25759
  } catch (err) {
25401
25760
  return fail2(err);
25402
25761
  }
@@ -25405,7 +25764,7 @@ function registerAbleTimeTools(server) {
25405
25764
  server.tool(
25406
25765
  "abletime_comment",
25407
25766
  "Add a markdown comment on an AbleTime task (id or CRM-232).",
25408
- { id: import_zod2.z.string(), body: import_zod2.z.string() },
25767
+ { id: import_zod3.z.string(), body: import_zod3.z.string() },
25409
25768
  async (args) => {
25410
25769
  try {
25411
25770
  return text2(await commentAbleTimeTask(args));
@@ -25418,10 +25777,10 @@ function registerAbleTimeTools(server) {
25418
25777
  "abletime_update_task",
25419
25778
  "Update an AbleTime task (id or CRM-232). Pass title, description, and/or state.",
25420
25779
  {
25421
- id: import_zod2.z.string(),
25422
- title: import_zod2.z.string().optional(),
25423
- description: import_zod2.z.string().optional(),
25424
- state: import_zod2.z.string().optional()
25780
+ id: import_zod3.z.string(),
25781
+ title: import_zod3.z.string().optional(),
25782
+ description: import_zod3.z.string().optional(),
25783
+ state: import_zod3.z.string().optional()
25425
25784
  },
25426
25785
  async (args) => {
25427
25786
  try {
@@ -25440,12 +25799,12 @@ function registerAbleTimeTools(server) {
25440
25799
  "abletime_create_task",
25441
25800
  "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).",
25442
25801
  {
25443
- title: import_zod2.z.string(),
25444
- description: import_zod2.z.string().optional(),
25445
- projectId: import_zod2.z.string().optional(),
25446
- categoryId: import_zod2.z.string().optional(),
25447
- state: import_zod2.z.enum(["backlog", "todo"]).optional(),
25448
- parent: import_zod2.z.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
25802
+ title: import_zod3.z.string(),
25803
+ description: import_zod3.z.string().optional(),
25804
+ projectId: import_zod3.z.string().optional(),
25805
+ categoryId: import_zod3.z.string().optional(),
25806
+ state: import_zod3.z.enum(["backlog", "todo"]).optional(),
25807
+ parent: import_zod3.z.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
25449
25808
  },
25450
25809
  async (args) => {
25451
25810
  try {
@@ -25459,10 +25818,10 @@ function registerAbleTimeTools(server) {
25459
25818
  "abletime_ensure_task",
25460
25819
  "Find or create an AbleTime task for this work, then create_thread with sourceType=ticket.",
25461
25820
  {
25462
- title: import_zod2.z.string(),
25463
- description: import_zod2.z.string().optional(),
25464
- projectId: import_zod2.z.string().optional(),
25465
- categoryId: import_zod2.z.string().optional()
25821
+ title: import_zod3.z.string(),
25822
+ description: import_zod3.z.string().optional(),
25823
+ projectId: import_zod3.z.string().optional(),
25824
+ categoryId: import_zod3.z.string().optional()
25466
25825
  },
25467
25826
  async (args) => {
25468
25827
  try {
@@ -25476,7 +25835,7 @@ function registerAbleTimeTools(server) {
25476
25835
  }
25477
25836
 
25478
25837
  // src/mcp/github-tools.ts
25479
- var import_zod3 = require("zod");
25838
+ var import_zod4 = require("zod");
25480
25839
  function text3(payload, isError = false) {
25481
25840
  return mcpJson(payload, isError);
25482
25841
  }
@@ -25486,15 +25845,15 @@ function fail3(err) {
25486
25845
  true
25487
25846
  );
25488
25847
  }
25489
- var repoPathSchema = import_zod3.z.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
25848
+ var repoPathSchema = import_zod4.z.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
25490
25849
  function registerGithubIssueTools(server) {
25491
25850
  server.tool(
25492
25851
  "github_get_issue",
25493
- "Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Uses Account gh.",
25494
- { id: import_zod3.z.string(), repoPath: repoPathSchema },
25495
- async ({ id, repoPath }) => {
25852
+ "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.",
25853
+ { id: import_zod4.z.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
25854
+ async ({ id, include, repoPath }) => {
25496
25855
  try {
25497
- return text3(await getGitHubIssue(id, { repoPath }));
25856
+ return text3(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
25498
25857
  } catch (err) {
25499
25858
  return fail3(err);
25500
25859
  }
@@ -25503,7 +25862,7 @@ function registerGithubIssueTools(server) {
25503
25862
  server.tool(
25504
25863
  "github_comment",
25505
25864
  "Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh.",
25506
- { id: import_zod3.z.string(), body: import_zod3.z.string(), repoPath: repoPathSchema },
25865
+ { id: import_zod4.z.string(), body: import_zod4.z.string(), repoPath: repoPathSchema },
25507
25866
  async ({ id, body, repoPath }) => {
25508
25867
  try {
25509
25868
  return text3(await commentGitHubIssue({ id, body }, { repoPath }));
@@ -25516,10 +25875,10 @@ function registerGithubIssueTools(server) {
25516
25875
  "github_update_issue",
25517
25876
  "Update a GitHub issue (#123). Pass title, body, and/or state (open|closed).",
25518
25877
  {
25519
- id: import_zod3.z.string(),
25520
- title: import_zod3.z.string().optional(),
25521
- body: import_zod3.z.string().optional(),
25522
- state: import_zod3.z.string().optional().describe("open or closed"),
25878
+ id: import_zod4.z.string(),
25879
+ title: import_zod4.z.string().optional(),
25880
+ body: import_zod4.z.string().optional(),
25881
+ state: import_zod4.z.string().optional().describe("open or closed"),
25523
25882
  repoPath: repoPathSchema
25524
25883
  },
25525
25884
  async (args) => {
@@ -25534,9 +25893,9 @@ function registerGithubIssueTools(server) {
25534
25893
  "github_create_issue",
25535
25894
  "Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
25536
25895
  {
25537
- title: import_zod3.z.string(),
25538
- body: import_zod3.z.string().optional(),
25539
- parent: import_zod3.z.string().optional().describe("Parent issue #123 or URL for a spin-off."),
25896
+ title: import_zod4.z.string(),
25897
+ body: import_zod4.z.string().optional(),
25898
+ parent: import_zod4.z.string().optional().describe("Parent issue #123 or URL for a spin-off."),
25540
25899
  repoPath: repoPathSchema
25541
25900
  },
25542
25901
  async (args) => {
@@ -25550,7 +25909,7 @@ function registerGithubIssueTools(server) {
25550
25909
  }
25551
25910
 
25552
25911
  // src/mcp/linear-tools.ts
25553
- var import_zod4 = require("zod");
25912
+ var import_zod5 = require("zod");
25554
25913
  function text4(payload, isError = false) {
25555
25914
  return mcpJson(payload, isError);
25556
25915
  }
@@ -25560,7 +25919,7 @@ function fail4(err) {
25560
25919
  true
25561
25920
  );
25562
25921
  }
25563
- var prioritySchema = import_zod4.z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
25922
+ var prioritySchema = import_zod5.z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
25564
25923
  function registerLinearTools(server) {
25565
25924
  server.tool(
25566
25925
  "linear_list_teams",
@@ -25578,9 +25937,9 @@ function registerLinearTools(server) {
25578
25937
  "linear_search_issues",
25579
25938
  "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.",
25580
25939
  {
25581
- query: import_zod4.z.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
25582
- 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."),
25583
- limit: import_zod4.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
25940
+ query: import_zod5.z.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
25941
+ 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."),
25942
+ limit: import_zod5.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
25584
25943
  },
25585
25944
  async ({ query, assignee, limit }) => {
25586
25945
  try {
@@ -25607,11 +25966,11 @@ function registerLinearTools(server) {
25607
25966
  );
25608
25967
  server.tool(
25609
25968
  "linear_get_issue",
25610
- "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children.",
25611
- { id: import_zod4.z.string() },
25612
- async ({ id }) => {
25969
+ "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.",
25970
+ { id: import_zod5.z.string(), include: mcpIssueIncludeSchema },
25971
+ async ({ id, include }) => {
25613
25972
  try {
25614
- return text4(await getLinearIssue(id));
25973
+ return text4(formatLinearIssuePayload(await getLinearIssue(id), include));
25615
25974
  } catch (err) {
25616
25975
  return fail4(err);
25617
25976
  }
@@ -25621,13 +25980,13 @@ function registerLinearTools(server) {
25621
25980
  "linear_create_issue",
25622
25981
  '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.',
25623
25982
  {
25624
- team: import_zod4.z.string(),
25625
- title: import_zod4.z.string(),
25626
- description: import_zod4.z.string().optional(),
25627
- state: import_zod4.z.string().optional(),
25628
- assignee: import_zod4.z.string().nullable().optional(),
25983
+ team: import_zod5.z.string(),
25984
+ title: import_zod5.z.string(),
25985
+ description: import_zod5.z.string().optional(),
25986
+ state: import_zod5.z.string().optional(),
25987
+ assignee: import_zod5.z.string().nullable().optional(),
25629
25988
  priority: prioritySchema,
25630
- parent: import_zod4.z.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
25989
+ parent: import_zod5.z.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
25631
25990
  },
25632
25991
  async (args) => {
25633
25992
  try {
@@ -25641,11 +26000,11 @@ function registerLinearTools(server) {
25641
26000
  "linear_update_issue",
25642
26001
  "Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority.",
25643
26002
  {
25644
- id: import_zod4.z.string(),
25645
- title: import_zod4.z.string().optional(),
25646
- description: import_zod4.z.string().optional(),
25647
- state: import_zod4.z.string().optional(),
25648
- assignee: import_zod4.z.string().nullable().optional(),
26003
+ id: import_zod5.z.string(),
26004
+ title: import_zod5.z.string().optional(),
26005
+ description: import_zod5.z.string().optional(),
26006
+ state: import_zod5.z.string().optional(),
26007
+ assignee: import_zod5.z.string().nullable().optional(),
25649
26008
  priority: prioritySchema
25650
26009
  },
25651
26010
  async (args) => {
@@ -25660,8 +26019,8 @@ function registerLinearTools(server) {
25660
26019
  "linear_comment",
25661
26020
  "Add a markdown comment on a Linear issue (uuid or ENG-123).",
25662
26021
  {
25663
- id: import_zod4.z.string(),
25664
- body: import_zod4.z.string()
26022
+ id: import_zod5.z.string(),
26023
+ body: import_zod5.z.string()
25665
26024
  },
25666
26025
  async (args) => {
25667
26026
  try {
@@ -25681,7 +26040,7 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
25681
26040
  }
25682
26041
 
25683
26042
  // src/mcp/viewer-context-tools.ts
25684
- var import_zod5 = require("zod");
26043
+ var import_zod6 = require("zod");
25685
26044
  init_worktree();
25686
26045
  init_app_settings();
25687
26046
  init_workspaces();
@@ -25716,7 +26075,7 @@ function registerViewerContextTools(server) {
25716
26075
  "get_viewer_context",
25717
26076
  "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).",
25718
26077
  {
25719
- repoPath: import_zod5.z.string().optional().describe(
26078
+ repoPath: import_zod6.z.string().optional().describe(
25720
26079
  "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
25721
26080
  )
25722
26081
  },
@@ -25736,10 +26095,10 @@ function registerViewerContextTools(server) {
25736
26095
  "update_viewer_context",
25737
26096
  "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.",
25738
26097
  {
25739
- scope: import_zod5.z.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
25740
- context: import_zod5.z.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
25741
- confirmed: import_zod5.z.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
25742
- repoPath: import_zod5.z.string().optional().describe(
26098
+ scope: import_zod6.z.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
26099
+ context: import_zod6.z.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
26100
+ confirmed: import_zod6.z.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
26101
+ repoPath: import_zod6.z.string().optional().describe(
25743
26102
  "Required for scope=project from orchestration (path from list_workspaces). Worktree turns may omit it (cwd)."
25744
26103
  )
25745
26104
  },
@@ -25820,14 +26179,11 @@ init_list_prs();
25820
26179
  init_app_settings();
25821
26180
 
25822
26181
  // src/mcp/schedule-tools.ts
25823
- var import_zod6 = require("zod");
26182
+ var import_zod7 = require("zod");
25824
26183
  init_schedules();
25825
26184
  init_schedule_runner();
25826
26185
  function text6(payload, isError = false) {
25827
- return {
25828
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
25829
- ...isError ? { isError: true } : {}
25830
- };
26186
+ return mcpJson(payload, isError);
25831
26187
  }
25832
26188
  function fail5(err) {
25833
26189
  return text6(
@@ -25865,17 +26221,17 @@ function registerScheduleTools(server) {
25865
26221
  "create_schedule",
25866
26222
  "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.",
25867
26223
  {
25868
- prompt: import_zod6.z.string().describe("User message / goal queued when the schedule fires"),
25869
- name: import_zod6.z.string().optional(),
25870
- at: import_zod6.z.string().optional().describe("ISO datetime for a one-shot"),
25871
- every: import_zod6.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
25872
- cron: import_zod6.z.string().optional().describe("5-field cron expression"),
25873
- tz: import_zod6.z.string().optional().describe("IANA timezone for cron (default: system)"),
25874
- threadId: import_zod6.z.string().optional().describe(
26224
+ prompt: import_zod7.z.string().describe("User message / goal queued when the schedule fires"),
26225
+ name: import_zod7.z.string().optional(),
26226
+ at: import_zod7.z.string().optional().describe("ISO datetime for a one-shot"),
26227
+ every: import_zod7.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
26228
+ cron: import_zod7.z.string().optional().describe("5-field cron expression"),
26229
+ tz: import_zod7.z.string().optional().describe("IANA timezone for cron (default: system)"),
26230
+ threadId: import_zod7.z.string().optional().describe(
25875
26231
  'Existing orchestration chat id, or "self" for this coordinator. Omit to create a new Global chat on fire.'
25876
26232
  ),
25877
- agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
25878
- model: import_zod6.z.string().optional()
26233
+ agent: import_zod7.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
26234
+ model: import_zod7.z.string().optional()
25879
26235
  },
25880
26236
  async (args) => {
25881
26237
  try {
@@ -25908,17 +26264,17 @@ function registerScheduleTools(server) {
25908
26264
  "update_schedule",
25909
26265
  "Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
25910
26266
  {
25911
- id: import_zod6.z.string(),
25912
- prompt: import_zod6.z.string().optional(),
25913
- name: import_zod6.z.string().optional(),
25914
- at: import_zod6.z.string().optional(),
25915
- every: import_zod6.z.string().optional(),
25916
- cron: import_zod6.z.string().optional(),
25917
- tz: import_zod6.z.string().optional(),
25918
- threadId: import_zod6.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
25919
- agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
25920
- model: import_zod6.z.string().optional(),
25921
- enabled: import_zod6.z.boolean().optional()
26267
+ id: import_zod7.z.string(),
26268
+ prompt: import_zod7.z.string().optional(),
26269
+ name: import_zod7.z.string().optional(),
26270
+ at: import_zod7.z.string().optional(),
26271
+ every: import_zod7.z.string().optional(),
26272
+ cron: import_zod7.z.string().optional(),
26273
+ tz: import_zod7.z.string().optional(),
26274
+ threadId: import_zod7.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
26275
+ agent: import_zod7.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
26276
+ model: import_zod7.z.string().optional(),
26277
+ enabled: import_zod7.z.boolean().optional()
25922
26278
  },
25923
26279
  async (args) => {
25924
26280
  try {
@@ -25956,7 +26312,7 @@ function registerScheduleTools(server) {
25956
26312
  server.tool(
25957
26313
  "delete_schedule",
25958
26314
  "Delete a local schedule. Pass id from list_schedules.",
25959
- { id: import_zod6.z.string() },
26315
+ { id: import_zod7.z.string() },
25960
26316
  async ({ id }) => {
25961
26317
  try {
25962
26318
  deleteSchedule(id);
@@ -25969,7 +26325,7 @@ function registerScheduleTools(server) {
25969
26325
  server.tool(
25970
26326
  "run_schedule",
25971
26327
  "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.",
25972
- { id: import_zod6.z.string() },
26328
+ { id: import_zod7.z.string() },
25973
26329
  async ({ id }) => {
25974
26330
  try {
25975
26331
  if (!getSchedule(id)) {
@@ -26463,13 +26819,13 @@ async function startMcpServer() {
26463
26819
  "list_board",
26464
26820
  "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.",
26465
26821
  {
26466
- query: import_zod7.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
26467
- repoPath: import_zod7.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
26468
- kind: import_zod7.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
26469
- column: import_zod7.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
26822
+ query: import_zod8.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
26823
+ repoPath: import_zod8.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
26824
+ kind: import_zod8.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
26825
+ column: import_zod8.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
26470
26826
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
26471
26827
  ),
26472
- limit: import_zod7.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
26828
+ limit: import_zod8.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
26473
26829
  },
26474
26830
  async ({ query, repoPath, kind, column, limit }) => {
26475
26831
  const workspaces = orch.listWorkspaces();
@@ -26484,30 +26840,19 @@ async function startMcpServer() {
26484
26840
  limit,
26485
26841
  workspaceName: (path2) => names.get(path2) ?? ""
26486
26842
  });
26487
- return {
26488
- content: [
26489
- {
26490
- type: "text",
26491
- text: JSON.stringify(
26492
- {
26493
- columns: snap.columns,
26494
- hidden: snap.hidden,
26495
- totals: snap.totals,
26496
- columnDefs: BOARD_COLUMN_DEFS,
26497
- hint: HOME_BOARD_AGENT_HINT
26498
- },
26499
- null,
26500
- 2
26501
- )
26502
- }
26503
- ]
26504
- };
26843
+ return mcpJson({
26844
+ columns: snap.columns,
26845
+ hidden: snap.hidden,
26846
+ totals: snap.totals,
26847
+ columnDefs: BOARD_COLUMN_DEFS,
26848
+ hint: HOME_BOARD_AGENT_HINT
26849
+ });
26505
26850
  }
26506
26851
  );
26507
26852
  server.tool(
26508
26853
  "get_thread",
26509
26854
  "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.",
26510
- { ref: import_zod7.z.string() },
26855
+ { ref: import_zod8.z.string() },
26511
26856
  async ({ ref }) => {
26512
26857
  const t = orch.getThread(ref);
26513
26858
  if (!t) {
@@ -26541,7 +26886,7 @@ async function startMcpServer() {
26541
26886
  usage: spend.usage,
26542
26887
  lastTurnUsage: spend.lastTurnUsage
26543
26888
  };
26544
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
26889
+ return mcpJson(summary);
26545
26890
  }
26546
26891
  );
26547
26892
  }
@@ -26549,17 +26894,17 @@ async function startMcpServer() {
26549
26894
  "present_artifact",
26550
26895
  "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.",
26551
26896
  {
26552
- title: import_zod7.z.string().describe("Short title shown in the artifact pane header"),
26553
- type: import_zod7.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
26897
+ title: import_zod8.z.string().describe("Short title shown in the artifact pane header"),
26898
+ type: import_zod8.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
26554
26899
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
26555
26900
  ),
26556
- content: import_zod7.z.string().describe(
26901
+ content: import_zod8.z.string().describe(
26557
26902
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
26558
26903
  ),
26559
- artifact_id: import_zod7.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
26560
- status: import_zod7.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
26561
- phase: import_zod7.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
26562
- mode: import_zod7.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
26904
+ artifact_id: import_zod8.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
26905
+ status: import_zod8.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
26906
+ phase: import_zod8.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
26907
+ mode: import_zod8.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
26563
26908
  },
26564
26909
  async ({ title, type, artifact_id, status, phase, mode }) => {
26565
26910
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26573,22 +26918,22 @@ async function startMcpServer() {
26573
26918
  mode: type === "log" ? mode ?? "append" : void 0,
26574
26919
  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."
26575
26920
  };
26576
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26921
+ return mcpJson(payload);
26577
26922
  }
26578
26923
  );
26579
26924
  server.tool(
26580
26925
  "ask_user",
26581
26926
  "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.",
26582
26927
  {
26583
- questions: import_zod7.z.array(
26584
- import_zod7.z.object({
26585
- question: import_zod7.z.string().describe("Full question text ending with ?"),
26586
- header: import_zod7.z.string().max(24).optional().describe("Short label shown above the question"),
26587
- multiSelect: import_zod7.z.boolean().optional().describe("Allow selecting multiple options"),
26588
- options: import_zod7.z.array(
26589
- import_zod7.z.object({
26590
- label: import_zod7.z.string(),
26591
- description: import_zod7.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
26928
+ questions: import_zod8.z.array(
26929
+ import_zod8.z.object({
26930
+ question: import_zod8.z.string().describe("Full question text ending with ?"),
26931
+ header: import_zod8.z.string().max(24).optional().describe("Short label shown above the question"),
26932
+ multiSelect: import_zod8.z.boolean().optional().describe("Allow selecting multiple options"),
26933
+ options: import_zod8.z.array(
26934
+ import_zod8.z.object({
26935
+ label: import_zod8.z.string(),
26936
+ description: import_zod8.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
26592
26937
  })
26593
26938
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
26594
26939
  })
@@ -26600,16 +26945,16 @@ async function startMcpServer() {
26600
26945
  questions,
26601
26946
  message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
26602
26947
  };
26603
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26948
+ return mcpJson(payload);
26604
26949
  }
26605
26950
  );
26606
26951
  server.tool(
26607
26952
  "present_plan",
26608
26953
  "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.",
26609
26954
  {
26610
- title: import_zod7.z.string().optional().describe("Short plan title (defaults to Plan)"),
26611
- content: import_zod7.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
26612
- thread_id: import_zod7.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
26955
+ title: import_zod8.z.string().optional().describe("Short plan title (defaults to Plan)"),
26956
+ content: import_zod8.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
26957
+ thread_id: import_zod8.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
26613
26958
  },
26614
26959
  async ({ title, content, thread_id }) => {
26615
26960
  const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
@@ -26625,22 +26970,22 @@ async function startMcpServer() {
26625
26970
  title: title?.trim() || "Plan",
26626
26971
  message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
26627
26972
  };
26628
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26973
+ return mcpJson(payload);
26629
26974
  }
26630
26975
  );
26631
26976
  server.tool(
26632
26977
  "present_schema",
26633
26978
  "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.",
26634
26979
  {
26635
- title: import_zod7.z.string().describe("Pane title"),
26636
- mode: import_zod7.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
26637
- datasource: import_zod7.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
26638
- resource_id: import_zod7.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
26639
- record_id: import_zod7.z.string().optional().describe("Record id when opening form mode"),
26640
- resource: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
26641
- record: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
26642
- records: import_zod7.z.array(import_zod7.z.record(import_zod7.z.unknown())).optional().describe("Inline records for table mode"),
26643
- pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
26980
+ title: import_zod8.z.string().describe("Pane title"),
26981
+ mode: import_zod8.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
26982
+ datasource: import_zod8.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
26983
+ resource_id: import_zod8.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
26984
+ record_id: import_zod8.z.string().optional().describe("Record id when opening form mode"),
26985
+ resource: import_zod8.z.record(import_zod8.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
26986
+ record: import_zod8.z.record(import_zod8.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
26987
+ records: import_zod8.z.array(import_zod8.z.record(import_zod8.z.unknown())).optional().describe("Inline records for table mode"),
26988
+ pane_id: import_zod8.z.string().optional().describe("Stable pane id across updates")
26644
26989
  },
26645
26990
  async (args) => {
26646
26991
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26654,17 +26999,17 @@ async function startMcpServer() {
26654
26999
  record_id: args.record_id,
26655
27000
  message: "Schema pane accepted. Sideboard desktop opens the CMS column beside chat."
26656
27001
  };
26657
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
27002
+ return mcpJson(payload);
26658
27003
  }
26659
27004
  );
26660
27005
  server.tool(
26661
27006
  "present_files",
26662
27007
  "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.",
26663
27008
  {
26664
- title: import_zod7.z.string().optional().describe("Pane title (default: Files)"),
26665
- datasource: import_zod7.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
26666
- path: import_zod7.z.string().optional().describe("Initial folder path (e.g. public)"),
26667
- pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
27009
+ title: import_zod8.z.string().optional().describe("Pane title (default: Files)"),
27010
+ datasource: import_zod8.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
27011
+ path: import_zod8.z.string().optional().describe("Initial folder path (e.g. public)"),
27012
+ pane_id: import_zod8.z.string().optional().describe("Stable pane id across updates")
26668
27013
  },
26669
27014
  async (args) => {
26670
27015
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26676,21 +27021,21 @@ async function startMcpServer() {
26676
27021
  path: args.path,
26677
27022
  message: "Files pane accepted. Sideboard desktop opens the Files column beside chat."
26678
27023
  };
26679
- return { content: [{ type: "text", text: JSON.stringify(payload) }] };
27024
+ return mcpJson(payload);
26680
27025
  }
26681
27026
  );
26682
27027
  server.tool(
26683
27028
  "wait_for_job",
26684
27029
  "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.",
26685
27030
  {
26686
- id: import_zod7.z.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
26687
- timeoutMs: import_zod7.z.number().optional()
27031
+ id: import_zod8.z.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
27032
+ timeoutMs: import_zod8.z.number().optional()
26688
27033
  },
26689
27034
  async ({ id, timeoutMs }) => {
26690
27035
  const result = await waitForDetachedJob(process.cwd(), id, {
26691
27036
  timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
26692
27037
  });
26693
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
27038
+ return mcpJson(result);
26694
27039
  }
26695
27040
  );
26696
27041
  if (!worktreeProfile) {
@@ -26705,56 +27050,44 @@ async function startMcpServer() {
26705
27050
  "set_caffeinate",
26706
27051
  "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.",
26707
27052
  {
26708
- enabled: import_zod7.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
27053
+ enabled: import_zod8.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
26709
27054
  },
26710
27055
  async ({ enabled }) => {
26711
27056
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
26712
27057
  const state = setCaffeinateHold2(enabled, { threadId });
26713
27058
  if (enabled && !state.held) {
26714
- return {
26715
- content: [
26716
- {
26717
- type: "text",
26718
- text: JSON.stringify({
26719
- ...state,
26720
- ok: false,
26721
- message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
26722
- })
26723
- }
26724
- ],
26725
- isError: true
26726
- };
26727
- }
26728
- return {
26729
- content: [
27059
+ return mcpJson(
26730
27060
  {
26731
- type: "text",
26732
- text: JSON.stringify({
26733
- ...getCaffeinateHold2(),
26734
- ok: true,
26735
- 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)."
26736
- })
26737
- }
26738
- ]
26739
- };
27061
+ ...state,
27062
+ ok: false,
27063
+ message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
27064
+ },
27065
+ true
27066
+ );
27067
+ }
27068
+ return mcpJson({
27069
+ ...getCaffeinateHold2(),
27070
+ ok: true,
27071
+ 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)."
27072
+ });
26740
27073
  }
26741
27074
  );
26742
27075
  server.tool(
26743
27076
  "create_thread",
26744
27077
  `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.`,
26745
27078
  {
26746
- sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]),
26747
- sourceRef: import_zod7.z.string(),
26748
- agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
26749
- model: import_zod7.z.string().nullable().optional().describe(
27079
+ sourceType: import_zod8.z.enum(["branch", "pr", "ticket"]),
27080
+ sourceRef: import_zod8.z.string(),
27081
+ agent: import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
27082
+ model: import_zod8.z.string().nullable().optional().describe(
26750
27083
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
26751
27084
  ),
26752
- repoPath: import_zod7.z.string(),
26753
- title: import_zod7.z.string().optional(),
26754
- cowboy: import_zod7.z.boolean().optional().describe(
27085
+ repoPath: import_zod8.z.string(),
27086
+ title: import_zod8.z.string().optional(),
27087
+ cowboy: import_zod8.z.boolean().optional().describe(
26755
27088
  "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."
26756
27089
  ),
26757
- parentThreadId: import_zod7.z.string().optional().describe(
27090
+ parentThreadId: import_zod8.z.string().optional().describe(
26758
27091
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
26759
27092
  )
26760
27093
  },
@@ -26770,10 +27103,10 @@ async function startMcpServer() {
26770
27103
  "start_board_card",
26771
27104
  "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.",
26772
27105
  {
26773
- kind: import_zod7.z.enum(["ticket", "pr", "branch"]),
26774
- ref: import_zod7.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
26775
- repoPath: import_zod7.z.string().describe("Workspace path from list_workspaces / list_board"),
26776
- title: import_zod7.z.string().optional()
27106
+ kind: import_zod8.z.enum(["ticket", "pr", "branch"]),
27107
+ ref: import_zod8.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
27108
+ repoPath: import_zod8.z.string().describe("Workspace path from list_workspaces / list_board"),
27109
+ title: import_zod8.z.string().optional()
26777
27110
  },
26778
27111
  async ({ kind, ref, repoPath, title }) => {
26779
27112
  const root = await resolveRepoRoot(repoPath);
@@ -26790,20 +27123,13 @@ async function startMcpServer() {
26790
27123
  }))
26791
27124
  );
26792
27125
  if (existing) {
26793
- return {
26794
- content: [
26795
- {
26796
- type: "text",
26797
- text: JSON.stringify({
26798
- alreadyStarted: true,
26799
- id: existing.id,
26800
- title: existing.title,
26801
- status: existing.status,
26802
- link: `sideboard://thread/${existing.id}`
26803
- })
26804
- }
26805
- ]
26806
- };
27126
+ return mcpJson({
27127
+ alreadyStarted: true,
27128
+ id: existing.id,
27129
+ title: existing.title,
27130
+ status: existing.status,
27131
+ link: `sideboard://thread/${existing.id}`
27132
+ });
26807
27133
  }
26808
27134
  const workspaces = orch.listWorkspaces();
26809
27135
  const loaded = await getHomeBoardInputs(workspaces);
@@ -26877,9 +27203,9 @@ async function startMcpServer() {
26877
27203
  "send_to_thread",
26878
27204
  '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.',
26879
27205
  {
26880
- ref: import_zod7.z.string(),
26881
- prompt: import_zod7.z.string(),
26882
- force_stop: import_zod7.z.boolean().optional()
27206
+ ref: import_zod8.z.string(),
27207
+ prompt: import_zod8.z.string(),
27208
+ force_stop: import_zod8.z.boolean().optional()
26883
27209
  },
26884
27210
  async ({ ref, prompt, force_stop }) => {
26885
27211
  if (force_stop) {
@@ -26889,79 +27215,58 @@ async function startMcpServer() {
26889
27215
  }
26890
27216
  }
26891
27217
  const thread = await orch.send(ref, prompt);
26892
- return {
26893
- content: [
26894
- {
26895
- type: "text",
26896
- text: JSON.stringify({
26897
- id: thread.id,
26898
- status: thread.status,
26899
- queueLength: thread.queue.length,
26900
- forceStopped: Boolean(force_stop)
26901
- })
26902
- }
26903
- ]
26904
- };
27218
+ return mcpJson({
27219
+ id: thread.id,
27220
+ status: thread.status,
27221
+ queueLength: thread.queue.length,
27222
+ forceStopped: Boolean(force_stop)
27223
+ });
26905
27224
  }
26906
27225
  );
26907
27226
  server.tool(
26908
27227
  "wait_for_turn",
26909
27228
  "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).",
26910
27229
  {
26911
- ref: import_zod7.z.string(),
26912
- timeoutMs: import_zod7.z.number().optional()
27230
+ ref: import_zod8.z.string(),
27231
+ timeoutMs: import_zod8.z.number().optional()
26913
27232
  },
26914
27233
  async ({ ref, timeoutMs }) => {
26915
27234
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
26916
27235
  resolveIfStillRunning: true
26917
27236
  });
26918
27237
  const result = orch.getTurnResult(thread.id);
26919
- return {
26920
- content: [
26921
- {
26922
- type: "text",
26923
- text: JSON.stringify({
26924
- id: thread.id,
26925
- status: result.status,
26926
- text: result.text,
26927
- lastError: result.lastError,
26928
- stillRunning: result.stillRunning,
26929
- progress: result.progress,
26930
- lastActivityAt: result.lastActivityAt,
26931
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
26932
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
26933
- })
26934
- }
26935
- ]
26936
- };
27238
+ return mcpJson({
27239
+ id: thread.id,
27240
+ status: result.status,
27241
+ text: result.text,
27242
+ lastError: result.lastError,
27243
+ stillRunning: result.stillRunning,
27244
+ progress: result.progress,
27245
+ lastActivityAt: result.lastActivityAt,
27246
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
27247
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
27248
+ });
26937
27249
  }
26938
27250
  );
26939
27251
  server.tool(
26940
27252
  "get_turn_result",
26941
27253
  "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).",
26942
- { ref: import_zod7.z.string() },
27254
+ { ref: import_zod8.z.string() },
26943
27255
  async ({ ref }) => {
26944
27256
  const result = orch.getTurnResult(ref);
26945
- return {
26946
- content: [
26947
- {
26948
- type: "text",
26949
- text: JSON.stringify({
26950
- ...result,
26951
- hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
26952
- incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
26953
- })
26954
- }
26955
- ]
26956
- };
27257
+ return mcpJson({
27258
+ ...result,
27259
+ hint: result.stillRunning ? mcpWaitStillRunningHint(result.status) : mcpWaitFinishedHint(result.status),
27260
+ incomplete: !result.stillRunning && Boolean(mcpWaitFinishedHint(result.status))
27261
+ });
26957
27262
  }
26958
27263
  );
26959
27264
  server.tool(
26960
27265
  "stop_thread",
26961
27266
  "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.",
26962
27267
  {
26963
- ref: import_zod7.z.string(),
26964
- force: import_zod7.z.boolean().optional()
27268
+ ref: import_zod8.z.string(),
27269
+ force: import_zod8.z.boolean().optional()
26965
27270
  },
26966
27271
  async ({ ref, force }) => {
26967
27272
  const t = orch.getThread(ref);
@@ -26974,24 +27279,17 @@ async function startMcpServer() {
26974
27279
  const clearQueue = force !== false;
26975
27280
  const hadQueued = t.queue.length > 0;
26976
27281
  const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
26977
- return {
26978
- content: [
26979
- {
26980
- type: "text",
26981
- text: JSON.stringify({
26982
- id: stopped.id,
26983
- status: stopped.status,
26984
- clearedQueue: clearQueue && hadQueued
26985
- })
26986
- }
26987
- ]
26988
- };
27282
+ return mcpJson({
27283
+ id: stopped.id,
27284
+ status: stopped.status,
27285
+ clearedQueue: clearQueue && hadQueued
27286
+ });
26989
27287
  }
26990
27288
  );
26991
27289
  server.tool(
26992
27290
  "archive_thread",
26993
27291
  "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.",
26994
- { ref: import_zod7.z.string() },
27292
+ { ref: import_zod8.z.string() },
26995
27293
  async ({ ref }) => {
26996
27294
  const t = orch.getThread(ref);
26997
27295
  if (!t) {
@@ -27008,38 +27306,24 @@ async function startMcpServer() {
27008
27306
  };
27009
27307
  }
27010
27308
  const archived = await orch.archive(ref);
27011
- return {
27012
- content: [
27013
- {
27014
- type: "text",
27015
- text: JSON.stringify({
27016
- id: archived.id,
27017
- status: archived.status
27018
- })
27019
- }
27020
- ]
27021
- };
27309
+ return mcpJson({
27310
+ id: archived.id,
27311
+ status: archived.status
27312
+ });
27022
27313
  }
27023
27314
  );
27024
27315
  server.tool(
27025
27316
  "restore_thread",
27026
27317
  "Restore an archived thread (recreates worktree from branch when needed)",
27027
- { ref: import_zod7.z.string() },
27318
+ { ref: import_zod8.z.string() },
27028
27319
  async ({ ref }) => {
27029
27320
  try {
27030
27321
  const restored = await orch.restore(ref);
27031
- return {
27032
- content: [
27033
- {
27034
- type: "text",
27035
- text: JSON.stringify({
27036
- id: restored.id,
27037
- status: restored.status,
27038
- worktreePath: restored.worktreePath
27039
- })
27040
- }
27041
- ]
27042
- };
27322
+ return mcpJson({
27323
+ id: restored.id,
27324
+ status: restored.status,
27325
+ worktreePath: restored.worktreePath
27326
+ });
27043
27327
  } catch (err) {
27044
27328
  const message = err instanceof Error ? err.message : String(err);
27045
27329
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27050,34 +27334,25 @@ async function startMcpServer() {
27050
27334
  "get_diff",
27051
27335
  "Compact diff summary (capped hunks, paginated)",
27052
27336
  {
27053
- ref: import_zod7.z.string(),
27054
- maxFiles: import_zod7.z.number().optional()
27337
+ ref: import_zod8.z.string(),
27338
+ maxFiles: import_zod8.z.number().optional()
27055
27339
  },
27056
27340
  async ({ ref, maxFiles }) => {
27057
27341
  const summary = await orch.diffSummary(ref);
27058
- return {
27059
- content: [
27060
- {
27061
- type: "text",
27062
- text: JSON.stringify({
27063
- ...summary,
27064
- files: summary.files.slice(0, maxFiles ?? 10)
27065
- })
27066
- }
27067
- ]
27068
- };
27342
+ return mcpJson({
27343
+ ...summary,
27344
+ files: summary.files.slice(0, maxFiles ?? 10)
27345
+ });
27069
27346
  }
27070
27347
  );
27071
27348
  server.tool(
27072
27349
  "get_pr_checks",
27073
27350
  "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.",
27074
- { ref: import_zod7.z.string().describe("Worktree thread id/ref") },
27351
+ { ref: import_zod8.z.string().describe("Worktree thread id/ref") },
27075
27352
  async ({ ref }) => {
27076
27353
  try {
27077
27354
  const checks = await orch.getPrChecks(ref);
27078
- return {
27079
- content: [{ type: "text", text: JSON.stringify(checks) }]
27080
- };
27355
+ return mcpJson(checks);
27081
27356
  } catch (err) {
27082
27357
  const message = err instanceof Error ? err.message : String(err);
27083
27358
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27087,25 +27362,18 @@ async function startMcpServer() {
27087
27362
  server.tool(
27088
27363
  "request_review",
27089
27364
  '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.',
27090
- { ref: import_zod7.z.string().describe("Worktree thread id/ref to review") },
27365
+ { ref: import_zod8.z.string().describe("Worktree thread id/ref to review") },
27091
27366
  async ({ ref }) => {
27092
27367
  try {
27093
27368
  const tab = await orch.requestReview(ref);
27094
27369
  const from = orch.getThread(ref);
27095
- return {
27096
- content: [
27097
- {
27098
- type: "text",
27099
- text: JSON.stringify({
27100
- id: tab.id,
27101
- title: tab.title,
27102
- status: tab.status,
27103
- fromThreadId: from?.id ?? ref,
27104
- link: `sideboard://thread/${tab.id}`
27105
- })
27106
- }
27107
- ]
27108
- };
27370
+ return mcpJson({
27371
+ id: tab.id,
27372
+ title: tab.title,
27373
+ status: tab.status,
27374
+ fromThreadId: from?.id ?? ref,
27375
+ link: `sideboard://thread/${tab.id}`
27376
+ });
27109
27377
  } catch (err) {
27110
27378
  const message = err instanceof Error ? err.message : String(err);
27111
27379
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27116,53 +27384,37 @@ async function startMcpServer() {
27116
27384
  "ask_git",
27117
27385
  "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.",
27118
27386
  {
27119
- ref: import_zod7.z.string().describe("Worktree thread id/ref"),
27120
- action: import_zod7.z.enum(AGENT_GIT_ACTIONS).describe(
27387
+ ref: import_zod8.z.string().describe("Worktree thread id/ref"),
27388
+ action: import_zod8.z.enum(AGENT_GIT_ACTIONS).describe(
27121
27389
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
27122
27390
  )
27123
27391
  },
27124
27392
  async ({ ref, action }) => {
27125
27393
  try {
27126
27394
  const thread = await orch.askGit(ref, action);
27127
- return {
27128
- content: [
27129
- {
27130
- type: "text",
27131
- text: JSON.stringify({
27132
- id: thread.id,
27133
- status: thread.status,
27134
- queueLength: thread.queue.length,
27135
- action,
27136
- link: `sideboard://thread/${thread.id}`
27137
- })
27138
- }
27139
- ]
27140
- };
27395
+ return mcpJson({
27396
+ id: thread.id,
27397
+ status: thread.status,
27398
+ queueLength: thread.queue.length,
27399
+ action,
27400
+ link: `sideboard://thread/${thread.id}`
27401
+ });
27141
27402
  } catch (err) {
27142
27403
  const raw = err instanceof Error ? err.message : String(err);
27143
27404
  const message = formatGhLandError(raw);
27144
- return {
27145
- content: [
27146
- {
27147
- type: "text",
27148
- text: JSON.stringify(
27149
- {
27150
- error: true,
27151
- action,
27152
- lastError: message,
27153
- 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
27154
- },
27155
- null,
27156
- 2
27157
- )
27158
- }
27159
- ],
27160
- isError: true
27161
- };
27405
+ return mcpJson(
27406
+ {
27407
+ error: true,
27408
+ action,
27409
+ lastError: message,
27410
+ 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
27411
+ },
27412
+ true
27413
+ );
27162
27414
  }
27163
27415
  }
27164
27416
  );
27165
- const agentEnum = import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
27417
+ const agentEnum = import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
27166
27418
  server.tool(
27167
27419
  "list_models",
27168
27420
  "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.",
@@ -27172,9 +27424,7 @@ async function startMcpServer() {
27172
27424
  async ({ agent }) => {
27173
27425
  try {
27174
27426
  const catalogs = await listModelsForAgent(agent);
27175
- return {
27176
- content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
27177
- };
27427
+ return mcpJson(catalogs);
27178
27428
  } catch (err) {
27179
27429
  const message = err instanceof Error ? err.message : String(err);
27180
27430
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27185,11 +27435,11 @@ async function startMcpServer() {
27185
27435
  "fork_worktree",
27186
27436
  "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.",
27187
27437
  {
27188
- ref: import_zod7.z.string().describe("Worktree thread id/ref to fork"),
27189
- through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27438
+ ref: import_zod8.z.string().describe("Worktree thread id/ref to fork"),
27439
+ through_index: import_zod8.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27190
27440
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
27191
- model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27192
- title: import_zod7.z.string().optional()
27441
+ model: import_zod8.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27442
+ title: import_zod8.z.string().optional()
27193
27443
  },
27194
27444
  async ({ ref, through_index, agent, model, title }) => {
27195
27445
  try {
@@ -27202,24 +27452,17 @@ async function startMcpServer() {
27202
27452
  model,
27203
27453
  title
27204
27454
  });
27205
- return {
27206
- content: [
27207
- {
27208
- type: "text",
27209
- text: JSON.stringify({
27210
- id: thread.id,
27211
- title: thread.title,
27212
- status: thread.status,
27213
- agent: thread.agent,
27214
- model: thread.model,
27215
- branchName: thread.branchName,
27216
- worktreePath: thread.worktreePath,
27217
- fromThreadId: source?.id ?? ref,
27218
- link: `sideboard://thread/${thread.id}`
27219
- })
27220
- }
27221
- ]
27222
- };
27455
+ return mcpJson({
27456
+ id: thread.id,
27457
+ title: thread.title,
27458
+ status: thread.status,
27459
+ agent: thread.agent,
27460
+ model: thread.model,
27461
+ branchName: thread.branchName,
27462
+ worktreePath: thread.worktreePath,
27463
+ fromThreadId: source?.id ?? ref,
27464
+ link: `sideboard://thread/${thread.id}`
27465
+ });
27223
27466
  } catch (err) {
27224
27467
  const message = err instanceof Error ? err.message : String(err);
27225
27468
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27230,11 +27473,11 @@ async function startMcpServer() {
27230
27473
  "fork_chat",
27231
27474
  "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.",
27232
27475
  {
27233
- ref: import_zod7.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
27234
- through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27476
+ ref: import_zod8.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
27477
+ through_index: import_zod8.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27235
27478
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
27236
- model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27237
- title: import_zod7.z.string().optional()
27479
+ model: import_zod8.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27480
+ title: import_zod8.z.string().optional()
27238
27481
  },
27239
27482
  async ({ ref, through_index, agent, model, title }) => {
27240
27483
  try {
@@ -27252,24 +27495,17 @@ async function startMcpServer() {
27252
27495
  model,
27253
27496
  title
27254
27497
  });
27255
- return {
27256
- content: [
27257
- {
27258
- type: "text",
27259
- text: JSON.stringify({
27260
- id: tab.id,
27261
- title: tab.title,
27262
- status: tab.status,
27263
- agent: tab.agent,
27264
- model: tab.model,
27265
- sourceType: tab.sourceType,
27266
- worktreePath: tab.worktreePath,
27267
- fromThreadId: source.id,
27268
- link: `sideboard://thread/${tab.id}`
27269
- })
27270
- }
27271
- ]
27272
- };
27498
+ return mcpJson({
27499
+ id: tab.id,
27500
+ title: tab.title,
27501
+ status: tab.status,
27502
+ agent: tab.agent,
27503
+ model: tab.model,
27504
+ sourceType: tab.sourceType,
27505
+ worktreePath: tab.worktreePath,
27506
+ fromThreadId: source.id,
27507
+ link: `sideboard://thread/${tab.id}`
27508
+ });
27273
27509
  } catch (err) {
27274
27510
  const message = err instanceof Error ? err.message : String(err);
27275
27511
  return { content: [{ type: "text", text: message }], isError: true };
@@ -27280,49 +27516,35 @@ async function startMcpServer() {
27280
27516
  "run_dev_script",
27281
27517
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
27282
27518
  {
27283
- ref: import_zod7.z.string(),
27284
- name: import_zod7.z.string().optional()
27519
+ ref: import_zod8.z.string(),
27520
+ name: import_zod8.z.string().optional()
27285
27521
  },
27286
27522
  async ({ ref, name }) => {
27287
27523
  const result = await orch.startDev(ref, name);
27288
- return {
27289
- content: [
27290
- {
27291
- type: "text",
27292
- text: JSON.stringify({
27293
- port: result.port,
27294
- scriptName: result.scriptName,
27295
- ports: result.ports,
27296
- url: `http://localhost:${result.port}`
27297
- })
27298
- }
27299
- ]
27300
- };
27524
+ return mcpJson({
27525
+ port: result.port,
27526
+ scriptName: result.scriptName,
27527
+ ports: result.ports,
27528
+ url: `http://localhost:${result.port}`
27529
+ });
27301
27530
  }
27302
27531
  );
27303
27532
  server.tool(
27304
27533
  "list_run_scripts",
27305
27534
  "List named run scripts available for a thread",
27306
- { ref: import_zod7.z.string() },
27535
+ { ref: import_zod8.z.string() },
27307
27536
  async ({ ref }) => {
27308
27537
  const scripts = orch.listThreadRunScripts(ref);
27309
27538
  const active = orch.getActiveRuns(ref);
27310
- return {
27311
- content: [
27312
- {
27313
- type: "text",
27314
- text: JSON.stringify({ scripts, active }, null, 2)
27315
- }
27316
- ]
27317
- };
27539
+ return mcpJson({ scripts, active });
27318
27540
  }
27319
27541
  );
27320
27542
  server.tool(
27321
27543
  "stop_dev_script",
27322
27544
  "Stop a running script for a thread (all scripts if name omitted)",
27323
27545
  {
27324
- ref: import_zod7.z.string(),
27325
- name: import_zod7.z.string().optional()
27546
+ ref: import_zod8.z.string(),
27547
+ name: import_zod8.z.string().optional()
27326
27548
  },
27327
27549
  async ({ ref, name }) => {
27328
27550
  orch.stopDev(ref, name);
@@ -27332,27 +27554,25 @@ async function startMcpServer() {
27332
27554
  server.tool(
27333
27555
  "run_setup",
27334
27556
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
27335
- { ref: import_zod7.z.string() },
27557
+ { ref: import_zod8.z.string() },
27336
27558
  async ({ ref }) => {
27337
27559
  const result = await orch.runSetup(ref);
27338
- return {
27339
- content: [{ type: "text", text: JSON.stringify(result) }]
27340
- };
27560
+ return mcpJson(result);
27341
27561
  }
27342
27562
  );
27343
27563
  server.tool(
27344
27564
  "add_workspace",
27345
27565
  "Register a git repo as a Sideboard workspace",
27346
- { repoPath: import_zod7.z.string() },
27566
+ { repoPath: import_zod8.z.string() },
27347
27567
  async ({ repoPath }) => {
27348
27568
  const ws = await orch.addWorkspace(repoPath);
27349
- return { content: [{ type: "text", text: JSON.stringify(ws) }] };
27569
+ return mcpJson(ws);
27350
27570
  }
27351
27571
  );
27352
27572
  server.tool(
27353
27573
  "remove_workspace",
27354
27574
  "Unregister a Sideboard workspace (does not archive threads)",
27355
- { repoPath: import_zod7.z.string() },
27575
+ { repoPath: import_zod8.z.string() },
27356
27576
  async ({ repoPath }) => {
27357
27577
  orch.removeWorkspace(repoPath);
27358
27578
  return { content: [{ type: "text", text: "ok" }] };
@@ -27362,40 +27582,31 @@ async function startMcpServer() {
27362
27582
  "fanout",
27363
27583
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
27364
27584
  {
27365
- prompt: import_zod7.z.string(),
27366
- agents: import_zod7.z.array(import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
27367
- repoPath: import_zod7.z.string(),
27368
- sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]).optional(),
27369
- sourceRef: import_zod7.z.string().optional(),
27370
- title: import_zod7.z.string().optional()
27585
+ prompt: import_zod8.z.string(),
27586
+ agents: import_zod8.z.array(import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
27587
+ repoPath: import_zod8.z.string(),
27588
+ sourceType: import_zod8.z.enum(["branch", "pr", "ticket"]).optional(),
27589
+ sourceRef: import_zod8.z.string().optional(),
27590
+ title: import_zod8.z.string().optional()
27371
27591
  },
27372
27592
  async (args) => {
27373
27593
  const threads = await orch.bestOfN(args);
27374
- return {
27375
- content: [
27376
- {
27377
- type: "text",
27378
- text: JSON.stringify(
27379
- threads.map((t) => ({
27380
- id: t.id,
27381
- agent: t.agent,
27382
- branchName: t.branchName,
27383
- worktreePath: t.worktreePath
27384
- })),
27385
- null,
27386
- 2
27387
- )
27388
- }
27389
- ]
27390
- };
27594
+ return mcpJson(
27595
+ threads.map((t) => ({
27596
+ id: t.id,
27597
+ agent: t.agent,
27598
+ branchName: t.branchName,
27599
+ worktreePath: t.worktreePath
27600
+ }))
27601
+ );
27391
27602
  }
27392
27603
  );
27393
27604
  server.tool(
27394
27605
  "list_branches",
27395
27606
  "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).",
27396
27607
  {
27397
- repoPath: import_zod7.z.string(),
27398
- unmergedOnly: import_zod7.z.boolean().optional()
27608
+ repoPath: import_zod8.z.string(),
27609
+ unmergedOnly: import_zod8.z.boolean().optional()
27399
27610
  },
27400
27611
  async ({ repoPath, unmergedOnly }) => {
27401
27612
  const root = await resolveRepoRoot(repoPath);
@@ -27416,19 +27627,19 @@ async function startMcpServer() {
27416
27627
  "list_prs",
27417
27628
  '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.',
27418
27629
  {
27419
- repoPath: import_zod7.z.string(),
27420
- query: import_zod7.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
27421
- queue: import_zod7.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
27630
+ repoPath: import_zod8.z.string(),
27631
+ query: import_zod8.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
27632
+ queue: import_zod8.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
27422
27633
  'review = unclaimed eng-review inbox (use for "get me N tickets to review"). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes.'
27423
27634
  ),
27424
- state: import_zod7.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
27425
- label: import_zod7.z.string().optional().describe(
27635
+ state: import_zod8.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
27636
+ label: import_zod8.z.string().optional().describe(
27426
27637
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
27427
27638
  ),
27428
- reviewer: import_zod7.z.string().optional().describe(
27639
+ reviewer: import_zod8.z.string().optional().describe(
27429
27640
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
27430
27641
  ),
27431
- limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27642
+ limit: import_zod8.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27432
27643
  },
27433
27644
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
27434
27645
  const root = await resolveRepoRoot(repoPath);
@@ -27460,89 +27671,65 @@ async function startMcpServer() {
27460
27671
  server.tool(
27461
27672
  "get_pr_stack",
27462
27673
  "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).",
27463
- { ref: import_zod7.z.string() },
27674
+ { ref: import_zod8.z.string() },
27464
27675
  async ({ ref }) => {
27465
27676
  const stack = await orch.getPrStack(ref);
27466
- return {
27467
- content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
27468
- };
27677
+ return mcpJson(stack);
27469
27678
  }
27470
27679
  );
27471
27680
  server.tool(
27472
27681
  "open_pr_stack_layers",
27473
27682
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
27474
27683
  {
27475
- ref: import_zod7.z.string(),
27476
- layer: import_zod7.z.number().int().positive().optional()
27684
+ ref: import_zod8.z.string(),
27685
+ layer: import_zod8.z.number().int().positive().optional()
27477
27686
  },
27478
27687
  async ({ ref, layer }) => {
27479
27688
  const result = await orch.openPrStackLayers(ref, { layer });
27480
- return {
27481
- content: [
27482
- {
27483
- type: "text",
27484
- text: JSON.stringify(
27485
- {
27486
- stackNumber: result.stack.stackNumber,
27487
- trunk: result.stack.trunk,
27488
- threads: result.threads.map((t) => ({
27489
- id: t.id,
27490
- title: t.title,
27491
- branchName: t.branchName,
27492
- stackLayer: t.stackLayer,
27493
- worktreePath: t.worktreePath,
27494
- prUrl: t.prUrl,
27495
- link: `sideboard://thread/${t.id}`
27496
- }))
27497
- },
27498
- null,
27499
- 2
27500
- )
27501
- }
27502
- ]
27503
- };
27689
+ return mcpJson({
27690
+ stackNumber: result.stack.stackNumber,
27691
+ trunk: result.stack.trunk,
27692
+ threads: result.threads.map((t) => ({
27693
+ id: t.id,
27694
+ title: t.title,
27695
+ branchName: t.branchName,
27696
+ stackLayer: t.stackLayer,
27697
+ worktreePath: t.worktreePath,
27698
+ prUrl: t.prUrl,
27699
+ link: `sideboard://thread/${t.id}`
27700
+ }))
27701
+ });
27504
27702
  }
27505
27703
  );
27506
27704
  server.tool(
27507
27705
  "add_stack_layer",
27508
27706
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
27509
27707
  {
27510
- ref: import_zod7.z.string(),
27511
- branchName: import_zod7.z.string(),
27512
- title: import_zod7.z.string().optional()
27708
+ ref: import_zod8.z.string(),
27709
+ branchName: import_zod8.z.string(),
27710
+ title: import_zod8.z.string().optional()
27513
27711
  },
27514
27712
  async ({ ref, branchName, title }) => {
27515
27713
  const result = await orch.addStackLayer(ref, branchName, { title });
27516
- return {
27517
- content: [
27518
- {
27519
- type: "text",
27520
- text: JSON.stringify(
27521
- {
27522
- id: result.thread.id,
27523
- title: result.thread.title,
27524
- branchName: result.thread.branchName,
27525
- stackLayer: result.thread.stackLayer,
27526
- worktreePath: result.thread.worktreePath,
27527
- link: `sideboard://thread/${result.thread.id}`
27528
- },
27529
- null,
27530
- 2
27531
- )
27532
- }
27533
- ]
27534
- };
27714
+ return mcpJson({
27715
+ id: result.thread.id,
27716
+ title: result.thread.title,
27717
+ branchName: result.thread.branchName,
27718
+ stackLayer: result.thread.stackLayer,
27719
+ worktreePath: result.thread.worktreePath,
27720
+ link: `sideboard://thread/${result.thread.id}`
27721
+ });
27535
27722
  }
27536
27723
  );
27537
27724
  server.tool(
27538
27725
  "create_pr_stack",
27539
27726
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
27540
27727
  {
27541
- repoPath: import_zod7.z.string(),
27542
- branches: import_zod7.z.array(import_zod7.z.string()).min(1),
27543
- agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
27544
- base: import_zod7.z.string().optional(),
27545
- title: import_zod7.z.string().optional()
27728
+ repoPath: import_zod8.z.string(),
27729
+ branches: import_zod8.z.array(import_zod8.z.string()).min(1),
27730
+ agent: import_zod8.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
27731
+ base: import_zod8.z.string().optional(),
27732
+ title: import_zod8.z.string().optional()
27546
27733
  },
27547
27734
  async (args) => {
27548
27735
  const result = await orch.createPrStack({
@@ -27552,39 +27739,28 @@ async function startMcpServer() {
27552
27739
  base: args.base,
27553
27740
  title: args.title
27554
27741
  });
27555
- return {
27556
- content: [
27557
- {
27558
- type: "text",
27559
- text: JSON.stringify(
27560
- {
27561
- stackNumber: result.stack.stackNumber,
27562
- trunk: result.stack.trunk,
27563
- threads: result.threads.map((t) => ({
27564
- id: t.id,
27565
- title: t.title,
27566
- branchName: t.branchName,
27567
- stackLayer: t.stackLayer,
27568
- worktreePath: t.worktreePath,
27569
- link: `sideboard://thread/${t.id}`
27570
- }))
27571
- },
27572
- null,
27573
- 2
27574
- )
27575
- }
27576
- ]
27577
- };
27742
+ return mcpJson({
27743
+ stackNumber: result.stack.stackNumber,
27744
+ trunk: result.stack.trunk,
27745
+ threads: result.threads.map((t) => ({
27746
+ id: t.id,
27747
+ title: t.title,
27748
+ branchName: t.branchName,
27749
+ stackLayer: t.stackLayer,
27750
+ worktreePath: t.worktreePath,
27751
+ link: `sideboard://thread/${t.id}`
27752
+ }))
27753
+ });
27578
27754
  }
27579
27755
  );
27580
27756
  server.tool(
27581
27757
  "list_issues",
27582
27758
  "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.",
27583
27759
  {
27584
- repoPath: import_zod7.z.string(),
27585
- query: import_zod7.z.string().optional().describe("Search title, identifier, or description"),
27586
- assignee: import_zod7.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
27587
- limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27760
+ repoPath: import_zod8.z.string(),
27761
+ query: import_zod8.z.string().optional().describe("Search title, identifier, or description"),
27762
+ assignee: import_zod8.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
27763
+ limit: import_zod8.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27588
27764
  },
27589
27765
  async ({ repoPath, query, assignee, limit }) => {
27590
27766
  const root = await resolveRepoRoot(repoPath);
@@ -29892,6 +30068,7 @@ init_outbound_watch();
29892
30068
  GLOBAL_WORKSPACE_ID,
29893
30069
  HARNESS_ENV_KEYS,
29894
30070
  HOME_BOARD_CACHE_TTL_MS,
30071
+ IMPLIED_TICKET_RESOLVE_PROMPT,
29895
30072
  ISSUE_SOURCE_LABELS,
29896
30073
  LEGACY_ATTACHMENTS_DIR,
29897
30074
  LEGACY_DETACHED_JOBS_DIR,
@@ -30294,6 +30471,7 @@ init_outbound_watch();
30294
30471
  issueMatchesAssignee,
30295
30472
  issueSourceLabel,
30296
30473
  issueTicketFromThread,
30474
+ lastAssistantMessageText,
30297
30475
  lastRequestOccupancy,
30298
30476
  latestPendingPlanQuestions,
30299
30477
  linearAuthorizationHeader,
@@ -30444,6 +30622,7 @@ init_outbound_watch();
30444
30622
  resolveCodexGitWritableRoots,
30445
30623
  resolveCommandBinarySync,
30446
30624
  resolveConductorCursorAgentId,
30625
+ resolveCreateFirstPrompt,
30447
30626
  resolveCursorModelId,
30448
30627
  resolveDefaultBranch,
30449
30628
  resolveDetachedJobScript,