@sideboard-ai/core 0.1.279 → 0.1.281

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{abletime-VCVPSLSU.js → abletime-WLDBJJUI.js} +3 -3
  2. package/dist/{abletime-VRATUMDO.js → abletime-X64E3GUJ.js} +3 -3
  3. package/dist/{abletime-oauth-4K7MICAD.js → abletime-oauth-AXR676W5.js} +2 -2
  4. package/dist/{abletime-oauth-4C4G2J3R.js → abletime-oauth-UWOCEICP.js} +2 -2
  5. package/dist/{abletime-rest-PIBCNHF3.js → abletime-rest-LPUDJUEP.js} +2 -2
  6. package/dist/{abletime-rest-6N63ZVQ3.js → abletime-rest-S4XWEXV2.js} +2 -2
  7. package/dist/{agents-DYCFLDNL.js → agents-2IE4HKHA.js} +5 -5
  8. package/dist/{agents-RXP5NZBC.js → agents-XA2E64AW.js} +5 -5
  9. package/dist/{chunk-V5DMHB6Y.js → chunk-2KNO2GJQ.js} +781 -159
  10. package/dist/{chunk-TZ5JHIYK.js → chunk-2SJXQ4UX.js} +68 -21
  11. package/dist/{chunk-K7MWJLR7.js → chunk-5MGLNVNK.js} +6 -906
  12. package/dist/{chunk-RK5PIWCW.js → chunk-ADXVTKLC.js} +12 -4
  13. package/dist/{chunk-Y7JPL4WA.js → chunk-BDAUPNHL.js} +3 -4
  14. package/dist/{chunk-R7HLXD4I.js → chunk-BS3QLVPL.js} +93 -57
  15. package/dist/{chunk-CPGJ5ICZ.js → chunk-BYZ7F2BQ.js} +766 -148
  16. package/dist/{chunk-2J34G3WJ.js → chunk-DGPROCE5.js} +93 -57
  17. package/dist/{chunk-HQXBBUQX.js → chunk-DQZPF6WL.js} +12 -11
  18. package/dist/{chunk-YDXNCBA2.js → chunk-EIEAR62M.js} +12 -11
  19. package/dist/{chunk-XKZAVGTT.js → chunk-I6HSGKMZ.js} +1 -1
  20. package/dist/chunk-JPH7PYSK.js +1241 -0
  21. package/dist/{chunk-IPPY3FG5.js → chunk-K5XSTHHP.js} +6 -906
  22. package/dist/chunk-N5UGX3JL.js +1239 -0
  23. package/dist/{chunk-HNNS25SG.js → chunk-OCUIZD4Y.js} +14 -4
  24. package/dist/{chunk-IJDQKC5A.js → chunk-PY4UCMRI.js} +4 -4
  25. package/dist/{chunk-NQ7JMVSU.js → chunk-U7HAVAGJ.js} +1 -1
  26. package/dist/{chunk-PZURG3XN.js → chunk-VA55JG2I.js} +4 -4
  27. package/dist/{chunk-CPA43FBK.js → chunk-W7HIK2A6.js} +3 -4
  28. package/dist/{chunk-SKSOCIJT.js → chunk-WY2IAB7J.js} +68 -21
  29. package/dist/{chunk-QINJKNLR.js → chunk-XTPQEZZA.js} +2 -2
  30. package/dist/{conductor-J6WDOXWM.js → conductor-CNSCOC54.js} +5 -5
  31. package/dist/{conductor-OW6NAK3N.js → conductor-U776JSXQ.js} +5 -5
  32. package/dist/{coordinator-prompt-Z2D32HWK.js → coordinator-prompt-4DJIHSE6.js} +3 -3
  33. package/dist/{coordinator-prompt-ERTHREGJ.js → coordinator-prompt-SVVGU6AZ.js} +3 -3
  34. package/dist/{global-workspace-5PJXO45Z.js → global-workspace-UPUDWIS6.js} +3 -3
  35. package/dist/{global-workspace-AK5PSPQ2.js → global-workspace-XON4YZXE.js} +3 -3
  36. package/dist/index.cjs +3819 -2995
  37. package/dist/index.d.cts +103 -30
  38. package/dist/index.d.ts +103 -30
  39. package/dist/index.js +1013 -1001
  40. package/dist/mcp/run-stdio.cjs +2075 -1313
  41. package/dist/mcp/run-stdio.js +743 -785
  42. package/dist/{orchestrator-KY2KBBE2.js → orchestrator-FPGVKEID.js} +7 -7
  43. package/dist/{orchestrator-E5WK57DA.js → orchestrator-HWJHK5NL.js} +7 -7
  44. package/dist/{thread-store-B3ZM4QEZ.js → thread-store-7XBOUQ5K.js} +1 -1
  45. package/dist/{thread-store-SHCUBWB3.js → thread-store-ZQZDREVH.js} +1 -1
  46. package/dist/{workspaces-UP6DQ3WL.js → workspaces-7AJ5RZ3P.js} +7 -3
  47. package/dist/{workspaces-WU2RNLDV.js → workspaces-F76BWCSI.js} +7 -3
  48. package/dist/{worktree-L57T3KH7.js → worktree-NKLNVNXX.js} +20 -15
  49. package/dist/{worktree-Q6UJHG25.js → worktree-YTZC3UQN.js} +14 -9
  50. package/package.json +1 -1
  51. package/dist/chunk-FWNL62VA.js +0 -308
  52. package/dist/chunk-JANCPF7Z.js +0 -310
@@ -18,9 +18,9 @@ import {
18
18
  searchAbleTimeTasks,
19
19
  toAbleTimeIssueInfo,
20
20
  updateAbleTimeTask
21
- } from "../chunk-R7HLXD4I.js";
22
- import "../chunk-XKZAVGTT.js";
23
- import "../chunk-IJDQKC5A.js";
21
+ } from "../chunk-BS3QLVPL.js";
22
+ import "../chunk-I6HSGKMZ.js";
23
+ import "../chunk-PY4UCMRI.js";
24
24
  import {
25
25
  AGENT_GIT_ACTIONS,
26
26
  BOARD_COLUMN_DEFS,
@@ -29,8 +29,11 @@ import {
29
29
  HOME_BOARD_CACHE_TTL_MS,
30
30
  MAX_RUN_LOG_CHARS,
31
31
  agentBlockFields,
32
+ applyIssueListWindow,
33
+ applyWorkspaceTags,
32
34
  assembleHomeBoard,
33
35
  chatBlockedSuffix,
36
+ clampMcpIssueLimit,
34
37
  createSchedule,
35
38
  dedupeBoardIssues,
36
39
  dedupeBoardPrs,
@@ -40,15 +43,23 @@ import {
40
43
  findBoardPr,
41
44
  findLiveThreadForCreate,
42
45
  fireSchedule,
46
+ formatGitHubSearchUpdatedSince,
47
+ formatListedIssuesForMcp,
43
48
  getOrchestrator,
44
49
  getSchedule,
45
50
  isAccountWideIssueSource,
46
51
  isInjectedNoticeText,
52
+ issueMatchesUpdatedSince,
47
53
  issueNeedsWorkspacePick,
48
54
  listSchedules,
49
55
  localhostPreviewUrl,
56
+ mcpIssueUpdatedSinceSchema,
57
+ mcpJson,
50
58
  notifyOrchestrator,
59
+ parseIssueSince,
60
+ previewIssueCommentBody,
51
61
  readTurnLive,
62
+ registerViewerContextTools,
52
63
  resolveNotifyCallerThread,
53
64
  resolveOrchChildFollowUp,
54
65
  resolveScheduleThreadId,
@@ -58,8 +69,9 @@ import {
58
69
  stampNotifyBlock,
59
70
  stampPlanBlock,
60
71
  syncBoardPins,
61
- updateSchedule
62
- } from "../chunk-CPGJ5ICZ.js";
72
+ updateSchedule,
73
+ workspaceTagTargetRef
74
+ } from "../chunk-BYZ7F2BQ.js";
63
75
  import "../chunk-LX5R5JOR.js";
64
76
  import {
65
77
  PRESENT_PLAN_REQUIRES_PLAN_MODE,
@@ -78,7 +90,7 @@ import {
78
90
  sideboardMcpProfile,
79
91
  trustSystemCertificates,
80
92
  worktreeMcpToolNames
81
- } from "../chunk-RK5PIWCW.js";
93
+ } from "../chunk-ADXVTKLC.js";
82
94
  import "../chunk-TQRSVE7D.js";
83
95
  import "../chunk-P2BSMNHB.js";
84
96
  import {
@@ -94,17 +106,16 @@ import {
94
106
  waitForDetachedJob,
95
107
  waitForTurnToolResult,
96
108
  workspaceAgentChatSummaries
97
- } from "../chunk-YDXNCBA2.js";
109
+ } from "../chunk-EIEAR62M.js";
98
110
  import {
99
111
  clipToolResultForStore,
100
112
  isInternalAgentStatusText,
101
113
  looksLikeHugeToolResultDump
102
- } from "../chunk-CPA43FBK.js";
114
+ } from "../chunk-W7HIK2A6.js";
103
115
  import {
104
116
  GLOBAL_WORKSPACE_ID,
105
- isCloudCoordinatorThread,
106
- listWorkspaces
107
- } from "../chunk-TZ5JHIYK.js";
117
+ isCloudCoordinatorThread
118
+ } from "../chunk-2SJXQ4UX.js";
108
119
  import {
109
120
  canonicalizeRepoPath,
110
121
  formatGhLandError,
@@ -115,7 +126,7 @@ import {
115
126
  resolveListPrsOptions,
116
127
  resolveRepoRoot,
117
128
  warmGithubAgentAuth
118
- } from "../chunk-K7MWJLR7.js";
129
+ } from "../chunk-5MGLNVNK.js";
119
130
  import {
120
131
  CLI_DIR
121
132
  } from "../chunk-N2WW62LF.js";
@@ -131,13 +142,11 @@ import {
131
142
  isAbleTimeConnected,
132
143
  isLinearConnected,
133
144
  loadAppSettings,
134
- readViewerContext,
135
145
  resolveEffectiveIssueSource,
136
146
  resolveProjectReviewLabel,
137
147
  resolveViewerProfile,
138
148
  resolveViewerProfileForRepo,
139
- saveLinearOAuth,
140
- writeViewerContext
149
+ saveLinearOAuth
141
150
  } from "../chunk-WRIMAEF2.js";
142
151
  import "../chunk-NSTQ6QKD.js";
