@sideboard-ai/core 0.1.73 → 0.1.77

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 (32) hide show
  1. package/dist/agents/cursor-runner.cjs +1 -1
  2. package/dist/agents/cursor-runner.js +1 -1
  3. package/dist/{agents-77GE7VRW.js → agents-LKUHPPEQ.js} +3 -3
  4. package/dist/{agents-GUFUYXKP.js → agents-XJV6J3K2.js} +4 -4
  5. package/dist/{caffeinate-hold-KLC6SABD.js → caffeinate-hold-EAZNWJBE.js} +5 -1
  6. package/dist/caffeinate-hold-OHAUWOA6.js +20 -0
  7. package/dist/{chunk-AFW3M6LU.js → chunk-2FJI5JXX.js} +17 -11
  8. package/dist/{chunk-CXT2PLO7.js → chunk-2ZIPJRJE.js} +1 -1
  9. package/dist/{chunk-CYKPUTXN.js → chunk-75U65MBE.js} +1 -1
  10. package/dist/{chunk-V4ACEJX2.js → chunk-EEKSZTMU.js} +14 -12
  11. package/dist/{chunk-POW7JCB5.js → chunk-GWTLKXXP.js} +1 -1
  12. package/dist/chunk-MFF2RE3I.js +168 -0
  13. package/dist/{chunk-WSFZPOPH.js → chunk-QD37IILI.js} +17 -11
  14. package/dist/chunk-RV6DBEDQ.js +170 -0
  15. package/dist/{chunk-3WJAUKIL.js → chunk-SEOICVGB.js} +1 -1
  16. package/dist/{chunk-NF6Y4GTE.js → chunk-VERLUKN2.js} +1 -1
  17. package/dist/{chunk-6RFPKZNC.js → chunk-XJNCWYFR.js} +14 -12
  18. package/dist/{coordinator-prompt-4U2QNHGT.js → coordinator-prompt-LLFJUO2R.js} +1 -1
  19. package/dist/{coordinator-prompt-ZHBDHMZB.js → coordinator-prompt-MRCMMRSF.js} +1 -1
  20. package/dist/{global-workspace-VF56FTPY.js → global-workspace-TWHMYLTZ.js} +2 -2
  21. package/dist/{global-workspace-S3B6ESZS.js → global-workspace-YNMNO4CL.js} +2 -2
  22. package/dist/index.cjs +1203 -468
  23. package/dist/index.d.cts +177 -23
  24. package/dist/index.d.ts +177 -23
  25. package/dist/index.js +1049 -401
  26. package/dist/mcp/run-stdio.cjs +848 -306
  27. package/dist/mcp/run-stdio.js +656 -182
  28. package/dist/{workspaces-ZJ45O4CD.js → workspaces-GQ4XKBD3.js} +3 -3
  29. package/dist/{workspaces-R66324S7.js → workspaces-O3U5BENH.js} +3 -3
  30. package/package.json +1 -1
  31. package/dist/caffeinate-hold-BEJIYPJ7.js +0 -107
  32. package/dist/chunk-JAWEDVVA.js +0 -106
@@ -16,19 +16,22 @@ import {
16
16
  resolveQuotaFallbackAgent,
17
17
  sideboardMcpProfile,
18
18
  summarizeTurnStderr
19
- } from "../chunk-AFW3M6LU.js";
19
+ } from "../chunk-2FJI5JXX.js";
20
20
  import "../chunk-VROPG6QF.js";
21
21
  import {
22
22
  addWorkspace,
23
23
  ensureWorkspace,
24
24
  removeWorkspace,
25
25
  syncWorkspacesFromThreads
26
- } from "../chunk-CXT2PLO7.js";
26
+ } from "../chunk-2ZIPJRJE.js";
27
27
  import {
28
28
  extractPresentedPlan,
29
29
  readPlanFile,
30
30
  writePlanFile
31
31
  } from "../chunk-N5PM7HGQ.js";
