@sideboard-ai/core 0.1.74 → 0.1.79

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 (37) hide show
  1. package/dist/{agents-H7M5CQY2.js → agents-EWPHOM6Y.js} +6 -6
  2. package/dist/{agents-2S2JVLZ6.js → agents-GRAPUXWW.js} +5 -5
  3. package/dist/{app-settings-3TLA2EJ7.js → app-settings-6IOQYGBK.js} +7 -1
  4. package/dist/{app-settings-P42Z3VOB.js → app-settings-NILNWNNQ.js} +7 -1
  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-77W62MTX.js → chunk-5LXWTU3J.js} +6 -6
  8. package/dist/{chunk-GJ2LZQJI.js → chunk-5VTWBI3I.js} +2 -2
  9. package/dist/{chunk-2NKSHIRI.js → chunk-6T2SKGCS.js} +3 -3
  10. package/dist/{chunk-OE7YBB5X.js → chunk-FUUVPN4A.js} +108 -3
  11. package/dist/{chunk-HCWAIEBU.js → chunk-HCGEFU3J.js} +8 -7
  12. package/dist/{chunk-XA2FQJTN.js → chunk-HSLQXL6M.js} +6 -5
  13. package/dist/{chunk-W7YOTDVO.js → chunk-IFPN6TER.js} +47 -2
  14. package/dist/chunk-MFF2RE3I.js +168 -0
  15. package/dist/{chunk-KBKPVKYZ.js → chunk-P33ZQ7ZC.js} +3 -3
  16. package/dist/{chunk-DG3S2UXP.js → chunk-PW5YHHP3.js} +2 -2
  17. package/dist/chunk-RV6DBEDQ.js +170 -0
  18. package/dist/{chunk-5KJMNNCB.js → chunk-SHCR6RPU.js} +3 -3
  19. package/dist/{chunk-PTJJIQK7.js → chunk-XKNBSYA7.js} +47 -2
  20. package/dist/{chunk-MF2YMEGD.js → chunk-YVVXWWCC.js} +110 -3
  21. package/dist/{coordinator-prompt-3XXSG7M2.js → coordinator-prompt-2OVON2LQ.js} +4 -4
  22. package/dist/{coordinator-prompt-RTUCCPCI.js → coordinator-prompt-K2R34A5T.js} +3 -3
  23. package/dist/{global-workspace-PJU6HISJ.js → global-workspace-RI367AM6.js} +4 -4
  24. package/dist/{global-workspace-CSIS62Z4.js → global-workspace-VG44RZUH.js} +5 -5
  25. package/dist/index.cjs +1548 -614
  26. package/dist/index.d.cts +226 -28
  27. package/dist/index.d.ts +226 -28
  28. package/dist/index.js +1277 -575
  29. package/dist/mcp/run-stdio.cjs +3153 -2509
  30. package/dist/mcp/run-stdio.js +690 -262
  31. package/dist/{workspaces-L4TXMUNM.js → workspaces-BQWQEZWE.js} +5 -5
  32. package/dist/{workspaces-W72KZL4B.js → workspaces-TYPJNUSM.js} +6 -6
  33. package/dist/{worktree-VOCH3WS3.js → worktree-5VLLFI5W.js} +5 -1
  34. package/dist/{worktree-KWXHMKRN.js → worktree-UNSOCYZU.js} +5 -1
  35. package/package.json +1 -1
  36. package/dist/caffeinate-hold-BEJIYPJ7.js +0 -107
  37. package/dist/chunk-JAWEDVVA.js +0 -106