143
152
  import {
@@ -145,7 +154,9 @@ import {
145
154
  } from "../chunk-WWUABK6B.js";
146
155
  import "../chunk-FJZWM2ON.js";
147
156
  import "../chunk-UN5Q7POQ.js";
148
- import "../chunk-JANCPF7Z.js";
157
+ import {
158
+ normalizeWorkspaceTags
159
+ } from "../chunk-JPH7PYSK.js";
149
160
  import "../chunk-4RZRHTSZ.js";
150
161
  import "../chunk-KGZBYWZ3.js";
151
162
  import {
@@ -240,7 +251,7 @@ function createClippedMcpServer() {
240
251
  }
241
252
 
242
253
  // src/mcp/server.ts
243
- import { z as z8 } from "zod";
254
+ import { z as z6 } from "zod";
244
255
  import { randomUUID as randomUUID3 } from "crypto";
245
256
  import { basename } from "path";
246
257
 
@@ -264,7 +275,7 @@ function extractGitHubIssueAttachments(issue) {
264
275
  const texts = [issue.body ?? "", ...issue.comments.map((comment) => comment.body)];
265
276
  return mergeIssueAttachments(
266
277
  texts.flatMap(
267
- (text6) => extractHttpUrls(text6).filter(isGitHubIssueAttachmentUrl).map((url) => ({
278
+ (text5) => extractHttpUrls(text5).filter(isGitHubIssueAttachmentUrl).map((url) => ({
268
279
  id: url,
269
280
  name: url.split("/").pop() || "attachment",
270
281
  url
@@ -293,74 +304,127 @@ async function downloadGitHubIssueAttachment(input, opts) {
293
304
  });
294
305
  }
295
306
 
296
- // src/integrations/issue-since.ts
297
- var ISSUE_SINCE_ERROR = "updatedSince must be an ISO datetime, YYYY-MM-DD, or relative time (yesterday, 2d, 3 hours ago)";
298
- var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
299
- var DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
300
- var RELATIVE_RE = /^(\d+)\s*(hours?|hrs?|h|days?|d|weeks?|wks?|w)(?:\s+ago)?$/i;
301
- function startOfLocalDay(date) {
302
- return new Date(date.getFullYear(), date.getMonth(), date.getDate());
303
- }
304
- function addLocalDays(date, days) {
305
- return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
306
- }
307
- function parseIssueSince(input, now = /* @__PURE__ */ new Date()) {
308
- const raw = input.trim();
309
- if (!raw) throw new Error(ISSUE_SINCE_ERROR);
310
- const dateOnly = raw.match(DATE_ONLY_RE);
311
- if (dateOnly) {
312
- const year = Number(dateOnly[1]);
313
- const month = Number(dateOnly[2]);
314
- const day = Number(dateOnly[3]);
315
- const local = new Date(year, month - 1, day);
316
- if (local.getFullYear() !== year || local.getMonth() !== month - 1 || local.getDate() !== day) {
317
- throw new Error(ISSUE_SINCE_ERROR);
307
+ // src/integrations/github-projects.ts
308
+ function rewriteGitHubProjectReadError(detail) {
309
+ const text5 = detail.trim() || "gh could not read GitHub Projects";
310
+ if (/read:project|project scope|missing required scopes/i.test(text5)) {
311
+ return `${text5} \u2014 run \`gh auth refresh -s read:project\` so Sideboard can read GitHub Projects.`;
312
+ }
313
+ return text5;
314
+ }
315
+ function githubProjectsUnreadable(result) {
316
+ return /project|scope|unknown json field/i.test(`${result.stderr}
317
+ ${result.stdout}`);
318
+ }
319
+ function githubProjectsOnIssue(raw) {
320
+ const out = [];
321
+ const seen = /* @__PURE__ */ new Set();
322
+ const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
323
+ if (!Array.isArray(fromItems)) return out;
324
+ for (const item of fromItems) {
325
+ if (typeof item === "string") {
326
+ const name2 = item.trim();
327
+ const key2 = name2.toLowerCase();
328
+ if (!name2 || seen.has(key2)) continue;
329
+ seen.add(key2);
330
+ out.push({ name: name2 });
331
+ continue;
332
+ }
333
+ if (!item || typeof item !== "object") continue;
334
+ const rec = item;
335
+ const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
336
+ const id = String(nested.id ?? rec.id ?? "").trim();
337
+ const name = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
338
+ const label = name || id;
339
+ const key = label.toLowerCase();
340
+ if (!label || seen.has(key)) continue;
341
+ seen.add(key);
342
+ out.push({ ...id ? { id } : {}, name: label });
343
+ }
344
+ return out;
345
+ }
346
+ async function readGitHubIssueProjects(number, cwd, repoArgs) {
347
+ const result = await gh(
348
+ ["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
349
+ cwd,
350
+ { reject: false }
351
+ );
352
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
353
+ const detail = (result.stderr || result.stdout).trim();
354
+ if (detail && githubProjectsUnreadable(result)) {
355
+ return { projects: [], error: rewriteGitHubProjectReadError(detail) };
356
+ }
357
+ return { projects: [] };
358
+ }
359
+ try {
360
+ const parsed = JSON.parse(result.stdout);
361
+ return { projects: githubProjectsOnIssue(parsed) };
362
+ } catch {
363
+ return { projects: [] };
364
+ }
365
+ }
366
+ async function listGitHubIssueProjectNames(number, cwd, repoArgs) {
367
+ const read = await readGitHubIssueProjects(number, cwd, repoArgs);
368
+ return read.projects.map((project) => project.name).filter(Boolean);
369
+ }
370
+ function parseGitHubProjectList(stdout) {
371
+ let parsed;
372
+ try {
373
+ parsed = JSON.parse(stdout);
374
+ } catch {
375
+ return [];
376
+ }
377
+ const rows = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.projects) ? parsed.projects ?? [] : [];
378
+ const out = [];
379
+ for (const row of rows) {
380
+ if (!row || typeof row !== "object") continue;
381
+ const rec = row;
382
+ const title = String(rec.title ?? rec.name ?? "").trim();
383
+ if (!title) continue;
384
+ const ownerRec = rec.owner && typeof rec.owner === "object" ? rec.owner : null;
385
+ const owner = ownerRec?.login?.trim() || (typeof rec.owner === "string" ? rec.owner.trim() : "");
386
+ const number = Number(rec.number);
387
+ out.push({
388
+ ...typeof rec.id === "string" && rec.id.trim() ? { id: rec.id.trim() } : {},
389
+ ...Number.isFinite(number) ? { number } : {},
390
+ title,
391
+ ...typeof rec.url === "string" && rec.url.trim() ? { url: rec.url.trim() } : {},
392
+ ...typeof rec.closed === "boolean" ? { closed: rec.closed } : {},
393
+ ...owner ? { owner } : {}
394
+ });
395
+ }
396
+ return out;
397
+ }
398
+ async function listGitHubProjects(opts) {
399
+ const cwd = await resolveRepoRoot((opts?.repoPath ?? "").trim() || process.cwd());
400
+ const slug = await resolveGithubRepoSlug(cwd);
401
+ const owners = /* @__PURE__ */ new Set([""]);
402
+ const repoOwner = slug?.split("/")[0]?.trim();
403
+ if (repoOwner) owners.add(repoOwner);
404
+ const out = [];
405
+ const seen = /* @__PURE__ */ new Set();
406
+ let lastError = "";
407
+ let anyOk = false;
408
+ for (const owner of owners) {
409
+ const args = ["project", "list", "--limit", "100", "--format", "json"];
410
+ if (owner) args.push("--owner", owner);
411
+ const result = await gh(args, cwd, { reject: false });
412
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
413
+ lastError = (result.stderr || result.stdout).trim() || lastError;
414
+ continue;
318
415
  }
319
- return startOfLocalDay(local).toISOString();
320
- }
321
- if (DATE_TIME_RE.test(raw)) {
322
- const ms = Date.parse(raw);
323
- if (!Number.isFinite(ms)) throw new Error(ISSUE_SINCE_ERROR);
324
- return new Date(ms).toISOString();
325
- }
326
- const lower = raw.toLowerCase();
327
- if (lower === "today") return startOfLocalDay(now).toISOString();
328
- if (lower === "yesterday") return addLocalDays(startOfLocalDay(now), -1).toISOString();
329
- if (lower === "last week" || lower === "lastweek") {
330
- return addLocalDays(startOfLocalDay(now), -7).toISOString();
331
- }
332
- const relative = lower.match(RELATIVE_RE);
333
- if (relative) {
334
- const n = Number(relative[1]);
335
- if (!Number.isInteger(n) || n < 1) throw new Error(ISSUE_SINCE_ERROR);
336
- const unit = relative[2];
337
- if (unit.startsWith("h")) return new Date(now.getTime() - n * 36e5).toISOString();
338
- if (unit.startsWith("w")) return addLocalDays(startOfLocalDay(now), -7 * n).toISOString();
339
- return addLocalDays(startOfLocalDay(now), -n).toISOString();
340
- }
341
- throw new Error(ISSUE_SINCE_ERROR);
342
- }
343
- function formatGitHubSearchUpdatedSince(iso) {
344
- return iso.replace(/\.\d{3}Z$/, "Z");
345
- }
346
- function issueMatchesUpdatedSince(issue, sinceIso) {
347
- const ts = issue.updatedAt || issue.createdAt;
348
- if (!ts) return false;
349
- const value = Date.parse(ts);
350
- const since = Date.parse(sinceIso);
351
- return Number.isFinite(value) && Number.isFinite(since) && value >= since;
352
- }
353
- var ISSUE_COMMENT_PREVIEW_CHARS = 240;
354
- function previewIssueCommentBody(body) {
355
- const flat = body.replace(/\s+/g, " ").trim();
356
- if (flat.length <= ISSUE_COMMENT_PREVIEW_CHARS) return flat;
357
- return `${flat.slice(0, ISSUE_COMMENT_PREVIEW_CHARS - 1).trimEnd()}\u2026`;
358
- }
359
- function issueActivityKind(issue, sinceIso) {
360
- const created = issue.createdAt ? Date.parse(issue.createdAt) : Number.NaN;
361
- const since = Date.parse(sinceIso);
362
- if (Number.isFinite(created) && Number.isFinite(since) && created >= since) return "created";
363
- return "updated";
416
+ anyOk = true;
417
+ for (const project of parseGitHubProjectList(result.stdout)) {
418
+ const key = (project.id || `${project.owner ?? ""}:${project.number ?? ""}:${project.title}`).toLowerCase();
419
+ if (seen.has(key)) continue;
420
+ seen.add(key);
421
+ out.push(project);
422
+ }
423
+ }
424
+ if (!anyOk) {
425
+ throw new Error(rewriteGitHubProjectReadError(lastError || "gh project list failed"));
426
+ }
427
+ return out;
364
428
  }
365
429
 
366
430
  // src/integrations/github-issues.ts
@@ -529,7 +593,11 @@ async function getGitHubIssue(id, opts) {
529
593
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
530
594
  throw new Error(`GitHub issue not found: #${number}`);
531
595
  }
532
- return toGitHubIssue(parsed);
596
+ const issue = toGitHubIssue(parsed);
597
+ const projects = await readGitHubIssueProjects(number, cwd, repoArgs);
598
+ issue.projects = projects.projects;
599
+ if (projects.error) issue.projectsError = projects.error;
600
+ return issue;
533
601
  }
534
602
  async function commentGitHubIssue(input, opts) {
535
603
  const number = parseGitHubIssueNumber(input.id);
@@ -567,46 +635,6 @@ function githubRelationEditFlag(type, remove = false) {
567
635
  function labelsEqual(a, b) {
568
636
  return a.trim().toLowerCase() === b.trim().toLowerCase();
569
637
  }
570
- function parseProjectTitles(raw) {
571
- const out = [];
572
- const seen = /* @__PURE__ */ new Set();
573
- const push = (name) => {
574
- const title = name.trim();
575
- const key = title.toLowerCase();
576
- if (!title || seen.has(key)) return;
577
- seen.add(key);
578
- out.push(title);
579
- };
580
- const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
581
- if (Array.isArray(fromItems)) {
582
- for (const item of fromItems) {
583
- if (typeof item === "string") {
584
- push(item);
585
- continue;
586
- }
587
- if (!item || typeof item !== "object") continue;
588
- const rec = item;
589
- const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
590
- const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
591
- if (title) push(title);
592
- }
593
- }
594
- return out;
595
- }
596
- async function listGitHubIssueProjects(number, cwd, repoArgs) {
597
- const result = await gh(
598
- ["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
599
- cwd,
600
- { reject: false }
601
- );
602
- if (result.exitCode !== 0 || !result.stdout.trim()) return [];
603
- try {
604
- const parsed = JSON.parse(result.stdout);
605
- return parseProjectTitles(parsed);
606
- } catch {
607
- return [];
608
- }
609
- }
610
638
  async function updateGitHubIssue(input, opts) {
611
639
  const number = parseGitHubIssueNumber(input.id);
612
640
  const title = input.title?.trim();
@@ -646,7 +674,7 @@ async function updateGitHubIssue(input, opts) {
646
674
  if (remove.length) args.push("--remove-label", remove.join(","));
647
675
  }
648
676
  if (input.project !== void 0) {
649
- const current = await listGitHubIssueProjects(number, cwd, repoArgs);
677
+ const current = await listGitHubIssueProjectNames(number, cwd, repoArgs);
650
678
  if (isGitHubNoneToken(input.project)) {
651
679
  for (const name of current) args.push("--remove-project", name);
652
680
  } else {
@@ -945,6 +973,7 @@ var LIST_ISSUE_FIELDS = `
945
973
  team { id key }
946
974
  labels(first: 10) { nodes { name } }
947
975
  cycle { id name number startsAt endsAt completedAt }
976
+ project { id name }
948
977
  `;
949
978
  var ISSUE_REF_FIELDS = `
950
979
  id
@@ -1121,9 +1150,10 @@ query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
1121
1150
  }
1122
1151
  `;
1123
1152
  var PROJECTS_QUERY = `
1124
- query SideboardProjects($first: Int!, $filter: ProjectFilter) {
1125
- projects(first: $first, filter: $filter) {
1153
+ query SideboardProjects($first: Int!, $filter: ProjectFilter, $after: String) {
1154
+ projects(first: $first, filter: $filter, after: $after) {
1126
1155
  nodes { id name slugId }
1156
+ pageInfo { hasNextPage endCursor }
1127
1157
  }
1128
1158
  }
1129
1159
  `;
@@ -1345,6 +1375,7 @@ function toIssueInfo(issue) {
1345
1375
  assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
1346
1376
  cycle: issue.cycle ?? null,
1347
1377
  teamKey: issue.team?.key || void 0,
1378
+ ...issue.project ? { projects: [issue.project] } : {},
1348
1379
  ...issue.createdAt ? { createdAt: issue.createdAt } : {},
1349
1380
  ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {}
1350
1381
  };
@@ -1510,30 +1541,35 @@ async function listLinearLabels(opts) {
1510
1541
  }
1511
1542
  return listLinearWorkspaceLabels(opts);
1512
1543
  }
1513
- async function listLinearProjects(filter, opts) {
1514
- const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {} }, opts);
1515
- return (json.projects?.nodes ?? []).flatMap((node) => {
1516
- const id = String(node.id ?? "").trim();
1517
- if (!id) return [];
1518
- return [
1519
- {
1520
- id,
1521
- name: String(node.name ?? ""),
1522
- ...node.slugId ? { slugId: String(node.slugId) } : {}
1523
- }
1524
- ];
1525
- });
1544
+ async function queryLinearProjects(filter, opts) {
1545
+ const out = [];
1546
+ let after;
1547
+ for (let page = 0; page < 4; page++) {
1548
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {}, ...after ? { after } : {} }, opts);
1549
+ for (const node of json.projects?.nodes ?? []) {
1550
+ const id = String(node.id ?? "").trim();
1551
+ if (!id) continue;
1552
+ out.push({ id, name: String(node.name ?? ""), ...node.slugId ? { slugId: String(node.slugId) } : {} });
1553
+ }
1554
+ const pageInfo = json.projects?.pageInfo;
1555
+ if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
1556
+ after = pageInfo.endCursor;
1557
+ }
1558
+ return out;
1559
+ }
1560
+ function listLinearProjects(opts) {
1561
+ return queryLinearProjects(void 0, opts);
1526
1562
  }
1527
1563
  async function resolveLinearProjectId(project, opts) {
1528
1564
  if (isLinearNoneToken(project)) return null;
1529
1565
  const s = (project ?? "").trim();
1530
1566
  const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
1531
- const matched = await listLinearProjects(filter, opts);
1567
+ const matched = await queryLinearProjects(filter, opts);
1532
1568
  try {
1533
1569
  return resolveLinearProject(matched, s);
1534
1570
  } catch {
1535
1571
  if (UUID_RE.test(s)) return s;
1536
- const listed = await listLinearProjects(void 0, opts);
1572
+ const listed = await queryLinearProjects(void 0, opts);
1537
1573
  return resolveLinearProject(listed, s);
1538
1574
  }
1539
1575
  }
@@ -1915,32 +1951,43 @@ function issueMatchesAssignee(issue, assignee, viewerName = "") {
1915
1951
  async function listGitHubIssues(repoPath, opts) {
1916
1952
  const limit = Math.max(1, Math.min(1e3, opts?.limit ?? 200));
1917
1953
  const slug = await resolveGithubRepoSlug(repoPath);
1918
- const args = [
1919
- "issue",
1920
- "list",
1921
- "--json",
1922
- "number,title,url,labels,assignees,createdAt,updatedAt",
1923
- "--limit",
1924
- String(limit),
1925
- "--state",
1926
- "open"
1927
- ];
1928
- const query = opts?.query?.trim() ?? "";
1929
- const assignee = opts?.assignee?.trim() ?? "";
1930
- const key = assignee.toLowerCase();
1931
- const searchParts = [];
1932
- if (query) searchParts.push(query);
1933
- if (opts?.updatedSince) {
1934
- searchParts.push(`updated:>=${formatGitHubSearchUpdatedSince(opts.updatedSince)}`);
1935
- }
1936
- if (key === "unassigned" || key === "none" || key === "null") {
1937
- searchParts.push("no:assignee");
1938
- } else if (key && key !== "all" && key !== "*") {
1939
- args.push("--assignee", key === "me" || key === "@me" ? "@me" : assignee);
1954
+ const plainFields = "number,title,url,labels,assignees,createdAt,updatedAt";
1955
+ const buildArgs = (jsonFields) => {
1956
+ const args = [
1957
+ "issue",
1958
+ "list",
1959
+ "--json",
1960
+ jsonFields,
1961
+ "--limit",
1962
+ String(limit),
1963
+ "--state",
1964
+ "open"
1965
+ ];
1966
+ const query = opts?.query?.trim() ?? "";
1967
+ const assignee = opts?.assignee?.trim() ?? "";
1968
+ const key = assignee.toLowerCase();
1969
+ const searchParts = [];
1970
+ if (query) searchParts.push(query);
1971
+ if (opts?.updatedSince) {
1972
+ searchParts.push(`updated:>=${formatGitHubSearchUpdatedSince(opts.updatedSince)}`);
1973
+ }
1974
+ if (key === "unassigned" || key === "none" || key === "null") {
1975
+ searchParts.push("no:assignee");
1976
+ } else if (key && key !== "all" && key !== "*") {
1977
+ args.push("--assignee", key === "me" || key === "@me" ? "@me" : assignee);
1978
+ }
1979
+ if (searchParts.length) args.push("--search", searchParts.join(" "));
1980
+ if (slug) args.push("--repo", slug);
1981
+ return args;
1982
+ };
1983
+ let { stdout, exitCode } = await gh(
1984
+ buildArgs(`${plainFields},projectItems`),
1985
+ repoPath,
1986
+ { reject: false }
1987
+ );
1988
+ if (exitCode !== 0) {
1989
+ ({ stdout, exitCode } = await gh(buildArgs(plainFields), repoPath, { reject: false }));
1940
1990
  }
1941
- if (searchParts.length) args.push("--search", searchParts.join(" "));
1942
- if (slug) args.push("--repo", slug);
1943
- const { stdout, exitCode } = await gh(args, repoPath, { reject: false });
1944
1991
  if (exitCode !== 0 || !stdout.trim()) return [];
1945
1992
  let parsed;
1946
1993
  try {
@@ -1959,6 +2006,9 @@ async function listGitHubIssues(repoPath, opts) {
1959
2006
  const assignees = (item.assignees ?? []).map((a) => typeof a === "string" ? a : String(a?.login ?? "")).map((login) => login.trim()).filter(Boolean);
1960
2007
  const createdAt = item.createdAt?.trim() || void 0;
1961
2008
  const updatedAt = item.updatedAt?.trim() || void 0;
2009
+ const record = raw;
2010
+ const projects = githubProjectsOnIssue(record);
2011
+ const readProjects = "projectItems" in record || "projects" in record || "projectItemsV2" in record;
1962
2012
  return {
1963
2013
  id: Number.isFinite(number) ? `gh-${number}` : identifier,
1964
2014
  identifier,
@@ -1968,6 +2018,7 @@ async function listGitHubIssues(repoPath, opts) {
1968
2018
  provider: "github",
1969
2019
  assignee: assignees[0],
1970
2020
  assignees,
2021
+ ...readProjects ? { projects } : {},
1971
2022
  ...createdAt ? { createdAt } : {},
1972
2023
  ...updatedAt ? { updatedAt } : {}
1973
2024
  };
@@ -2101,13 +2152,164 @@ function resolveRunScriptThreadRef(ref) {
2101
2152
  return thread.id;
2102
2153
  }
2103
2154
 
2155
+ // src/mcp/create-thread-agent.ts
2156
+ function resolveOrchCreateThreadOptions(input) {
2157
+ const { resolveNewThreadOptions } = input;
2158
+ const account = resolveNewThreadOptions({});
2159
+ const requested = input.requestedAgent;
2160
+ const parent = input.parentAgent ?? void 0;
2161
+ const cursorReady = input.cursorReady ?? isCursorApiKeyConfigured();
2162
+ let agent = account.agent;
2163
+ let ignoredAgent;
2164
+ let honorRequestedModel = false;
2165
+ if (requested) {
2166
+ const parentEcho = Boolean(parent) && requested === parent && requested !== account.agent;
2167
+ const cursorAutofill = requested === "cursor" && account.agent !== "cursor";
2168
+ if (parentEcho || cursorAutofill) {
2169
+ ignoredAgent = requested;
2170
+ } else {
2171
+ agent = requested;
2172
+ honorRequestedModel = input.requestedModel !== void 0;
2173
+ }
2174
+ }
2175
+ let coercedFrom;
2176
+ if (agent === "codex" && parent === "codex" && cursorReady) {
2177
+ coercedFrom = requested ?? "codex";
2178
+ agent = "cursor";
2179
+ }
2180
+ const opts = resolveNewThreadOptions({
2181
+ agent,
2182
+ model: honorRequestedModel ? input.requestedModel : void 0
2183
+ });
2184
+ return {
2185
+ ...opts,
2186
+ ...ignoredAgent ? { ignoredAgent } : {},
2187
+ ...coercedFrom ? { coercedFrom } : {}
2188
+ };
2189
+ }
2190
+ function orchCreateThreadAgentNote(input) {
2191
+ if (input.coercedFrom) {
2192
+ return `Avoid nested Codex under a Codex orchestrator \u2014 used agent=${input.agent}`;
2193
+ }
2194
+ if (input.ignoredAgent) {
2195
+ return `Ignored agent=${input.ignoredAgent} \u2014 used Account default agent=${input.agent}`;
2196
+ }
2197
+ return void 0;
2198
+ }
2199
+
2200
+ // src/mcp/create-orch-child.ts
2201
+ var MAX_ORCH_THREADS = 5;
2202
+ var CREATE_THREAD_TIMEOUT_MS = 9e4;
2203
+ function withTimeout(promise, ms, label) {
2204
+ return new Promise((resolve, reject) => {
2205
+ const timer = setTimeout(() => {
2206
+ reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`));
2207
+ }, ms);
2208
+ promise.then(
2209
+ (value) => {
2210
+ clearTimeout(timer);
2211
+ resolve(value);
2212
+ },
2213
+ (err) => {
2214
+ clearTimeout(timer);
2215
+ reject(err);
2216
+ }
2217
+ );
2218
+ });
2219
+ }
2220
+ async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
2221
+ const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
2222
+ let parentId = args.parentChatId?.trim() || args.parentThreadId?.trim() || "";
2223
+ let parent = parentId ? orch.getThread(parentId) : null;
2224
+ let parentCorrectedFrom;
2225
+ if (envParentId) {
2226
+ const envParent = orch.getThread(envParentId);
2227
+ if (envParent) {
2228
+ if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
2229
+ else if (!parentId) parentCorrectedFrom = void 0;
2230
+ parentId = envParentId;
2231
+ parent = envParent;
2232
+ }
2233
+ }
2234
+ if (parentId && !parent) {
2235
+ parentCorrectedFrom = parentId;
2236
+ parentId = "";
2237
+ parent = null;
2238
+ }
2239
+ if (parentId) {
2240
+ const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
2241
+ if (children.length >= MAX_ORCH_THREADS) {
2242
+ return {
2243
+ ok: false,
2244
+ text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
2245
+ };
2246
+ }
2247
+ }
2248
+ const opts = resolveOrchCreateThreadOptions({
2249
+ requestedAgent: args.agent,
2250
+ requestedModel: args.model,
2251
+ parentAgent: parent?.agent,
2252
+ resolveNewThreadOptions
2253
+ });
2254
+ const agentNote = orchCreateThreadAgentNote(opts);
2255
+ try {
2256
+ const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
2257
+ const thread = await withTimeout(
2258
+ orch.createThread({
2259
+ sourceType: args.sourceType,
2260
+ sourceRef: args.sourceRef,
2261
+ agent: opts.agent,
2262
+ model: opts.model,
2263
+ effort: opts.effort,
2264
+ fast: opts.fast,
2265
+ repoPath: args.repoPath,
2266
+ title: args.title,
2267
+ parentThreadId: parentId || null,
2268
+ cowboy: args.cowboy || void 0,
2269
+ attachments: args.attachments,
2270
+ tags: args.tags === void 0 ? void 0 : normalizeWorkspaceTags(args.tags)
2271
+ }),
2272
+ CREATE_THREAD_TIMEOUT_MS,
2273
+ "create_workspace"
2274
+ );
2275
+ const alreadyStarted = priorIds.has(thread.id);
2276
+ const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
2277
+ return {
2278
+ ok: true,
2279
+ text: JSON.stringify({
2280
+ id: thread.id,
2281
+ title: thread.title,
2282
+ branchName: thread.branchName,
2283
+ worktreePath: thread.worktreePath,
2284
+ agent: thread.agent,
2285
+ model: thread.model,
2286
+ status: thread.status,
2287
+ cowboy: Boolean(thread.cowboy),
2288
+ link: `sideboard://chat/${thread.id}`,
2289
+ parentChatId: thread.parentThreadId,
2290
+ parentThreadId: thread.parentThreadId,
2291
+ tags: thread.tags ?? [],
2292
+ chats: workspaceAgentChatSummaries(thread.worktreePath),
2293
+ ...alreadyStarted ? { alreadyStarted: true } : {},
2294
+ ...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
2295
+ ...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
2296
+ ...agentNote ? { note: agentNote } : {},
2297
+ ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
2298
+ })
2299
+ };
2300
+ } catch (err) {
2301
+ const message = err instanceof Error ? err.message : String(err);
2302
+ return { ok: false, text: `create_workspace failed: ${message}` };
2303
+ }
2304
+ }
2305
+
2104
2306
  // src/mcp/thread-visibility.ts
2105
2307
  function lastMessagePreview(messages, max = 160) {
2106
2308
  if (!messages?.length) return null;
2107
2309
  for (let i = messages.length - 1; i >= 0; i--) {
2108
- const text6 = messages[i]?.text?.trim();
2109
- if (!text6 || isInternalAgentStatusText(text6) || isInjectedNoticeText(text6)) continue;
2110
- const flat = text6.replace(/\s+/g, " ");
2310
+ const text5 = messages[i]?.text?.trim();
2311
+ if (!text5 || isInternalAgentStatusText(text5) || isInjectedNoticeText(text5)) continue;
2312
+ const flat = text5.replace(/\s+/g, " ");
2111
2313
  return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
2112
2314
  }
2113
2315
  return null;
@@ -2124,97 +2326,28 @@ function childThreadRefs(parentId, threads) {
2124
2326
  }
2125
2327
 
2126
2328
  // src/mcp/abletime-tools.ts
2127
- import { z as z3 } from "zod";
2329
+ import { z as z2 } from "zod";
2128
2330
 
2129
- // src/mcp/issue-list.ts
2331
+ // src/mcp/issue-payload.ts
2130
2332
  import { z } from "zod";
2131
- var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
2132
- var MCP_ISSUE_LIST_MAX_LIMIT = 250;
2133
- function clampMcpIssueLimit(limit) {
2134
- if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
2135
- return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
2136
- }
2137
- function applyIssueListWindow(items, limit) {
2138
- const truncated = items.length > limit;
2139
- return {
2140
- items: truncated ? items.slice(0, limit) : items,
2141
- truncated
2142
- };
2333
+
2334
+ // src/mcp/smart-crush.ts
2335
+ var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
2336
+ var CRUSH_MIN_ITEMS = 5;
2337
+ var CRUSH_MIN_CHARS = 800;
2338
+ var CRUSH_COMMENT_MAX_CHARS = 16e3;
2339
+ var CRUSH_BODY_MIN_CHARS = 12e3;
2340
+ var CRUSH_BODY_MAX_CHARS = 8e3;
2341
+ var NEAR_DUP_JACCARD = 0.92;
2342
+ function looksLikeMustKeep(text5) {
2343
+ return MUST_KEEP_TEXT_RE.test(text5);
2143
2344
  }
2144
- var MCP_ISSUE_UPDATED_SINCE_DESCRIPTION = "ISO datetime, YYYY-MM-DD, or relative (yesterday, 2d, 3 hours ago). Tickets updated since then plus new comments. Use for \u201Cany updates since yesterday?\u201D";
2145
- var mcpIssueUpdatedSinceSchema = z.string().min(1).optional().describe(MCP_ISSUE_UPDATED_SINCE_DESCRIPTION);
2146
- function compactIssueRow(issue, opts) {
2147
- return {
2148
- identifier: issue.identifier,
2149
- title: issue.title,
2150
- ...issue.assignee ? { assignee: issue.assignee } : {},
2151
- ...issue.labels.length > 0 ? { labels: issue.labels } : {},
2152
- ...issue.teamKey ? { team: issue.teamKey } : {},
2153
- ...issue.cycle?.name ? { cycle: issue.cycle.name } : {},
2154
- ...issue.createdAt ? { createdAt: issue.createdAt } : {},
2155
- ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {},
2156
- ...opts?.since ? { kind: issueActivityKind(issue, opts.since) } : {}
2157
- };
2158
- }
2159
- function formatMcpIssueList(input) {
2160
- const truncated = input.truncated || Boolean(input.commentsTruncated);
2161
- const comments = input.comments ?? [];
2162
- return {
2163
- ...input.source ? { source: input.source } : {},
2164
- ...input.viewer ? { viewer: input.viewer } : {},
2165
- limit: input.limit,
2166
- returned: input.issues.length,
2167
- truncated,
2168
- ...input.since ? { since: input.since } : {},
2169
- ...truncated ? {
2170
- hint: input.since ? "More issues or comments match. Pass a higher limit (max 250) or a tighter query." : "More issues match. Pass a higher limit (max 250) or a tighter query."
2171
- } : {},
2172
- issues: input.issues.map((issue) => compactIssueRow(issue, { since: input.since })),
2173
- ...input.since || comments.length > 0 ? { comments } : {}
2174
- };
2175
- }
2176
- function formatListedIssuesForMcp(result, page) {
2177
- const windowed = applyIssueListWindow(result.issues, page);
2178
- const comments = result.comments ?? [];
2179
- const commentWindow = applyIssueListWindow(comments, page);
2180
- return formatMcpIssueList({
2181
- source: result.source,
2182
- viewer: result.viewer?.name || result.viewer?.login,
2183
- limit: page,
2184
- issues: windowed.items,
2185
- truncated: windowed.truncated,
2186
- since: result.since,
2187
- comments: commentWindow.items,
2188
- commentsTruncated: commentWindow.truncated
2189
- });
2190
- }
2191
- function mcpJson(payload, isError = false) {
2192
- return {
2193
- content: [{ type: "text", text: JSON.stringify(payload) }],
2194
- ...isError ? { isError: true } : {}
2195
- };
2196
- }
2197
-
2198
- // src/mcp/issue-payload.ts
2199
- import { z as z2 } from "zod";
2200
-
2201
- // src/mcp/smart-crush.ts
2202
- var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
2203
- var CRUSH_MIN_ITEMS = 5;
2204
- var CRUSH_MIN_CHARS = 800;
2205
- var CRUSH_COMMENT_MAX_CHARS = 16e3;
2206
- var CRUSH_BODY_MIN_CHARS = 12e3;
2207
- var CRUSH_BODY_MAX_CHARS = 8e3;
2208
- var NEAR_DUP_JACCARD = 0.92;
2209
- function looksLikeMustKeep(text6) {
2210
- return MUST_KEEP_TEXT_RE.test(text6);
2211
- }
2212
- function normalizeText(text6) {
2213
- return text6.replace(/\s+/g, " ").trim().toLowerCase();
2345
+ function normalizeText(text5) {
2346
+ return text5.replace(/\s+/g, " ").trim().toLowerCase();
2214
2347
  }
2215
- function wordSet(text6) {
2348
+ function wordSet(text5) {
2216
2349
  return new Set(
2217
- text6.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
2350
+ text5.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
2218
2351
  );
2219
2352
  }
2220
2353
  function jaccard(a, b) {
@@ -2226,11 +2359,11 @@ function jaccard(a, b) {
2226
2359
  const union = a.size + b.size - inter;
2227
2360
  return union === 0 ? 0 : inter / union;
2228
2361
  }
2229
- function importance(text6) {
2230
- let score = text6.length;
2231
- if (/```/.test(text6)) score += 400;
2232
- if (/https?:\/\//.test(text6)) score += 50;
2233
- if (looksLikeMustKeep(text6)) score += 1e4;
2362
+ function importance(text5) {
2363
+ let score = text5.length;
2364
+ if (/```/.test(text5)) score += 400;
2365
+ if (/https?:\/\//.test(text5)) score += 50;
2366
+ if (looksLikeMustKeep(text5)) score += 1e4;
2234
2367
  return score;
2235
2368
  }
2236
2369
  function meanStd(values) {
@@ -2244,15 +2377,15 @@ function dedupTextItems(items, getText) {
2244
2377
  const keptNorm = [];
2245
2378
  const keptWords = [];
2246
2379
  for (const item of items) {
2247
- const text6 = getText(item);
2248
- const norm = normalizeText(text6);
2380
+ const text5 = getText(item);
2381
+ const norm = normalizeText(text5);
2249
2382
  if (!norm) {
2250
2383
  kept.push(item);
2251
2384
  keptNorm.push(norm);
2252
- keptWords.push(wordSet(text6));
2385
+ keptWords.push(wordSet(text5));
2253
2386
  continue;
2254
2387
  }
2255
- const words = wordSet(text6);
2388
+ const words = wordSet(text5);
2256
2389
  let dupAt = -1;
2257
2390
  for (let i = 0; i < kept.length; i++) {
2258
2391
  if (keptNorm[i] === norm) {
@@ -2270,7 +2403,7 @@ function dedupTextItems(items, getText) {
2270
2403
  keptWords.push(words);
2271
2404
  continue;
2272
2405
  }
2273
- if (text6.length > getText(kept[dupAt]).length) {
2406
+ if (text5.length > getText(kept[dupAt]).length) {
2274
2407
  kept[dupAt] = item;
2275
2408
  keptNorm[dupAt] = norm;
2276
2409
  keptWords[dupAt] = words;
@@ -2311,8 +2444,8 @@ function crushTextItems(items, getText, opts = {}) {
2311
2444
  const outlierFloor = mean + 2 * std;
2312
2445
  const mustKeep = /* @__PURE__ */ new Set();
2313
2446
  unique.forEach((item, i) => {
2314
- const text6 = getText(item);
2315
- if (looksLikeMustKeep(text6) || text6.length > outlierFloor) mustKeep.add(i);
2447
+ const text5 = getText(item);
2448
+ if (looksLikeMustKeep(text5) || text5.length > outlierFloor) mustKeep.add(i);
2316
2449
  });
2317
2450
  const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
2318
2451
  let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
@@ -2354,9 +2487,9 @@ function crushTextItems(items, getText, opts = {}) {
2354
2487
  reason: "budget"
2355
2488
  };
2356
2489
  }
2357
- function splitBlocks(text6) {
2358
- if (/\n\n/.test(text6)) return text6.split(/\n\n+/);
2359
- return text6.split("\n");
2490
+ function splitBlocks(text5) {
2491
+ if (/\n\n/.test(text5)) return text5.split(/\n\n+/);
2492
+ return text5.split("\n");
2360
2493
  }
2361
2494
  function capOversizedBlock(block, maxChars) {
2362
2495
  if (block.length <= maxChars) return block;
@@ -2368,8 +2501,8 @@ function capOversizedBlock(block, maxChars) {
2368
2501
 
2369
2502
  ${block.slice(-tail)}`;
2370
2503
  }
2371
- function crushMarkdown(text6, opts) {
2372
- const original = text6 ?? "";
2504
+ function crushMarkdown(text5, opts) {
2505
+ const original = text5 ?? "";
2373
2506
  const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
2374
2507
  const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
2375
2508
  if (original.length < minChars) {
@@ -2441,7 +2574,7 @@ function crushMarkdown(text6, opts) {
2441
2574
 
2442
2575
  // src/mcp/issue-payload.ts
2443
2576
  var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
2444
- var mcpIssueIncludeSchema = z2.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
2577
+ var mcpIssueIncludeSchema = z.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
2445
2578
  "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."
2446
2579
  );
2447
2580
  var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
@@ -2459,13 +2592,13 @@ function withComments(comments, meta) {
2459
2592
  }
2460
2593
  return { comments: crushed.items, meta };
2461
2594
  }
2462
- function withBody(text6, meta) {
2463
- const crushed = crushMarkdown(text6);
2595
+ function withBody(text5, meta) {
2596
+ const crushed = crushMarkdown(text5);
2464
2597
  if (crushed.truncated) {
2465
2598
  meta.body = { originalChars: crushed.originalChars };
2466
2599
  return { value: crushed.text, meta };
2467
2600
  }
2468
- return { value: text6, meta };
2601
+ return { value: text5, meta };
2469
2602
  }
2470
2603
  function attachCrush(payload, meta) {
2471
2604
  if (!meta.comments && !meta.body) return payload;
@@ -2587,7 +2720,7 @@ function registerAbleTimeTools(server) {
2587
2720
  server.tool(
2588
2721
  "abletime_list_tasks",
2589
2722
  "List open AbleTime tasks (excludes done/archived). Optional projectId filter. Pass updatedSince for tasks updated since then and any comments on that page.",
2590
- { projectId: z3.string().optional(), updatedSince: mcpIssueUpdatedSinceSchema },
2723
+ { projectId: z2.string().optional(), updatedSince: mcpIssueUpdatedSinceSchema },
2591
2724
  async ({ projectId, updatedSince }) => {
2592
2725
  try {
2593
2726
  const tasks = await listAbleTimeTasks({ projectId });
@@ -2600,7 +2733,7 @@ function registerAbleTimeTools(server) {
2600
2733
  server.tool(
2601
2734
  "abletime_search_tasks",
2602
2735
  "Search AbleTime tasks by text, reference, or tag. Pass updatedSince for tasks updated since then and any comments on that page.",
2603
- { query: z3.string(), updatedSince: mcpIssueUpdatedSinceSchema },
2736
+ { query: z2.string(), updatedSince: mcpIssueUpdatedSinceSchema },
2604
2737
  async ({ query, updatedSince }) => {
2605
2738
  try {
2606
2739
  const tasks = await searchAbleTimeTasks(query);
@@ -2612,8 +2745,8 @@ function registerAbleTimeTools(server) {
2612
2745
  );
2613
2746
  server.tool(
2614
2747
  "abletime_get_task",
2615
- "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments, attachments. 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. To save a file, call abletime_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
2616
- { id: z3.string(), include: mcpIssueIncludeSchema },
2748
+ "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, project, comments, attachments. 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. To save a file, call abletime_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
2749
+ { id: z2.string(), include: mcpIssueIncludeSchema },
2617
2750
  async ({ id, include }) => {
2618
2751
  try {
2619
2752
  return text(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
@@ -2626,9 +2759,9 @@ function registerAbleTimeTools(server) {
2626
2759
  "abletime_download_attachment",
2627
2760
  "Download an AbleTime task file into `.context/attachments/` using Account AbleTime auth. Pass the attachment id or URL from abletime_get_task. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
2628
2761
  {
2629
- id: z3.string().describe("Attachment id or URL from abletime_get_task."),
2630
- name: z3.string().optional().describe("Optional filename override."),
2631
- repoPath: z3.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
2762
+ id: z2.string().describe("Attachment id or URL from abletime_get_task."),
2763
+ name: z2.string().optional().describe("Optional filename override."),
2764
+ repoPath: z2.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
2632
2765
  },
2633
2766
  async ({ id, name, repoPath }) => {
2634
2767
  try {
@@ -2641,7 +2774,7 @@ function registerAbleTimeTools(server) {
2641
2774
  server.tool(
2642
2775
  "abletime_comment",
2643
2776
  "Add a markdown comment on an AbleTime task (id or CRM-232). When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
2644
- { id: z3.string(), body: z3.string() },
2777
+ { id: z2.string(), body: z2.string() },
2645
2778
  async (args) => {
2646
2779
  try {
2647
2780
  return text(await commentAbleTimeTask(args));
@@ -2650,23 +2783,23 @@ function registerAbleTimeTools(server) {
2650
2783
  }
2651
2784
  }
2652
2785
  );
2653
- const relationInputSchema = z3.object({
2654
- type: z3.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
2655
- issue: z3.string().describe("Related task id or reference (CRM-232).")
2786
+ const relationInputSchema = z2.object({
2787
+ type: z2.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
2788
+ issue: z2.string().describe("Related task id or reference (CRM-232).")
2656
2789
  });
2657
2790
  server.tool(
2658
2791
  "abletime_update_task",
2659
2792
  'Update an AbleTime task (id or CRM-232). Pass title, description, state, labels, project, parent, and/or relations. labels replaces tags. project is name or id (tasks stay in a project). parent is CRM-232 or "none". relations sets the wait-on dependency (blockedBy/blocks); removeRelations clears it. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.',
2660
2793
  {
2661
- id: z3.string(),
2662
- title: z3.string().optional(),
2663
- description: z3.string().optional(),
2664
- state: z3.string().optional(),
2665
- labels: z3.array(z3.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
2666
- project: z3.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
2667
- parent: z3.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
2668
- relations: z3.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
2669
- removeRelations: z3.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
2794
+ id: z2.string(),
2795
+ title: z2.string().optional(),
2796
+ description: z2.string().optional(),
2797
+ state: z2.string().optional(),
2798
+ labels: z2.array(z2.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
2799
+ project: z2.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
2800
+ parent: z2.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
2801
+ relations: z2.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
2802
+ removeRelations: z2.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
2670
2803
  },
2671
2804
  async (args) => {
2672
2805
  try {
@@ -2685,12 +2818,12 @@ function registerAbleTimeTools(server) {
2685
2818
  "abletime_create_task",
2686
2819
  "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).",
2687
2820
  {
2688
- title: z3.string(),
2689
- description: z3.string().optional(),
2690
- projectId: z3.string().optional(),
2691
- categoryId: z3.string().optional(),
2692
- state: z3.enum(["backlog", "todo"]).optional(),
2693
- parent: z3.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
2821
+ title: z2.string(),
2822
+ description: z2.string().optional(),
2823
+ projectId: z2.string().optional(),
2824
+ categoryId: z2.string().optional(),
2825
+ state: z2.enum(["backlog", "todo"]).optional(),
2826
+ parent: z2.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
2694
2827
  },
2695
2828
  async (args) => {
2696
2829
  try {
@@ -2704,10 +2837,10 @@ function registerAbleTimeTools(server) {
2704
2837
  "abletime_ensure_task",
2705
2838
  "Find or create an AbleTime task for this work, then create_workspace with sourceType=ticket.",
2706
2839
  {
2707
- title: z3.string(),
2708
- description: z3.string().optional(),
2709
- projectId: z3.string().optional(),
2710
- categoryId: z3.string().optional()
2840
+ title: z2.string(),
2841
+ description: z2.string().optional(),
2842
+ projectId: z2.string().optional(),
2843
+ categoryId: z2.string().optional()
2711
2844
  },
2712
2845
  async (args) => {
2713
2846
  try {
@@ -2721,7 +2854,7 @@ function registerAbleTimeTools(server) {
2721
2854
  }
2722
2855
 
2723
2856
  // src/mcp/github-tools.ts
2724
- import { z as z4 } from "zod";
2857
+ import { z as z3 } from "zod";
2725
2858
  function text2(payload, isError = false) {
2726
2859
  return mcpJson(payload, isError);
2727
2860
  }
@@ -2731,15 +2864,15 @@ function fail2(err) {
2731
2864
  true
2732
2865
  );
2733
2866
  }
2734
- var repoPathSchema = z4.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
2867
+ var repoPathSchema = z3.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
2735
2868
  function registerGithubIssueTools(server) {
2736
2869
  server.tool(
2737
2870
  "github_search_issues",
2738
2871
  "Search or list open GitHub issues in this repo via Account gh. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all, or a login. Pass updatedSince for new/updated tickets and new comments (e.g. \u201Cany updates since yesterday?\u201D).",
2739
2872
  {
2740
- query: z4.string().optional().describe("Search text (title, body, identifiers). Omit to list by assignee only."),
2741
- assignee: z4.string().optional().describe("me, unassigned, all, or a GitHub login. Default: all when query is set, otherwise me."),
2742
- limit: z4.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
2873
+ query: z3.string().optional().describe("Search text (title, body, identifiers). Omit to list by assignee only."),
2874
+ assignee: z3.string().optional().describe("me, unassigned, all, or a GitHub login. Default: all when query is set, otherwise me."),
2875
+ limit: z3.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
2743
2876
  updatedSince: mcpIssueUpdatedSinceSchema,
2744
2877
  repoPath: repoPathSchema
2745
2878
  },
@@ -2776,10 +2909,22 @@ function registerGithubIssueTools(server) {
2776
2909
  }
2777
2910
  }
2778
2911
  );
2912
+ server.tool(
2913
+ "github_list_projects",
2914
+ "List open GitHub Projects for the signed-in user and this repo owner (title, number, id). Pass a title as project on github_update_issue. If GitHub says the token is missing read:project, run `gh auth refresh -s read:project`.",
2915
+ { repoPath: repoPathSchema },
2916
+ async ({ repoPath }) => {
2917
+ try {
2918
+ return text2({ projects: await listGitHubProjects({ repoPath }) });
2919
+ } catch (err) {
2920
+ return fail2(err);
2921
+ }
2922
+ }
2923
+ );
2779
2924
  server.tool(
2780
2925
  "github_get_issue",
2781
- "Get a GitHub issue (#123 or URL): body, comments, state, attachments. 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. To save a file, call github_download_attachment with the attachment URL (writes `.context/attachments/`; do not dump bytes into the tool result).",
2782
- { id: z4.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
2926
+ "Get a GitHub issue (#123 or URL): body, comments, state, projects, attachments. Re-fetch to read new comments. projectsError means GitHub refused Projects (usually missing read:project \u2014 run `gh auth refresh -s read:project`). 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. To save a file, call github_download_attachment with the attachment URL (writes `.context/attachments/`; do not dump bytes into the tool result).",
2927
+ { id: z3.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
2783
2928
  async ({ id, include, repoPath }) => {
2784
2929
  try {
2785
2930
  return text2(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
@@ -2792,8 +2937,8 @@ function registerGithubIssueTools(server) {
2792
2937
  "github_download_attachment",
2793
2938
  "Download a GitHub issue file (github.com/user-attachments/\u2026 or githubusercontent.com) into `.context/attachments/` using Account gh. Pass the URL from github_get_issue attachments. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
2794
2939
  {
2795
- url: z4.string().describe("Attachment URL from github_get_issue."),
2796
- name: z4.string().optional().describe("Optional filename override."),
2940
+ url: z3.string().describe("Attachment URL from github_get_issue."),
2941
+ name: z3.string().optional().describe("Optional filename override."),
2797
2942
  repoPath: repoPathSchema
2798
2943
  },
2799
2944
  async ({ url, name, repoPath }) => {
@@ -2807,7 +2952,7 @@ function registerGithubIssueTools(server) {
2807
2952
  server.tool(
2808
2953
  "github_comment",
2809
2954
  "Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh. When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
2810
- { id: z4.string(), body: z4.string(), repoPath: repoPathSchema },
2955
+ { id: z3.string(), body: z3.string(), repoPath: repoPathSchema },
2811
2956
  async ({ id, body, repoPath }) => {
2812
2957
  try {
2813
2958
  return text2(await commentGitHubIssue({ id, body }, { repoPath }));
@@ -2816,23 +2961,23 @@ function registerGithubIssueTools(server) {
2816
2961
  }
2817
2962
  }
2818
2963
  );
2819
- const relationInputSchema = z4.object({
2820
- type: z4.string().describe("blocks or blockedBy (this issue's view)."),
2821
- issue: z4.string().describe("Related issue #123 or URL.")
2964
+ const relationInputSchema = z3.object({
2965
+ type: z3.string().describe("blocks or blockedBy (this issue's view)."),
2966
+ issue: z3.string().describe("Related issue #123 or URL.")
2822
2967
  });
2823
2968
  server.tool(
2824
2969
  "github_update_issue",
2825
2970
  'Update a GitHub issue (#123). Pass title, body, state (open|closed), labels, project, parent, and/or relations. labels replaces all names (pass [] to clear). project is a Project title, or "none" to remove. parent is #123 or "none". relations adds blockedBy/blocks; removeRelations drops them (`gh` 2.94+). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.',
2826
2971
  {
2827
- id: z4.string(),
2828
- title: z4.string().optional(),
2829
- body: z4.string().optional(),
2830
- state: z4.string().optional().describe("open or closed"),
2831
- labels: z4.array(z4.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
2832
- project: z4.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
2833
- parent: z4.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
2834
- relations: z4.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
2835
- removeRelations: z4.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
2972
+ id: z3.string(),
2973
+ title: z3.string().optional(),
2974
+ body: z3.string().optional(),
2975
+ state: z3.string().optional().describe("open or closed"),
2976
+ labels: z3.array(z3.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
2977
+ project: z3.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
2978
+ parent: z3.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
2979
+ relations: z3.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
2980
+ removeRelations: z3.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
2836
2981
  repoPath: repoPathSchema
2837
2982
  },
2838
2983
  async (args) => {
@@ -2847,9 +2992,9 @@ function registerGithubIssueTools(server) {
2847
2992
  "github_create_issue",
2848
2993
  "Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
2849
2994
  {
2850
- title: z4.string(),
2851
- body: z4.string().optional(),
2852
- parent: z4.string().optional().describe("Parent issue #123 or URL for a spin-off."),
2995
+ title: z3.string(),
2996
+ body: z3.string().optional(),
2997
+ parent: z3.string().optional().describe("Parent issue #123 or URL for a spin-off."),
2853
2998
  repoPath: repoPathSchema
2854
2999
  },
2855
3000
  async (args) => {
@@ -2863,7 +3008,7 @@ function registerGithubIssueTools(server) {
2863
3008
  }
2864
3009
 
2865
3010
  // src/mcp/linear-tools.ts
2866
- import { z as z5 } from "zod";
3011
+ import { z as z4 } from "zod";
2867
3012
  function text3(payload, isError = false) {
2868
3013
  return mcpJson(payload, isError);
2869
3014
  }
@@ -2873,7 +3018,7 @@ function fail3(err) {
2873
3018
  true
2874
3019
  );
2875
3020
  }
2876
- var prioritySchema = z5.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
3021
+ var prioritySchema = z4.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
2877
3022
  function registerLinearTools(server) {
2878
3023
  server.tool(
2879
3024
  "linear_list_teams",
@@ -2887,13 +3032,25 @@ function registerLinearTools(server) {
2887
3032
  }
2888
3033
  }
2889
3034
  );
3035
+ server.tool(
3036
+ "linear_list_projects",
3037
+ "List Linear projects (id, name, slugId). Pass a name or id as project on linear_update_issue. linear_get_issue and search rows also include the project a ticket is on.",
3038
+ {},
3039
+ async () => {
3040
+ try {
3041
+ return text3({ projects: await listLinearProjects() });
3042
+ } catch (err) {
3043
+ return fail3(err);
3044
+ }
3045
+ }
3046
+ );
2890
3047
  server.tool(
2891
3048
  "linear_search_issues",
2892
3049
  "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. Pass updatedSince for new/updated tickets and new comments (e.g. \u201Cany updates since yesterday?\u201D).",
2893
3050
  {
2894
- query: z5.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
2895
- assignee: z5.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
2896
- limit: z5.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
3051
+ query: z4.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
3052
+ assignee: z4.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
3053
+ limit: z4.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
2897
3054
  updatedSince: mcpIssueUpdatedSinceSchema
2898
3055
  },
2899
3056
  async ({ query, assignee, limit, updatedSince }) => {
@@ -2932,8 +3089,8 @@ function registerLinearTools(server) {
2932
3089
  );
2933
3090
  server.tool(
2934
3091
  "linear_get_issue",
2935
- "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children, attachments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call linear_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
2936
- { id: z5.string(), include: mcpIssueIncludeSchema },
3092
+ "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children, project, attachments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call linear_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
3093
+ { id: z4.string(), include: mcpIssueIncludeSchema },
2937
3094
  async ({ id, include }) => {
2938
3095
  try {
2939
3096
  return text3(formatLinearIssuePayload(await getLinearIssue(id), include));
@@ -2946,9 +3103,9 @@ function registerLinearTools(server) {
2946
3103
  "linear_download_attachment",
2947
3104
  "Download a Linear file attachment into `.context/attachments/` using Account Linear auth. Pass the attachment uuid or uploads.linear.app URL from linear_get_issue. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
2948
3105
  {
2949
- id: z5.string().describe("Attachment uuid or upload URL from linear_get_issue."),
2950
- name: z5.string().optional().describe("Optional filename override."),
2951
- repoPath: z5.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
3106
+ id: z4.string().describe("Attachment uuid or upload URL from linear_get_issue."),
3107
+ name: z4.string().optional().describe("Optional filename override."),
3108
+ repoPath: z4.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
2952
3109
  },
2953
3110
  async ({ id, name, repoPath }) => {
2954
3111
  try {
@@ -2962,13 +3119,13 @@ function registerLinearTools(server) {
2962
3119
  "linear_create_issue",
2963
3120
  '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.',
2964
3121
  {
2965
- team: z5.string(),
2966
- title: z5.string(),
2967
- description: z5.string().optional(),
2968
- state: z5.string().optional(),
2969
- assignee: z5.string().nullable().optional(),
3122
+ team: z4.string(),
3123
+ title: z4.string(),
3124
+ description: z4.string().optional(),
3125
+ state: z4.string().optional(),
3126
+ assignee: z4.string().nullable().optional(),
2970
3127
  priority: prioritySchema,
2971
- parent: z5.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
3128
+ parent: z4.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
2972
3129
  },
2973
3130
  async (args) => {
2974
3131
  try {
@@ -2978,26 +3135,26 @@ function registerLinearTools(server) {
2978
3135
  }
2979
3136
  }
2980
3137
  );
2981
- const relationInputSchema = z5.object({
2982
- type: z5.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
2983
- issue: z5.string().describe("Related issue uuid or identifier (ENG-123).")
3138
+ const relationInputSchema = z4.object({
3139
+ type: z4.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
3140
+ issue: z4.string().describe("Related issue uuid or identifier (ENG-123).")
2984
3141
  });
2985
3142
  server.tool(
2986
3143
  "linear_update_issue",
2987
3144
  'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, cycle, labels, project, parent, and/or relations. Cycle/project/parent: name or id, or "none" to clear. labels replaces all names (pass [] to clear; get the issue first to keep existing). relations adds; removeRelations removes by type + issue. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
2988
3145
  {
2989
- id: z5.string(),
2990
- title: z5.string().optional(),
2991
- description: z5.string().optional(),
2992
- state: z5.string().optional(),
2993
- assignee: z5.string().nullable().optional(),
3146
+ id: z4.string(),
3147
+ title: z4.string().optional(),
3148
+ description: z4.string().optional(),
3149
+ state: z4.string().optional(),
3150
+ assignee: z4.string().nullable().optional(),
2994
3151
  priority: prioritySchema,
2995
- cycle: z5.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
2996
- labels: z5.array(z5.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
2997
- project: z5.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
2998
- parent: z5.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
2999
- relations: z5.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
3000
- removeRelations: z5.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
3152
+ cycle: z4.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
3153
+ labels: z4.array(z4.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
3154
+ project: z4.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
3155
+ parent: z4.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
3156
+ relations: z4.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
3157
+ removeRelations: z4.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
3001
3158
  },
3002
3159
  async (args) => {
3003
3160
  try {
@@ -3011,8 +3168,8 @@ function registerLinearTools(server) {
3011
3168
  "linear_comment",
3012
3169
  "Add a markdown comment on a Linear issue (uuid or ENG-123). When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
3013
3170
  {
3014
- id: z5.string(),
3015
- body: z5.string()
3171
+ id: z4.string(),
3172
+ body: z4.string()
3016
3173
  },
3017
3174
  async (args) => {
3018
3175
  try {
@@ -3031,104 +3188,6 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
3031
3188
  if (isAbleTimeConnected(settings)) registerAbleTimeTools(server);
3032
3189
  }
3033
3190
 
3034
- // src/mcp/viewer-context-tools.ts
3035
- import { z as z6 } from "zod";
3036
- function text4(payload, isError = false) {
3037
- return mcpJson(payload, isError);
3038
- }
3039
- function matchRegisteredWorkspace(repoPath) {
3040
- const hint = (repoPath ?? "").trim().replace(/\/+$/, "");
3041
- if (!hint) return null;
3042
- let best = null;
3043
- for (const w of listWorkspaces()) {
3044
- const stored = w.path.replace(/\/+$/, "");
3045
- if (stored === hint || hint.startsWith(`${stored}/`)) {
3046
- if (!best || stored.length > best.length) best = stored;
3047
- }
3048
- }
3049
- return best;
3050
- }
3051
- async function resolveProjectRepoPath(repoPath) {
3052
- const hint = repoPath?.trim() || process.cwd();
3053
- let root = hint;
3054
- try {
3055
- root = await resolveRepoRoot(hint);
3056
- } catch {
3057
- root = hint.replace(/\/+$/, "");
3058
- }
3059
- return matchRegisteredWorkspace(root) ?? matchRegisteredWorkspace(hint);
3060
- }
3061
- var PROJECT_PATH_HINT = "Pass repoPath from list_projects. Orchestration cwd is synthetic and empty \u2014 do not omit repoPath for project context.";
3062
- function registerViewerContextTools(server) {
3063
- server.tool(
3064
- "get_viewer_context",
3065
- "Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_projects for project context. Worktree turns may omit repoPath (uses cwd).",
3066
- {
3067
- repoPath: z6.string().optional().describe(
3068
- "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
3069
- )
3070
- },
3071
- async ({ repoPath }) => {
3072
- try {
3073
- const path = await resolveProjectRepoPath(repoPath);
3074
- return text4(readViewerContext(path ?? void 0));
3075
- } catch (err) {
3076
- return text4(
3077
- { error: err instanceof Error ? err.message : String(err) },
3078
- true
3079
- );
3080
- }
3081
- }
3082
- );
3083
- server.tool(
3084
- "update_viewer_context",
3085
- "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.",
3086
- {
3087
- scope: z6.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
3088
- context: z6.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
3089
- confirmed: z6.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
3090
- repoPath: z6.string().optional().describe(
3091
- "Required for scope=project from orchestration (path from list_projects). Worktree turns may omit it (cwd)."
3092
- )
3093
- },
3094
- async ({ scope, context, confirmed, repoPath }) => {
3095
- try {
3096
- if (scope === "project") {
3097
- const path = await resolveProjectRepoPath(repoPath);
3098
- if (!path) {
3099
- return text4(
3100
- {
3101
- ok: false,
3102
- error: "repoPath required",
3103
- message: PROJECT_PATH_HINT
3104
- },
3105
- true
3106
- );
3107
- }
3108
- const result2 = writeViewerContext({
3109
- scope,
3110
- context,
3111
- repoPath: path,
3112
- confirmed: confirmed === true
3113
- });
3114
- return text4(result2, !result2.ok);
3115
- }
3116
- const result = writeViewerContext({
3117
- scope,
3118
- context,
3119
- confirmed: confirmed === true
3120
- });
3121
- return text4(result, !result.ok);
3122
- } catch (err) {
3123
- return text4(
3124
- { error: err instanceof Error ? err.message : String(err) },
3125
- true
3126
- );
3127
- }
3128
- }
3129
- );
3130
- }
3131
-
3132
3191
  // src/mcp/pr-list.ts
3133
3192
  function compactPrRow(pr) {
3134
3193
  return {
@@ -3164,12 +3223,12 @@ function formatMcpPrList(input) {
3164
3223
  }
3165
3224
 
3166
3225
  // src/mcp/schedule-tools.ts
3167
- import { z as z7 } from "zod";
3168
- function text5(payload, isError = false) {
3226
+ import { z as z5 } from "zod";
3227
+ function text4(payload, isError = false) {
3169
3228
  return mcpJson(payload, isError);
3170
3229
  }
3171
3230
  function fail4(err) {
3172
- return text5(
3231
+ return text4(
3173
3232
  { error: err instanceof Error ? err.message : String(err) },
3174
3233
  true
3175
3234
  );
@@ -3194,7 +3253,7 @@ function registerScheduleTools(server) {
3194
3253
  {},
3195
3254
  async () => {
3196
3255
  try {
3197
- return text5({ schedules: listSchedules() });
3256
+ return text4({ schedules: listSchedules() });
3198
3257
  } catch (err) {
3199
3258
  return fail4(err);
3200
3259
  }
@@ -3204,18 +3263,18 @@ function registerScheduleTools(server) {
3204
3263
  "create_schedule",
3205
3264
  "Create a local schedule that, when due, sends a prompt to an existing chat (chatId) or starts a new Global orchestration chat (omit chatId). Pass chatId=self to continue this chat. If that chat is missing or archived when the job fires, Sideboard starts a new Global chat (recurring jobs then continue that chat). Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without chatId open a new Global chat each run. Creating or enabling a future job turns on Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled so the Mac stays awake until due. Sideboard.app must be running for the job to fire.",
3206
3265
  {
3207
- prompt: z7.string().describe("User message / goal queued when the schedule fires"),
3208
- name: z7.string().optional(),
3209
- at: z7.string().optional().describe("ISO datetime for a one-shot"),
3210
- every: z7.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
3211
- cron: z7.string().optional().describe("5-field cron expression"),
3212
- tz: z7.string().optional().describe("IANA timezone for cron (default: system)"),
3213
- chatId: z7.string().optional().describe(
3266
+ prompt: z5.string().describe("User message / goal queued when the schedule fires"),
3267
+ name: z5.string().optional(),
3268
+ at: z5.string().optional().describe("ISO datetime for a one-shot"),
3269
+ every: z5.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
3270
+ cron: z5.string().optional().describe("5-field cron expression"),
3271
+ tz: z5.string().optional().describe("IANA timezone for cron (default: system)"),
3272
+ chatId: z5.string().optional().describe(
3214
3273
  'Existing chat id (orchestration or workspace), or "self" for this chat. Omit to create a new Global chat on fire.'
3215
3274
  ),
3216
- threadId: z7.string().optional().describe("Legacy alias for chatId."),
3217
- agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
3218
- model: z7.string().optional()
3275
+ threadId: z5.string().optional().describe("Legacy alias for chatId."),
3276
+ agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
3277
+ model: z5.string().optional()
3219
3278
  },
3220
3279
  async (args) => {
3221
3280
  try {
@@ -3238,7 +3297,7 @@ function registerScheduleTools(server) {
3238
3297
  model: args.model,
3239
3298
  createdBy: "mcp"
3240
3299
  });
3241
- return text5({
3300
+ return text4({
3242
3301
  schedule,
3243
3302
  hint: scheduleNeedsCaffeinate(schedule) ? "Fires while Sideboard.app is running. Recurring jobs without chatId create a new orchestration chat each run. Caffeinate while schedules are enabled is now on so the Mac can stay awake until this job is due." : "Fires while Sideboard.app is running. Recurring jobs without chatId create a new orchestration chat each run."
3244
3303
  });
@@ -3251,20 +3310,20 @@ function registerScheduleTools(server) {
3251
3310
  "update_schedule",
3252
3311
  "Update a local schedule (prompt, cadence, target chat, enabled). Pass id from list_schedules.",
3253
3312
  {
3254
- id: z7.string(),
3255
- prompt: z7.string().optional(),
3256
- name: z7.string().optional(),
3257
- at: z7.string().optional(),
3258
- every: z7.string().optional(),
3259
- cron: z7.string().optional(),
3260
- tz: z7.string().optional(),
3261
- chatId: z7.string().optional().describe(
3313
+ id: z5.string(),
3314
+ prompt: z5.string().optional(),
3315
+ name: z5.string().optional(),
3316
+ at: z5.string().optional(),
3317
+ every: z5.string().optional(),
3318
+ cron: z5.string().optional(),
3319
+ tz: z5.string().optional(),
3320
+ chatId: z5.string().optional().describe(
3262
3321
  'Existing chat (orchestration or workspace), "self", or empty string to create a new Global chat each run'
3263
3322
  ),
3264
- threadId: z7.string().optional().describe("Legacy alias for chatId."),
3265
- agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional(),
3266
- model: z7.string().optional(),
3267
- enabled: z7.boolean().optional()
3323
+ threadId: z5.string().optional().describe("Legacy alias for chatId."),
3324
+ agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional(),
3325
+ model: z5.string().optional(),
3326
+ enabled: z5.boolean().optional()
3268
3327
  },
3269
3328
  async (args) => {
3270
3329
  try {
@@ -3296,7 +3355,7 @@ function registerScheduleTools(server) {
3296
3355
  enabled: args.enabled,
3297
3356
  model: args.model
3298
3357
  });
3299
- return text5({ schedule });
3358
+ return text4({ schedule });
3300
3359
  } catch (err) {
3301
3360
  return fail4(err);
3302
3361
  }
@@ -3305,11 +3364,11 @@ function registerScheduleTools(server) {
3305
3364
  server.tool(
3306
3365
  "delete_schedule",
3307
3366
  "Delete a local schedule. Pass id from list_schedules.",
3308
- { id: z7.string() },
3367
+ { id: z5.string() },
3309
3368
  async ({ id }) => {
3310
3369
  try {
3311
3370
  deleteSchedule(id);
3312
- return text5({ ok: true, id });
3371
+ return text4({ ok: true, id });
3313
3372
  } catch (err) {
3314
3373
  return fail4(err);
3315
3374
  }
@@ -3318,14 +3377,14 @@ function registerScheduleTools(server) {
3318
3377
  server.tool(
3319
3378
  "run_schedule",
3320
3379
  "Fire a schedule now (does not wait for nextRunAt). Queues the prompt on the target chat or starts a new Global chat. If Sideboard.app is running, the desktop drains the turn.",
3321
- { id: z7.string() },
3380
+ { id: z5.string() },
3322
3381
  async ({ id }) => {
3323
3382
  try {
3324
3383
  if (!getSchedule(id)) {
3325
3384
  return fail4(new Error(`Schedule not found: ${id}`));
3326
3385
  }
3327
3386
  const schedule = await fireSchedule(id);
3328
- return text5({ schedule });
3387
+ return text4({ schedule });
3329
3388
  } catch (err) {
3330
3389
  return fail4(err);
3331
3390
  }
@@ -3590,9 +3649,9 @@ async function getGitHubStatus() {
3590
3649
  };
3591
3650
  }
3592
3651
  const status = await run("gh", ["auth", "status"], { reject: false });
3593
- const text6 = `${status.stdout}
3652
+ const text5 = `${status.stdout}
3594
3653
  ${status.stderr}`;
3595
- const loginMatch = text6.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text6.match(/Logged in to ([^\s]+) as (\S+)/i);
3654
+ const loginMatch = text5.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text5.match(/Logged in to ([^\s]+) as (\S+)/i);
3596
3655
  if (loginMatch) {
3597
3656
  return {
3598
3657
  connected: true,
@@ -3609,154 +3668,7 @@ ${status.stderr}`;
3609
3668
  };
3610
3669
  }
3611
3670
 
3612
- // src/mcp/create-thread-agent.ts
3613
- function resolveOrchCreateThreadOptions(input) {
3614
- const { resolveNewThreadOptions } = input;
3615
- const account = resolveNewThreadOptions({});
3616
- const requested = input.requestedAgent;
3617
- const parent = input.parentAgent ?? void 0;
3618
- const cursorReady = input.cursorReady ?? isCursorApiKeyConfigured();
3619
- let agent = account.agent;
3620
- let ignoredAgent;
3621
- let honorRequestedModel = false;
3622
- if (requested) {
3623
- const parentEcho = Boolean(parent) && requested === parent && requested !== account.agent;
3624
- const cursorAutofill = requested === "cursor" && account.agent !== "cursor";
3625
- if (parentEcho || cursorAutofill) {
3626
- ignoredAgent = requested;
3627
- } else {
3628
- agent = requested;
3629
- honorRequestedModel = input.requestedModel !== void 0;
3630
- }
3631
- }
3632
- let coercedFrom;
3633
- if (agent === "codex" && parent === "codex" && cursorReady) {
3634
- coercedFrom = requested ?? "codex";
3635
- agent = "cursor";
3636
- }
3637
- const opts = resolveNewThreadOptions({
3638
- agent,
3639
- model: honorRequestedModel ? input.requestedModel : void 0
3640
- });
3641
- return {
3642
- ...opts,
3643
- ...ignoredAgent ? { ignoredAgent } : {},
3644
- ...coercedFrom ? { coercedFrom } : {}
3645
- };
3646
- }
3647
- function orchCreateThreadAgentNote(input) {
3648
- if (input.coercedFrom) {
3649
- return `Avoid nested Codex under a Codex orchestrator \u2014 used agent=${input.agent}`;
3650
- }
3651
- if (input.ignoredAgent) {
3652
- return `Ignored agent=${input.ignoredAgent} \u2014 used Account default agent=${input.agent}`;
3653
- }
3654
- return void 0;
3655
- }
3656
-
3657
3671
  // src/mcp/server.ts
3658
- var MAX_ORCH_THREADS = 5;
3659
- var CREATE_THREAD_TIMEOUT_MS = 9e4;
3660
- function withTimeout(promise, ms, label) {
3661
- return new Promise((resolve, reject) => {
3662
- const timer = setTimeout(() => {
3663
- reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`));
3664
- }, ms);
3665
- promise.then(
3666
- (value) => {
3667
- clearTimeout(timer);
3668
- resolve(value);
3669
- },
3670
- (err) => {
3671
- clearTimeout(timer);
3672
- reject(err);
3673
- }
3674
- );
3675
- });
3676
- }
3677
- async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
3678
- const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
3679
- let parentId = args.parentChatId?.trim() || args.parentThreadId?.trim() || "";
3680
- let parent = parentId ? orch.getThread(parentId) : null;
3681
- let parentCorrectedFrom;
3682
- if (envParentId) {
3683
- const envParent = orch.getThread(envParentId);
3684
- if (envParent) {
3685
- if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
3686
- else if (!parentId) parentCorrectedFrom = void 0;
3687
- parentId = envParentId;
3688
- parent = envParent;
3689
- }
3690
- }
3691
- if (parentId && !parent) {
3692
- parentCorrectedFrom = parentId;
3693
- parentId = "";
3694
- parent = null;
3695
- }
3696
- if (parentId) {
3697
- const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
3698
- if (children.length >= MAX_ORCH_THREADS) {
3699
- return {
3700
- ok: false,
3701
- text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
3702
- };
3703
- }
3704
- }
3705
- const opts = resolveOrchCreateThreadOptions({
3706
- requestedAgent: args.agent,
3707
- requestedModel: args.model,
3708
- parentAgent: parent?.agent,
3709
- resolveNewThreadOptions
3710
- });
3711
- const agentNote = orchCreateThreadAgentNote(opts);
3712
- try {
3713
- const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
3714
- const thread = await withTimeout(
3715
- orch.createThread({
3716
- sourceType: args.sourceType,
3717
- sourceRef: args.sourceRef,
3718
- agent: opts.agent,
3719
- model: opts.model,
3720
- effort: opts.effort,
3721
- fast: opts.fast,
3722
- repoPath: args.repoPath,
3723
- title: args.title,
3724
- parentThreadId: parentId || null,
3725
- cowboy: args.cowboy || void 0,
3726
- attachments: args.attachments
3727
- }),
3728
- CREATE_THREAD_TIMEOUT_MS,
3729
- "create_workspace"
3730
- );
3731
- const alreadyStarted = priorIds.has(thread.id);
3732
- const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
3733
- return {
3734
- ok: true,
3735
- text: JSON.stringify({
3736
- id: thread.id,
3737
- title: thread.title,
3738
- branchName: thread.branchName,
3739
- worktreePath: thread.worktreePath,
3740
- agent: thread.agent,
3741
- model: thread.model,
3742
- status: thread.status,
3743
- cowboy: Boolean(thread.cowboy),
3744
- link: `sideboard://chat/${thread.id}`,
3745
- parentChatId: thread.parentThreadId,
3746
- parentThreadId: thread.parentThreadId,
3747
- chats: workspaceAgentChatSummaries(thread.worktreePath),
3748
- ...alreadyStarted ? { alreadyStarted: true } : {},
3749
- ...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
3750
- ...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
3751
- ...agentNote ? { note: agentNote } : {},
3752
- ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
3753
- })
3754
- };
3755
- } catch (err) {
3756
- const message = err instanceof Error ? err.message : String(err);
3757
- return { ok: false, text: `create_workspace failed: ${message}` };
3758
- }
3759
- }
3760
3672
  function previewHint(preview) {
3761
3673
  return preview === "window" ? "Electron: the native window is the app. url is the renderer/HMR origin only \u2014 do not open it in a browser (no preload/IPC)." : "Open url in a browser, curl, or Playwright to use the app.";
3762
3674
  }
@@ -3854,16 +3766,16 @@ async function startMcpServer() {
3854
3766
  "list_board",
3855
3767
  "Home Kanban of workspaces (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chats on that card are separate agents. 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), ownership (mine = your PRs / WIP; reviewing = someone else's PR), column, limit (default 40). create_workspace adds a workspace (and a Home card), or returns the live checkout if that ticket/PR/named branch is already checked out. That reuse is one checkout, not one agent \u2014 fork_chat to add another.",
3856
3768
  {
3857
- query: z8.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
3858
- repoPath: z8.string().optional().describe("Limit to one project path from list_projects"),
3859
- kind: z8.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
3860
- ownership: z8.enum(["all", "mine", "reviewing"]).optional().describe(
3769
+ query: z6.string().optional().describe("Case-insensitive token search across title, id, labels, workspace tags, repo"),
3770
+ repoPath: z6.string().optional().describe("Limit to one project path from list_projects"),
3771
+ kind: z6.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
3772
+ ownership: z6.enum(["all", "mine", "reviewing"]).optional().describe(
3861
3773
  "Mine = PRs you authored or local WIP; reviewing = someone else's PR. Uses the gh login."
3862
3774
  ),
3863
- column: z8.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
3775
+ column: z6.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
3864
3776
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
3865
3777
  ),
3866
- limit: z8.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
3778
+ limit: z6.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
3867
3779
  },
3868
3780
  async ({ query, repoPath, kind, ownership, column, limit }) => {
3869
3781
  const workspaces = orch.listWorkspaces();
@@ -3893,7 +3805,7 @@ async function startMcpServer() {
3893
3805
  server.tool(
3894
3806
  "get_chat",
3895
3807
  "Get a compact chat summary by id/ref. Includes last message preview, parentChatId, and child workspace chats (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (billed token + costUsd totals when providers reported cost) and lastTurnUsage. blockedReason is why that agent is waiting on a person (question, plan approval, or a reported block).",
3896
- { ref: z8.string() },
3808
+ { ref: z6.string() },
3897
3809
  async ({ ref }) => {
3898
3810
  const t = orch.getThread(ref);
3899
3811
  if (!t) {
@@ -3915,6 +3827,7 @@ async function startMcpServer() {
3915
3827
  sessionId: t.sessionId,
3916
3828
  parentChatId: t.parentThreadId,
3917
3829
  parentThreadId: t.parentThreadId,
3830
+ tags: t.tags ?? [],
3918
3831
  children: childThreadRefs(t.id, orch.getThreads(false)),
3919
3832
  queueLength: t.queue.length,
3920
3833
  messageCount: t.messages.length,
@@ -3939,17 +3852,17 @@ async function startMcpServer() {
3939
3852
  "present_artifact",
3940
3853
  "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.",
3941
3854
  {
3942
- title: z8.string().describe("Short title shown in the artifact pane header"),
3943
- type: z8.enum(["html", "svg", "markdown", "react", "log"]).describe(
3855
+ title: z6.string().describe("Short title shown in the artifact pane header"),
3856
+ type: z6.enum(["html", "svg", "markdown", "react", "log"]).describe(
3944
3857
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
3945
3858
  ),
3946
- content: z8.string().describe(
3859
+ content: z6.string().describe(
3947
3860
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
3948
3861
  ),
3949
- artifact_id: z8.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
3950
- status: z8.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
3951
- phase: z8.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
3952
- mode: z8.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
3862
+ artifact_id: z6.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
3863
+ status: z6.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
3864
+ phase: z6.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
3865
+ mode: z6.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
3953
3866
  },
3954
3867
  async ({ title, type, artifact_id, status, phase, mode }) => {
3955
3868
  return mcpJson(
@@ -3962,15 +3875,15 @@ async function startMcpServer() {
3962
3875
  "ask_user",
3963
3876
  "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.",
3964
3877
  {
3965
- questions: z8.array(
3966
- z8.object({
3967
- question: z8.string().describe("Full question text ending with ?"),
3968
- header: z8.string().max(24).optional().describe("Short label shown above the question"),
3969
- multiSelect: z8.boolean().optional().describe("Allow selecting multiple options"),
3970
- options: z8.array(
3971
- z8.object({
3972
- label: z8.string(),
3973
- description: z8.string().optional().describe("What this option means / when to choose it (strongly preferred)")
3878
+ questions: z6.array(
3879
+ z6.object({
3880
+ question: z6.string().describe("Full question text ending with ?"),
3881
+ header: z6.string().max(24).optional().describe("Short label shown above the question"),
3882
+ multiSelect: z6.boolean().optional().describe("Allow selecting multiple options"),
3883
+ options: z6.array(
3884
+ z6.object({
3885
+ label: z6.string(),
3886
+ description: z6.string().optional().describe("What this option means / when to choose it (strongly preferred)")
3974
3887
  })
3975
3888
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
3976
3889
  })
@@ -3996,9 +3909,9 @@ async function startMcpServer() {
3996
3909
  "present_plan",
3997
3910
  PRESENT_PLAN_TOOL_DESCRIPTION,
3998
3911
  {
3999
- title: z8.string().optional().describe("Short plan title (defaults to Plan)"),
4000
- content: z8.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
4001
- thread_id: z8.string().optional().describe("Sideboard thread id when cwd is not the worktree")
3912
+ title: z6.string().optional().describe("Short plan title (defaults to Plan)"),
3913
+ content: z6.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
3914
+ thread_id: z6.string().optional().describe("Sideboard thread id when cwd is not the worktree")
4002
3915
  },
4003
3916
  async ({ title, content, thread_id }) => {
4004
3917
  const { writePlanFile } = await import("../plan-file-VAHV5WBB.js");
@@ -4027,15 +3940,15 @@ async function startMcpServer() {
4027
3940
  "present_schema",
4028
3941
  "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.",
4029
3942
  {
4030
- title: z8.string().describe("Pane title"),
4031
- mode: z8.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
4032
- datasource: z8.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
4033
- resource_id: z8.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
4034
- record_id: z8.string().optional().describe("Record id when opening form mode"),
4035
- resource: z8.record(z8.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
4036
- record: z8.record(z8.unknown()).optional().describe("Inline record { id, data, published_at? }"),
4037
- records: z8.array(z8.record(z8.unknown())).optional().describe("Inline records for table mode"),
4038
- pane_id: z8.string().optional().describe("Stable pane id across updates")
3943
+ title: z6.string().describe("Pane title"),
3944
+ mode: z6.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
3945
+ datasource: z6.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
3946
+ resource_id: z6.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
3947
+ record_id: z6.string().optional().describe("Record id when opening form mode"),
3948
+ resource: z6.record(z6.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
3949
+ record: z6.record(z6.unknown()).optional().describe("Inline record { id, data, published_at? }"),
3950
+ records: z6.array(z6.record(z6.unknown())).optional().describe("Inline records for table mode"),
3951
+ pane_id: z6.string().optional().describe("Stable pane id across updates")
4039
3952
  },
4040
3953
  async (args) => {
4041
3954
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -4056,10 +3969,10 @@ async function startMcpServer() {
4056
3969
  "present_files",
4057
3970
  "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.",
4058
3971
  {
4059
- title: z8.string().optional().describe("Pane title (default: Files)"),
4060
- datasource: z8.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
4061
- path: z8.string().optional().describe("Initial folder path (e.g. public)"),
4062
- pane_id: z8.string().optional().describe("Stable pane id across updates")
3972
+ title: z6.string().optional().describe("Pane title (default: Files)"),
3973
+ datasource: z6.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
3974
+ path: z6.string().optional().describe("Initial folder path (e.g. public)"),
3975
+ pane_id: z6.string().optional().describe("Stable pane id across updates")
4063
3976
  },
4064
3977
  async (args) => {
4065
3978
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -4079,8 +3992,8 @@ async function startMcpServer() {
4079
3992
  "wait_for_job",
4080
3993
  "Wait on a detached long job (tests, pack, deploy, connector CLI) started with detached-job.cjs. MCP clients kill tools around 60s, so this returns within 45s. Sideboard opens a type=log pane from this result (artifact_id = job id, content = delta). stillRunning is the source of truth \u2014 if true, call wait_for_job again. If the job is hanging, producing no useful output, or doing the wrong thing, call stop_job instead of looping forever. Do not end the turn or tell the user you will let them know later. If false, ok/failed is the result.",
4081
3994
  {
4082
- id: z8.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
4083
- timeoutMs: z8.number().optional()
3995
+ id: z6.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
3996
+ timeoutMs: z6.number().optional()
4084
3997
  },
4085
3998
  async ({ id, timeoutMs }) => {
4086
3999
  const result = await waitForDetachedJob(process.cwd(), id, {
@@ -4093,8 +4006,8 @@ async function startMcpServer() {
4093
4006
  "stop_job",
4094
4007
  "Stop a detached job you started with detached-job.cjs when it is hanging, producing no useful output, buffering forever, or doing the wrong thing (wrong project, infinite watch, huge dump). Do not stop a pack/test/deploy that is clearly making progress. The type=log pane updates to status=failed with the last delta. Then decide the next step.",
4095
4008
  {
4096
- id: z8.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
4097
- reason: z8.string().optional().describe("Why you are stopping (hanging, no progress, wrong output, already have the answer)")
4009
+ id: z6.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
4010
+ reason: z6.string().optional().describe("Why you are stopping (hanging, no progress, wrong output, already have the answer)")
4098
4011
  },
4099
4012
  async ({ id, reason }) => {
4100
4013
  const result = await stopDetachedJob(process.cwd(), id, { reason });
@@ -4106,7 +4019,7 @@ async function startMcpServer() {
4106
4019
  "list_run_scripts",
4107
4020
  "List .sideboard/.conductor run scripts for this thread (same menu as the desktop Dev button) and which are active. Each script and active run includes preview: url (open http://localhost:<SIDEBOARD_PORT>) or window (Electron: native window is the app; url is renderer/HMR only). Never guess :3000. Worktree turns may omit ref (uses cwd).",
4108
4021
  {
4109
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd).")
4022
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd).")
4110
4023
  },
4111
4024
  async ({ ref }) => {
4112
4025
  try {
@@ -4136,8 +4049,8 @@ async function startMcpServer() {
4136
4049
  "run_dev_script",
4137
4050
  "Start a .sideboard/.conductor run script (same as the desktop Dev / play button). Several named scripts can run at once; each gets its own port, and starting one does not stop the others. Logs and Stop appear in the thread UI \u2014 do not detached-job or shell-spawn the same command. Returns {port, ports, url, preview}. preview=url \u2192 open url (browser, curl, Playwright). preview=window \u2192 Electron: the native window is the app; url is renderer/HMR only (no preload/IPC in a browser). The port is allocated SIDEBOARD_PORT, not 3000. Worktree turns may omit ref (uses cwd). Omit name for the default script.",
4138
4051
  {
4139
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
4140
- name: z8.string().optional().describe("Script name from list_run_scripts (default script if omitted).")
4052
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
4053
+ name: z6.string().optional().describe("Script name from list_run_scripts (default script if omitted).")
4141
4054
  },
4142
4055
  async ({ ref, name }) => {
4143
4056
  try {
@@ -4165,8 +4078,8 @@ async function startMcpServer() {
4165
4078
  "stop_dev_script",
4166
4079
  "Stop a run script started with run_dev_script or the desktop Dev button (same as UI Stop). Omit name to stop all scripts on this worktree. Worktree turns may omit ref (uses cwd).",
4167
4080
  {
4168
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
4169
- name: z8.string().optional().describe("Script name to stop; omit to stop all active scripts.")
4081
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
4082
+ name: z6.string().optional().describe("Script name to stop; omit to stop all active scripts.")
4170
4083
  },
4171
4084
  async ({ ref, name }) => {
4172
4085
  try {
@@ -4183,9 +4096,9 @@ async function startMcpServer() {
4183
4096
  "get_run_log",
4184
4097
  "Read the Run-tab terminal for a worktree script (same stdout/stderr as the desktop Dev pane). Default is a tail \u2014 raise `tail` only if the error is missing. Worktree turns may omit ref (uses cwd). Omit name for the default script.",
4185
4098
  {
4186
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
4187
- name: z8.string().optional().describe("Script name from list_run_scripts (default script if omitted)."),
4188
- tail: z8.number().int().min(1).max(MAX_RUN_LOG_CHARS).optional().describe(
4099
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
4100
+ name: z6.string().optional().describe("Script name from list_run_scripts (default script if omitted)."),
4101
+ tail: z6.number().int().min(1).max(MAX_RUN_LOG_CHARS).optional().describe(
4189
4102
  `Max characters to return from the end (default ${DEFAULT_RUN_LOG_TAIL_CHARS}). Do not request the full buffer unless the tail is truncated and you still need more.`
4190
4103
  )
4191
4104
  },
@@ -4223,8 +4136,8 @@ async function startMcpServer() {
4223
4136
  "notify_orchestrator",
4224
4137
  "Wake the parent Global orchestrator with a short status so it does not have to poll wait_for_turn. Use when you are blocked and the parent may have moved on (missing access, waiting on something the coordinator must handle). The message is shown on the sidebar and board until the next user message in this chat. ask_user already notifies for input-required \u2014 do not call this for the same question. Information only: never a git command. Do not use this to steer other worktrees.",
4225
4138
  {
4226
- message: z8.string().min(1).max(1500).describe("Why you are blocked / what the parent should know (short)."),
4227
- reason: z8.enum(["input-required", "blocked"]).optional().describe(
4139
+ message: z6.string().min(1).max(1500).describe("Why you are blocked / what the parent should know (short)."),
4140
+ reason: z6.enum(["input-required", "blocked"]).optional().describe(
4228
4141
  "input-required = waiting on the user in this chat; blocked = need the coordinator (default)."
4229
4142
  )
4230
4143
  },
@@ -4250,13 +4163,45 @@ async function startMcpServer() {
4250
4163
  ...result.deduped ? { deduped: true } : {}
4251
4164
  });
4252
4165
  } catch (err) {
4253
- const text6 = err instanceof Error ? err.message : String(err);
4254
- return { content: [{ type: "text", text: text6 }], isError: true };
4166
+ const text5 = err instanceof Error ? err.message : String(err);
4167
+ return { content: [{ type: "text", text: text5 }], isError: true };
4255
4168
  }
4256
4169
  }
4257
4170
  );
4258
4171
  }
4259
4172
  registerScheduleTools(server);
4173
+ server.tool(
4174
+ "set_workspace_tags",
4175
+ worktreeProfile ? "Update tags on this workspace (the checkout you are in). Omit ref to use this chat. mode=replace (default), add, or remove. Short labels \u2014 the same ones the orchestrator used for this task. You cannot retag a different checkout." : "Label a project workspace so related checkouts stay recognizable in the sidebar and on the phone. Pass any chat id on that workspace. Use the same tags on every workspace from one task. mode=replace is the default; add and remove change the set. create_workspace tags= sets them at create time. Not for orchestration chats.",
4176
+ {
4177
+ ref: z6.string().optional().describe(
4178
+ worktreeProfile ? "Omit for this chat. Another chat id only if it is on this same checkout." : "Chat id on the workspace to label."
4179
+ ),
4180
+ tags: z6.union([z6.string(), z6.array(z6.string())]).describe("Labels to set. A string may be comma-separated."),
4181
+ mode: z6.enum(["replace", "add", "remove"]).optional()
4182
+ },
4183
+ async ({ ref, tags, mode }) => {
4184
+ try {
4185
+ const callerId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
4186
+ const target = workspaceTagTargetRef({
4187
+ ownWorktreeOnly: worktreeProfile,
4188
+ caller: callerId ? orch.getThread(callerId) : null,
4189
+ requestedRef: ref,
4190
+ find: (id) => orch.getThread(id)
4191
+ });
4192
+ const thread = applyWorkspaceTags(target, tags, mode ?? "replace");
4193
+ return mcpJson({
4194
+ id: thread.id,
4195
+ worktreePath: thread.worktreePath,
4196
+ tags: thread.tags ?? [],
4197
+ chats: workspaceAgentChatSummaries(thread.worktreePath)
4198
+ });
4199
+ } catch (err) {
4200
+ const message = err instanceof Error ? err.message : String(err);
4201
+ return mcpJson({ ok: false, message }, true);
4202
+ }
4203
+ }
4204
+ );
4260
4205
  if (!worktreeProfile) {
4261
4206
  registerConnectedIssueVendorTools(server);
4262
4207
  const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-7JM5H5P3.js");
@@ -4267,7 +4212,7 @@ async function startMcpServer() {
4267
4212
  "set_caffeinate",
4268
4213
  "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.",
4269
4214
  {
4270
- enabled: z8.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
4215
+ enabled: z6.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
4271
4216
  },
4272
4217
  async ({ enabled }) => {
4273
4218
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
@@ -4291,25 +4236,28 @@ async function startMcpServer() {
4291
4236
  );
4292
4237
  server.tool(
4293
4238
  "create_workspace",
4294
- `Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy).`,
4239
+ `Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Pass the same tags on every workspace you open for one task (short labels such as "phone-sync") so they still match in the sidebar the next day. Reusing a checkout adds those tags.`,
4295
4240
  {
4296
- sourceType: z8.enum(["branch", "pr", "ticket"]),
4297
- sourceRef: z8.string(),
4298
- agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(
4241
+ sourceType: z6.enum(["branch", "pr", "ticket"]),
4242
+ sourceRef: z6.string(),
4243
+ agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(
4299
4244
  `Usually omit. Sideboard uses Account default agent (${accountDefaults.agent}). Passing your own agent or cursor is ignored.`
4300
4245
  ),
4301
- model: z8.string().nullable().optional().describe(
4246
+ model: z6.string().nullable().optional().describe(
4302
4247
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
4303
4248
  ),
4304
- repoPath: z8.string(),
4305
- title: z8.string().optional(),
4306
- cowboy: z8.boolean().optional().describe(
4249
+ repoPath: z6.string(),
4250
+ title: z6.string().optional(),
4251
+ cowboy: z6.boolean().optional().describe(
4307
4252
  "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."
4308
4253
  ),
4309
- parentChatId: z8.string().optional().describe(
4254
+ parentChatId: z6.string().optional().describe(
4310
4255
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
4311
4256
  ),
4312
- parentThreadId: z8.string().optional().describe("Legacy alias for parentChatId.")
4257
+ parentThreadId: z6.string().optional().describe("Legacy alias for parentChatId."),
4258
+ tags: z6.union([z6.string(), z6.array(z6.string())]).optional().describe(
4259
+ "Short labels for this workspace (or a comma-separated string). Use the same tags on every checkout for one task. Added when the checkout already exists."
4260
+ )
4313
4261
  },
4314
4262
  async (args) => {
4315
4263
  const result = await createOrchChildThread(orch, args, resolveNewThreadOptions);
@@ -4323,12 +4271,15 @@ async function startMcpServer() {
4323
4271
  "start_board_card",
4324
4272
  "Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second checkout when one already matches \u2014 returns that workspace (alreadyStarted) and its chats. A workspace can have many agents. fork_chat to add one; send_to_chat only the chat that should do this job.",
4325
4273
  {
4326
- kind: z8.enum(["ticket", "pr", "branch"]),
4327
- ref: z8.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
4328
- repoPath: z8.string().describe("Project path from list_projects / list_board"),
4329
- title: z8.string().optional()
4274
+ kind: z6.enum(["ticket", "pr", "branch"]),
4275
+ ref: z6.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
4276
+ repoPath: z6.string().describe("Project path from list_projects / list_board"),
4277
+ title: z6.string().optional(),
4278
+ tags: z6.union([z6.string(), z6.array(z6.string())]).optional().describe(
4279
+ "Short labels for this workspace. Same tags on every checkout for one task. Added when the checkout already exists."
4280
+ )
4330
4281
  },
4331
- async ({ kind, ref, repoPath, title }) => {
4282
+ async ({ kind, ref, repoPath, title, tags }) => {
4332
4283
  const root = await resolveRepoRoot(repoPath);
4333
4284
  const existing = findLiveThreadForCreate(
4334
4285
  {
@@ -4343,13 +4294,15 @@ async function startMcpServer() {
4343
4294
  }))
4344
4295
  );
4345
4296
  if (existing) {
4297
+ const tagged = tags === void 0 ? orch.getThread(existing.id) : applyWorkspaceTags(existing.id, tags, "add");
4346
4298
  return mcpJson({
4347
4299
  alreadyStarted: true,
4348
4300
  id: existing.id,
4349
4301
  title: existing.title,
4350
4302
  status: existing.status,
4303
+ tags: tagged?.tags ?? [],
4351
4304
  link: `sideboard://chat/${existing.id}`,
4352
- chats: workspaceAgentChatSummaries(orch.getThread(existing.id)?.worktreePath)
4305
+ chats: workspaceAgentChatSummaries(tagged?.worktreePath)
4353
4306
  });
4354
4307
  }
4355
4308
  const workspaces = orch.listWorkspaces();
@@ -4363,7 +4316,8 @@ async function startMcpServer() {
4363
4316
  sourceType: "branch",
4364
4317
  sourceRef,
4365
4318
  repoPath: root,
4366
- title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef)
4319
+ title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef),
4320
+ tags
4367
4321
  },
4368
4322
  resolveNewThreadOptions
4369
4323
  );
@@ -4393,7 +4347,8 @@ async function startMcpServer() {
4393
4347
  sourceRef: ident,
4394
4348
  repoPath: root,
4395
4349
  title: title?.trim() || issue?.title,
4396
- attachments
4350
+ attachments,
4351
+ tags
4397
4352
  },
4398
4353
  resolveNewThreadOptions
4399
4354
  );
@@ -4410,7 +4365,8 @@ async function startMcpServer() {
4410
4365
  sourceType: "pr",
4411
4366
  sourceRef: number,
4412
4367
  repoPath: root,
4413
- title: title?.trim() || pr?.title
4368
+ title: title?.trim() || pr?.title,
4369
+ tags
4414
4370
  },
4415
4371
  resolveNewThreadOptions
4416
4372
  );
@@ -4422,11 +4378,11 @@ async function startMcpServer() {
4422
4378
  );
4423
4379
  server.tool(
4424
4380
  "send_to_chat",
4425
- 'Steer a prompt on a workspace chat (Settings \u2192 Follow-up, default steer: front of the queue, skips the inbox; while the desktop board is running it starts as soon as the in-flight turn yields). Use after create_workspace 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 send_to_chat to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that interrupts the child mid-thought. Call wait_for_turn again instead.',
4381
+ 'Steer a prompt on a workspace chat (Settings \u2192 Follow-up, default steer: interrupt the in-flight turn and start now, same as the workspace composer). Use after create_workspace 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 send_to_chat to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that interrupts the child mid-thought. Call wait_for_turn again instead.',
4426
4382
  {
4427
- ref: z8.string(),
4428
- prompt: z8.string(),
4429
- force_stop: z8.boolean().optional()
4383
+ ref: z6.string(),
4384
+ prompt: z6.string(),
4385
+ force_stop: z6.boolean().optional()
4430
4386
  },
4431
4387
  async ({ ref, prompt, force_stop }) => {
4432
4388
  if (force_stop) {
@@ -4451,8 +4407,8 @@ async function startMcpServer() {
4451
4407
  "wait_for_turn",
4452
4408
  "Wait until the chat 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. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. blockedReason is why the child is waiting on a person (the question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress and call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_chat or tell the user. On input-required, wait for the user in that chat. When finished, text is this turn\u2019s assistant reply and usage is that turn\u2019s tokens + costUsd (this turn, not the Claude session total; sessionCostUsd is the provider session total when present).",
4453
4409
  {
4454
- ref: z8.string(),
4455
- timeoutMs: z8.number().optional()
4410
+ ref: z6.string(),
4411
+ timeoutMs: z6.number().optional()
4456
4412
  },
4457
4413
  async ({ ref, timeoutMs }) => {
4458
4414
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
@@ -4480,7 +4436,7 @@ async function startMcpServer() {
4480
4436
  server.tool(
4481
4437
  "get_turn_result",
4482
4438
  "Assistant message and last-turn usage when the turn finished. While stillRunning (submitted/working), text and usage are empty so a previous reply cannot be mistaken for this turn \u2014 use progress for tools/thinking. Not the full transcript. Includes taskState (A2A lifecycle) and blockedReason (why the agent is waiting on a person).",
4483
- { ref: z8.string() },
4439
+ { ref: z6.string() },
4484
4440
  async ({ ref }) => {
4485
4441
  const result = orch.getTurnResult(ref);
4486
4442
  const hint = mcpWaitTaskHint(result.taskState, result.status, result.blockedReason);
@@ -4494,12 +4450,13 @@ async function startMcpServer() {
4494
4450
  );
4495
4451
  server.tool(
4496
4452
  "stop_chat",
4497
- "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.",
4453
+ "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. Pass reason \u2014 a short note on why this chat is stopping. It is stored on the thread and kept when the turn unwinds. If you omit it, Sideboard still records that the queue was cleared.",
4498
4454
  {
4499
- ref: z8.string(),
4500
- force: z8.boolean().optional()
4455
+ ref: z6.string(),
4456
+ force: z6.boolean().optional(),
4457
+ reason: z6.string().optional().describe("Why this chat is stopping. Stored on the thread for the next session.")
4501
4458
  },
4502
- async ({ ref, force }) => {
4459
+ async ({ ref, force, reason }) => {
4503
4460
  const t = orch.getThread(ref);
4504
4461
  if (!t) {
4505
4462
  return {
@@ -4509,18 +4466,19 @@ async function startMcpServer() {
4509
4466
  }
4510
4467
  const clearQueue = force !== false;
4511
4468
  const hadQueued = t.queue.length > 0;
4512
- const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
4469
+ const stopped = orch.stop(ref, { clearQueue, notifyParent: false, reason });
4513
4470
  return mcpJson({
4514
4471
  id: stopped.id,
4515
4472
  status: stopped.status,
4516
- clearedQueue: clearQueue && hadQueued
4473
+ clearedQueue: clearQueue && hadQueued,
4474
+ lastError: stopped.lastError ?? null
4517
4475
  });
4518
4476
  }
4519
4477
  );
4520
4478
  server.tool(
4521
4479
  "archive_chat",
4522
4480
  "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.",
4523
- { ref: z8.string() },
4481
+ { ref: z6.string() },
4524
4482
  async ({ ref }) => {
4525
4483
  const t = orch.getThread(ref);
4526
4484
  if (!t) {
@@ -4546,7 +4504,7 @@ async function startMcpServer() {
4546
4504
  server.tool(
4547
4505
  "restore_chat",
4548
4506
  "Restore an archived thread (recreates worktree from branch when needed)",
4549
- { ref: z8.string() },
4507
+ { ref: z6.string() },
4550
4508
  async ({ ref }) => {
4551
4509
  try {
4552
4510
  const restored = await orch.restore(ref);
@@ -4565,8 +4523,8 @@ async function startMcpServer() {
4565
4523
  "get_diff",
4566
4524
  "Compact diff summary (capped hunks, paginated)",
4567
4525
  {
4568
- ref: z8.string(),
4569
- maxFiles: z8.number().optional()
4526
+ ref: z6.string(),
4527
+ maxFiles: z6.number().optional()
4570
4528
  },
4571
4529
  async ({ ref, maxFiles }) => {
4572
4530
  const summary = await orch.diffSummary(ref);
@@ -4579,7 +4537,7 @@ async function startMcpServer() {
4579
4537
  server.tool(
4580
4538
  "get_pr_checks",
4581
4539
  "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.",
4582
- { ref: z8.string().describe("Worktree thread id/ref") },
4540
+ { ref: z6.string().describe("Worktree thread id/ref") },
4583
4541
  async ({ ref }) => {
4584
4542
  try {
4585
4543
  const checks = await orch.getPrChecks(ref);
@@ -4593,7 +4551,7 @@ async function startMcpServer() {
4593
4551
  server.tool(
4594
4552
  "request_review",
4595
4553
  'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). If that worktree has a single unused idle agent (no messages), runs the review there; otherwise 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 in that chat. The review stays there so the user can read it and type next steps \u2014 do not comment on or update the PR or ticket, and do not ask_user after the review, until they ask. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review thread id.',
4596
- { ref: z8.string().describe("Worktree thread id/ref to review") },
4554
+ { ref: z6.string().describe("Worktree thread id/ref to review") },
4597
4555
  async ({ ref }) => {
4598
4556
  try {
4599
4557
  const tab = await orch.requestReview(ref);
@@ -4615,8 +4573,8 @@ async function startMcpServer() {
4615
4573
  "ask_git",
4616
4574
  "Commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge \u2014 same prompts as the desktop git buttons (including Resolve). Always steers the worktree agent; does not push or open the PR locally. 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=ready-for-review and action=merge only when the user explicitly asked (or clicked the desktop button). Do not run git or gh from the orchestration cwd.",
4617
4575
  {
4618
- ref: z8.string().describe("Worktree thread id/ref"),
4619
- action: z8.enum(AGENT_GIT_ACTIONS).describe(
4576
+ ref: z6.string().describe("Worktree thread id/ref"),
4577
+ action: z6.enum(AGENT_GIT_ACTIONS).describe(
4620
4578
  "commit-push | create-draft | create-web | resolve-conflicts | ready-for-review | merge"
4621
4579
  )
4622
4580
  },
@@ -4645,7 +4603,7 @@ async function startMcpServer() {
4645
4603
  }
4646
4604
  }
4647
4605
  );
4648
- const agentEnum = z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
4606
+ const agentEnum = z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
4649
4607
  server.tool(
4650
4608
  "list_models",
4651
4609
  "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.",
@@ -4666,11 +4624,11 @@ async function startMcpServer() {
4666
4624
  "fork_workspace",
4667
4625
  "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_chat / wait_for_turn (loop while stillRunning) on the returned id.",
4668
4626
  {
4669
- ref: z8.string().describe("Worktree thread id/ref to fork"),
4670
- through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4627
+ ref: z6.string().describe("Worktree thread id/ref to fork"),
4628
+ through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4671
4629
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
4672
- model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4673
- title: z8.string().optional()
4630
+ model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4631
+ title: z6.string().optional()
4674
4632
  },
4675
4633
  async ({ ref, through_index, agent, model, title }) => {
4676
4634
  try {
@@ -4704,11 +4662,11 @@ async function startMcpServer() {
4704
4662
  "fork_chat",
4705
4663
  "Add another agent chat on the SAME workspace (same files, Run, and git). Also forks a Global orchestration chat into a new orchestration tab. Pass agent when the new chat should use a different harness. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Use this for a second agent on a checkout (review, another harness, parallel work on the same branch). Do not create_workspace again for that \u2014 it returns the existing checkout. Then send_to_chat / wait_for_turn (loop while stillRunning) on the returned id. Use fork_workspace only when the work needs its own git checkout.",
4706
4664
  {
4707
- ref: z8.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
4708
- through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4665
+ ref: z6.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
4666
+ through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4709
4667
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
4710
- model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4711
- title: z8.string().optional()
4668
+ model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4669
+ title: z6.string().optional()
4712
4670
  },
4713
4671
  async ({ ref, through_index, agent, model, title }) => {
4714
4672
  try {
@@ -4746,7 +4704,7 @@ async function startMcpServer() {
4746
4704
  server.tool(
4747
4705
  "run_setup",
4748
4706
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
4749
- { ref: z8.string() },
4707
+ { ref: z6.string() },
4750
4708
  async ({ ref }) => {
4751
4709
  const result = await orch.runSetup(ref);
4752
4710
  return mcpJson(result);
@@ -4755,7 +4713,7 @@ async function startMcpServer() {
4755
4713
  server.tool(
4756
4714
  "add_project",
4757
4715
  "Register a git repo as a Sideboard workspace",
4758
- { repoPath: z8.string() },
4716
+ { repoPath: z6.string() },
4759
4717
  async ({ repoPath }) => {
4760
4718
  const ws = await orch.addWorkspace(repoPath);
4761
4719
  return mcpJson(ws);
@@ -4763,10 +4721,10 @@ async function startMcpServer() {
4763
4721
  );
4764
4722
  server.tool(
4765
4723
  "remove_project",
4766
- "Unregister a Sideboard workspace (does not archive threads)",
4767
- { repoPath: z8.string() },
4724
+ "Unregister a Sideboard workspace and archive its chats",
4725
+ { repoPath: z6.string() },
4768
4726
  async ({ repoPath }) => {
4769
- orch.removeWorkspace(repoPath);
4727
+ await orch.removeWorkspace(repoPath);
4770
4728
  return { content: [{ type: "text", text: "ok" }] };
4771
4729
  }
4772
4730
  );
@@ -4774,12 +4732,12 @@ async function startMcpServer() {
4774
4732
  "fanout",
4775
4733
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
4776
4734
  {
4777
- prompt: z8.string(),
4778
- agents: z8.array(z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
4779
- repoPath: z8.string(),
4780
- sourceType: z8.enum(["branch", "pr", "ticket"]).optional(),
4781
- sourceRef: z8.string().optional(),
4782
- title: z8.string().optional()
4735
+ prompt: z6.string(),
4736
+ agents: z6.array(z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
4737
+ repoPath: z6.string(),
4738
+ sourceType: z6.enum(["branch", "pr", "ticket"]).optional(),
4739
+ sourceRef: z6.string().optional(),
4740
+ title: z6.string().optional()
4783
4741
  },
4784
4742
  async (args) => {
4785
4743
  const threads = await orch.bestOfN(args);
@@ -4797,8 +4755,8 @@ async function startMcpServer() {
4797
4755
  "list_branches",
4798
4756
  "List git branches in a registered workspace. Pass repoPath from list_projects (unmerged into the default branch by default \u2014 for create_workspace sourceType=branch).",
4799
4757
  {
4800
- repoPath: z8.string(),
4801
- unmergedOnly: z8.boolean().optional()
4758
+ repoPath: z6.string(),
4759
+ unmergedOnly: z6.boolean().optional()
4802
4760
  },
4803
4761
  async ({ repoPath, unmergedOnly }) => {
4804
4762
  const root = await resolveRepoRoot(repoPath);
@@ -4819,19 +4777,19 @@ async function startMcpServer() {
4819
4777
  "list_prs",
4820
4778
  `List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_projects. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled with this project's ready-for-review GitHub label (Settings \u2192 Projects; default 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_workspace with sourceType=pr.`,
4821
4779
  {
4822
- repoPath: z8.string(),
4823
- query: z8.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
4824
- queue: z8.enum(["review", "mine", "approved", "changes"]).optional().describe(
4780
+ repoPath: z6.string(),
4781
+ query: z6.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
4782
+ queue: z6.enum(["review", "mine", "approved", "changes"]).optional().describe(
4825
4783
  "review = unclaimed ready-for-review inbox (Settings \u2192 Projects label, default eng-review). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes."
4826
4784
  ),
4827
- state: z8.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
4828
- label: z8.string().optional().describe(
4785
+ state: z6.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
4786
+ label: z6.string().optional().describe(
4829
4787
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
4830
4788
  ),
4831
- reviewer: z8.string().optional().describe(
4789
+ reviewer: z6.string().optional().describe(
4832
4790
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
4833
4791
  ),
4834
- limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
4792
+ limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
4835
4793
  },
4836
4794
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
4837
4795
  const root = await resolveRepoRoot(repoPath);
@@ -4864,7 +4822,7 @@ async function startMcpServer() {
4864
4822
  server.tool(
4865
4823
  "get_pr_stack",
4866
4824
  "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).",
4867
- { ref: z8.string() },
4825
+ { ref: z6.string() },
4868
4826
  async ({ ref }) => {
4869
4827
  const stack = await orch.getPrStack(ref);
4870
4828
  return mcpJson(stack);
@@ -4874,8 +4832,8 @@ async function startMcpServer() {
4874
4832
  "open_pr_stack_layers",
4875
4833
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
4876
4834
  {
4877
- ref: z8.string(),
4878
- layer: z8.number().int().positive().optional()
4835
+ ref: z6.string(),
4836
+ layer: z6.number().int().positive().optional()
4879
4837
  },
4880
4838
  async ({ ref, layer }) => {
4881
4839
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -4898,9 +4856,9 @@ async function startMcpServer() {
4898
4856
  "add_stack_layer",
4899
4857
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
4900
4858
  {
4901
- ref: z8.string(),
4902
- branchName: z8.string(),
4903
- title: z8.string().optional()
4859
+ ref: z6.string(),
4860
+ branchName: z6.string(),
4861
+ title: z6.string().optional()
4904
4862
  },
4905
4863
  async ({ ref, branchName, title }) => {
4906
4864
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -4918,11 +4876,11 @@ async function startMcpServer() {
4918
4876
  "create_pr_stack",
4919
4877
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
4920
4878
  {
4921
- repoPath: z8.string(),
4922
- branches: z8.array(z8.string()).min(1),
4923
- agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
4924
- base: z8.string().optional(),
4925
- title: z8.string().optional()
4879
+ repoPath: z6.string(),
4880
+ branches: z6.array(z6.string()).min(1),
4881
+ agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
4882
+ base: z6.string().optional(),
4883
+ title: z6.string().optional()
4926
4884
  },
4927
4885
  async (args) => {
4928
4886
  const result = await orch.createPrStack({
@@ -4950,10 +4908,10 @@ async function startMcpServer() {
4950
4908
  "list_issues",
4951
4909
  "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. Pass updatedSince for new/updated tickets and new comments (\u201Cany updates since yesterday?\u201D). Then create_workspace with sourceType=ticket.",
4952
4910
  {
4953
- repoPath: z8.string(),
4954
- query: z8.string().optional().describe("Search title, identifier, or description"),
4955
- assignee: z8.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
4956
- limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
4911
+ repoPath: z6.string(),
4912
+ query: z6.string().optional().describe("Search title, identifier, or description"),
4913
+ assignee: z6.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
4914
+ limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
4957
4915
  updatedSince: mcpIssueUpdatedSinceSchema
4958
4916
  },
4959
4917
  async ({ repoPath, query, assignee, limit, updatedSince }) => {