32
+ import {
33
+ releaseCaffeinateHoldForThread
34
+ } from "../chunk-RV6DBEDQ.js";
32
35
  import {
33
36
  GLOBAL_WORKSPACE_ID,
34
37
  assertOrchestratorCapableAgent,
@@ -38,14 +41,14 @@ import {
38
41
  isGlobalThread,
39
42
  isOrchestratorThread,
40
43
  orchestratorSessionPoisonedByBuiltins
41
- } from "../chunk-POW7JCB5.js";
44
+ } from "../chunk-GWTLKXXP.js";
42
45
  import {
43
46
  SLACK_REPLY_FORMATTING,
44
47
  coordinatorSystemPrompt,
45
48
  coordinatorTurnReminder,
46
49
  enrichWorkspacesWithGithub,
47
50
  ensureGlobalCoordinatorCwd
48
- } from "../chunk-6RFPKZNC.js";
51
+ } from "../chunk-XJNCWYFR.js";
49
52
  import {
50
53
  addPrStackLayer,
51
54
  allocateTeamName,
@@ -140,7 +143,7 @@ import {
140
143
  // src/mcp/server.ts
141
144
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
142
145
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
143
- import { z as z2 } from "zod";
146
+ import { z as z3 } from "zod";
144
147
  import { basename as basename4 } from "path";
145
148
 
146
149
  // src/orchestrator/orchestrator.ts
@@ -341,8 +344,8 @@ ${input.permalink}` : "";
341
344
 
342
345
  ${body}${link}`;
343
346
  }
344
- function isSlackExternalReplyPrompt(text2) {
345
- return text2.startsWith("Slack reply from ") && text2.includes("not a command");
347
+ function isSlackExternalReplyPrompt(text3) {
348
+ return text3.startsWith("Slack reply from ") && text3.includes("not a command");
346
349
  }
347
350
  function pendingSlackExternalReplies(messages) {
348
351
  let i = messages.length - 1;
@@ -366,9 +369,9 @@ function formatSlackRepliesForTurn(replies) {
366
369
  function listSlackOutboundWatches() {
367
370
  return pruneWatches(readStore3());
368
371
  }
369
- function formatOwnerSlackFyi(userName, text2) {
372
+ function formatOwnerSlackFyi(userName, text3) {
370
373
  const who = userName.trim() || "Someone";
371
- const body = text2.trim() || "(no text)";
374
+ const body = text3.trim() || "(no text)";
372
375
  return `${who} replied in Slack:
373
376
  ${body}`;
374
377
  }
@@ -383,7 +386,7 @@ async function relayExternalReply(opts) {
383
386
  const thread = readThread(threadId);
384
387
  if (!thread || thread.status === "archived") return true;
385
388
  try {
386
- const text2 = formatSlackExternalReplyPrompt({
389
+ const text3 = formatSlackExternalReplyPrompt({
387
390
  userName: opts.reply.userName,
388
391
  kind: opts.watch.kind,
389
392
  toLabel: opts.watch.toLabel,
@@ -392,7 +395,7 @@ async function relayExternalReply(opts) {
392
395
  });
393
396
  appendMessage(threadId, {
394
397
  role: "agent",
395
- text: text2,
398
+ text: text3,
396
399
  ts: (/* @__PURE__ */ new Date()).toISOString()
397
400
  });
398
401
  } catch {
@@ -576,7 +579,7 @@ async function refreshSlackReplyBadges(opts) {
576
579
  const messages = await fetchMessages(token, watch, opts?.fetchImpl);
577
580
  const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
578
581
  if (replies.length === 0) continue;
579
- const injected = new Set(watch.injectedReplyTs ?? []);
582
+ const injected2 = new Set(watch.injectedReplyTs ?? []);
580
583
  const collected = [...watch.replies ?? []];
581
584
  let lastSeenTs = watch.lastSeenTs;
582
585
  let latestUser;
@@ -610,7 +613,7 @@ async function refreshSlackReplyBadges(opts) {
610
613
  latestName = replyUserName;
611
614
  latestText = reply.text;
612
615
  latestPermalink = permalink;
613
- if (injected.has(ts)) {
616
+ if (injected2.has(ts)) {
614
617
  lastSeenTs = ts;
615
618
  continue;
616
619
  }
@@ -621,7 +624,7 @@ async function refreshSlackReplyBadges(opts) {
621
624
  fetchImpl: opts?.fetchImpl
622
625
  });
623
626
  if (!delivered) break;
624
- injected.add(ts);
627
+ injected2.add(ts);
625
628
  lastSeenTs = ts;
626
629
  }
627
630
  watches[i] = {
@@ -633,7 +636,7 @@ async function refreshSlackReplyBadges(opts) {
633
636
  replyTs: lastSeenTs,
634
637
  replyPreview: latestText.slice(0, 140),
635
638
  permalink: latestPermalink,
636
- injectedReplyTs: [...injected],
639
+ injectedReplyTs: [...injected2],
637
640
  replies: collected.slice(-MAX_REPLIES_PER_WATCH)
638
641
  };
639
642
  changed = true;
@@ -723,9 +726,9 @@ function toolFilePath(input) {
723
726
  if (!input) return void 0;
724
727
  return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
725
728
  }
726
- function countLines(text2) {
727
- if (!text2) return 0;
728
- return text2.split("\n").length;
729
+ function countLines(text3) {
730
+ if (!text3) return 0;
731
+ return text3.split("\n").length;
729
732
  }
730
733
  function diffFromInput(input) {
731
734
  if (!input) return {};
@@ -852,9 +855,9 @@ function applyAgentEvent(parts, event) {
852
855
  function partsToAssistantText(parts) {
853
856
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
854
857
  }
855
- function stripBrightsyNdjsonNoise(text2) {
856
- if (!text2 || !text2.includes('"type"')) return text2;
857
- let out = text2;
858
+ function stripBrightsyNdjsonNoise(text3) {
859
+ if (!text3 || !text3.includes('"type"')) return text3;
860
+ let out = text3;
858
861
  out = out.replace(
859
862
  /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
860
863
  ""
@@ -882,14 +885,28 @@ function sumOptional(a, b) {
882
885
  if (a == null && b == null) return void 0;
883
886
  return (a ?? 0) + (b ?? 0);
884
887
  }
888
+ function requestOccupancy(u) {
889
+ return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWriteTokens ?? 0);
890
+ }
885
891
  function mergeUsage(a, b) {
886
892
  return {
887
893
  inputTokens: (a?.inputTokens ?? 0) + b.inputTokens,
888
894
  outputTokens: (a?.outputTokens ?? 0) + b.outputTokens,
889
895
  cacheReadTokens: sumOptional(a?.cacheReadTokens, b.cacheReadTokens),
890
- cacheWriteTokens: sumOptional(a?.cacheWriteTokens, b.cacheWriteTokens)
896
+ cacheWriteTokens: sumOptional(a?.cacheWriteTokens, b.cacheWriteTokens),
897
+ lastRequestTokens: b.lastRequestTokens ?? a?.lastRequestTokens
891
898
  };
892
899
  }
900
+ function applyTurnUsage(current, incoming, scope = "request") {
901
+ if (scope === "turn") {
902
+ return {
903
+ ...incoming,
904
+ lastRequestTokens: current?.lastRequestTokens ?? requestOccupancy(incoming)
905
+ };
906
+ }
907
+ const merged = mergeUsage(current, incoming);
908
+ return { ...merged, lastRequestTokens: requestOccupancy(incoming) };
909
+ }
893
910
 
894
911
  // src/agents/spawn.ts
895
912
  async function spawnAgentTurn(thread, input, onEvent) {
@@ -899,9 +916,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
899
916
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
900
917
  );
901
918
  }
902
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-VF56FTPY.js");
919
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-TWHMYLTZ.js");
903
920
  if (isGlobalThread2(thread)) {
904
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-ZHBDHMZB.js");
921
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-MRCMMRSF.js");
905
922
  ensureGlobalCoordinatorCwd2(
906
923
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
907
924
  );
@@ -963,7 +980,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
963
980
  continue;
964
981
  }
965
982
  if (parsed.type === "usage") {
966
- usage = mergeUsage(usage, parsed.data);
983
+ usage = applyTurnUsage(usage, parsed.data, parsed.scope ?? "request");
967
984
  onEvent(parsed);
968
985
  continue;
969
986
  }
@@ -988,8 +1005,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
988
1005
  onEvent({ type: "exit", data: exitCode });
989
1006
  const finalized = finalizeParts(parts);
990
1007
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
991
- const text2 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
992
- return { exitCode, sessionId, assistantText: text2, parts: finalized, usage };
1008
+ const text3 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
1009
+ return { exitCode, sessionId, assistantText: text3, parts: finalized, usage };
993
1010
  });
994
1011
  return {
995
1012
  pid: child.pid,
@@ -1003,6 +1020,31 @@ async function spawnAgentTurn(thread, input, onEvent) {
1003
1020
  };
1004
1021
  }
1005
1022
 
1023
+ // src/git/agent-git-actions.ts
1024
+ var AGENT_GIT_ACTIONS = [
1025
+ "commit-push",
1026
+ "create-draft",
1027
+ "create-web",
1028
+ "resolve-conflicts",
1029
+ "merge"
1030
+ ];
1031
+ function agentGitPrompt(action, opts) {
1032
+ switch (action) {
1033
+ case "commit-push":
1034
+ return "Commit and push.";
1035
+ case "create-draft":
1036
+ return "Commit, push, and open a draft PR.";
1037
+ case "create-web":
1038
+ return "Commit, push, and open a PR in the browser.";
1039
+ case "resolve-conflicts": {
1040
+ const base = opts?.prBase?.trim().replace(/^refs\/heads\//, "");
1041
+ return base ? `Merge origin/${base} into this branch. Then push.` : "Fix merge conflicts.";
1042
+ }
1043
+ case "merge":
1044
+ return "Merge PR.";
1045
+ }
1046
+ }
1047
+
1006
1048
  // src/git/pr-merge-archive.ts
1007
1049
  function normalizePrState(state) {
1008
1050
  return (state ?? "").trim().toUpperCase();
@@ -1717,7 +1759,7 @@ async function createThread(input, _onSetupLine) {
1717
1759
  return readThread(thread.id) ?? thread;
1718
1760
  }
1719
1761
  async function listLinearIssues(agent, repoPath) {
1720
- const { getAdapter: getAdapter2 } = await import("../agents-77GE7VRW.js");
1762
+ const { getAdapter: getAdapter2 } = await import("../agents-LKUHPPEQ.js");
1721
1763
  await requireAgent(agent, { requireLinear: true });
1722
1764
  const adapter = getAdapter2(agent);
1723
1765
  if (!adapter.listLinearIssues) {
@@ -1773,9 +1815,9 @@ async function tryClaudeSummary(transcript, opts) {
1773
1815
  { cwd: opts?.cwd, reject: false }
1774
1816
  );
1775
1817
  if (exitCode !== 0) return null;
1776
- const text2 = stdout.trim();
1777
- if (text2.length < 40) return null;
1778
- return text2;
1818
+ const text3 = stdout.trim();
1819
+ if (text3.length < 40) return null;
1820
+ return text3;
1779
1821
  } catch {
1780
1822
  return null;
1781
1823
  }
@@ -1824,9 +1866,9 @@ function extractiveSummary(transcript) {
1824
1866
  );
1825
1867
  return parts.join("\n");
1826
1868
  }
1827
- function clipSummary(text2) {
1828
- if (text2.length <= MAX_SUMMARY_CHARS) return text2;
1829
- return `${text2.slice(0, MAX_SUMMARY_CHARS)}
1869
+ function clipSummary(text3) {
1870
+ if (text3.length <= MAX_SUMMARY_CHARS) return text3;
1871
+ return `${text3.slice(0, MAX_SUMMARY_CHARS)}
1830
1872
 
1831
1873
  [\u2026summary truncated\u2026]`;
1832
1874
  }
@@ -2439,8 +2481,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
2439
2481
  );
2440
2482
  const recent = from.messages.slice(-8).map((m) => {
2441
2483
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
2442
- const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
2443
- return text2 ? `- ${role}: ${text2}` : null;
2484
+ const text3 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
2485
+ return text3 ? `- ${role}: ${text3}` : null;
2444
2486
  }).filter(Boolean);
2445
2487
  const body = [
2446
2488
  `# Orchestration handoff`,
@@ -2536,13 +2578,13 @@ function resolveConductorCursorAgentId(workspacePath) {
2536
2578
  for (const hash of hashes) {
2537
2579
  const agentsFile = join9(CURSOR_SDK_STORE, hash, "agents.ndjson");
2538
2580
  if (!existsSync9(agentsFile)) continue;
2539
- let text2;
2581
+ let text3;
2540
2582
  try {
2541
- text2 = readFileSync6(agentsFile, "utf8");
2583
+ text3 = readFileSync6(agentsFile, "utf8");
2542
2584
  } catch {
2543
2585
  continue;
2544
2586
  }
2545
- for (const line of text2.split("\n")) {
2587
+ for (const line of text3.split("\n")) {
2546
2588
  const trimmed = line.trim();
2547
2589
  if (!trimmed) continue;
2548
2590
  try {
@@ -2581,7 +2623,7 @@ async function adoptThread(input) {
2581
2623
  messages: input.messages ?? []
2582
2624
  });
2583
2625
  writeThread(thread);
2584
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-ZJ45O4CD.js");
2626
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-GQ4XKBD3.js");
2585
2627
  await ensureWorkspace2(repoPath);
2586
2628
  return thread;
2587
2629
  }
@@ -3305,13 +3347,13 @@ function formatStat(files) {
3305
3347
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
3306
3348
  }
3307
3349
  function emptyScopeStats() {
3308
- const z3 = emptyStat();
3350
+ const z4 = emptyStat();
3309
3351
  return {
3310
- commits: z3,
3311
- uncommitted: z3,
3312
- staged: z3,
3313
- unstaged: z3,
3314
- last_turn: z3
3352
+ commits: z4,
3353
+ uncommitted: z4,
3354
+ staged: z4,
3355
+ unstaged: z4,
3356
+ last_turn: z4
3315
3357
  };
3316
3358
  }
3317
3359
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -3582,10 +3624,10 @@ async function getDiff(worktreePath, repoPath, opts) {
3582
3624
  listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
3583
3625
  isDirty(worktreePath),
3584
3626
  countUnpushedCommits(worktreePath)
3585
- ]).then(([scopeStats, commits, dirty, unpushed]) => ({
3627
+ ]).then(([scopeStats, commits, dirty2, unpushed]) => ({
3586
3628
  scopeStats,
3587
3629
  commits,
3588
- dirty,
3630
+ dirty: dirty2,
3589
3631
  unpushed
3590
3632
  })) : Promise.resolve({
3591
3633
  scopeStats: emptyScopeStats(),
@@ -3662,13 +3704,14 @@ async function getDiff(worktreePath, repoPath, opts) {
3662
3704
  }
3663
3705
  }
3664
3706
  const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
3707
+ const dirty = includeMeta ? meta.dirty : scope === "commits" ? false : files.length > 0;
3665
3708
  return {
3666
3709
  scope,
3667
3710
  commitSha: scope === "commits" ? commitSha : null,
3668
3711
  base: labelBase,
3669
3712
  files,
3670
3713
  stat: formatStat(files),
3671
- dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
3714
+ dirty,
3672
3715
  unpushed: meta.unpushed,
3673
3716
  hasLastTurnBase,
3674
3717
  commits: meta.commits,
@@ -4380,7 +4423,7 @@ function formatRenameBranchDirective(thread, opts) {
4380
4423
  "- Rename the git branch to a short kebab-case name that describes this task (what you are changing), e.g. `fix/panel-width` or `feat/dark-mode`:",
4381
4424
  " `git branch -m <new-name>`",
4382
4425
  "- Prefer Conventional Commits style prefixes when they fit (`fix/`, `feat/`, `chore/`, `docs/`).",
4383
- "- Never push or merge to main/master from here."
4426
+ "- Never push this placeholder to main/master."
4384
4427
  ];
4385
4428
  const custom = opts?.customPrompt?.trim();
4386
4429
  if (custom) {
@@ -4423,7 +4466,7 @@ function formatWorktreeDirective(thread, opts) {
4423
4466
  "- Prefer a concise imperative title (Conventional Commits style when it fits: feat:/fix:/chore:/docs:). Body should summarize intent, key changes, and test notes."
4424
4467
  );
4425
4468
  lines.push(
4426
- "- Commit with messages that state the purpose of the change (same standard as the PR). Stay on this thread branch; never push or merge to main/master from here."
4469
+ "- Commit with messages that state the purpose of the change (same standard as the PR). Stay on this thread branch. Never push directly to main/master or merge locally into the main checkout. When asked to merge the PR, use GitHub from this worktree (`gh pr merge` / `gh stack merge`)."
4427
4470
  );
4428
4471
  if (thread.prUrl) {
4429
4472
  lines.push(
@@ -4455,13 +4498,13 @@ function formatWorktreeDirective(thread, opts) {
4455
4498
  '- "Fix CI: <name>." \u2192 investigate that failing check, fix it, commit, and push.'
4456
4499
  );
4457
4500
  lines.push(
4458
- '- "Update the branch." / "Fix merge conflicts." \u2192 sync with the PR base (merge or rebase), resolve conflicts carefully, commit, and push until the PR is mergeable.'
4501
+ '- "Update the branch." / "Fix merge conflicts." / "Merge origin/<base> into this branch. Then push." \u2192 sync with the PR base (merge or rebase), resolve conflicts carefully, commit, and push until the PR is mergeable.'
4459
4502
  );
4460
4503
  lines.push(
4461
4504
  '- "Address review comments." \u2192 read PR review feedback, make the requested changes, commit, and push.'
4462
4505
  );
4463
4506
  lines.push(
4464
- '- "Merge PR." \u2192 merge this thread\'s open pull request with `gh pr merge` (respect repo defaults / squash vs merge); do not force-push main/master.'
4507
+ '- "Merge PR." \u2192 merge this thread\'s open pull request on GitHub. If `gh stack view` shows a stack, use `gh stack merge`; otherwise `gh pr merge` (respect repo defaults / squash vs merge). Do not force-push main/master or merge locally into the main checkout.'
4465
4508
  );
4466
4509
  return lines.join("\n");
4467
4510
  }
@@ -4857,11 +4900,11 @@ var Orchestrator = class {
4857
4900
  return results;
4858
4901
  }
4859
4902
  /** Edit the text of a not-yet-started queued message. */
4860
- async editQueuedMessage(threadRef, index, text2) {
4903
+ async editQueuedMessage(threadRef, index, text3) {
4861
4904
  const thread = this.requireThread(threadRef);
4862
4905
  return withThreadLock(thread.id, async () => {
4863
4906
  const current = this.requireThread(thread.id);
4864
- const trimmed = text2.trim();
4907
+ const trimmed = text3.trim();
4865
4908
  if (!trimmed || index < 0 || index >= current.queue.length) {
4866
4909
  return current;
4867
4910
  }
@@ -5621,6 +5664,14 @@ var Orchestrator = class {
5621
5664
  throw new Error(`${action} is not available on the global coordinator`);
5622
5665
  }
5623
5666
  }
5667
+ /** MCP set_caffeinate is a detached hold — closing the chat must not leave the Mac awake. */
5668
+ releaseOrchestratorCaffeinate(thread) {
5669
+ if (!isOrchestratorThread(thread)) return;
5670
+ try {
5671
+ releaseCaffeinateHoldForThread(thread.id);
5672
+ } catch {
5673
+ }
5674
+ }
5624
5675
  async diff(threadRef, opts) {
5625
5676
  const thread = this.requireThread(threadRef);
5626
5677
  this.assertNotGlobal(thread, "Diff");
@@ -5890,6 +5941,36 @@ var Orchestrator = class {
5890
5941
  this.emit({ type: "status_changed", threadId: tab.id, status: tab.status });
5891
5942
  return tab;
5892
5943
  }
5944
+ /**
5945
+ * Queue a desktop-git-button prompt on a worktree agent (commit/push/PR/merge).
5946
+ * Orchestrators use this instead of running git/gh from the synthetic home.
5947
+ */
5948
+ async askGit(threadRef, action) {
5949
+ if (!AGENT_GIT_ACTIONS.includes(action)) {
5950
+ throw new Error(`Unknown git action: ${action}`);
5951
+ }
5952
+ const thread = this.requireThread(threadRef);
5953
+ this.assertNotGlobal(thread, "ask_git");
5954
+ if (isOrchestratorThread(thread)) {
5955
+ throw new Error(
5956
+ "ask_git targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
5957
+ );
5958
+ }
5959
+ if (action === "merge" && !thread.prUrl) {
5960
+ throw new Error(
5961
+ "No pull request linked. Ask the worktree agent to open a draft PR first (ask_git create-draft)."
5962
+ );
5963
+ }
5964
+ let prBase;
5965
+ if (action === "resolve-conflicts") {
5966
+ try {
5967
+ const details = await this.getPrDetails(threadRef);
5968
+ prBase = details?.baseRefName?.trim() || void 0;
5969
+ } catch {
5970
+ }
5971
+ }
5972
+ return this.send(threadRef, agentGitPrompt(action, { prBase }));
5973
+ }
5893
5974
  setThreadOptions(threadRef, patch) {
5894
5975
  const thread = this.requireThread(threadRef);
5895
5976
  const next = {};
@@ -5960,6 +6041,7 @@ var Orchestrator = class {
5960
6041
  async archive(threadRef) {
5961
6042
  const thread = this.requireThread(threadRef);
5962
6043
  this.stop(thread.id);
6044
+ this.releaseOrchestratorCaffeinate(thread);
5963
6045
  if (isGlobalThread(thread)) {
5964
6046
  const archived2 = setStatus(thread.id, "archived");
5965
6047
  this.emit({ type: "status_changed", threadId: archived2.id, status: "archived" });
@@ -5984,7 +6066,7 @@ var Orchestrator = class {
5984
6066
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
5985
6067
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
5986
6068
  try {
5987
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-ZJ45O4CD.js");
6069
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-GQ4XKBD3.js");
5988
6070
  await ensureWorkspace2(thread.repoPath);
5989
6071
  } catch {
5990
6072
  }
@@ -5994,6 +6076,7 @@ var Orchestrator = class {
5994
6076
  async purge(threadRef, opts) {
5995
6077
  const thread = this.requireThread(threadRef);
5996
6078
  this.stop(thread.id);
6079
+ this.releaseOrchestratorCaffeinate(thread);
5997
6080
  if (isGlobalThread(thread)) {
5998
6081
  deleteThreadRecord(thread.id);
5999
6082
  return;
@@ -6098,13 +6181,36 @@ function getOrchestrator() {
6098
6181
  return singleton;
6099
6182
  }
6100
6183
 
6184
+ // src/http/fetch.ts
6185
+ var injected = null;
6186
+ function formatFetchError(err, url) {
6187
+ if (!(err instanceof Error)) return `${String(err)} (${url})`;
6188
+ const cause = err.cause;
6189
+ let detail = "";
6190
+ if (cause instanceof Error) {
6191
+ const code = typeof cause.code === "string" ? cause.code : void 0;
6192
+ detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
6193
+ } else if (cause != null) {
6194
+ detail = ` [${String(cause)}]`;
6195
+ }
6196
+ return `${err.message}${detail} (${url})`;
6197
+ }
6198
+ async function httpFetch(input, init) {
6199
+ const url = typeof input === "string" ? input : input.href;
6200
+ const fn = injected ?? globalThis.fetch.bind(globalThis);
6201
+ try {
6202
+ return await fn(url, init);
6203
+ } catch (err) {
6204
+ throw new Error(formatFetchError(err, url));
6205
+ }
6206
+ }
6207
+
6101
6208
  // src/integrations/linear-oauth.ts
6102
6209
  import { createHash, randomBytes } from "crypto";
6103
6210
  import { createServer as createServer2 } from "http";
6104
6211
 
6105
6212
  // src/integrations/linear-app.ts
6106
6213
  var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
6107
- var BAKED_LINEAR_CLIENT_SECRET = "";
6108
6214
 
6109
6215
  // src/integrations/linear-oauth.ts
6110
6216
  var LINEAR_OAUTH_PORT = 19848;
@@ -6114,7 +6220,7 @@ var REFRESH_SKEW_MS = 5 * 6e4;
6114
6220
  function linearOAuthCredentials() {
6115
6221
  const settings = loadAppSettings();
6116
6222
  const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
6117
- const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || BAKED_LINEAR_CLIENT_SECRET.trim();
6223
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
6118
6224
  if (!clientId) {
6119
6225
  throw new Error(
6120
6226
  `Linear browser sign-in needs a Linear OAuth app Client ID. Create one at linear.app/settings/api/applications/new with callback ${LINEAR_OAUTH_REDIRECT}, then set SIDEBOARD_LINEAR_CLIENT_ID (PKCE does not require a secret). You can still paste a personal API key.`
@@ -6130,7 +6236,7 @@ function linearAuthorizationHeader(token) {
6130
6236
  return `Bearer ${t}`;
6131
6237
  }
6132
6238
  async function exchangeLinearToken(body) {
6133
- const res = await fetch(LINEAR_TOKEN, {
6239
+ const res = await httpFetch(LINEAR_TOKEN, {
6134
6240
  method: "POST",
6135
6241
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
6136
6242
  body
@@ -6197,6 +6303,18 @@ async function getLinearAuthToken(settings = loadAppSettings()) {
6197
6303
 
6198
6304
  // src/integrations/linear.ts
6199
6305
  var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
6306
+ var ISSUE_FIELDS = `
6307
+ id
6308
+ identifier
6309
+ title
6310
+ description
6311
+ url
6312
+ priority
6313
+ state { id name type }
6314
+ assignee { id name }
6315
+ team { id key name states { nodes { id name type } } }
6316
+ labels { nodes { name } }
6317
+ `;
6200
6318
  var ASSIGNED_ISSUES_QUERY = `
6201
6319
  query SideboardAssignedIssues($first: Int!) {
6202
6320
  viewer {
@@ -6205,53 +6323,280 @@ query SideboardAssignedIssues($first: Int!) {
6205
6323
  orderBy: updatedAt
6206
6324
  filter: { state: { type: { nin: ["completed", "canceled"] } } }
6207
6325
  ) {
6208
- nodes {
6209
- id
6210
- identifier
6211
- title
6212
- url
6213
- labels { nodes { name } }
6214
- }
6326
+ nodes { ${ISSUE_FIELDS} }
6215
6327
  }
6216
6328
  }
6217
6329
  }
6218
6330
  `;
6219
- async function listLinearIssuesDirect(opts) {
6220
- const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
6221
- if (!apiKey) {
6331
+ var TEAMS_QUERY = `
6332
+ query SideboardTeams {
6333
+ viewer { id name }
6334
+ teams(first: 50) {
6335
+ nodes {
6336
+ id
6337
+ key
6338
+ name
6339
+ states { nodes { id name type } }
6340
+ }
6341
+ }
6342
+ }
6343
+ `;
6344
+ var ISSUE_QUERY = `
6345
+ query SideboardIssue($id: String!) {
6346
+ issue(id: $id) { ${ISSUE_FIELDS} }
6347
+ }
6348
+ `;
6349
+ var ISSUE_CREATE = `
6350
+ mutation SideboardIssueCreate($input: IssueCreateInput!) {
6351
+ issueCreate(input: $input) {
6352
+ success
6353
+ issue { ${ISSUE_FIELDS} }
6354
+ }
6355
+ }
6356
+ `;
6357
+ var ISSUE_UPDATE = `
6358
+ mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
6359
+ issueUpdate(id: $id, input: $input) {
6360
+ success
6361
+ issue { ${ISSUE_FIELDS} }
6362
+ }
6363
+ }
6364
+ `;
6365
+ var COMMENT_CREATE = `
6366
+ mutation SideboardCommentCreate($input: CommentCreateInput!) {
6367
+ commentCreate(input: $input) {
6368
+ success
6369
+ comment { id body url }
6370
+ }
6371
+ }
6372
+ `;
6373
+ function rewriteLinearError(message) {
6374
+ if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
6375
+ return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
6376
+ }
6377
+ return message;
6378
+ }
6379
+ async function requireLinearToken(apiKey) {
6380
+ const token = (apiKey ?? await getLinearAuthToken())?.trim();
6381
+ if (!token) {
6222
6382
  throw new Error("Linear is not connected \u2014 sign in from Account settings");
6223
6383
  }
6224
- const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
6225
- const res = await fetch(LINEAR_GRAPHQL, {
6384
+ return token;
6385
+ }
6386
+ async function linearGraphql(query, variables, opts) {
6387
+ const apiKey = await requireLinearToken(opts?.apiKey);
6388
+ const res = await httpFetch(LINEAR_GRAPHQL, {
6226
6389
  method: "POST",
6227
6390
  headers: {
6228
6391
  "Content-Type": "application/json",
6229
6392
  Authorization: linearAuthorizationHeader(apiKey)
6230
6393
  },
6231
- body: JSON.stringify({
6232
- query: ASSIGNED_ISSUES_QUERY,
6233
- variables: { first }
6234
- })
6394
+ body: JSON.stringify({ query, variables })
6235
6395
  });
6236
6396
  if (!res.ok) {
6237
6397
  const body = await res.text().catch(() => "");
6238
6398
  throw new Error(
6239
- `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
6399
+ rewriteLinearError(
6400
+ `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
6401
+ )
6240
6402
  );
6241
6403
  }
6242
6404
  const json = await res.json();
6243
6405
  if (json.errors?.length) {
6244
- throw new Error(json.errors.map((e) => e.message ?? "Linear error").join("; "));
6406
+ throw new Error(
6407
+ rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
6408
+ );
6409
+ }
6410
+ if (json.data == null) {
6411
+ throw new Error("Linear API returned no data");
6245
6412
  }
6246
- const nodes = json.data?.viewer?.assignedIssues?.nodes ?? [];
6247
- return nodes.map((node) => ({
6413
+ return json.data;
6414
+ }
6415
+ function mapState(node) {
6416
+ if (!node?.id) return void 0;
6417
+ return {
6418
+ id: String(node.id),
6419
+ name: String(node.name ?? ""),
6420
+ type: String(node.type ?? "")
6421
+ };
6422
+ }
6423
+ function mapIssue(node) {
6424
+ const team = node.team;
6425
+ return {
6248
6426
  id: String(node.id ?? node.identifier ?? ""),
6249
6427
  identifier: String(node.identifier ?? node.id ?? ""),
6250
6428
  title: String(node.title ?? ""),
6251
6429
  url: String(node.url ?? ""),
6252
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
6430
+ description: node.description?.trim() || void 0,
6431
+ priority: typeof node.priority === "number" ? node.priority : void 0,
6432
+ state: mapState(node.state),
6433
+ assignee: node.assignee?.id ? { id: String(node.assignee.id), name: String(node.assignee.name ?? "") } : void 0,
6434
+ team: team?.id ? {
6435
+ id: String(team.id),
6436
+ key: String(team.key ?? ""),
6437
+ name: String(team.name ?? ""),
6438
+ states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
6439
+ } : void 0,
6440
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
6441
+ };
6442
+ }
6443
+ function toIssueInfo(issue) {
6444
+ return {
6445
+ id: issue.id,
6446
+ identifier: issue.identifier,
6447
+ title: issue.title,
6448
+ url: issue.url,
6449
+ labels: issue.labels,
6253
6450
  provider: "linear"
6254
- }));
6451
+ };
6452
+ }
6453
+ function mapTeam(node) {
6454
+ return {
6455
+ id: String(node.id ?? ""),
6456
+ key: String(node.key ?? ""),
6457
+ name: String(node.name ?? ""),
6458
+ states: (node.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
6459
+ };
6460
+ }
6461
+ function resolveLinearTeam(teams, team) {
6462
+ const t = team.trim();
6463
+ if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
6464
+ const lower = t.toLowerCase();
6465
+ const found = teams.find(
6466
+ (x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
6467
+ );
6468
+ if (!found) {
6469
+ const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
6470
+ throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
6471
+ }
6472
+ return found;
6473
+ }
6474
+ function resolveLinearState(team, state) {
6475
+ const s = state.trim();
6476
+ if (!s) throw new Error("Linear state is empty");
6477
+ const lower = s.toLowerCase();
6478
+ const found = team.states.find((x) => x.id === s) || team.states.find((x) => x.name.toLowerCase() === lower) || team.states.find((x) => x.type.toLowerCase() === lower);
6479
+ if (!found) {
6480
+ const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
6481
+ throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
6482
+ }
6483
+ return found;
6484
+ }
6485
+ function normalizePriority(priority) {
6486
+ if (priority == null) return void 0;
6487
+ if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
6488
+ throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
6489
+ }
6490
+ return priority;
6491
+ }
6492
+ async function listLinearIssuesDirect(opts) {
6493
+ const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
6494
+ const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
6495
+ return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
6496
+ }
6497
+ async function listLinearTeams(opts) {
6498
+ const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
6499
+ return {
6500
+ viewer: {
6501
+ id: String(json.viewer?.id ?? ""),
6502
+ name: String(json.viewer?.name ?? "")
6503
+ },
6504
+ teams: (json.teams?.nodes ?? []).map(mapTeam)
6505
+ };
6506
+ }
6507
+ async function getLinearIssue(id, opts) {
6508
+ const issueId = id.trim();
6509
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6510
+ const json = await linearGraphql(
6511
+ ISSUE_QUERY,
6512
+ { id: issueId },
6513
+ opts
6514
+ );
6515
+ if (!json.issue) {
6516
+ throw new Error(`Linear issue not found: ${issueId}`);
6517
+ }
6518
+ return mapIssue(json.issue);
6519
+ }
6520
+ async function createLinearIssue(input, opts) {
6521
+ const title = input.title.trim();
6522
+ if (!title) throw new Error("Linear issue title is required");
6523
+ const { viewer, teams } = await listLinearTeams(opts);
6524
+ const team = resolveLinearTeam(teams, input.team);
6525
+ const mutationInput = {
6526
+ teamId: team.id,
6527
+ title
6528
+ };
6529
+ const description = input.description?.trim();
6530
+ if (description) mutationInput.description = description;
6531
+ if (input.state?.trim()) {
6532
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
6533
+ }
6534
+ const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
6535
+ if (assignee === "me") mutationInput.assigneeId = viewer.id;
6536
+ else if (assignee) mutationInput.assigneeId = assignee;
6537
+ const priority = normalizePriority(input.priority);
6538
+ if (priority != null) mutationInput.priority = priority;
6539
+ const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
6540
+ if (!json.issueCreate?.success || !json.issueCreate.issue) {
6541
+ throw new Error("Linear issueCreate failed");
6542
+ }
6543
+ return mapIssue(json.issueCreate.issue);
6544
+ }
6545
+ async function updateLinearIssue(input, opts) {
6546
+ const issueId = input.id.trim();
6547
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6548
+ const mutationInput = {};
6549
+ if (input.title !== void 0) {
6550
+ const title = input.title.trim();
6551
+ if (!title) throw new Error("Linear issue title cannot be empty");
6552
+ mutationInput.title = title;
6553
+ }
6554
+ if (input.description !== void 0) {
6555
+ mutationInput.description = input.description;
6556
+ }
6557
+ if (input.state?.trim()) {
6558
+ const existing = await getLinearIssue(issueId, opts);
6559
+ const team = existing.team;
6560
+ if (!team?.states.length) {
6561
+ throw new Error(`Linear issue ${existing.identifier} has no workflow states to resolve "${input.state}"`);
6562
+ }
6563
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
6564
+ }
6565
+ if (input.assignee !== void 0) {
6566
+ const assignee = input.assignee?.trim() || null;
6567
+ if (assignee === "me") {
6568
+ const { viewer } = await listLinearTeams(opts);
6569
+ mutationInput.assigneeId = viewer.id;
6570
+ } else {
6571
+ mutationInput.assigneeId = assignee;
6572
+ }
6573
+ }
6574
+ if (input.priority !== void 0) {
6575
+ mutationInput.priority = normalizePriority(input.priority);
6576
+ }
6577
+ if (Object.keys(mutationInput).length === 0) {
6578
+ throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority");
6579
+ }
6580
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
6581
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
6582
+ throw new Error("Linear issueUpdate failed");
6583
+ }
6584
+ return mapIssue(json.issueUpdate.issue);
6585
+ }
6586
+ async function commentLinearIssue(input, opts) {
6587
+ const issueId = input.id.trim();
6588
+ const body = input.body.trim();
6589
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6590
+ if (!body) throw new Error("Linear comment body is required");
6591
+ const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
6592
+ if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
6593
+ throw new Error("Linear commentCreate failed");
6594
+ }
6595
+ return {
6596
+ id: String(json.commentCreate.comment.id),
6597
+ body: String(json.commentCreate.comment.body ?? body),
6598
+ url: json.commentCreate.comment.url?.trim() || void 0
6599
+ };
6255
6600
  }
6256
6601
 
6257
6602
  // src/integrations/issues.ts
@@ -6533,8 +6878,8 @@ function labelGithubUrl(url) {
6533
6878
  }
6534
6879
  return { kind: "other", label: "GitHub", url: raw };
6535
6880
  }
6536
- function appendGithubLink(text2, githubUrl) {
6537
- const body = text2.trimEnd();
6881
+ function appendGithubLink(text3, githubUrl) {
6882
+ const body = text3.trimEnd();
6538
6883
  if (!githubUrl?.trim()) return body;
6539
6884
  const labeled = labelGithubUrl(githubUrl);
6540
6885
  if (!labeled) {
@@ -6817,6 +7162,101 @@ function registerSlackTools(server) {
6817
7162
  );
6818
7163
  }
6819
7164
 
7165
+ // src/mcp/linear-tools.ts
7166
+ import { z as z2 } from "zod";
7167
+ function text2(payload, isError = false) {
7168
+ return {
7169
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
7170
+ ...isError ? { isError: true } : {}
7171
+ };
7172
+ }
7173
+ function fail2(err) {
7174
+ return text2(
7175
+ { error: err instanceof Error ? err.message : String(err) },
7176
+ true
7177
+ );
7178
+ }
7179
+ var prioritySchema = z2.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
7180
+ function registerLinearTools(server) {
7181
+ server.tool(
7182
+ "linear_list_teams",
7183
+ "List Linear teams (id, key, name) and workflow states for the connected Account. Use team key (e.g. ENG) on linear_create_issue; use state name or type (started, completed) on create/update. Viewer id is for assignee=me.",
7184
+ {},
7185
+ async () => {
7186
+ try {
7187
+ return text2(await listLinearTeams());
7188
+ } catch (err) {
7189
+ return fail2(err);
7190
+ }
7191
+ }
7192
+ );
7193
+ server.tool(
7194
+ "linear_get_issue",
7195
+ "Get a Linear issue by uuid or identifier (ENG-123).",
7196
+ { id: z2.string() },
7197
+ async ({ id }) => {
7198
+ try {
7199
+ return text2(await getLinearIssue(id));
7200
+ } catch (err) {
7201
+ return fail2(err);
7202
+ }
7203
+ }
7204
+ );
7205
+ server.tool(
7206
+ "linear_create_issue",
7207
+ 'Create a Linear issue. Call linear_list_teams first. team is id, key, or name. state is name, type (unstarted/started/completed/canceled/backlog), or id. assignee is "me" or a user id.',
7208
+ {
7209
+ team: z2.string(),
7210
+ title: z2.string(),
7211
+ description: z2.string().optional(),
7212
+ state: z2.string().optional(),
7213
+ assignee: z2.string().nullable().optional(),
7214
+ priority: prioritySchema
7215
+ },
7216
+ async (args) => {
7217
+ try {
7218
+ return text2(await createLinearIssue(args));
7219
+ } catch (err) {
7220
+ return fail2(err);
7221
+ }
7222
+ }
7223
+ );
7224
+ server.tool(
7225
+ "linear_update_issue",
7226
+ 'Update a Linear issue (uuid or ENG-123). Pass at least one of title, description, state, assignee, priority. state is name, type, or id. assignee is "me", a user id, or null to unassign.',
7227
+ {
7228
+ id: z2.string(),
7229
+ title: z2.string().optional(),
7230
+ description: z2.string().optional(),
7231
+ state: z2.string().optional(),
7232
+ assignee: z2.string().nullable().optional(),
7233
+ priority: prioritySchema
7234
+ },
7235
+ async (args) => {
7236
+ try {
7237
+ return text2(await updateLinearIssue(args));
7238
+ } catch (err) {
7239
+ return fail2(err);
7240
+ }
7241
+ }
7242
+ );
7243
+ server.tool(
7244
+ "linear_comment",
7245
+ "Add a markdown comment on a Linear issue (uuid or ENG-123).",
7246
+ {
7247
+ id: z2.string(),
7248
+ body: z2.string()
7249
+ },
7250
+ async (args) => {
7251
+ try {
7252
+ return text2(await commentLinearIssue(args));
7253
+ } catch (err) {
7254
+ return fail2(err);
7255
+ }
7256
+ }
7257
+ );
7258
+ }
7259
+
6820
7260
  // src/mcp/server.ts
6821
7261
  var MAX_ORCH_THREADS = 5;
6822
7262
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -6893,7 +7333,7 @@ async function startMcpServer() {
6893
7333
  server.tool(
6894
7334
  "get_thread",
6895
7335
  "Get a compact thread summary by id/ref",
6896
- { ref: z2.string() },
7336
+ { ref: z3.string() },
6897
7337
  async ({ ref }) => {
6898
7338
  const t = orch.getThread(ref);
6899
7339
  if (!t) {
@@ -6922,14 +7362,14 @@ async function startMcpServer() {
6922
7362
  "present_artifact",
6923
7363
  "Show an HTML, SVG, markdown, or React document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Prefer type=html for interactive pages. For type=react, pass a single component module that `export default`s a component (JSX/TSX ok) \u2014 Sideboard bootstraps React/ReactDOM/Babel and renders it; only `react`/`react-dom` imports are available, no other npm packages.",
6924
7364
  {
6925
- title: z2.string().describe("Short title shown in the artifact pane header"),
6926
- type: z2.enum(["html", "svg", "markdown", "react"]).describe(
7365
+ title: z3.string().describe("Short title shown in the artifact pane header"),
7366
+ type: z3.enum(["html", "svg", "markdown", "react"]).describe(
6927
7367
  "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
6928
7368
  ),
6929
- content: z2.string().describe(
7369
+ content: z3.string().describe(
6930
7370
  "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
6931
7371
  ),
6932
- artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
7372
+ artifact_id: z3.string().optional().describe("Stable id when updating the same artifact across turns")
6933
7373
  },
6934
7374
  async ({ title, type, artifact_id }) => {
6935
7375
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -6947,15 +7387,15 @@ async function startMcpServer() {
6947
7387
  "ask_user",
6948
7388
  "Ask the user clarifying multiple-choice questions in Sideboard\u2019s composer (plan mode). Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. Use for approach forks and requirements \u2014 not for \u201Cis the plan ready?\u201D. After calling, stop and wait for their next message with answers.",
6949
7389
  {
6950
- questions: z2.array(
6951
- z2.object({
6952
- question: z2.string().describe("Full question text ending with ?"),
6953
- header: z2.string().max(24).optional().describe("Short label shown above the question"),
6954
- multiSelect: z2.boolean().optional().describe("Allow selecting multiple options"),
6955
- options: z2.array(
6956
- z2.object({
6957
- label: z2.string(),
6958
- description: z2.string().optional().describe("What this option means / when to choose it (strongly preferred)")
7390
+ questions: z3.array(
7391
+ z3.object({
7392
+ question: z3.string().describe("Full question text ending with ?"),
7393
+ header: z3.string().max(24).optional().describe("Short label shown above the question"),
7394
+ multiSelect: z3.boolean().optional().describe("Allow selecting multiple options"),
7395
+ options: z3.array(
7396
+ z3.object({
7397
+ label: z3.string(),
7398
+ description: z3.string().optional().describe("What this option means / when to choose it (strongly preferred)")
6959
7399
  })
6960
7400
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
6961
7401
  })
@@ -6974,9 +7414,9 @@ async function startMcpServer() {
6974
7414
  "present_plan",
6975
7415
  "Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
6976
7416
  {
6977
- title: z2.string().optional().describe("Short plan title (defaults to Plan)"),
6978
- content: z2.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
6979
- thread_id: z2.string().optional().describe("Sideboard thread id when cwd is not the worktree")
7417
+ title: z3.string().optional().describe("Short plan title (defaults to Plan)"),
7418
+ content: z3.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
7419
+ thread_id: z3.string().optional().describe("Sideboard thread id when cwd is not the worktree")
6980
7420
  },
6981
7421
  async ({ title, content, thread_id }) => {
6982
7422
  const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
@@ -6999,15 +7439,15 @@ async function startMcpServer() {
6999
7439
  "present_schema",
7000
7440
  "Open Sideboard\u2019s schema-driven side column (filterable table and/or form). 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.",
7001
7441
  {
7002
- title: z2.string().describe("Pane title"),
7003
- mode: z2.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
7004
- datasource: z2.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
7005
- resource_id: z2.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
7006
- record_id: z2.string().optional().describe("Record id when opening form mode"),
7007
- resource: z2.record(z2.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
7008
- record: z2.record(z2.unknown()).optional().describe("Inline record { id, data, published_at? }"),
7009
- records: z2.array(z2.record(z2.unknown())).optional().describe("Inline records for table mode"),
7010
- pane_id: z2.string().optional().describe("Stable pane id across updates")
7442
+ title: z3.string().describe("Pane title"),
7443
+ mode: z3.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
7444
+ datasource: z3.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
7445
+ resource_id: z3.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
7446
+ record_id: z3.string().optional().describe("Record id when opening form mode"),
7447
+ resource: z3.record(z3.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
7448
+ record: z3.record(z3.unknown()).optional().describe("Inline record { id, data, published_at? }"),
7449
+ records: z3.array(z3.record(z3.unknown())).optional().describe("Inline records for table mode"),
7450
+ pane_id: z3.string().optional().describe("Stable pane id across updates")
7011
7451
  },
7012
7452
  async (args) => {
7013
7453
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7028,10 +7468,10 @@ async function startMcpServer() {
7028
7468
  "present_files",
7029
7469
  "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.",
7030
7470
  {
7031
- title: z2.string().optional().describe("Pane title (default: Files)"),
7032
- datasource: z2.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
7033
- path: z2.string().optional().describe("Initial folder path (e.g. public)"),
7034
- pane_id: z2.string().optional().describe("Stable pane id across updates")
7471
+ title: z3.string().optional().describe("Pane title (default: Files)"),
7472
+ datasource: z3.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
7473
+ path: z3.string().optional().describe("Initial folder path (e.g. public)"),
7474
+ pane_id: z3.string().optional().describe("Stable pane id across updates")
7035
7475
  },
7036
7476
  async (args) => {
7037
7477
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7047,19 +7487,21 @@ async function startMcpServer() {
7047
7487
  }
7048
7488
  );
7049
7489
  registerSlackTools(server);
7490
+ registerLinearTools(server);
7050
7491
  if (sideboardMcpProfile() !== "worktree") {
7051
- const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-BEJIYPJ7.js");
7492
+ const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-OHAUWOA6.js");
7052
7493
  const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-P42Z3VOB.js");
7053
7494
  const accountDefaults = resolveThreadDefaults();
7054
7495
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
7055
7496
  server.tool(
7056
7497
  "set_caffeinate",
7057
- "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. macOS only.",
7498
+ "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.",
7058
7499
  {
7059
- enabled: z2.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
7500
+ enabled: z3.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
7060
7501
  },
7061
7502
  async ({ enabled }) => {
7062
- const state = setCaffeinateHold(enabled);
7503
+ const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
7504
+ const state = setCaffeinateHold(enabled, { threadId });
7063
7505
  if (enabled && !state.held) {
7064
7506
  return {
7065
7507
  content: [
@@ -7082,7 +7524,7 @@ async function startMcpServer() {
7082
7524
  text: JSON.stringify({
7083
7525
  ...getCaffeinateHold(),
7084
7526
  ok: true,
7085
- message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false (or the user says they are done)." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
7527
+ message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false, the user says they are done, or this orchestration chat is closed." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
7086
7528
  })
7087
7529
  }
7088
7530
  ]
@@ -7093,15 +7535,15 @@ async function startMcpServer() {
7093
7535
  "create_thread",
7094
7536
  `Create a new worktree thread (chat) from branch, pr, or ticket. Pass repoPath from list_workspaces. From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Then use send_to_thread to chat.`,
7095
7537
  {
7096
- sourceType: z2.enum(["branch", "pr", "ticket"]),
7097
- sourceRef: z2.string(),
7098
- agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
7099
- model: z2.string().nullable().optional().describe(
7538
+ sourceType: z3.enum(["branch", "pr", "ticket"]),
7539
+ sourceRef: z3.string(),
7540
+ agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
7541
+ model: z3.string().nullable().optional().describe(
7100
7542
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
7101
7543
  ),
7102
- repoPath: z2.string(),
7103
- title: z2.string().optional(),
7104
- parentThreadId: z2.string().optional().describe(
7544
+ repoPath: z3.string(),
7545
+ title: z3.string().optional(),
7546
+ parentThreadId: z3.string().optional().describe(
7105
7547
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
7106
7548
  )
7107
7549
  },
@@ -7206,11 +7648,11 @@ async function startMcpServer() {
7206
7648
  );
7207
7649
  server.tool(
7208
7650
  "send_to_thread",
7209
- "Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
7651
+ "Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. For commit/push/PR/merge, prefer ask_git (canonical desktop-button phrases). Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
7210
7652
  {
7211
- ref: z2.string(),
7212
- prompt: z2.string(),
7213
- force_stop: z2.boolean().optional()
7653
+ ref: z3.string(),
7654
+ prompt: z3.string(),
7655
+ force_stop: z3.boolean().optional()
7214
7656
  },
7215
7657
  async ({ ref, prompt, force_stop }) => {
7216
7658
  if (force_stop) {
@@ -7237,10 +7679,10 @@ async function startMcpServer() {
7237
7679
  );
7238
7680
  server.tool(
7239
7681
  "wait_for_turn",
7240
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
7682
+ "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
7241
7683
  {
7242
- ref: z2.string(),
7243
- timeoutMs: z2.number().optional()
7684
+ ref: z3.string(),
7685
+ timeoutMs: z3.number().optional()
7244
7686
  },
7245
7687
  async ({ ref, timeoutMs }) => {
7246
7688
  const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
@@ -7262,7 +7704,7 @@ async function startMcpServer() {
7262
7704
  server.tool(
7263
7705
  "get_turn_result",
7264
7706
  "Final assistant message only (not full transcript)",
7265
- { ref: z2.string() },
7707
+ { ref: z3.string() },
7266
7708
  async ({ ref }) => {
7267
7709
  const result = orch.getTurnResult(ref);
7268
7710
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
@@ -7272,8 +7714,8 @@ async function startMcpServer() {
7272
7714
  "stop_thread",
7273
7715
  "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.",
7274
7716
  {
7275
- ref: z2.string(),
7276
- force: z2.boolean().optional()
7717
+ ref: z3.string(),
7718
+ force: z3.boolean().optional()
7277
7719
  },
7278
7720
  async ({ ref, force }) => {
7279
7721
  const t = orch.getThread(ref);
@@ -7302,8 +7744,8 @@ async function startMcpServer() {
7302
7744
  );
7303
7745
  server.tool(
7304
7746
  "archive_thread",
7305
- "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators open PRs only by asking the worktree agent.",
7306
- { ref: z2.string() },
7747
+ "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, open PRs, and merge only by asking the worktree agent (ask_git).",
7748
+ { ref: z3.string() },
7307
7749
  async ({ ref }) => {
7308
7750
  const t = orch.getThread(ref);
7309
7751
  if (!t) {
@@ -7336,7 +7778,7 @@ async function startMcpServer() {
7336
7778
  server.tool(
7337
7779
  "restore_thread",
7338
7780
  "Restore an archived thread (recreates worktree from branch when needed)",
7339
- { ref: z2.string() },
7781
+ { ref: z3.string() },
7340
7782
  async ({ ref }) => {
7341
7783
  try {
7342
7784
  const restored = await orch.restore(ref);
@@ -7362,8 +7804,8 @@ async function startMcpServer() {
7362
7804
  "get_diff",
7363
7805
  "Compact diff summary (capped hunks, paginated)",
7364
7806
  {
7365
- ref: z2.string(),
7366
- maxFiles: z2.number().optional()
7807
+ ref: z3.string(),
7808
+ maxFiles: z3.number().optional()
7367
7809
  },
7368
7810
  async ({ ref, maxFiles }) => {
7369
7811
  const summary = await orch.diffSummary(ref);
@@ -7383,7 +7825,7 @@ async function startMcpServer() {
7383
7825
  server.tool(
7384
7826
  "request_review",
7385
7827
  'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn / get_turn_result on the returned review tab id.',
7386
- { ref: z2.string().describe("Worktree thread id/ref to review") },
7828
+ { ref: z3.string().describe("Worktree thread id/ref to review") },
7387
7829
  async ({ ref }) => {
7388
7830
  try {
7389
7831
  const tab = await orch.requestReview(ref);
@@ -7408,7 +7850,39 @@ async function startMcpServer() {
7408
7850
  }
7409
7851
  }
7410
7852
  );
7411
- const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7853
+ server.tool(
7854
+ "ask_git",
7855
+ "Tell a worktree agent to commit & push, open a draft PR, resolve conflicts, or merge the linked PR \u2014 same short prompts as the desktop git buttons. The worktree agent runs git/gh (`gh pr merge`); this only queues the prompt. Pass a worktree thread ref (not the orchestrator). Then wait_for_turn / get_turn_result. Do not run git or gh from the orchestration cwd.",
7856
+ {
7857
+ ref: z3.string().describe("Worktree thread id/ref"),
7858
+ action: z3.enum(AGENT_GIT_ACTIONS).describe(
7859
+ "commit-push | create-draft | create-web | resolve-conflicts | merge"
7860
+ )
7861
+ },
7862
+ async ({ ref, action }) => {
7863
+ try {
7864
+ const thread = await orch.askGit(ref, action);
7865
+ return {
7866
+ content: [
7867
+ {
7868
+ type: "text",
7869
+ text: JSON.stringify({
7870
+ id: thread.id,
7871
+ status: thread.status,
7872
+ queueLength: thread.queue.length,
7873
+ action,
7874
+ link: `sideboard://thread/${thread.id}`
7875
+ })
7876
+ }
7877
+ ]
7878
+ };
7879
+ } catch (err) {
7880
+ const message = err instanceof Error ? err.message : String(err);
7881
+ return { content: [{ type: "text", text: message }], isError: true };
7882
+ }
7883
+ }
7884
+ );
7885
+ const agentEnum = z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7412
7886
  server.tool(
7413
7887
  "list_models",
7414
7888
  "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.",
@@ -7431,11 +7905,11 @@ async function startMcpServer() {
7431
7905
  "fork_worktree",
7432
7906
  "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn on the returned id.",
7433
7907
  {
7434
- ref: z2.string().describe("Worktree thread id/ref to fork"),
7435
- through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7908
+ ref: z3.string().describe("Worktree thread id/ref to fork"),
7909
+ through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7436
7910
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
7437
- model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7438
- title: z2.string().optional()
7911
+ model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7912
+ title: z3.string().optional()
7439
7913
  },
7440
7914
  async ({ ref, through_index, agent, model, title }) => {
7441
7915
  try {
@@ -7476,11 +7950,11 @@ async function startMcpServer() {
7476
7950
  "fork_chat",
7477
7951
  "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
7478
7952
  {
7479
- ref: z2.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
7480
- through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7953
+ ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
7954
+ through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7481
7955
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
7482
- model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7483
- title: z2.string().optional()
7956
+ model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7957
+ title: z3.string().optional()
7484
7958
  },
7485
7959
  async ({ ref, through_index, agent, model, title }) => {
7486
7960
  try {
@@ -7526,8 +8000,8 @@ async function startMcpServer() {
7526
8000
  "run_dev_script",
7527
8001
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
7528
8002
  {
7529
- ref: z2.string(),
7530
- name: z2.string().optional()
8003
+ ref: z3.string(),
8004
+ name: z3.string().optional()
7531
8005
  },
7532
8006
  async ({ ref, name }) => {
7533
8007
  const result = await orch.startDev(ref, name);
@@ -7549,7 +8023,7 @@ async function startMcpServer() {
7549
8023
  server.tool(
7550
8024
  "list_run_scripts",
7551
8025
  "List named run scripts available for a thread",
7552
- { ref: z2.string() },
8026
+ { ref: z3.string() },
7553
8027
  async ({ ref }) => {
7554
8028
  const scripts = orch.listThreadRunScripts(ref);
7555
8029
  const active = orch.getActiveRuns(ref);
@@ -7567,8 +8041,8 @@ async function startMcpServer() {
7567
8041
  "stop_dev_script",
7568
8042
  "Stop a running script for a thread (all scripts if name omitted)",
7569
8043
  {
7570
- ref: z2.string(),
7571
- name: z2.string().optional()
8044
+ ref: z3.string(),
8045
+ name: z3.string().optional()
7572
8046
  },
7573
8047
  async ({ ref, name }) => {
7574
8048
  orch.stopDev(ref, name);
@@ -7578,7 +8052,7 @@ async function startMcpServer() {
7578
8052
  server.tool(
7579
8053
  "run_setup",
7580
8054
  "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
7581
- { ref: z2.string() },
8055
+ { ref: z3.string() },
7582
8056
  async ({ ref }) => {
7583
8057
  const result = await orch.runSetup(ref);
7584
8058
  return {
@@ -7589,7 +8063,7 @@ async function startMcpServer() {
7589
8063
  server.tool(
7590
8064
  "add_workspace",
7591
8065
  "Register a git repo as a Sideboard workspace",
7592
- { repoPath: z2.string() },
8066
+ { repoPath: z3.string() },
7593
8067
  async ({ repoPath }) => {
7594
8068
  const ws = await orch.addWorkspace(repoPath);
7595
8069
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -7598,7 +8072,7 @@ async function startMcpServer() {
7598
8072
  server.tool(
7599
8073
  "remove_workspace",
7600
8074
  "Unregister a Sideboard workspace (does not archive threads)",
7601
- { repoPath: z2.string() },
8075
+ { repoPath: z3.string() },
7602
8076
  async ({ repoPath }) => {
7603
8077
  orch.removeWorkspace(repoPath);
7604
8078
  return { content: [{ type: "text", text: "ok" }] };
@@ -7608,12 +8082,12 @@ async function startMcpServer() {
7608
8082
  "fanout",
7609
8083
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
7610
8084
  {
7611
- prompt: z2.string(),
7612
- agents: z2.array(z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
7613
- repoPath: z2.string(),
7614
- sourceType: z2.enum(["branch", "pr", "ticket"]).optional(),
7615
- sourceRef: z2.string().optional(),
7616
- title: z2.string().optional()
8085
+ prompt: z3.string(),
8086
+ agents: z3.array(z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
8087
+ repoPath: z3.string(),
8088
+ sourceType: z3.enum(["branch", "pr", "ticket"]).optional(),
8089
+ sourceRef: z3.string().optional(),
8090
+ title: z3.string().optional()
7617
8091
  },
7618
8092
  async (args) => {
7619
8093
  const threads = await orch.bestOfN(args);
@@ -7640,8 +8114,8 @@ async function startMcpServer() {
7640
8114
  "list_branches",
7641
8115
  "List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
7642
8116
  {
7643
- repoPath: z2.string(),
7644
- unmergedOnly: z2.boolean().optional()
8117
+ repoPath: z3.string(),
8118
+ unmergedOnly: z3.boolean().optional()
7645
8119
  },
7646
8120
  async ({ repoPath, unmergedOnly }) => {
7647
8121
  const root = await resolveRepoRoot(repoPath);
@@ -7661,7 +8135,7 @@ async function startMcpServer() {
7661
8135
  server.tool(
7662
8136
  "list_prs",
7663
8137
  "List open GitHub PRs for a registered workspace. Pass repoPath from list_workspaces (uses gh against that repo remote). Then create_thread with sourceType=pr.",
7664
- { repoPath: z2.string() },
8138
+ { repoPath: z3.string() },
7665
8139
  async ({ repoPath }) => {
7666
8140
  const root = await resolveRepoRoot(repoPath);
7667
8141
  const prs = await listPrs(root);
@@ -7679,8 +8153,8 @@ async function startMcpServer() {
7679
8153
  );
7680
8154
  server.tool(
7681
8155
  "get_pr_stack",
7682
- "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 mergePr on stacked PRs.",
7683
- { ref: z2.string() },
8156
+ "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.",
8157
+ { ref: z3.string() },
7684
8158
  async ({ ref }) => {
7685
8159
  const stack = await orch.getPrStack(ref);
7686
8160
  return {
@@ -7692,8 +8166,8 @@ async function startMcpServer() {
7692
8166
  "open_pr_stack_layers",
7693
8167
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
7694
8168
  {
7695
- ref: z2.string(),
7696
- layer: z2.number().int().positive().optional()
8169
+ ref: z3.string(),
8170
+ layer: z3.number().int().positive().optional()
7697
8171
  },
7698
8172
  async ({ ref, layer }) => {
7699
8173
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -7727,9 +8201,9 @@ async function startMcpServer() {
7727
8201
  "add_stack_layer",
7728
8202
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
7729
8203
  {
7730
- ref: z2.string(),
7731
- branchName: z2.string(),
7732
- title: z2.string().optional()
8204
+ ref: z3.string(),
8205
+ branchName: z3.string(),
8206
+ title: z3.string().optional()
7733
8207
  },
7734
8208
  async ({ ref, branchName, title }) => {
7735
8209
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -7758,11 +8232,11 @@ async function startMcpServer() {
7758
8232
  "create_pr_stack",
7759
8233
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
7760
8234
  {
7761
- repoPath: z2.string(),
7762
- branches: z2.array(z2.string()).min(1),
7763
- agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
7764
- base: z2.string().optional(),
7765
- title: z2.string().optional()
8235
+ repoPath: z3.string(),
8236
+ branches: z3.array(z3.string()).min(1),
8237
+ agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
8238
+ base: z3.string().optional(),
8239
+ title: z3.string().optional()
7766
8240
  },
7767
8241
  async (args) => {
7768
8242
  const result = await orch.createPrStack({
@@ -7800,7 +8274,7 @@ async function startMcpServer() {
7800
8274
  server.tool(
7801
8275
  "list_issues",
7802
8276
  "List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Then create_thread with sourceType=ticket.",
7803
- { repoPath: z2.string() },
8277
+ { repoPath: z3.string() },
7804
8278
  async ({ repoPath }) => {
7805
8279
  const root = await resolveRepoRoot(repoPath);
7806
8280
  const result = await listIssues(root);
@@ -7811,8 +8285,8 @@ async function startMcpServer() {
7811
8285
  "list_linear_issues",
7812
8286
  "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
7813
8287
  {
7814
- agent: z2.enum(["claude", "codex", "opencode"]),
7815
- repoPath: z2.string()
8288
+ agent: z3.enum(["claude", "codex", "opencode"]),
8289
+ repoPath: z3.string()
7816
8290
  },
7817
8291
  async ({ agent, repoPath }) => {
7818
8292
  const issues = await listLinearIssues(agent, repoPath);