@@ -16,19 +16,22 @@ import {
16
16
  resolveQuotaFallbackAgent,
17
17
  sideboardMcpProfile,
18
18
  summarizeTurnStderr
19
- } from "../chunk-KBKPVKYZ.js";
19
+ } from "../chunk-P33ZQ7ZC.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-GJ2LZQJI.js";
26
+ } from "../chunk-5VTWBI3I.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-2NKSHIRI.js";
44
+ } from "../chunk-6T2SKGCS.js";
42
45
  import {
43
46
  SLACK_REPLY_FORMATTING,
44
47
  coordinatorSystemPrompt,
45
48
  coordinatorTurnReminder,
46
49
  enrichWorkspacesWithGithub,
47
50
  ensureGlobalCoordinatorCwd
48
- } from "../chunk-XA2FQJTN.js";
51
+ } from "../chunk-HSLQXL6M.js";
49
52
  import {
50
53
  addPrStackLayer,
51
54
  allocateTeamName,
@@ -54,9 +57,11 @@ import {
54
57
  createExistingBranchWorktree,
55
58
  createOrUpdatePr,
56
59
  createThreadWorktree,
60
+ currentBranch,
57
61
  detectGhStack,
58
62
  fetchPrHead,
59
63
  formatGhLandError,
64
+ formatGitAuthModeDirective,
60
65
  getPr,
61
66
  getPrChecks,
62
67
  getPrDetails,
@@ -82,7 +87,7 @@ import {
82
87
  takenSlugsFromThread,
83
88
  threadDisplayLabel,
84
89
  worktreeNameFromPath
85
- } from "../chunk-OE7YBB5X.js";
90
+ } from "../chunk-FUUVPN4A.js";
86
91
  import {
87
92
  ATTACHMENTS_DIR,
88
93
  LEGACY_ATTACHMENTS_DIR,
@@ -122,7 +127,7 @@ import {
122
127
  saveLinearOAuth,
123
128
  updateAdvancedSettings,
124
129
  writeSecureJson
125
- } from "../chunk-PTJJIQK7.js";
130
+ } from "../chunk-XKNBSYA7.js";
126
131
  import {
127
132
  writePrivateFile
128
133
  } from "../chunk-NSTQ6QKD.js";
@@ -140,7 +145,7 @@ import {
140
145
  // src/mcp/server.ts
141
146
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
142
147
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
143
- import { z as z2 } from "zod";
148
+ import { z as z3 } from "zod";
144
149
  import { basename as basename4 } from "path";
145
150
 
146
151
  // src/orchestrator/orchestrator.ts
@@ -341,8 +346,8 @@ ${input.permalink}` : "";
341
346
 
342
347
  ${body}${link}`;
343
348
  }
344
- function isSlackExternalReplyPrompt(text2) {
345
- return text2.startsWith("Slack reply from ") && text2.includes("not a command");
349
+ function isSlackExternalReplyPrompt(text3) {
350
+ return text3.startsWith("Slack reply from ") && text3.includes("not a command");
346
351
  }
347
352
  function pendingSlackExternalReplies(messages) {
348
353
  let i = messages.length - 1;
@@ -366,9 +371,9 @@ function formatSlackRepliesForTurn(replies) {
366
371
  function listSlackOutboundWatches() {
367
372
  return pruneWatches(readStore3());
368
373
  }
369
- function formatOwnerSlackFyi(userName, text2) {
374
+ function formatOwnerSlackFyi(userName, text3) {
370
375
  const who = userName.trim() || "Someone";
371
- const body = text2.trim() || "(no text)";
376
+ const body = text3.trim() || "(no text)";
372
377
  return `${who} replied in Slack:
373
378
  ${body}`;
374
379
  }
@@ -383,7 +388,7 @@ async function relayExternalReply(opts) {
383
388
  const thread = readThread(threadId);
384
389
  if (!thread || thread.status === "archived") return true;
385
390
  try {
386
- const text2 = formatSlackExternalReplyPrompt({
391
+ const text3 = formatSlackExternalReplyPrompt({
387
392
  userName: opts.reply.userName,
388
393
  kind: opts.watch.kind,
389
394
  toLabel: opts.watch.toLabel,
@@ -392,7 +397,7 @@ async function relayExternalReply(opts) {
392
397
  });
393
398
  appendMessage(threadId, {
394
399
  role: "agent",
395
- text: text2,
400
+ text: text3,
396
401
  ts: (/* @__PURE__ */ new Date()).toISOString()
397
402
  });
398
403
  } catch {
@@ -576,7 +581,7 @@ async function refreshSlackReplyBadges(opts) {
576
581
  const messages = await fetchMessages(token, watch, opts?.fetchImpl);
577
582
  const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
578
583
  if (replies.length === 0) continue;
579
- const injected = new Set(watch.injectedReplyTs ?? []);
584
+ const injected2 = new Set(watch.injectedReplyTs ?? []);
580
585
  const collected = [...watch.replies ?? []];
581
586
  let lastSeenTs = watch.lastSeenTs;
582
587
  let latestUser;
@@ -610,7 +615,7 @@ async function refreshSlackReplyBadges(opts) {
610
615
  latestName = replyUserName;
611
616
  latestText = reply.text;
612
617
  latestPermalink = permalink;
613
- if (injected.has(ts)) {
618
+ if (injected2.has(ts)) {
614
619
  lastSeenTs = ts;
615
620
  continue;
616
621
  }
@@ -621,7 +626,7 @@ async function refreshSlackReplyBadges(opts) {
621
626
  fetchImpl: opts?.fetchImpl
622
627
  });
623
628
  if (!delivered) break;
624
- injected.add(ts);
629
+ injected2.add(ts);
625
630
  lastSeenTs = ts;
626
631
  }
627
632
  watches[i] = {
@@ -633,7 +638,7 @@ async function refreshSlackReplyBadges(opts) {
633
638
  replyTs: lastSeenTs,
634
639
  replyPreview: latestText.slice(0, 140),
635
640
  permalink: latestPermalink,
636
- injectedReplyTs: [...injected],
641
+ injectedReplyTs: [...injected2],
637
642
  replies: collected.slice(-MAX_REPLIES_PER_WATCH)
638
643
  };
639
644
  changed = true;
@@ -723,9 +728,9 @@ function toolFilePath(input) {
723
728
  if (!input) return void 0;
724
729
  return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
725
730
  }
726
- function countLines(text2) {
727
- if (!text2) return 0;
728
- return text2.split("\n").length;
731
+ function countLines(text3) {
732
+ if (!text3) return 0;
733
+ return text3.split("\n").length;
729
734
  }
730
735
  function diffFromInput(input) {
731
736
  if (!input) return {};
@@ -852,9 +857,9 @@ function applyAgentEvent(parts, event) {
852
857
  function partsToAssistantText(parts) {
853
858
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
854
859
  }
855
- function stripBrightsyNdjsonNoise(text2) {
856
- if (!text2 || !text2.includes('"type"')) return text2;
857
- let out = text2;
860
+ function stripBrightsyNdjsonNoise(text3) {
861
+ if (!text3 || !text3.includes('"type"')) return text3;
862
+ let out = text3;
858
863
  out = out.replace(
859
864
  /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
860
865
  ""
@@ -913,9 +918,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
913
918
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
914
919
  );
915
920
  }
916
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-PJU6HISJ.js");
921
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-RI367AM6.js");
917
922
  if (isGlobalThread2(thread)) {
918
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-RTUCCPCI.js");
923
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-K2R34A5T.js");
919
924
  ensureGlobalCoordinatorCwd2(
920
925
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
921
926
  );
@@ -932,8 +937,13 @@ async function spawnAgentTurn(thread, input, onEvent) {
932
937
  }
933
938
  const env = childEnvWithAppSettings(cmd.env);
934
939
  if (!isOrchestratorThread(thread)) {
935
- const originEnv = await originGhRepoEnv(thread.worktreePath).catch(() => ({}));
936
- Object.assign(env, originEnv);
940
+ try {
941
+ const originEnv = await originGhRepoEnv(thread.worktreePath, { env });
942
+ Object.assign(env, originEnv);
943
+ } catch (err) {
944
+ const detail = err instanceof Error ? err.message : String(err);
945
+ console.warn(`[sideboard] originGhRepoEnv failed (${thread.worktreePath}): ${detail}`);
946
+ }
937
947
  }
938
948
  const child = execa(cmd.file, cmd.args, {
939
949
  cwd: cmd.cwd,
@@ -1002,8 +1012,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
1002
1012
  onEvent({ type: "exit", data: exitCode });
1003
1013
  const finalized = finalizeParts(parts);
1004
1014
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
1005
- const text2 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
1006
- return { exitCode, sessionId, assistantText: text2, parts: finalized, usage };
1015
+ const text3 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
1016
+ return { exitCode, sessionId, assistantText: text3, parts: finalized, usage };
1007
1017
  });
1008
1018
  return {
1009
1019
  pid: child.pid,
@@ -1017,6 +1027,80 @@ async function spawnAgentTurn(thread, input, onEvent) {
1017
1027
  };
1018
1028
  }
1019
1029
 
1030
+ // src/land/pr-metadata.ts
1031
+ var GENERIC_TITLE_RE = /^(sideboard:|thread\/|wip\b|tmp\b|update\b|changes?\b|misc\b|stuff\b)/i;
1032
+ function looksGeneric(title) {
1033
+ const t = title.trim();
1034
+ if (!t) return true;
1035
+ if (t.length < 4) return true;
1036
+ if (GENERIC_TITLE_RE.test(t)) return true;
1037
+ if (!/[\s:/\-_]/.test(t) && t.length <= 24) return true;
1038
+ return false;
1039
+ }
1040
+ function truncateTitle(title, max = 72) {
1041
+ const t = title.trim().replace(/\s+/g, " ");
1042
+ if (t.length <= max) return t;
1043
+ return `${t.slice(0, max - 1).trimEnd()}\u2026`;
1044
+ }
1045
+ function titleFromPaths(paths) {
1046
+ if (paths.length === 0) return null;
1047
+ const top = paths.slice(0, 3).map((p) => p.replace(/\/$/, ""));
1048
+ if (paths.length === 1) return truncateTitle(`Update ${top[0]}`);
1049
+ if (paths.length <= 3) return truncateTitle(`Update ${top.join(", ")}`);
1050
+ return truncateTitle(`Update ${top[0]} and ${paths.length - 1} more files`);
1051
+ }
1052
+ async function suggestPrMetadata(worktreePath, opts) {
1053
+ const [{ stdout: logOut }, { stdout: nameStatus }, { stdout: statOut }] = await Promise.all([
1054
+ git(["log", "--format=%s", `${opts.base}..HEAD`], worktreePath, {
1055
+ reject: false
1056
+ }),
1057
+ git(["diff", "--name-only", `${opts.base}...HEAD`], worktreePath, {
1058
+ reject: false
1059
+ }),
1060
+ git(["diff", "--stat", `${opts.base}...HEAD`], worktreePath, {
1061
+ reject: false
1062
+ })
1063
+ ]);
1064
+ const { stdout: dirtyNames } = await git(["diff", "--name-only", "HEAD"], worktreePath, {
1065
+ reject: false
1066
+ });
1067
+ const { stdout: untracked } = await git(
1068
+ ["ls-files", "--others", "--exclude-standard"],
1069
+ worktreePath,
1070
+ { reject: false }
1071
+ );
1072
+ const subjects = logOut.split("\n").map((s) => s.trim()).filter(Boolean);
1073
+ const paths = [
1074
+ ...new Set(
1075
+ [...nameStatus.split("\n"), ...dirtyNames.split("\n"), ...untracked.split("\n")].map((p) => p.trim()).filter(Boolean)
1076
+ )
1077
+ ].sort((a, b) => a.localeCompare(b));
1078
+ const meaningfulSubjects = subjects.filter((s) => !looksGeneric(s));
1079
+ const fallback = opts.fallbackTitle?.trim() || null;
1080
+ const fallbackOk = fallback && !looksGeneric(fallback) ? fallback : null;
1081
+ const title = truncateTitle(
1082
+ meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? "Sideboard changes"
1083
+ );
1084
+ const commitMessage = meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? `sideboard: ${title}`;
1085
+ const summaryLines = meaningfulSubjects.length > 0 ? meaningfulSubjects.slice(0, 12).map((s) => `- ${s}`) : paths.slice(0, 20).map((p) => `- \`${p}\``);
1086
+ const bodyParts = [
1087
+ "## Summary",
1088
+ summaryLines.length > 0 ? summaryLines.join("\n") : "- Changes landed via Sideboard.",
1089
+ ""
1090
+ ];
1091
+ if (statOut.trim()) {
1092
+ bodyParts.push("## Diff", "```", statOut.trim(), "```", "");
1093
+ }
1094
+ if (opts.sourceLabel) {
1095
+ bodyParts.push(`_Landed via Sideboard from ${opts.sourceLabel}_`);
1096
+ }
1097
+ return {
1098
+ title,
1099
+ body: bodyParts.join("\n").trim() + "\n",
1100
+ commitMessage: truncateTitle(commitMessage, 72)
1101
+ };
1102
+ }
1103
+
1020
1104
  // src/git/agent-git-actions.ts
1021
1105
  var AGENT_GIT_ACTIONS = [
1022
1106
  "commit-push",
@@ -1151,8 +1235,20 @@ async function captureLoginEnv() {
1151
1235
  cachedLoginEnv = { ...process.env };
1152
1236
  return { ...cachedLoginEnv };
1153
1237
  }
1238
+ var NESTED_ELECTRON_ENV_PREFIXES = ["ELECTRON_", "CHROME_"];
1239
+ function stripNestedElectronEnv(env) {
1240
+ const out = { ...env };
1241
+ for (const key of Object.keys(out)) {
1242
+ if (NESTED_ELECTRON_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
1243
+ delete out[key];
1244
+ }
1245
+ }
1246
+ return out;
1247
+ }
1154
1248
  function buildWorkspaceScriptEnv(opts, baseEnv) {
1155
- const env = { ...baseEnv ?? process.env };
1249
+ const env = stripNestedElectronEnv({
1250
+ ...baseEnv ?? process.env
1251
+ });
1156
1252
  const name = opts.workspaceName ?? basename(opts.worktreePath);
1157
1253
  const ports = opts.ports ?? [];
1158
1254
  const primary = ports[0];
@@ -1756,7 +1852,7 @@ async function createThread(input, _onSetupLine) {
1756
1852
  return readThread(thread.id) ?? thread;
1757
1853
  }
1758
1854
  async function listLinearIssues(agent, repoPath) {
1759
- const { getAdapter: getAdapter2 } = await import("../agents-2S2JVLZ6.js");
1855
+ const { getAdapter: getAdapter2 } = await import("../agents-GRAPUXWW.js");
1760
1856
  await requireAgent(agent, { requireLinear: true });
1761
1857
  const adapter = getAdapter2(agent);
1762
1858
  if (!adapter.listLinearIssues) {
@@ -1812,9 +1908,9 @@ async function tryClaudeSummary(transcript, opts) {
1812
1908
  { cwd: opts?.cwd, reject: false }
1813
1909
  );
1814
1910
  if (exitCode !== 0) return null;
1815
- const text2 = stdout.trim();
1816
- if (text2.length < 40) return null;
1817
- return text2;
1911
+ const text3 = stdout.trim();
1912
+ if (text3.length < 40) return null;
1913
+ return text3;
1818
1914
  } catch {
1819
1915
  return null;
1820
1916
  }
@@ -1863,9 +1959,9 @@ function extractiveSummary(transcript) {
1863
1959
  );
1864
1960
  return parts.join("\n");
1865
1961
  }
1866
- function clipSummary(text2) {
1867
- if (text2.length <= MAX_SUMMARY_CHARS) return text2;
1868
- return `${text2.slice(0, MAX_SUMMARY_CHARS)}
1962
+ function clipSummary(text3) {
1963
+ if (text3.length <= MAX_SUMMARY_CHARS) return text3;
1964
+ return `${text3.slice(0, MAX_SUMMARY_CHARS)}
1869
1965
 
1870
1966
  [\u2026summary truncated\u2026]`;
1871
1967
  }
@@ -2478,8 +2574,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
2478
2574
  );
2479
2575
  const recent = from.messages.slice(-8).map((m) => {
2480
2576
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
2481
- const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
2482
- return text2 ? `- ${role}: ${text2}` : null;
2577
+ const text3 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
2578
+ return text3 ? `- ${role}: ${text3}` : null;
2483
2579
  }).filter(Boolean);
2484
2580
  const body = [
2485
2581
  `# Orchestration handoff`,
@@ -2575,13 +2671,13 @@ function resolveConductorCursorAgentId(workspacePath) {
2575
2671
  for (const hash of hashes) {
2576
2672
  const agentsFile = join9(CURSOR_SDK_STORE, hash, "agents.ndjson");
2577
2673
  if (!existsSync9(agentsFile)) continue;
2578
- let text2;
2674
+ let text3;
2579
2675
  try {
2580
- text2 = readFileSync6(agentsFile, "utf8");
2676
+ text3 = readFileSync6(agentsFile, "utf8");
2581
2677
  } catch {
2582
2678
  continue;
2583
2679
  }
2584
- for (const line of text2.split("\n")) {
2680
+ for (const line of text3.split("\n")) {
2585
2681
  const trimmed = line.trim();
2586
2682
  if (!trimmed) continue;
2587
2683
  try {
@@ -2620,7 +2716,7 @@ async function adoptThread(input) {
2620
2716
  messages: input.messages ?? []
2621
2717
  });
2622
2718
  writeThread(thread);
2623
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-L4TXMUNM.js");
2719
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-BQWQEZWE.js");
2624
2720
  await ensureWorkspace2(repoPath);
2625
2721
  return thread;
2626
2722
  }
@@ -3344,13 +3440,13 @@ function formatStat(files) {
3344
3440
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
3345
3441
  }
3346
3442
  function emptyScopeStats() {
3347
- const z3 = emptyStat();
3443
+ const z4 = emptyStat();
3348
3444
  return {
3349
- commits: z3,
3350
- uncommitted: z3,
3351
- staged: z3,
3352
- unstaged: z3,
3353
- last_turn: z3
3445
+ commits: z4,
3446
+ uncommitted: z4,
3447
+ staged: z4,
3448
+ unstaged: z4,
3449
+ last_turn: z4
3354
3450
  };
3355
3451
  }
3356
3452
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -3839,80 +3935,6 @@ async function getDiffSummary(worktreePath, repoPath, opts) {
3839
3935
  };
3840
3936
  }
3841
3937
 
3842
- // src/land/pr-metadata.ts
3843
- var GENERIC_TITLE_RE = /^(sideboard:|thread\/|wip\b|tmp\b|update\b|changes?\b|misc\b|stuff\b)/i;
3844
- function looksGeneric(title) {
3845
- const t = title.trim();
3846
- if (!t) return true;
3847
- if (t.length < 4) return true;
3848
- if (GENERIC_TITLE_RE.test(t)) return true;
3849
- if (!/[\s:/\-_]/.test(t) && t.length <= 24) return true;
3850
- return false;
3851
- }
3852
- function truncateTitle(title, max = 72) {
3853
- const t = title.trim().replace(/\s+/g, " ");
3854
- if (t.length <= max) return t;
3855
- return `${t.slice(0, max - 1).trimEnd()}\u2026`;
3856
- }
3857
- function titleFromPaths(paths) {
3858
- if (paths.length === 0) return null;
3859
- const top = paths.slice(0, 3).map((p) => p.replace(/\/$/, ""));
3860
- if (paths.length === 1) return truncateTitle(`Update ${top[0]}`);
3861
- if (paths.length <= 3) return truncateTitle(`Update ${top.join(", ")}`);
3862
- return truncateTitle(`Update ${top[0]} and ${paths.length - 1} more files`);
3863
- }
3864
- async function suggestPrMetadata(worktreePath, opts) {
3865
- const [{ stdout: logOut }, { stdout: nameStatus }, { stdout: statOut }] = await Promise.all([
3866
- git(["log", "--format=%s", `${opts.base}..HEAD`], worktreePath, {
3867
- reject: false
3868
- }),
3869
- git(["diff", "--name-only", `${opts.base}...HEAD`], worktreePath, {
3870
- reject: false
3871
- }),
3872
- git(["diff", "--stat", `${opts.base}...HEAD`], worktreePath, {
3873
- reject: false
3874
- })
3875
- ]);
3876
- const { stdout: dirtyNames } = await git(["diff", "--name-only", "HEAD"], worktreePath, {
3877
- reject: false
3878
- });
3879
- const { stdout: untracked } = await git(
3880
- ["ls-files", "--others", "--exclude-standard"],
3881
- worktreePath,
3882
- { reject: false }
3883
- );
3884
- const subjects = logOut.split("\n").map((s) => s.trim()).filter(Boolean);
3885
- const paths = [
3886
- ...new Set(
3887
- [...nameStatus.split("\n"), ...dirtyNames.split("\n"), ...untracked.split("\n")].map((p) => p.trim()).filter(Boolean)
3888
- )
3889
- ].sort((a, b) => a.localeCompare(b));
3890
- const meaningfulSubjects = subjects.filter((s) => !looksGeneric(s));
3891
- const fallback = opts.fallbackTitle?.trim() || null;
3892
- const fallbackOk = fallback && !looksGeneric(fallback) ? fallback : null;
3893
- const title = truncateTitle(
3894
- meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? "Sideboard changes"
3895
- );
3896
- const commitMessage = meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? `sideboard: ${title}`;
3897
- const summaryLines = meaningfulSubjects.length > 0 ? meaningfulSubjects.slice(0, 12).map((s) => `- ${s}`) : paths.slice(0, 20).map((p) => `- \`${p}\``);
3898
- const bodyParts = [
3899
- "## Summary",
3900
- summaryLines.length > 0 ? summaryLines.join("\n") : "- Changes landed via Sideboard.",
3901
- ""
3902
- ];
3903
- if (statOut.trim()) {
3904
- bodyParts.push("## Diff", "```", statOut.trim(), "```", "");
3905
- }
3906
- if (opts.sourceLabel) {
3907
- bodyParts.push(`_Landed via Sideboard from ${opts.sourceLabel}_`);
3908
- }
3909
- return {
3910
- title,
3911
- body: bodyParts.join("\n").trim() + "\n",
3912
- commitMessage: truncateTitle(commitMessage, 72)
3913
- };
3914
- }
3915
-
3916
3938
  // src/land/land.ts
3917
3939
  async function previewLand(thread) {
3918
3940
  if (thread.sourceIsFork) {
@@ -4456,6 +4478,8 @@ function formatWorktreeDirective(thread, opts) {
4456
4478
  lines.push(
4457
4479
  "- Push with `git push -u origin HEAD` (or the current branch name). Do not `git push upstream`."
4458
4480
  );
4481
+ lines.push("");
4482
+ lines.push(formatGitAuthModeDirective(opts?.gitAuthMode ?? "auto"));
4459
4483
  lines.push(
4460
4484
  "- Derive the PR title and body from what the changes actually do and why \u2014 inspect the diff/commits and the user request. Do not use the soccer-team worktree nickname or placeholder branch as the PR title."
4461
4485
  );
@@ -4660,7 +4684,7 @@ var Orchestrator = class {
4660
4684
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
4661
4685
  });
4662
4686
  }
4663
- const { autoCleanupOrphansEnabled } = await import("../app-settings-P42Z3VOB.js");
4687
+ const { autoCleanupOrphansEnabled } = await import("../app-settings-NILNWNNQ.js");
4664
4688
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
4665
4689
  await cleanupOrphanWorktrees({ repoPaths });
4666
4690
  }
@@ -4817,7 +4841,7 @@ var Orchestrator = class {
4817
4841
  }
4818
4842
  async finishCreateThread(threadId, prompt) {
4819
4843
  await this.runSetupAfterCreate(threadId);
4820
- const { autoRunAfterSetupEnabled } = await import("../app-settings-P42Z3VOB.js");
4844
+ const { autoRunAfterSetupEnabled } = await import("../app-settings-NILNWNNQ.js");
4821
4845
  if (autoRunAfterSetupEnabled()) {
4822
4846
  try {
4823
4847
  await this.startDev(threadId);
@@ -4897,11 +4921,11 @@ var Orchestrator = class {
4897
4921
  return results;
4898
4922
  }
4899
4923
  /** Edit the text of a not-yet-started queued message. */
4900
- async editQueuedMessage(threadRef, index, text2) {
4924
+ async editQueuedMessage(threadRef, index, text3) {
4901
4925
  const thread = this.requireThread(threadRef);
4902
4926
  return withThreadLock(thread.id, async () => {
4903
4927
  const current = this.requireThread(thread.id);
4904
- const trimmed = text2.trim();
4928
+ const trimmed = text3.trim();
4905
4929
  if (!trimmed || index < 0 || index >= current.queue.length) {
4906
4930
  return current;
4907
4931
  }
@@ -5095,14 +5119,16 @@ var Orchestrator = class {
5095
5119
  }
5096
5120
  const isBrightsy = fresh.agent === "brightsy";
5097
5121
  const isOrchestration = isOrchestratorThread(fresh);
5122
+ const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("../app-settings-NILNWNNQ.js");
5123
+ const gitAuthMode = getGithubGitAuthMode();
5098
5124
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
5099
5125
  githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
5100
5126
  () => null
5101
- )
5127
+ ),
5128
+ gitAuthMode
5102
5129
  });
5103
5130
  const artifactDirective = isBrightsy ? null : formatArtifactDirective();
5104
5131
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
5105
- const { autoRenameBranchEnabled } = await import("../app-settings-P42Z3VOB.js");
5106
5132
  const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled() ? formatRenameBranchDirective(fresh, {
5107
5133
  customPrompt: settings?.prompts?.renameBranch
5108
5134
  }) : null;
@@ -5661,6 +5687,14 @@ var Orchestrator = class {
5661
5687
  throw new Error(`${action} is not available on the global coordinator`);
5662
5688
  }
5663
5689
  }
5690
+ /** MCP set_caffeinate is a detached hold — closing the chat must not leave the Mac awake. */
5691
+ releaseOrchestratorCaffeinate(thread) {
5692
+ if (!isOrchestratorThread(thread)) return;
5693
+ try {
5694
+ releaseCaffeinateHoldForThread(thread.id);
5695
+ } catch {
5696
+ }
5697
+ }
5664
5698
  async diff(threadRef, opts) {
5665
5699
  const thread = this.requireThread(threadRef);
5666
5700
  this.assertNotGlobal(thread, "Diff");
@@ -5800,7 +5834,7 @@ var Orchestrator = class {
5800
5834
  updateThread(thread.id, { title: meta.title });
5801
5835
  }
5802
5836
  }
5803
- const { autoArchiveOnMergeEnabled } = await import("../app-settings-P42Z3VOB.js");
5837
+ const { autoArchiveOnMergeEnabled } = await import("../app-settings-NILNWNNQ.js");
5804
5838
  const latest = this.requireThread(thread.id);
5805
5839
  if (!shouldAutoArchiveOnPrMerge({
5806
5840
  previousPrState: prevState || null,
@@ -5931,8 +5965,9 @@ var Orchestrator = class {
5931
5965
  return tab;
5932
5966
  }
5933
5967
  /**
5934
- * Queue a desktop-git-button prompt on a worktree agent (commit/push/PR/merge).
5935
- * Orchestrators use this instead of running git/gh from the synthetic home.
5968
+ * Desktop git buttons + MCP `ask_git`.
5969
+ * When the worktree is clean, push / open the PR here (HTTPS via `gh` if SSH
5970
+ * is missing). When dirty, queue the worktree agent to commit first.
5936
5971
  */
5937
5972
  async askGit(threadRef, action) {
5938
5973
  if (!AGENT_GIT_ACTIONS.includes(action)) {
@@ -5950,6 +5985,10 @@ var Orchestrator = class {
5950
5985
  "No pull request linked. Ask the worktree agent to open a draft PR first (ask_git create-draft)."
5951
5986
  );
5952
5987
  }
5988
+ if ((action === "commit-push" || action === "create-draft" || action === "create-web") && thread.worktreePath?.trim() && !await isDirty(thread.worktreePath)) {
5989
+ await this.pushAndMaybeOpenPr(thread, action);
5990
+ return this.requireThread(threadRef);
5991
+ }
5953
5992
  let prBase;
5954
5993
  if (action === "resolve-conflicts") {
5955
5994
  try {
@@ -5960,6 +5999,34 @@ var Orchestrator = class {
5960
5999
  }
5961
6000
  return this.send(threadRef, agentGitPrompt(action, { prBase }));
5962
6001
  }
6002
+ /** Push origin (gh HTTPS fallback) and create/update the PR when requested. */
6003
+ async pushAndMaybeOpenPr(thread, action) {
6004
+ const cwd = thread.worktreePath;
6005
+ const branch = await currentBranch(cwd);
6006
+ await pushBranch(cwd, branch);
6007
+ if (action === "commit-push") return;
6008
+ const base = await resolveDefaultBranch(thread.repoPath);
6009
+ const meta = await suggestPrMetadata(cwd, {
6010
+ base,
6011
+ fallbackTitle: thread.prTitle ?? thread.title
6012
+ });
6013
+ const url = await createOrUpdatePr(cwd, {
6014
+ title: meta.title,
6015
+ body: meta.body,
6016
+ base,
6017
+ head: branch,
6018
+ draft: action === "create-draft",
6019
+ web: action === "create-web"
6020
+ });
6021
+ if (!url) return;
6022
+ const patch = { prUrl: url, prTitle: meta.title };
6023
+ try {
6024
+ const fetched = await getPrMeta(cwd, url);
6025
+ if (fetched?.title) patch.prTitle = fetched.title;
6026
+ } catch {
6027
+ }
6028
+ updateThread(thread.id, patch);
6029
+ }
5963
6030
  setThreadOptions(threadRef, patch) {
5964
6031
  const thread = this.requireThread(threadRef);
5965
6032
  const next = {};
@@ -6030,6 +6097,7 @@ var Orchestrator = class {
6030
6097
  async archive(threadRef) {
6031
6098
  const thread = this.requireThread(threadRef);
6032
6099
  this.stop(thread.id);
6100
+ this.releaseOrchestratorCaffeinate(thread);
6033
6101
  if (isGlobalThread(thread)) {
6034
6102
  const archived2 = setStatus(thread.id, "archived");
6035
6103
  this.emit({ type: "status_changed", threadId: archived2.id, status: "archived" });
@@ -6054,7 +6122,7 @@ var Orchestrator = class {
6054
6122
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
6055
6123
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
6056
6124
  try {
6057
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-L4TXMUNM.js");
6125
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-BQWQEZWE.js");
6058
6126
  await ensureWorkspace2(thread.repoPath);
6059
6127
  } catch {
6060
6128
  }
@@ -6064,6 +6132,7 @@ var Orchestrator = class {
6064
6132
  async purge(threadRef, opts) {
6065
6133
  const thread = this.requireThread(threadRef);
6066
6134
  this.stop(thread.id);
6135
+ this.releaseOrchestratorCaffeinate(thread);
6067
6136
  if (isGlobalThread(thread)) {
6068
6137
  deleteThreadRecord(thread.id);
6069
6138
  return;
@@ -6075,7 +6144,7 @@ var Orchestrator = class {
6075
6144
  await runArchiveScript(thread.repoPath, thread.worktreePath);
6076
6145
  } catch {
6077
6146
  }
6078
- const { deleteBranchOnPurgeEnabled } = await import("../app-settings-P42Z3VOB.js");
6147
+ const { deleteBranchOnPurgeEnabled } = await import("../app-settings-NILNWNNQ.js");
6079
6148
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
6080
6149
  await removeWorktree(thread.repoPath, thread.worktreePath, {
6081
6150
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -6096,7 +6165,7 @@ var Orchestrator = class {
6096
6165
  return restored2;
6097
6166
  }
6098
6167
  if (!existsSync15(thread.worktreePath)) {
6099
- const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-KWXHMKRN.js");
6168
+ const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-UNSOCYZU.js");
6100
6169
  const { execa: execa6 } = await import("execa");
6101
6170
  const slug = thread.worktreePath.split("/").pop();
6102
6171
  const dest = thread.worktreePath;
@@ -6168,13 +6237,36 @@ function getOrchestrator() {
6168
6237
  return singleton;
6169
6238
  }
6170
6239
 
6240
+ // src/http/fetch.ts
6241
+ var injected = null;
6242
+ function formatFetchError(err, url) {
6243
+ if (!(err instanceof Error)) return `${String(err)} (${url})`;
6244
+ const cause = err.cause;
6245
+ let detail = "";
6246
+ if (cause instanceof Error) {
6247
+ const code = typeof cause.code === "string" ? cause.code : void 0;
6248
+ detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
6249
+ } else if (cause != null) {
6250
+ detail = ` [${String(cause)}]`;
6251
+ }
6252
+ return `${err.message}${detail} (${url})`;
6253
+ }
6254
+ async function httpFetch(input, init) {
6255
+ const url = typeof input === "string" ? input : input.href;
6256
+ const fn = injected ?? globalThis.fetch.bind(globalThis);
6257
+ try {
6258
+ return await fn(url, init);
6259
+ } catch (err) {
6260
+ throw new Error(formatFetchError(err, url));
6261
+ }
6262
+ }
6263
+
6171
6264
  // src/integrations/linear-oauth.ts
6172
6265
  import { createHash, randomBytes } from "crypto";
6173
6266
  import { createServer as createServer2 } from "http";
6174
6267
 
6175
6268
  // src/integrations/linear-app.ts
6176
6269
  var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
6177
- var BAKED_LINEAR_CLIENT_SECRET = "";
6178
6270
 
6179
6271
  // src/integrations/linear-oauth.ts
6180
6272
  var LINEAR_OAUTH_PORT = 19848;
@@ -6184,7 +6276,7 @@ var REFRESH_SKEW_MS = 5 * 6e4;
6184
6276
  function linearOAuthCredentials() {
6185
6277
  const settings = loadAppSettings();
6186
6278
  const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
6187
- const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || BAKED_LINEAR_CLIENT_SECRET.trim();
6279
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
6188
6280
  if (!clientId) {
6189
6281
  throw new Error(
6190
6282
  `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.`
@@ -6200,7 +6292,7 @@ function linearAuthorizationHeader(token) {
6200
6292
  return `Bearer ${t}`;
6201
6293
  }
6202
6294
  async function exchangeLinearToken(body) {
6203
- const res = await fetch(LINEAR_TOKEN, {
6295
+ const res = await httpFetch(LINEAR_TOKEN, {
6204
6296
  method: "POST",
6205
6297
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
6206
6298
  body
@@ -6267,6 +6359,18 @@ async function getLinearAuthToken(settings = loadAppSettings()) {
6267
6359
 
6268
6360
  // src/integrations/linear.ts
6269
6361
  var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
6362
+ var ISSUE_FIELDS = `
6363
+ id
6364
+ identifier
6365
+ title
6366
+ description
6367
+ url
6368
+ priority
6369
+ state { id name type }
6370
+ assignee { id name }
6371
+ team { id key name states { nodes { id name type } } }
6372
+ labels { nodes { name } }
6373
+ `;
6270
6374
  var ASSIGNED_ISSUES_QUERY = `
6271
6375
  query SideboardAssignedIssues($first: Int!) {
6272
6376
  viewer {
@@ -6275,53 +6379,280 @@ query SideboardAssignedIssues($first: Int!) {
6275
6379
  orderBy: updatedAt
6276
6380
  filter: { state: { type: { nin: ["completed", "canceled"] } } }
6277
6381
  ) {
6278
- nodes {
6279
- id
6280
- identifier
6281
- title
6282
- url
6283
- labels { nodes { name } }
6284
- }
6382
+ nodes { ${ISSUE_FIELDS} }
6285
6383
  }
6286
6384
  }
6287
6385
  }
6288
6386
  `;
6289
- async function listLinearIssuesDirect(opts) {
6290
- const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
6291
- if (!apiKey) {
6387
+ var TEAMS_QUERY = `
6388
+ query SideboardTeams {
6389
+ viewer { id name }
6390
+ teams(first: 50) {
6391
+ nodes {
6392
+ id
6393
+ key
6394
+ name
6395
+ states { nodes { id name type } }
6396
+ }
6397
+ }
6398
+ }
6399
+ `;
6400
+ var ISSUE_QUERY = `
6401
+ query SideboardIssue($id: String!) {
6402
+ issue(id: $id) { ${ISSUE_FIELDS} }
6403
+ }
6404
+ `;
6405
+ var ISSUE_CREATE = `
6406
+ mutation SideboardIssueCreate($input: IssueCreateInput!) {
6407
+ issueCreate(input: $input) {
6408
+ success
6409
+ issue { ${ISSUE_FIELDS} }
6410
+ }
6411
+ }
6412
+ `;
6413
+ var ISSUE_UPDATE = `
6414
+ mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
6415
+ issueUpdate(id: $id, input: $input) {
6416
+ success
6417
+ issue { ${ISSUE_FIELDS} }
6418
+ }
6419
+ }
6420
+ `;
6421
+ var COMMENT_CREATE = `
6422
+ mutation SideboardCommentCreate($input: CommentCreateInput!) {
6423
+ commentCreate(input: $input) {
6424
+ success
6425
+ comment { id body url }
6426
+ }
6427
+ }
6428
+ `;
6429
+ function rewriteLinearError(message) {
6430
+ if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
6431
+ return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
6432
+ }
6433
+ return message;
6434
+ }
6435
+ async function requireLinearToken(apiKey) {
6436
+ const token = (apiKey ?? await getLinearAuthToken())?.trim();
6437
+ if (!token) {
6292
6438
  throw new Error("Linear is not connected \u2014 sign in from Account settings");
6293
6439
  }
6294
- const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
6295
- const res = await fetch(LINEAR_GRAPHQL, {
6440
+ return token;
6441
+ }
6442
+ async function linearGraphql(query, variables, opts) {
6443
+ const apiKey = await requireLinearToken(opts?.apiKey);
6444
+ const res = await httpFetch(LINEAR_GRAPHQL, {
6296
6445
  method: "POST",
6297
6446
  headers: {
6298
6447
  "Content-Type": "application/json",
6299
6448
  Authorization: linearAuthorizationHeader(apiKey)
6300
6449
  },
6301
- body: JSON.stringify({
6302
- query: ASSIGNED_ISSUES_QUERY,
6303
- variables: { first }
6304
- })
6450
+ body: JSON.stringify({ query, variables })
6305
6451
  });
6306
6452
  if (!res.ok) {
6307
6453
  const body = await res.text().catch(() => "");
6308
6454
  throw new Error(
6309
- `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
6455
+ rewriteLinearError(
6456
+ `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
6457
+ )
6310
6458
  );
6311
6459
  }
6312
6460
  const json = await res.json();
6313
6461
  if (json.errors?.length) {
6314
- throw new Error(json.errors.map((e) => e.message ?? "Linear error").join("; "));
6462
+ throw new Error(
6463
+ rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
6464
+ );
6315
6465
  }
6316
- const nodes = json.data?.viewer?.assignedIssues?.nodes ?? [];
6317
- return nodes.map((node) => ({
6466
+ if (json.data == null) {
6467
+ throw new Error("Linear API returned no data");
6468
+ }
6469
+ return json.data;
6470
+ }
6471
+ function mapState(node) {
6472
+ if (!node?.id) return void 0;
6473
+ return {
6474
+ id: String(node.id),
6475
+ name: String(node.name ?? ""),
6476
+ type: String(node.type ?? "")
6477
+ };
6478
+ }
6479
+ function mapIssue(node) {
6480
+ const team = node.team;
6481
+ return {
6318
6482
  id: String(node.id ?? node.identifier ?? ""),
6319
6483
  identifier: String(node.identifier ?? node.id ?? ""),
6320
6484
  title: String(node.title ?? ""),
6321
6485
  url: String(node.url ?? ""),
6322
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
6486
+ description: node.description?.trim() || void 0,
6487
+ priority: typeof node.priority === "number" ? node.priority : void 0,
6488
+ state: mapState(node.state),
6489
+ assignee: node.assignee?.id ? { id: String(node.assignee.id), name: String(node.assignee.name ?? "") } : void 0,
6490
+ team: team?.id ? {
6491
+ id: String(team.id),
6492
+ key: String(team.key ?? ""),
6493
+ name: String(team.name ?? ""),
6494
+ states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
6495
+ } : void 0,
6496
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
6497
+ };
6498
+ }
6499
+ function toIssueInfo(issue) {
6500
+ return {
6501
+ id: issue.id,
6502
+ identifier: issue.identifier,
6503
+ title: issue.title,
6504
+ url: issue.url,
6505
+ labels: issue.labels,
6323
6506
  provider: "linear"
6324
- }));
6507
+ };
6508
+ }
6509
+ function mapTeam(node) {
6510
+ return {
6511
+ id: String(node.id ?? ""),
6512
+ key: String(node.key ?? ""),
6513
+ name: String(node.name ?? ""),
6514
+ states: (node.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
6515
+ };
6516
+ }
6517
+ function resolveLinearTeam(teams, team) {
6518
+ const t = team.trim();
6519
+ if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
6520
+ const lower = t.toLowerCase();
6521
+ const found = teams.find(
6522
+ (x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
6523
+ );
6524
+ if (!found) {
6525
+ const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
6526
+ throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
6527
+ }
6528
+ return found;
6529
+ }
6530
+ function resolveLinearState(team, state) {
6531
+ const s = state.trim();
6532
+ if (!s) throw new Error("Linear state is empty");
6533
+ const lower = s.toLowerCase();
6534
+ 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);
6535
+ if (!found) {
6536
+ const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
6537
+ throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
6538
+ }
6539
+ return found;
6540
+ }
6541
+ function normalizePriority(priority) {
6542
+ if (priority == null) return void 0;
6543
+ if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
6544
+ throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
6545
+ }
6546
+ return priority;
6547
+ }
6548
+ async function listLinearIssuesDirect(opts) {
6549
+ const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
6550
+ const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
6551
+ return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
6552
+ }
6553
+ async function listLinearTeams(opts) {
6554
+ const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
6555
+ return {
6556
+ viewer: {
6557
+ id: String(json.viewer?.id ?? ""),
6558
+ name: String(json.viewer?.name ?? "")
6559
+ },
6560
+ teams: (json.teams?.nodes ?? []).map(mapTeam)
6561
+ };
6562
+ }
6563
+ async function getLinearIssue(id, opts) {
6564
+ const issueId = id.trim();
6565
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6566
+ const json = await linearGraphql(
6567
+ ISSUE_QUERY,
6568
+ { id: issueId },
6569
+ opts
6570
+ );
6571
+ if (!json.issue) {
6572
+ throw new Error(`Linear issue not found: ${issueId}`);
6573
+ }
6574
+ return mapIssue(json.issue);
6575
+ }
6576
+ async function createLinearIssue(input, opts) {
6577
+ const title = input.title.trim();
6578
+ if (!title) throw new Error("Linear issue title is required");
6579
+ const { viewer, teams } = await listLinearTeams(opts);
6580
+ const team = resolveLinearTeam(teams, input.team);
6581
+ const mutationInput = {
6582
+ teamId: team.id,
6583
+ title
6584
+ };
6585
+ const description = input.description?.trim();
6586
+ if (description) mutationInput.description = description;
6587
+ if (input.state?.trim()) {
6588
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
6589
+ }
6590
+ const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
6591
+ if (assignee === "me") mutationInput.assigneeId = viewer.id;
6592
+ else if (assignee) mutationInput.assigneeId = assignee;
6593
+ const priority = normalizePriority(input.priority);
6594
+ if (priority != null) mutationInput.priority = priority;
6595
+ const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
6596
+ if (!json.issueCreate?.success || !json.issueCreate.issue) {
6597
+ throw new Error("Linear issueCreate failed");
6598
+ }
6599
+ return mapIssue(json.issueCreate.issue);
6600
+ }
6601
+ async function updateLinearIssue(input, opts) {
6602
+ const issueId = input.id.trim();
6603
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6604
+ const mutationInput = {};
6605
+ if (input.title !== void 0) {
6606
+ const title = input.title.trim();
6607
+ if (!title) throw new Error("Linear issue title cannot be empty");
6608
+ mutationInput.title = title;
6609
+ }
6610
+ if (input.description !== void 0) {
6611
+ mutationInput.description = input.description;
6612
+ }
6613
+ if (input.state?.trim()) {
6614
+ const existing = await getLinearIssue(issueId, opts);
6615
+ const team = existing.team;
6616
+ if (!team?.states.length) {
6617
+ throw new Error(`Linear issue ${existing.identifier} has no workflow states to resolve "${input.state}"`);
6618
+ }
6619
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
6620
+ }
6621
+ if (input.assignee !== void 0) {
6622
+ const assignee = input.assignee?.trim() || null;
6623
+ if (assignee === "me") {
6624
+ const { viewer } = await listLinearTeams(opts);
6625
+ mutationInput.assigneeId = viewer.id;
6626
+ } else {
6627
+ mutationInput.assigneeId = assignee;
6628
+ }
6629
+ }
6630
+ if (input.priority !== void 0) {
6631
+ mutationInput.priority = normalizePriority(input.priority);
6632
+ }
6633
+ if (Object.keys(mutationInput).length === 0) {
6634
+ throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority");
6635
+ }
6636
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
6637
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
6638
+ throw new Error("Linear issueUpdate failed");
6639
+ }
6640
+ return mapIssue(json.issueUpdate.issue);
6641
+ }
6642
+ async function commentLinearIssue(input, opts) {
6643
+ const issueId = input.id.trim();
6644
+ const body = input.body.trim();
6645
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6646
+ if (!body) throw new Error("Linear comment body is required");
6647
+ const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
6648
+ if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
6649
+ throw new Error("Linear commentCreate failed");
6650
+ }
6651
+ return {
6652
+ id: String(json.commentCreate.comment.id),
6653
+ body: String(json.commentCreate.comment.body ?? body),
6654
+ url: json.commentCreate.comment.url?.trim() || void 0
6655
+ };
6325
6656
  }
6326
6657
 
6327
6658
  // src/integrations/issues.ts
@@ -6603,8 +6934,8 @@ function labelGithubUrl(url) {
6603
6934
  }
6604
6935
  return { kind: "other", label: "GitHub", url: raw };
6605
6936
  }
6606
- function appendGithubLink(text2, githubUrl) {
6607
- const body = text2.trimEnd();
6937
+ function appendGithubLink(text3, githubUrl) {
6938
+ const body = text3.trimEnd();
6608
6939
  if (!githubUrl?.trim()) return body;
6609
6940
  const labeled = labelGithubUrl(githubUrl);
6610
6941
  if (!labeled) {
@@ -6887,6 +7218,101 @@ function registerSlackTools(server) {
6887
7218
  );
6888
7219
  }
6889
7220
 
7221
+ // src/mcp/linear-tools.ts
7222
+ import { z as z2 } from "zod";
7223
+ function text2(payload, isError = false) {
7224
+ return {
7225
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
7226
+ ...isError ? { isError: true } : {}
7227
+ };
7228
+ }
7229
+ function fail2(err) {
7230
+ return text2(
7231
+ { error: err instanceof Error ? err.message : String(err) },
7232
+ true
7233
+ );
7234
+ }
7235
+ var prioritySchema = z2.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
7236
+ function registerLinearTools(server) {
7237
+ server.tool(
7238
+ "linear_list_teams",
7239
+ "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.",
7240
+ {},
7241
+ async () => {
7242
+ try {
7243
+ return text2(await listLinearTeams());
7244
+ } catch (err) {
7245
+ return fail2(err);
7246
+ }
7247
+ }
7248
+ );
7249
+ server.tool(
7250
+ "linear_get_issue",
7251
+ "Get a Linear issue by uuid or identifier (ENG-123).",
7252
+ { id: z2.string() },
7253
+ async ({ id }) => {
7254
+ try {
7255
+ return text2(await getLinearIssue(id));
7256
+ } catch (err) {
7257
+ return fail2(err);
7258
+ }
7259
+ }
7260
+ );
7261
+ server.tool(
7262
+ "linear_create_issue",
7263
+ '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.',
7264
+ {
7265
+ team: z2.string(),
7266
+ title: z2.string(),
7267
+ description: z2.string().optional(),
7268
+ state: z2.string().optional(),
7269
+ assignee: z2.string().nullable().optional(),
7270
+ priority: prioritySchema
7271
+ },
7272
+ async (args) => {
7273
+ try {
7274
+ return text2(await createLinearIssue(args));
7275
+ } catch (err) {
7276
+ return fail2(err);
7277
+ }
7278
+ }
7279
+ );
7280
+ server.tool(
7281
+ "linear_update_issue",
7282
+ '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.',
7283
+ {
7284
+ id: z2.string(),
7285
+ title: z2.string().optional(),
7286
+ description: z2.string().optional(),
7287
+ state: z2.string().optional(),
7288
+ assignee: z2.string().nullable().optional(),
7289
+ priority: prioritySchema
7290
+ },
7291
+ async (args) => {
7292
+ try {
7293
+ return text2(await updateLinearIssue(args));
7294
+ } catch (err) {
7295
+ return fail2(err);
7296
+ }
7297
+ }
7298
+ );
7299
+ server.tool(
7300
+ "linear_comment",
7301
+ "Add a markdown comment on a Linear issue (uuid or ENG-123).",
7302
+ {
7303
+ id: z2.string(),
7304
+ body: z2.string()
7305
+ },
7306
+ async (args) => {
7307
+ try {
7308
+ return text2(await commentLinearIssue(args));
7309
+ } catch (err) {
7310
+ return fail2(err);
7311
+ }
7312
+ }
7313
+ );
7314
+ }
7315
+
6890
7316
  // src/mcp/server.ts
6891
7317
  var MAX_ORCH_THREADS = 5;
6892
7318
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -6910,7 +7336,7 @@ function withTimeout(promise, ms, label) {
6910
7336
  async function startMcpServer() {
6911
7337
  const orch = getOrchestrator();
6912
7338
  try {
6913
- const { maxConcurrentAgents } = await import("../app-settings-P42Z3VOB.js");
7339
+ const { maxConcurrentAgents } = await import("../app-settings-NILNWNNQ.js");
6914
7340
  orch.setMaxConcurrent(maxConcurrentAgents());
6915
7341
  } catch {
6916
7342
  }
@@ -6963,7 +7389,7 @@ async function startMcpServer() {
6963
7389
  server.tool(
6964
7390
  "get_thread",
6965
7391
  "Get a compact thread summary by id/ref",
6966
- { ref: z2.string() },
7392
+ { ref: z3.string() },
6967
7393
  async ({ ref }) => {
6968
7394
  const t = orch.getThread(ref);
6969
7395
  if (!t) {
@@ -6992,14 +7418,14 @@ async function startMcpServer() {
6992
7418
  "present_artifact",
6993
7419
  "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.",
6994
7420
  {
6995
- title: z2.string().describe("Short title shown in the artifact pane header"),
6996
- type: z2.enum(["html", "svg", "markdown", "react"]).describe(
7421
+ title: z3.string().describe("Short title shown in the artifact pane header"),
7422
+ type: z3.enum(["html", "svg", "markdown", "react"]).describe(
6997
7423
  "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
6998
7424
  ),
6999
- content: z2.string().describe(
7425
+ content: z3.string().describe(
7000
7426
  "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
7001
7427
  ),
7002
- artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
7428
+ artifact_id: z3.string().optional().describe("Stable id when updating the same artifact across turns")
7003
7429
  },
7004
7430
  async ({ title, type, artifact_id }) => {
7005
7431
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7017,15 +7443,15 @@ async function startMcpServer() {
7017
7443
  "ask_user",
7018
7444
  "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.",
7019
7445
  {
7020
- questions: z2.array(
7021
- z2.object({
7022
- question: z2.string().describe("Full question text ending with ?"),
7023
- header: z2.string().max(24).optional().describe("Short label shown above the question"),
7024
- multiSelect: z2.boolean().optional().describe("Allow selecting multiple options"),
7025
- options: z2.array(
7026
- z2.object({
7027
- label: z2.string(),
7028
- description: z2.string().optional().describe("What this option means / when to choose it (strongly preferred)")
7446
+ questions: z3.array(
7447
+ z3.object({
7448
+ question: z3.string().describe("Full question text ending with ?"),
7449
+ header: z3.string().max(24).optional().describe("Short label shown above the question"),
7450
+ multiSelect: z3.boolean().optional().describe("Allow selecting multiple options"),
7451
+ options: z3.array(
7452
+ z3.object({
7453
+ label: z3.string(),
7454
+ description: z3.string().optional().describe("What this option means / when to choose it (strongly preferred)")
7029
7455
  })
7030
7456
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
7031
7457
  })
@@ -7044,9 +7470,9 @@ async function startMcpServer() {
7044
7470
  "present_plan",
7045
7471
  "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.",
7046
7472
  {
7047
- title: z2.string().optional().describe("Short plan title (defaults to Plan)"),
7048
- content: z2.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
7049
- thread_id: z2.string().optional().describe("Sideboard thread id when cwd is not the worktree")
7473
+ title: z3.string().optional().describe("Short plan title (defaults to Plan)"),
7474
+ content: z3.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
7475
+ thread_id: z3.string().optional().describe("Sideboard thread id when cwd is not the worktree")
7050
7476
  },
7051
7477
  async ({ title, content, thread_id }) => {
7052
7478
  const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
@@ -7069,15 +7495,15 @@ async function startMcpServer() {
7069
7495
  "present_schema",
7070
7496
  "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.",
7071
7497
  {
7072
- title: z2.string().describe("Pane title"),
7073
- mode: z2.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
7074
- datasource: z2.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
7075
- resource_id: z2.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
7076
- record_id: z2.string().optional().describe("Record id when opening form mode"),
7077
- resource: z2.record(z2.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
7078
- record: z2.record(z2.unknown()).optional().describe("Inline record { id, data, published_at? }"),
7079
- records: z2.array(z2.record(z2.unknown())).optional().describe("Inline records for table mode"),
7080
- pane_id: z2.string().optional().describe("Stable pane id across updates")
7498
+ title: z3.string().describe("Pane title"),
7499
+ mode: z3.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
7500
+ datasource: z3.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
7501
+ resource_id: z3.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
7502
+ record_id: z3.string().optional().describe("Record id when opening form mode"),
7503
+ resource: z3.record(z3.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
7504
+ record: z3.record(z3.unknown()).optional().describe("Inline record { id, data, published_at? }"),
7505
+ records: z3.array(z3.record(z3.unknown())).optional().describe("Inline records for table mode"),
7506
+ pane_id: z3.string().optional().describe("Stable pane id across updates")
7081
7507
  },
7082
7508
  async (args) => {
7083
7509
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7098,10 +7524,10 @@ async function startMcpServer() {
7098
7524
  "present_files",
7099
7525
  "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.",
7100
7526
  {
7101
- title: z2.string().optional().describe("Pane title (default: Files)"),
7102
- datasource: z2.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
7103
- path: z2.string().optional().describe("Initial folder path (e.g. public)"),
7104
- pane_id: z2.string().optional().describe("Stable pane id across updates")
7527
+ title: z3.string().optional().describe("Pane title (default: Files)"),
7528
+ datasource: z3.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
7529
+ path: z3.string().optional().describe("Initial folder path (e.g. public)"),
7530
+ pane_id: z3.string().optional().describe("Stable pane id across updates")
7105
7531
  },
7106
7532
  async (args) => {
7107
7533
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7117,19 +7543,21 @@ async function startMcpServer() {
7117
7543
  }
7118
7544
  );
7119
7545
  registerSlackTools(server);
7546
+ registerLinearTools(server);
7120
7547
  if (sideboardMcpProfile() !== "worktree") {
7121
- const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-BEJIYPJ7.js");
7122
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-P42Z3VOB.js");
7548
+ const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-OHAUWOA6.js");
7549
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-NILNWNNQ.js");
7123
7550
  const accountDefaults = resolveThreadDefaults();
7124
7551
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
7125
7552
  server.tool(
7126
7553
  "set_caffeinate",
7127
- "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.",
7554
+ "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.",
7128
7555
  {
7129
- enabled: z2.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
7556
+ enabled: z3.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
7130
7557
  },
7131
7558
  async ({ enabled }) => {
7132
- const state = setCaffeinateHold(enabled);
7559
+ const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
7560
+ const state = setCaffeinateHold(enabled, { threadId });
7133
7561
  if (enabled && !state.held) {
7134
7562
  return {
7135
7563
  content: [
@@ -7152,7 +7580,7 @@ async function startMcpServer() {
7152
7580
  text: JSON.stringify({
7153
7581
  ...getCaffeinateHold(),
7154
7582
  ok: true,
7155
- 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)."
7583
+ 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)."
7156
7584
  })
7157
7585
  }
7158
7586
  ]
@@ -7163,15 +7591,15 @@ async function startMcpServer() {
7163
7591
  "create_thread",
7164
7592
  `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.`,
7165
7593
  {
7166
- sourceType: z2.enum(["branch", "pr", "ticket"]),
7167
- sourceRef: z2.string(),
7168
- agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
7169
- model: z2.string().nullable().optional().describe(
7594
+ sourceType: z3.enum(["branch", "pr", "ticket"]),
7595
+ sourceRef: z3.string(),
7596
+ agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
7597
+ model: z3.string().nullable().optional().describe(
7170
7598
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
7171
7599
  ),
7172
- repoPath: z2.string(),
7173
- title: z2.string().optional(),
7174
- parentThreadId: z2.string().optional().describe(
7600
+ repoPath: z3.string(),
7601
+ title: z3.string().optional(),
7602
+ parentThreadId: z3.string().optional().describe(
7175
7603
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
7176
7604
  )
7177
7605
  },
@@ -7278,9 +7706,9 @@ async function startMcpServer() {
7278
7706
  "send_to_thread",
7279
7707
  "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.",
7280
7708
  {
7281
- ref: z2.string(),
7282
- prompt: z2.string(),
7283
- force_stop: z2.boolean().optional()
7709
+ ref: z3.string(),
7710
+ prompt: z3.string(),
7711
+ force_stop: z3.boolean().optional()
7284
7712
  },
7285
7713
  async ({ ref, prompt, force_stop }) => {
7286
7714
  if (force_stop) {
@@ -7309,8 +7737,8 @@ async function startMcpServer() {
7309
7737
  "wait_for_turn",
7310
7738
  "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
7311
7739
  {
7312
- ref: z2.string(),
7313
- timeoutMs: z2.number().optional()
7740
+ ref: z3.string(),
7741
+ timeoutMs: z3.number().optional()
7314
7742
  },
7315
7743
  async ({ ref, timeoutMs }) => {
7316
7744
  const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
@@ -7332,7 +7760,7 @@ async function startMcpServer() {
7332
7760
  server.tool(
7333
7761
  "get_turn_result",
7334
7762
  "Final assistant message only (not full transcript)",
7335
- { ref: z2.string() },
7763
+ { ref: z3.string() },
7336
7764
  async ({ ref }) => {
7337
7765
  const result = orch.getTurnResult(ref);
7338
7766
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
@@ -7342,8 +7770,8 @@ async function startMcpServer() {
7342
7770
  "stop_thread",
7343
7771
  "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.",
7344
7772
  {
7345
- ref: z2.string(),
7346
- force: z2.boolean().optional()
7773
+ ref: z3.string(),
7774
+ force: z3.boolean().optional()
7347
7775
  },
7348
7776
  async ({ ref, force }) => {
7349
7777
  const t = orch.getThread(ref);
@@ -7373,7 +7801,7 @@ async function startMcpServer() {
7373
7801
  server.tool(
7374
7802
  "archive_thread",
7375
7803
  "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).",
7376
- { ref: z2.string() },
7804
+ { ref: z3.string() },
7377
7805
  async ({ ref }) => {
7378
7806
  const t = orch.getThread(ref);
7379
7807
  if (!t) {
@@ -7406,7 +7834,7 @@ async function startMcpServer() {
7406
7834
  server.tool(
7407
7835
  "restore_thread",
7408
7836
  "Restore an archived thread (recreates worktree from branch when needed)",
7409
- { ref: z2.string() },
7837
+ { ref: z3.string() },
7410
7838
  async ({ ref }) => {
7411
7839
  try {
7412
7840
  const restored = await orch.restore(ref);
@@ -7432,8 +7860,8 @@ async function startMcpServer() {
7432
7860
  "get_diff",
7433
7861
  "Compact diff summary (capped hunks, paginated)",
7434
7862
  {
7435
- ref: z2.string(),
7436
- maxFiles: z2.number().optional()
7863
+ ref: z3.string(),
7864
+ maxFiles: z3.number().optional()
7437
7865
  },
7438
7866
  async ({ ref, maxFiles }) => {
7439
7867
  const summary = await orch.diffSummary(ref);
@@ -7453,7 +7881,7 @@ async function startMcpServer() {
7453
7881
  server.tool(
7454
7882
  "request_review",
7455
7883
  '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.',
7456
- { ref: z2.string().describe("Worktree thread id/ref to review") },
7884
+ { ref: z3.string().describe("Worktree thread id/ref to review") },
7457
7885
  async ({ ref }) => {
7458
7886
  try {
7459
7887
  const tab = await orch.requestReview(ref);
@@ -7480,10 +7908,10 @@ async function startMcpServer() {
7480
7908
  );
7481
7909
  server.tool(
7482
7910
  "ask_git",
7483
- "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.",
7911
+ "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` if SSH is missing). When dirty, queues the worktree agent to commit; then wait_for_turn. Pass a worktree thread ref (not the orchestrator). Do not run git or gh from the orchestration cwd.",
7484
7912
  {
7485
- ref: z2.string().describe("Worktree thread id/ref"),
7486
- action: z2.enum(AGENT_GIT_ACTIONS).describe(
7913
+ ref: z3.string().describe("Worktree thread id/ref"),
7914
+ action: z3.enum(AGENT_GIT_ACTIONS).describe(
7487
7915
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
7488
7916
  )
7489
7917
  },
@@ -7510,7 +7938,7 @@ async function startMcpServer() {
7510
7938
  }
7511
7939
  }
7512
7940
  );
7513
- const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7941
+ const agentEnum = z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7514
7942
  server.tool(
7515
7943
  "list_models",
7516
7944
  "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.",
@@ -7533,11 +7961,11 @@ async function startMcpServer() {
7533
7961
  "fork_worktree",
7534
7962
  "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.",
7535
7963
  {
7536
- ref: z2.string().describe("Worktree thread id/ref to fork"),
7537
- through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7964
+ ref: z3.string().describe("Worktree thread id/ref to fork"),
7965
+ through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7538
7966
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
7539
- model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7540
- title: z2.string().optional()
7967
+ model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7968
+ title: z3.string().optional()
7541
7969
  },
7542
7970
  async ({ ref, through_index, agent, model, title }) => {
7543
7971
  try {
@@ -7578,11 +8006,11 @@ async function startMcpServer() {
7578
8006
  "fork_chat",
7579
8007
  "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.",
7580
8008
  {
7581
- ref: z2.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
7582
- through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
8009
+ ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
8010
+ through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7583
8011
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
7584
- model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7585
- title: z2.string().optional()
8012
+ model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
8013
+ title: z3.string().optional()
7586
8014
  },
7587
8015
  async ({ ref, through_index, agent, model, title }) => {
7588
8016
  try {
@@ -7628,8 +8056,8 @@ async function startMcpServer() {
7628
8056
  "run_dev_script",
7629
8057
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
7630
8058
  {
7631
- ref: z2.string(),
7632
- name: z2.string().optional()
8059
+ ref: z3.string(),
8060
+ name: z3.string().optional()
7633
8061
  },
7634
8062
  async ({ ref, name }) => {
7635
8063
  const result = await orch.startDev(ref, name);
@@ -7651,7 +8079,7 @@ async function startMcpServer() {
7651
8079
  server.tool(
7652
8080
  "list_run_scripts",
7653
8081
  "List named run scripts available for a thread",
7654
- { ref: z2.string() },
8082
+ { ref: z3.string() },
7655
8083
  async ({ ref }) => {
7656
8084
  const scripts = orch.listThreadRunScripts(ref);
7657
8085
  const active = orch.getActiveRuns(ref);
@@ -7669,8 +8097,8 @@ async function startMcpServer() {
7669
8097
  "stop_dev_script",
7670
8098
  "Stop a running script for a thread (all scripts if name omitted)",
7671
8099
  {
7672
- ref: z2.string(),
7673
- name: z2.string().optional()
8100
+ ref: z3.string(),
8101
+ name: z3.string().optional()
7674
8102
  },
7675
8103
  async ({ ref, name }) => {
7676
8104
  orch.stopDev(ref, name);
@@ -7680,7 +8108,7 @@ async function startMcpServer() {
7680
8108
  server.tool(
7681
8109
  "run_setup",
7682
8110
  "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
7683
- { ref: z2.string() },
8111
+ { ref: z3.string() },
7684
8112
  async ({ ref }) => {
7685
8113
  const result = await orch.runSetup(ref);
7686
8114
  return {
@@ -7691,7 +8119,7 @@ async function startMcpServer() {
7691
8119
  server.tool(
7692
8120
  "add_workspace",
7693
8121
  "Register a git repo as a Sideboard workspace",
7694
- { repoPath: z2.string() },
8122
+ { repoPath: z3.string() },
7695
8123
  async ({ repoPath }) => {
7696
8124
  const ws = await orch.addWorkspace(repoPath);
7697
8125
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -7700,7 +8128,7 @@ async function startMcpServer() {
7700
8128
  server.tool(
7701
8129
  "remove_workspace",
7702
8130
  "Unregister a Sideboard workspace (does not archive threads)",
7703
- { repoPath: z2.string() },
8131
+ { repoPath: z3.string() },
7704
8132
  async ({ repoPath }) => {
7705
8133
  orch.removeWorkspace(repoPath);
7706
8134
  return { content: [{ type: "text", text: "ok" }] };
@@ -7710,12 +8138,12 @@ async function startMcpServer() {
7710
8138
  "fanout",
7711
8139
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
7712
8140
  {
7713
- prompt: z2.string(),
7714
- agents: z2.array(z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
7715
- repoPath: z2.string(),
7716
- sourceType: z2.enum(["branch", "pr", "ticket"]).optional(),
7717
- sourceRef: z2.string().optional(),
7718
- title: z2.string().optional()
8141
+ prompt: z3.string(),
8142
+ agents: z3.array(z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
8143
+ repoPath: z3.string(),
8144
+ sourceType: z3.enum(["branch", "pr", "ticket"]).optional(),
8145
+ sourceRef: z3.string().optional(),
8146
+ title: z3.string().optional()
7719
8147
  },
7720
8148
  async (args) => {
7721
8149
  const threads = await orch.bestOfN(args);
@@ -7742,8 +8170,8 @@ async function startMcpServer() {
7742
8170
  "list_branches",
7743
8171
  "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).",
7744
8172
  {
7745
- repoPath: z2.string(),
7746
- unmergedOnly: z2.boolean().optional()
8173
+ repoPath: z3.string(),
8174
+ unmergedOnly: z3.boolean().optional()
7747
8175
  },
7748
8176
  async ({ repoPath, unmergedOnly }) => {
7749
8177
  const root = await resolveRepoRoot(repoPath);
@@ -7763,7 +8191,7 @@ async function startMcpServer() {
7763
8191
  server.tool(
7764
8192
  "list_prs",
7765
8193
  "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.",
7766
- { repoPath: z2.string() },
8194
+ { repoPath: z3.string() },
7767
8195
  async ({ repoPath }) => {
7768
8196
  const root = await resolveRepoRoot(repoPath);
7769
8197
  const prs = await listPrs(root);
@@ -7782,7 +8210,7 @@ async function startMcpServer() {
7782
8210
  server.tool(
7783
8211
  "get_pr_stack",
7784
8212
  "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.",
7785
- { ref: z2.string() },
8213
+ { ref: z3.string() },
7786
8214
  async ({ ref }) => {
7787
8215
  const stack = await orch.getPrStack(ref);
7788
8216
  return {
@@ -7794,8 +8222,8 @@ async function startMcpServer() {
7794
8222
  "open_pr_stack_layers",
7795
8223
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
7796
8224
  {
7797
- ref: z2.string(),
7798
- layer: z2.number().int().positive().optional()
8225
+ ref: z3.string(),
8226
+ layer: z3.number().int().positive().optional()
7799
8227
  },
7800
8228
  async ({ ref, layer }) => {
7801
8229
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -7829,9 +8257,9 @@ async function startMcpServer() {
7829
8257
  "add_stack_layer",
7830
8258
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
7831
8259
  {
7832
- ref: z2.string(),
7833
- branchName: z2.string(),
7834
- title: z2.string().optional()
8260
+ ref: z3.string(),
8261
+ branchName: z3.string(),
8262
+ title: z3.string().optional()
7835
8263
  },
7836
8264
  async ({ ref, branchName, title }) => {
7837
8265
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -7860,11 +8288,11 @@ async function startMcpServer() {
7860
8288
  "create_pr_stack",
7861
8289
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
7862
8290
  {
7863
- repoPath: z2.string(),
7864
- branches: z2.array(z2.string()).min(1),
7865
- agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
7866
- base: z2.string().optional(),
7867
- title: z2.string().optional()
8291
+ repoPath: z3.string(),
8292
+ branches: z3.array(z3.string()).min(1),
8293
+ agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
8294
+ base: z3.string().optional(),
8295
+ title: z3.string().optional()
7868
8296
  },
7869
8297
  async (args) => {
7870
8298
  const result = await orch.createPrStack({
@@ -7902,7 +8330,7 @@ async function startMcpServer() {
7902
8330
  server.tool(
7903
8331
  "list_issues",
7904
8332
  "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.",
7905
- { repoPath: z2.string() },
8333
+ { repoPath: z3.string() },
7906
8334
  async ({ repoPath }) => {
7907
8335
  const root = await resolveRepoRoot(repoPath);
7908
8336
  const result = await listIssues(root);
@@ -7913,8 +8341,8 @@ async function startMcpServer() {
7913
8341
  "list_linear_issues",
7914
8342
  "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
7915
8343
  {
7916
- agent: z2.enum(["claude", "codex", "opencode"]),
7917
- repoPath: z2.string()
8344
+ agent: z3.enum(["claude", "codex", "opencode"]),
8345
+ repoPath: z3.string()
7918
8346
  },
7919
8347
  async ({ agent, repoPath }) => {
7920
8348
  const issues = await listLinearIssues(agent, repoPath);