@sideboard-ai/core 0.1.67 → 0.1.71

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 (44) hide show
  1. package/dist/{agents-OTBZVTZO.js → agents-GB7XL2LI.js} +8 -7
  2. package/dist/{agents-TS3EI2YI.js → agents-TCAK3MXN.js} +8 -7
  3. package/dist/{app-settings-ONKMFSEJ.js → app-settings-3TLA2EJ7.js} +16 -1
  4. package/dist/{app-settings-GJVZGU3M.js → app-settings-P42Z3VOB.js} +16 -1
  5. package/dist/caffeinate-hold-BEJIYPJ7.js +107 -0
  6. package/dist/caffeinate-hold-KLC6SABD.js +14 -0
  7. package/dist/{chunk-WAFIF22N.js → chunk-3CY6WJ7O.js} +2 -2
  8. package/dist/{chunk-7ZOQY4A7.js → chunk-57ROTAFD.js} +62 -3
  9. package/dist/{chunk-57WWNMFP.js → chunk-7NCIHRAU.js} +22 -4
  10. package/dist/{chunk-FGDJOJ2L.js → chunk-AE5VBOFE.js} +1 -1
  11. package/dist/{chunk-DB3I75FI.js → chunk-BTL7EMGT.js} +4 -4
  12. package/dist/{chunk-4UIYN56C.js → chunk-D4DPQ552.js} +1 -1
  13. package/dist/{chunk-JWZNK3CQ.js → chunk-DGPUAZA4.js} +62 -3
  14. package/dist/{chunk-ZRFAZVGL.js → chunk-H2IXNFQ2.js} +22 -4
  15. package/dist/chunk-JAWEDVVA.js +106 -0
  16. package/dist/chunk-JT7R45JB.js +20 -0
  17. package/dist/{chunk-4BUCGNZS.js → chunk-MF2YMEGD.js} +83 -29
  18. package/dist/{chunk-K7LM4CLD.js → chunk-NG7S3OPX.js} +12 -6
  19. package/dist/chunk-NSTQ6QKD.js +23 -0
  20. package/dist/{chunk-LW5CMQGZ.js → chunk-OE7YBB5X.js} +83 -29
  21. package/dist/{chunk-BPOTVI5J.js → chunk-PTJJIQK7.js} +472 -18
  22. package/dist/{chunk-B4PRRKMA.js → chunk-S25IHL5H.js} +12 -6
  23. package/dist/{chunk-5HTTCYWA.js → chunk-UH57WVFP.js} +2 -2
  24. package/dist/{chunk-M5V4QIWF.js → chunk-VROPG6QF.js} +1 -1
  25. package/dist/{chunk-MHR4OGZ4.js → chunk-W7YOTDVO.js} +493 -18
  26. package/dist/{connected-teams-RUNMOCG5.js → connected-teams-35EIRJCP.js} +2 -2
  27. package/dist/{connected-teams-OMDHTL6A.js → connected-teams-ZRS53OAT.js} +2 -2
  28. package/dist/{coordinator-prompt-6HMTOX4G.js → coordinator-prompt-2J4PIMUF.js} +7 -4
  29. package/dist/{coordinator-prompt-3BSMKZL4.js → coordinator-prompt-QYYG2IBB.js} +7 -4
  30. package/dist/{global-workspace-YZ2NJVLL.js → global-workspace-KHIFQUUM.js} +12 -5
  31. package/dist/{global-workspace-QS7A23CB.js → global-workspace-SVRYNJZA.js} +12 -5
  32. package/dist/index.cjs +4805 -1262
  33. package/dist/index.d.cts +570 -22
  34. package/dist/index.d.ts +570 -22
  35. package/dist/index.js +3084 -340
  36. package/dist/mcp/run-stdio.cjs +2339 -617
  37. package/dist/mcp/run-stdio.js +1289 -290
  38. package/dist/{run-Y2MI3EVV.js → run-A4RHY7HH.js} +1 -1
  39. package/dist/{run-JLD2Y52J.js → run-V2WZUMJA.js} +1 -1
  40. package/dist/{workspaces-EBFW7I7S.js → workspaces-AHFTHVBT.js} +7 -6
  41. package/dist/{workspaces-AIUYUXB7.js → workspaces-SVQIEVIZ.js} +7 -6
  42. package/dist/{worktree-WMDJW2BL.js → worktree-KWXHMKRN.js} +2 -2
  43. package/dist/{worktree-7NBPIVFZ.js → worktree-VOCH3WS3.js} +2 -2
  44. package/package.json +3 -1
@@ -16,14 +16,14 @@ import {
16
16
  resolveQuotaFallbackAgent,
17
17
  sideboardMcpProfile,
18
18
  summarizeTurnStderr
19
- } from "../chunk-K7LM4CLD.js";
20
- import "../chunk-M5V4QIWF.js";
19
+ } from "../chunk-NG7S3OPX.js";
20
+ import "../chunk-VROPG6QF.js";
21
21
  import {
22
22
  addWorkspace,
23
23
  ensureWorkspace,
24
24
  removeWorkspace,
25
25
  syncWorkspacesFromThreads
26
- } from "../chunk-5HTTCYWA.js";
26
+ } from "../chunk-UH57WVFP.js";
27
27
  import {
28
28
  extractPresentedPlan,
29
29
  readPlanFile,
@@ -38,13 +38,14 @@ import {
38
38
  isGlobalThread,
39
39
  isOrchestratorThread,
40
40
  orchestratorSessionPoisonedByBuiltins
41
- } from "../chunk-JWZNK3CQ.js";
41
+ } from "../chunk-DGPUAZA4.js";
42
42
  import {
43
+ SLACK_REPLY_FORMATTING,
43
44
  coordinatorSystemPrompt,
44
45
  coordinatorTurnReminder,
45
46
  enrichWorkspacesWithGithub,
46
47
  ensureGlobalCoordinatorCwd
47
- } from "../chunk-57WWNMFP.js";
48
+ } from "../chunk-7NCIHRAU.js";
48
49
  import {
49
50
  addPrStackLayer,
50
51
  allocateTeamName,
@@ -81,7 +82,7 @@ import {
81
82
  takenSlugsFromThread,
82
83
  threadDisplayLabel,
83
84
  worktreeNameFromPath
84
- } from "../chunk-LW5CMQGZ.js";
85
+ } from "../chunk-OE7YBB5X.js";
85
86
  import {
86
87
  ATTACHMENTS_DIR,
87
88
  LEGACY_ATTACHMENTS_DIR,
@@ -105,20 +106,29 @@ import {
105
106
  gh,
106
107
  git,
107
108
  run
108
- } from "../chunk-FGDJOJ2L.js";
109
+ } from "../chunk-AE5VBOFE.js";
109
110
  import {
110
111
  childEnvWithAppSettings,
111
- getLinearApiKey,
112
+ disconnectLinearConnection,
112
113
  isLinearConnected,
114
+ isSecureFileEncrypted,
113
115
  loadAppSettings,
114
116
  orchestrationQuotaFallbackAgent,
115
117
  orchestrationQuotaOnLimit,
118
+ readSecureJson,
116
119
  resolveEffectiveIssueSource,
117
120
  resolveNewThreadOptions,
118
- updateAdvancedSettings
119
- } from "../chunk-BPOTVI5J.js";
121
+ resolveVaultKey,
122
+ saveLinearOAuth,
123
+ updateAdvancedSettings,
124
+ writeSecureJson
125
+ } from "../chunk-PTJJIQK7.js";
126
+ import {
127
+ writePrivateFile
128
+ } from "../chunk-NSTQ6QKD.js";
120
129
  import "../chunk-KGZBYWZ3.js";
121
130
  import {
131
+ appDataDir,
122
132
  loadRepoSettings,
123
133
  loadWorkspaceSettings,
124
134
  sideboardReposDir,
@@ -130,7 +140,7 @@ import {
130
140
  // src/mcp/server.ts
131
141
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
132
142
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
133
- import { z } from "zod";
143
+ import { z as z2 } from "zod";
134
144
  import { basename as basename4 } from "path";
135
145
 
136
146
  // src/orchestrator/orchestrator.ts
@@ -215,9 +225,9 @@ function toolFilePath(input) {
215
225
  if (!input) return void 0;
216
226
  return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
217
227
  }
218
- function countLines(text) {
219
- if (!text) return 0;
220
- return text.split("\n").length;
228
+ function countLines(text2) {
229
+ if (!text2) return 0;
230
+ return text2.split("\n").length;
221
231
  }
222
232
  function diffFromInput(input) {
223
233
  if (!input) return {};
@@ -344,9 +354,9 @@ function applyAgentEvent(parts, event) {
344
354
  function partsToAssistantText(parts) {
345
355
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
346
356
  }
347
- function stripBrightsyNdjsonNoise(text) {
348
- if (!text || !text.includes('"type"')) return text;
349
- let out = text;
357
+ function stripBrightsyNdjsonNoise(text2) {
358
+ if (!text2 || !text2.includes('"type"')) return text2;
359
+ let out = text2;
350
360
  out = out.replace(
351
361
  /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
352
362
  ""
@@ -391,9 +401,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
391
401
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
392
402
  );
393
403
  }
394
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-YZ2NJVLL.js");
404
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-KHIFQUUM.js");
395
405
  if (isGlobalThread2(thread)) {
396
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-6HMTOX4G.js");
406
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-2J4PIMUF.js");
397
407
  ensureGlobalCoordinatorCwd2(
398
408
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
399
409
  );
@@ -480,8 +490,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
480
490
  onEvent({ type: "exit", data: exitCode });
481
491
  const finalized = finalizeParts(parts);
482
492
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
483
- const text = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
484
- return { exitCode, sessionId, assistantText: text, parts: finalized, usage };
493
+ const text2 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
494
+ return { exitCode, sessionId, assistantText: text2, parts: finalized, usage };
485
495
  });
486
496
  return {
487
497
  pid: child.pid,
@@ -1209,7 +1219,7 @@ async function createThread(input, _onSetupLine) {
1209
1219
  return readThread(thread.id) ?? thread;
1210
1220
  }
1211
1221
  async function listLinearIssues(agent, repoPath) {
1212
- const { getAdapter: getAdapter2 } = await import("../agents-OTBZVTZO.js");
1222
+ const { getAdapter: getAdapter2 } = await import("../agents-GB7XL2LI.js");
1213
1223
  await requireAgent(agent, { requireLinear: true });
1214
1224
  const adapter = getAdapter2(agent);
1215
1225
  if (!adapter.listLinearIssues) {
@@ -1265,9 +1275,9 @@ async function tryClaudeSummary(transcript, opts) {
1265
1275
  { cwd: opts?.cwd, reject: false }
1266
1276
  );
1267
1277
  if (exitCode !== 0) return null;
1268
- const text = stdout.trim();
1269
- if (text.length < 40) return null;
1270
- return text;
1278
+ const text2 = stdout.trim();
1279
+ if (text2.length < 40) return null;
1280
+ return text2;
1271
1281
  } catch {
1272
1282
  return null;
1273
1283
  }
@@ -1316,9 +1326,9 @@ function extractiveSummary(transcript) {
1316
1326
  );
1317
1327
  return parts.join("\n");
1318
1328
  }
1319
- function clipSummary(text) {
1320
- if (text.length <= MAX_SUMMARY_CHARS) return text;
1321
- return `${text.slice(0, MAX_SUMMARY_CHARS)}
1329
+ function clipSummary(text2) {
1330
+ if (text2.length <= MAX_SUMMARY_CHARS) return text2;
1331
+ return `${text2.slice(0, MAX_SUMMARY_CHARS)}
1322
1332
 
1323
1333
  [\u2026summary truncated\u2026]`;
1324
1334
  }
@@ -1931,8 +1941,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
1931
1941
  );
1932
1942
  const recent = from.messages.slice(-8).map((m) => {
1933
1943
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
1934
- const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
1935
- return text ? `- ${role}: ${text}` : null;
1944
+ const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
1945
+ return text2 ? `- ${role}: ${text2}` : null;
1936
1946
  }).filter(Boolean);
1937
1947
  const body = [
1938
1948
  `# Orchestration handoff`,
@@ -2028,13 +2038,13 @@ function resolveConductorCursorAgentId(workspacePath) {
2028
2038
  for (const hash of hashes) {
2029
2039
  const agentsFile = join6(CURSOR_SDK_STORE, hash, "agents.ndjson");
2030
2040
  if (!existsSync7(agentsFile)) continue;
2031
- let text;
2041
+ let text2;
2032
2042
  try {
2033
- text = readFileSync4(agentsFile, "utf8");
2043
+ text2 = readFileSync4(agentsFile, "utf8");
2034
2044
  } catch {
2035
2045
  continue;
2036
2046
  }
2037
- for (const line of text.split("\n")) {
2047
+ for (const line of text2.split("\n")) {
2038
2048
  const trimmed = line.trim();
2039
2049
  if (!trimmed) continue;
2040
2050
  try {
@@ -2058,7 +2068,7 @@ async function adoptThread(input) {
2058
2068
  throw new Error(`Worktree not found: ${input.worktreePath}`);
2059
2069
  }
2060
2070
  const repoPath = await resolveRepoRoot(input.worktreePath);
2061
- const { stdout: branch } = await import("../run-Y2MI3EVV.js").then(
2071
+ const { stdout: branch } = await import("../run-A4RHY7HH.js").then(
2062
2072
  ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], input.worktreePath)
2063
2073
  );
2064
2074
  const thread = createEmptyThread({
@@ -2073,7 +2083,7 @@ async function adoptThread(input) {
2073
2083
  messages: input.messages ?? []
2074
2084
  });
2075
2085
  writeThread(thread);
2076
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-AIUYUXB7.js");
2086
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-SVQIEVIZ.js");
2077
2087
  await ensureWorkspace2(repoPath);
2078
2088
  return thread;
2079
2089
  }
@@ -2630,24 +2640,25 @@ function capPatch(patch, maxHunk) {
2630
2640
 
2631
2641
  \u2026 truncated (${patch.length - maxHunk} more chars)`;
2632
2642
  }
2643
+ var mergeBaseCache = /* @__PURE__ */ new Map();
2644
+ var MERGE_BASE_TTL_MS = 45e3;
2633
2645
  async function resolveMergeBase(worktreePath, base, repoPath) {
2634
2646
  const baseRef = await resolveDiffBaseRef(worktreePath, base, repoPath);
2647
+ const cacheKey = `${worktreePath}\0${baseRef}`;
2648
+ const hit = mergeBaseCache.get(cacheKey);
2649
+ if (hit && Date.now() - hit.at < MERGE_BASE_TTL_MS) return hit.value;
2635
2650
  const { stdout } = await git(["merge-base", baseRef, "HEAD"], worktreePath, {
2636
2651
  reject: false
2637
2652
  });
2638
- const mb = stdout.trim();
2639
- return mb || baseRef;
2653
+ const mb = stdout.trim() || baseRef;
2654
+ mergeBaseCache.set(cacheKey, { at: Date.now(), value: mb });
2655
+ return mb;
2640
2656
  }
2641
2657
  async function countUnpushedCommits(worktreePath) {
2642
2658
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
2643
2659
  reject: false
2644
2660
  });
2645
2661
  const branch = head.stdout.trim();
2646
- if (branch && branch !== "HEAD") {
2647
- await git(["fetch", "origin", branch, "--quiet"], worktreePath, {
2648
- reject: false
2649
- });
2650
- }
2651
2662
  const upstream = await git(
2652
2663
  ["rev-list", "--count", "@{upstream}..HEAD"],
2653
2664
  worktreePath,
@@ -2680,11 +2691,37 @@ function splitCombinedDiff(stdout) {
2680
2691
  }
2681
2692
  return map;
2682
2693
  }
2694
+ function syntheticAddPatch(path, content, maxHunk) {
2695
+ const lines = content.split("\n");
2696
+ const body = lines.map((l) => `+${l}`).join("\n");
2697
+ const header = `diff --git a/${path} b/${path}
2698
+ new file mode 100644
2699
+ --- /dev/null
2700
+ +++ b/${path}
2701
+ @@ -0,0 +1,${lines.length} @@
2702
+ `;
2703
+ return {
2704
+ path,
2705
+ status: "A",
2706
+ patch: capPatch(`${header}${body}`, maxHunk),
2707
+ additions: lines.length,
2708
+ deletions: 0
2709
+ };
2710
+ }
2683
2711
  async function untrackedPatch(worktreePath, path, maxHunk) {
2712
+ const abs = join7(worktreePath, path);
2713
+ try {
2714
+ const st = statSync2(abs);
2715
+ if (st.isFile() && st.size > maxHunk) {
2716
+ const buf = readFileSync5(abs).subarray(0, maxHunk);
2717
+ return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
2718
+ }
2719
+ } catch {
2720
+ }
2684
2721
  const { stdout: patch } = await git(
2685
- ["diff", "--no-index", "--", "/dev/null", path],
2722
+ ["diff", "--no-ext-diff", "--no-color", "--no-index", "--", "/dev/null", path],
2686
2723
  worktreePath,
2687
- { reject: false }
2724
+ { reject: false, timeoutMs: 4e3 }
2688
2725
  );
2689
2726
  const normalized = patch.replace(/^diff --git a\/.+? b\/.+?$/m, `diff --git a/${path} b/${path}`).replace(/^--- .*$/m, "--- /dev/null").replace(/^\+\+\+ .*$/m, `+++ b/${path}`);
2690
2727
  const addLines = normalized.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
@@ -2696,22 +2733,97 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
2696
2733
  deletions: 0
2697
2734
  };
2698
2735
  }
2699
- async function listUntrackedPaths(worktreePath) {
2700
- const { stdout } = await git(
2701
- ["ls-files", "-z", "--others", "--exclude-standard"],
2702
- worktreePath,
2703
- { reject: false }
2704
- );
2736
+ async function listUntrackedPaths(worktreePath, path) {
2737
+ const args = ["ls-files", "-z", "--others", "--exclude-standard"];
2738
+ if (path) args.push("--", path);
2739
+ const { stdout } = await git(args, worktreePath, { reject: false });
2705
2740
  return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
2706
2741
  }
2707
- async function listUntrackedFiles(worktreePath, maxHunk) {
2708
- const paths = await listUntrackedPaths(worktreePath);
2709
- if (paths.length === 0) return [];
2710
- return Promise.all(paths.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
2742
+ async function listUntrackedFiles(worktreePath, maxHunk, paths) {
2743
+ const list = paths ?? await listUntrackedPaths(worktreePath);
2744
+ if (list.length === 0) return [];
2745
+ return Promise.all(list.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
2746
+ }
2747
+ function untrackedStubs(paths) {
2748
+ return paths.map((path) => ({
2749
+ path,
2750
+ status: "A",
2751
+ patch: "",
2752
+ additions: 0,
2753
+ deletions: 0
2754
+ }));
2755
+ }
2756
+ function pathspec(path) {
2757
+ return path ? ["--", path] : [];
2758
+ }
2759
+ function gitDiff(args, cwd, opts) {
2760
+ return git(["diff", "--no-ext-diff", "--no-color", ...args], cwd, {
2761
+ reject: false,
2762
+ timeoutMs: opts?.timeoutMs
2763
+ });
2764
+ }
2765
+ function fileFromPatch(path, patch, maxHunk) {
2766
+ let status = "M";
2767
+ if (/^new file mode /m.test(patch) || /^--- \/dev\/null/m.test(patch)) {
2768
+ status = "A";
2769
+ } else if (/^deleted file mode /m.test(patch) || /^\+\+\+ \/dev\/null/m.test(patch)) {
2770
+ status = "D";
2771
+ } else if (/^rename from /m.test(patch)) {
2772
+ status = "R";
2773
+ }
2774
+ const additions = patch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
2775
+ const deletions = patch.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
2776
+ return {
2777
+ path,
2778
+ status,
2779
+ patch: capPatch(patch, maxHunk),
2780
+ additions,
2781
+ deletions
2782
+ };
2783
+ }
2784
+ async function gitDiffParts(worktreePath, diffArgs, includePatches, path) {
2785
+ const extra = pathspec(path);
2786
+ if (includePatches) {
2787
+ const diff = await gitDiff([...diffArgs, ...extra], worktreePath, {
2788
+ timeoutMs: 4e3
2789
+ });
2790
+ return { nameStatus: "", numstat: "", combinedDiff: diff.stdout };
2791
+ }
2792
+ const [ns, num] = await Promise.all([
2793
+ gitDiff(["--name-status", ...diffArgs, ...extra], worktreePath),
2794
+ gitDiff(["--numstat", ...diffArgs, ...extra], worktreePath)
2795
+ ]);
2796
+ return {
2797
+ nameStatus: ns.stdout,
2798
+ numstat: num.stdout,
2799
+ combinedDiff: ""
2800
+ };
2801
+ }
2802
+ function formatStat(files) {
2803
+ if (files.length === 0) return "(no changes)";
2804
+ const additions = files.reduce((n2, f) => n2 + (f.additions ?? 0), 0);
2805
+ const deletions = files.reduce((n2, f) => n2 + (f.deletions ?? 0), 0);
2806
+ const n = files.length;
2807
+ return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
2808
+ }
2809
+ function emptyScopeStats() {
2810
+ const z3 = emptyStat();
2811
+ return {
2812
+ commits: z3,
2813
+ uncommitted: z3,
2814
+ staged: z3,
2815
+ unstaged: z3,
2816
+ last_turn: z3
2817
+ };
2711
2818
  }
2712
2819
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
2713
- const counts = parseNumstat(numstat);
2714
2820
  const patches = splitCombinedDiff(combinedDiff);
2821
+ if (!nameStatus.trim() && patches.size > 0) {
2822
+ return [...patches.entries()].map(
2823
+ ([path, patch]) => fileFromPatch(path, patch, maxHunk)
2824
+ );
2825
+ }
2826
+ const counts = parseNumstat(numstat);
2715
2827
  const files = [];
2716
2828
  for (const { status, path } of parseNameStatus(nameStatus)) {
2717
2829
  const n = counts.get(path);
@@ -2733,11 +2845,11 @@ async function computeScopeStats(worktreePath, mergeBase, lastTurnBase, untracke
2733
2845
  { stdout: unstagedNum },
2734
2846
  lastTurnNum
2735
2847
  ] = await Promise.all([
2736
- mergeBase ? git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" }),
2737
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
2738
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
2739
- git(["diff", "--numstat"], worktreePath, { reject: false }),
2740
- lastTurnBase ? git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" })
2848
+ mergeBase ? gitDiff(["--numstat", mergeBase], worktreePath) : Promise.resolve({ stdout: "" }),
2849
+ gitDiff(["--numstat", "HEAD"], worktreePath),
2850
+ gitDiff(["--numstat", "--cached"], worktreePath),
2851
+ gitDiff(["--numstat"], worktreePath),
2852
+ lastTurnBase ? gitDiff(["--numstat", lastTurnBase], worktreePath) : Promise.resolve({ stdout: "" })
2741
2853
  ]);
2742
2854
  return {
2743
2855
  commits: mergeBase ? statFromNumstat(commitsNum, untrackedCount) : emptyStat(),
@@ -2756,7 +2868,7 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
2756
2868
  if (head.exitCode !== 0) return [];
2757
2869
  let base = opts?.base;
2758
2870
  try {
2759
- base = base ?? await resolveDefaultBranch(repoPath);
2871
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
2760
2872
  } catch {
2761
2873
  base = "HEAD";
2762
2874
  }
@@ -2786,7 +2898,121 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
2786
2898
  }
2787
2899
  return commits;
2788
2900
  }
2901
+ var SHA_RE = /^[0-9a-f]{7,40}$/i;
2902
+ function emptyPathDiff(scope, base, hasLastTurnBase, commitSha = null) {
2903
+ return {
2904
+ scope,
2905
+ commitSha: scope === "commits" ? commitSha : null,
2906
+ base,
2907
+ files: [],
2908
+ stat: formatStat([]),
2909
+ dirty: false,
2910
+ unpushed: 0,
2911
+ hasLastTurnBase,
2912
+ commits: [],
2913
+ scopeStats: emptyScopeStats()
2914
+ };
2915
+ }
2916
+ async function getPathDiff(worktreePath, repoPath, opts) {
2917
+ const { path, scope, commitSha, lastTurnBase, hasLastTurnBase, includePatches, maxHunk } = opts;
2918
+ if (scope === "last_turn" && !lastTurnBase) {
2919
+ return emptyPathDiff(scope, "last turn", false);
2920
+ }
2921
+ let diffArgs;
2922
+ let labelBase;
2923
+ if (scope === "staged") {
2924
+ diffArgs = ["--cached"];
2925
+ labelBase = "staged";
2926
+ } else if (scope === "unstaged") {
2927
+ diffArgs = [];
2928
+ labelBase = "unstaged";
2929
+ } else if (scope === "uncommitted") {
2930
+ diffArgs = ["HEAD"];
2931
+ labelBase = "HEAD";
2932
+ } else if (scope === "last_turn") {
2933
+ diffArgs = [lastTurnBase];
2934
+ labelBase = "last turn";
2935
+ } else if (scope === "commits" && commitSha) {
2936
+ diffArgs = [`${commitSha}^!`];
2937
+ labelBase = commitSha.slice(0, 7);
2938
+ } else if (scope === "commits" && opts.base && SHA_RE.test(opts.base)) {
2939
+ diffArgs = [opts.base];
2940
+ labelBase = opts.base.slice(0, 7);
2941
+ } else if (scope === "commits") {
2942
+ let base = opts.base;
2943
+ try {
2944
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
2945
+ } catch {
2946
+ base = "HEAD";
2947
+ }
2948
+ const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
2949
+ diffArgs = [mergeBase];
2950
+ labelBase = mergeBase;
2951
+ } else {
2952
+ diffArgs = ["HEAD"];
2953
+ labelBase = "HEAD";
2954
+ }
2955
+ const diff = await gitDiff(
2956
+ includePatches ? [...diffArgs, "--", path] : ["--name-status", ...diffArgs, "--", path],
2957
+ worktreePath,
2958
+ { timeoutMs: 4e3 }
2959
+ );
2960
+ const filesMap = /* @__PURE__ */ new Map();
2961
+ if (includePatches) {
2962
+ const parsed = filesFromDiff("", "", diff.stdout, maxHunk);
2963
+ for (const file of parsed) filesMap.set(file.path, file);
2964
+ if (diff.stdout.trim() && !filesMap.has(path)) {
2965
+ filesMap.set(path, fileFromPatch(path, diff.stdout, maxHunk));
2966
+ }
2967
+ } else {
2968
+ for (const file of filesFromDiff(diff.stdout, "", "", maxHunk)) {
2969
+ filesMap.set(file.path, file);
2970
+ }
2971
+ }
2972
+ if (filesMap.size === 0) {
2973
+ const maybe = await listUntrackedPaths(worktreePath, path);
2974
+ if (maybe.length > 0) {
2975
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
2976
+ for (const file of extra) filesMap.set(file.path, file);
2977
+ }
2978
+ }
2979
+ const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
2980
+ return {
2981
+ scope,
2982
+ commitSha: scope === "commits" ? commitSha : null,
2983
+ base: labelBase,
2984
+ files,
2985
+ stat: formatStat(files),
2986
+ dirty: files.length > 0,
2987
+ unpushed: 0,
2988
+ hasLastTurnBase,
2989
+ commits: [],
2990
+ scopeStats: emptyScopeStats(),
2991
+ mergeBase: SHA_RE.test(diffArgs[0] ?? "") ? diffArgs[0] : null
2992
+ };
2993
+ }
2789
2994
  async function getDiff(worktreePath, repoPath, opts) {
2995
+ const maxHunk = opts?.maxHunkChars ?? 8e3;
2996
+ const scope = opts?.scope ?? "commits";
2997
+ const lastTurnBase = opts?.lastTurnBase?.trim() || null;
2998
+ const hasLastTurnBase = Boolean(lastTurnBase);
2999
+ const commitSha = opts?.commitSha?.trim() || null;
3000
+ const includePatches = opts?.includePatches ?? true;
3001
+ const pathFilter = opts?.path?.trim() || null;
3002
+ const fileOnly = Boolean(pathFilter);
3003
+ const includeMeta = (opts?.includeMeta ?? true) && !fileOnly;
3004
+ if (fileOnly && pathFilter && !includeMeta) {
3005
+ return getPathDiff(worktreePath, repoPath, {
3006
+ path: pathFilter,
3007
+ scope,
3008
+ commitSha,
3009
+ lastTurnBase,
3010
+ hasLastTurnBase,
3011
+ base: opts?.base,
3012
+ includePatches,
3013
+ maxHunk
3014
+ });
3015
+ }
2790
3016
  const status = await inspectGitWorktree(worktreePath);
2791
3017
  if (status === "missing_worktree") {
2792
3018
  throw new Error("Worktree not found");
@@ -2794,18 +3020,19 @@ async function getDiff(worktreePath, repoPath, opts) {
2794
3020
  if (status === "not_git") {
2795
3021
  throw new Error("Not a Git repository");
2796
3022
  }
2797
- const maxHunk = opts?.maxHunkChars ?? 8e3;
2798
- const scope = opts?.scope ?? "commits";
2799
- const lastTurnBase = opts?.lastTurnBase?.trim() || null;
2800
- const hasLastTurnBase = Boolean(lastTurnBase);
2801
- const commitSha = opts?.commitSha?.trim() || null;
2802
- const head = await git(["rev-parse", "--verify", "HEAD"], worktreePath, {
3023
+ const wantUntracked = opts?.includeUntracked !== void 0 ? opts.includeUntracked : !fileOnly && (scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn");
3024
+ const needsMergeBase = scope === "commits" && !commitSha || includeMeta;
3025
+ const headP = git(["rev-parse", "--verify", "HEAD"], worktreePath, {
2803
3026
  reject: false
2804
3027
  });
3028
+ const untrackedP = wantUntracked ? listUntrackedPaths(worktreePath, pathFilter) : Promise.resolve([]);
3029
+ const head = await headP;
2805
3030
  const hasHead = head.exitCode === 0;
2806
- const untrackedPaths = await listUntrackedPaths(worktreePath);
2807
3031
  if (!hasHead) {
2808
- const files2 = scope === "staged" ? [] : (await listUntrackedFiles(worktreePath, maxHunk)).sort(
3032
+ const untrackedPaths2 = await untrackedP;
3033
+ const files2 = scope === "staged" ? [] : includePatches ? (await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths2)).sort(
3034
+ (a, b) => a.path.localeCompare(b.path)
3035
+ ) : untrackedStubs(untrackedPaths2).sort(
2809
3036
  (a, b) => a.path.localeCompare(b.path)
2810
3037
  );
2811
3038
  const untrackedStat = {
@@ -2819,7 +3046,7 @@ async function getDiff(worktreePath, repoPath, opts) {
2819
3046
  commitSha: null,
2820
3047
  base: "(no commits)",
2821
3048
  files: files2,
2822
- stat: files2.length ? files2.map((f) => ` ${f.path} | untracked`).join("\n") : "(no changes)",
3049
+ stat: formatStat(files2),
2823
3050
  dirty: files2.length > 0,
2824
3051
  unpushed: 0,
2825
3052
  hasLastTurnBase,
@@ -2833,137 +3060,122 @@ async function getDiff(worktreePath, repoPath, opts) {
2833
3060
  }
2834
3061
  };
2835
3062
  }
2836
- let base = opts?.base;
2837
- try {
2838
- base = base ?? await resolveDefaultBranch(repoPath);
2839
- } catch {
2840
- base = "HEAD";
2841
- }
2842
- base = await resolveDiffBaseRef(worktreePath, base, repoPath);
2843
- if (base.startsWith("origin/")) {
2844
- const short = base.slice("origin/".length);
2845
- await git(["fetch", "origin", short, "--quiet"], worktreePath, {
2846
- reject: false
2847
- });
2848
- base = await resolveDiffBaseRef(worktreePath, short, repoPath);
3063
+ let mergeBase = "HEAD";
3064
+ let baseLabel = "HEAD";
3065
+ if (needsMergeBase) {
3066
+ let base = opts?.base;
3067
+ try {
3068
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
3069
+ } catch {
3070
+ base = "HEAD";
3071
+ }
3072
+ mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
3073
+ baseLabel = await resolveDiffBaseRef(worktreePath, base, repoPath);
3074
+ }
3075
+ const includeUntracked = wantUntracked;
3076
+ const metaPromise = includeMeta ? Promise.all([
3077
+ computeScopeStats(
3078
+ worktreePath,
3079
+ mergeBase,
3080
+ lastTurnBase,
3081
+ // Cheap: don't wait on the untracked walk just to pad the badge.
3082
+ 0
3083
+ ),
3084
+ listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
3085
+ isDirty(worktreePath),
3086
+ countUnpushedCommits(worktreePath)
3087
+ ]).then(([scopeStats, commits, dirty, unpushed]) => ({
3088
+ scopeStats,
3089
+ commits,
3090
+ dirty,
3091
+ unpushed
3092
+ })) : Promise.resolve({
3093
+ scopeStats: emptyScopeStats(),
3094
+ commits: [],
3095
+ dirty: false,
3096
+ unpushed: 0
3097
+ });
3098
+ if (scope === "last_turn" && !lastTurnBase) {
3099
+ const meta2 = await metaPromise;
3100
+ return {
3101
+ scope,
3102
+ commitSha: null,
3103
+ base: "last turn",
3104
+ files: [],
3105
+ stat: "(no last agent turn)",
3106
+ dirty: meta2.dirty,
3107
+ unpushed: meta2.unpushed,
3108
+ hasLastTurnBase: false,
3109
+ commits: meta2.commits,
3110
+ scopeStats: meta2.scopeStats
3111
+ };
2849
3112
  }
2850
- const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
2851
- const scopeStats = await computeScopeStats(
2852
- worktreePath,
2853
- mergeBase,
2854
- lastTurnBase,
2855
- untrackedPaths.length
2856
- );
2857
- const includeUntracked = scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn";
2858
- let nameStatus = "";
2859
- let numstat = "";
2860
- let combinedDiff = "";
2861
- let labelBase = base;
3113
+ let diffArgs;
3114
+ let labelBase = baseLabel;
2862
3115
  if (scope === "staged") {
2863
- const [ns, num, diff] = await Promise.all([
2864
- git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
2865
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
2866
- git(["diff", "--cached"], worktreePath, { reject: false })
2867
- ]);
2868
- nameStatus = ns.stdout;
2869
- numstat = num.stdout;
2870
- combinedDiff = diff.stdout;
3116
+ diffArgs = ["--cached"];
2871
3117
  labelBase = "staged";
2872
3118
  } else if (scope === "unstaged") {
2873
- const [ns, num, diff] = await Promise.all([
2874
- git(["diff", "--name-status"], worktreePath, { reject: false }),
2875
- git(["diff", "--numstat"], worktreePath, { reject: false }),
2876
- git(["diff"], worktreePath, { reject: false })
2877
- ]);
2878
- nameStatus = ns.stdout;
2879
- numstat = num.stdout;
2880
- combinedDiff = diff.stdout;
3119
+ diffArgs = [];
2881
3120
  labelBase = "unstaged";
2882
3121
  } else if (scope === "uncommitted") {
2883
- const [ns, num, diff] = await Promise.all([
2884
- git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
2885
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
2886
- git(["diff", "HEAD"], worktreePath, { reject: false })
2887
- ]);
2888
- nameStatus = ns.stdout;
2889
- numstat = num.stdout;
2890
- combinedDiff = diff.stdout;
3122
+ diffArgs = ["HEAD"];
2891
3123
  labelBase = "HEAD";
2892
3124
  } else if (scope === "last_turn") {
2893
- if (!lastTurnBase) {
2894
- const commits2 = await listBranchCommits(worktreePath, repoPath, { base });
2895
- return {
2896
- scope,
2897
- commitSha: null,
2898
- base: "last turn",
2899
- files: [],
2900
- stat: "(no last agent turn)",
2901
- dirty: await isDirty(worktreePath),
2902
- unpushed: await countUnpushedCommits(worktreePath),
2903
- hasLastTurnBase: false,
2904
- commits: commits2,
2905
- scopeStats
2906
- };
2907
- }
2908
- const [ns, num, diff] = await Promise.all([
2909
- git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
2910
- git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
2911
- git(["diff", lastTurnBase], worktreePath, { reject: false })
2912
- ]);
2913
- nameStatus = ns.stdout;
2914
- numstat = num.stdout;
2915
- combinedDiff = diff.stdout;
3125
+ diffArgs = [lastTurnBase];
2916
3126
  labelBase = "last turn";
2917
3127
  } else if (scope === "commits" && commitSha) {
2918
- const range = `${commitSha}^!`;
2919
- const [ns, num, diff] = await Promise.all([
2920
- git(["diff", "--name-status", range], worktreePath, { reject: false }),
2921
- git(["diff", "--numstat", range], worktreePath, { reject: false }),
2922
- git(["diff", range], worktreePath, { reject: false })
2923
- ]);
2924
- nameStatus = ns.stdout;
2925
- numstat = num.stdout;
2926
- combinedDiff = diff.stdout;
3128
+ diffArgs = [`${commitSha}^!`];
2927
3129
  labelBase = commitSha.slice(0, 7);
2928
3130
  } else {
2929
- const [ns, num, diff] = await Promise.all([
2930
- git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
2931
- git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
2932
- git(["diff", mergeBase], worktreePath, { reject: false })
2933
- ]);
2934
- nameStatus = ns.stdout;
2935
- numstat = num.stdout;
2936
- combinedDiff = diff.stdout;
2937
- labelBase = base;
2938
- }
2939
- const commits = await listBranchCommits(worktreePath, repoPath, { base });
3131
+ diffArgs = [mergeBase];
3132
+ labelBase = baseLabel;
3133
+ }
3134
+ const partsPromise = gitDiffParts(
3135
+ worktreePath,
3136
+ diffArgs,
3137
+ includePatches,
3138
+ pathFilter
3139
+ );
3140
+ const [parts, meta, untrackedPaths] = await Promise.all([
3141
+ partsPromise,
3142
+ metaPromise,
3143
+ untrackedP
3144
+ ]);
2940
3145
  const filesMap = /* @__PURE__ */ new Map();
2941
- for (const file of filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk)) {
3146
+ for (const file of filesFromDiff(
3147
+ parts.nameStatus,
3148
+ parts.numstat,
3149
+ parts.combinedDiff,
3150
+ maxHunk
3151
+ )) {
2942
3152
  filesMap.set(file.path, file);
2943
3153
  }
2944
3154
  if (includeUntracked) {
2945
- const untrackedFiles = await listUntrackedFiles(worktreePath, maxHunk);
2946
- for (const file of untrackedFiles) {
3155
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths) : untrackedStubs(untrackedPaths);
3156
+ for (const file of extra) {
2947
3157
  if (!filesMap.has(file.path)) filesMap.set(file.path, file);
2948
3158
  }
3159
+ } else if (fileOnly && pathFilter && !filesMap.has(pathFilter)) {
3160
+ const maybe = await listUntrackedPaths(worktreePath, pathFilter);
3161
+ if (maybe.length > 0) {
3162
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
3163
+ for (const file of extra) filesMap.set(file.path, file);
3164
+ }
2949
3165
  }
2950
3166
  const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
2951
- const { stdout: statOut } = await git(
2952
- scope === "staged" ? ["diff", "--stat", "--cached"] : scope === "unstaged" ? ["diff", "--stat"] : scope === "uncommitted" ? ["diff", "--stat", "HEAD"] : scope === "last_turn" && lastTurnBase ? ["diff", "--stat", lastTurnBase] : scope === "commits" && commitSha ? ["diff", "--stat", `${commitSha}^!`] : ["diff", "--stat", mergeBase],
2953
- worktreePath,
2954
- { reject: false }
2955
- );
2956
3167
  return {
2957
3168
  scope,
2958
3169
  commitSha: scope === "commits" ? commitSha : null,
2959
3170
  base: labelBase,
2960
3171
  files,
2961
- stat: statOut.trim() || (files.length ? `${files.length} file(s)` : "(no changes)"),
2962
- dirty: await isDirty(worktreePath),
2963
- unpushed: await countUnpushedCommits(worktreePath),
3172
+ stat: formatStat(files),
3173
+ dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
3174
+ unpushed: meta.unpushed,
2964
3175
  hasLastTurnBase,
2965
- commits,
2966
- scopeStats
3176
+ commits: meta.commits,
3177
+ scopeStats: meta.scopeStats,
3178
+ mergeBase: needsMergeBase ? mergeBase : null
2967
3179
  };
2968
3180
  }
2969
3181
  async function listWorktreeFiles(worktreePath, opts) {
@@ -3179,7 +3391,7 @@ async function previewLand(thread) {
3179
3391
  const target = await resolveDefaultBranch(thread.repoPath);
3180
3392
  if (thread.branchName === target || thread.branchName === "main" || thread.branchName === "master") {
3181
3393
  }
3182
- const { stdout: head } = await import("../run-Y2MI3EVV.js").then(
3394
+ const { stdout: head } = await import("../run-A4RHY7HH.js").then(
3183
3395
  ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], thread.worktreePath)
3184
3396
  );
3185
3397
  const current = head.trim();
@@ -3218,7 +3430,7 @@ async function confirmLand(thread, opts) {
3218
3430
  if (preview.dirty) {
3219
3431
  committed = await commitAll(thread.worktreePath, meta.commitMessage);
3220
3432
  }
3221
- const { git: git2 } = await import("../run-Y2MI3EVV.js");
3433
+ const { git: git2 } = await import("../run-A4RHY7HH.js");
3222
3434
  const { stdout: headOut } = await git2(
3223
3435
  ["rev-parse", "--abbrev-ref", "HEAD"],
3224
3436
  thread.worktreePath,
@@ -3910,7 +4122,7 @@ var Orchestrator = class {
3910
4122
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
3911
4123
  });
3912
4124
  }
3913
- const { autoCleanupOrphansEnabled } = await import("../app-settings-GJVZGU3M.js");
4125
+ const { autoCleanupOrphansEnabled } = await import("../app-settings-P42Z3VOB.js");
3914
4126
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
3915
4127
  await cleanupOrphanWorktrees({ repoPaths });
3916
4128
  }
@@ -4067,7 +4279,7 @@ var Orchestrator = class {
4067
4279
  }
4068
4280
  async finishCreateThread(threadId, prompt) {
4069
4281
  await this.runSetupAfterCreate(threadId);
4070
- const { autoRunAfterSetupEnabled } = await import("../app-settings-GJVZGU3M.js");
4282
+ const { autoRunAfterSetupEnabled } = await import("../app-settings-P42Z3VOB.js");
4071
4283
  if (autoRunAfterSetupEnabled()) {
4072
4284
  try {
4073
4285
  await this.startDev(threadId);
@@ -4147,11 +4359,11 @@ var Orchestrator = class {
4147
4359
  return results;
4148
4360
  }
4149
4361
  /** Edit the text of a not-yet-started queued message. */
4150
- async editQueuedMessage(threadRef, index, text) {
4362
+ async editQueuedMessage(threadRef, index, text2) {
4151
4363
  const thread = this.requireThread(threadRef);
4152
4364
  return withThreadLock(thread.id, async () => {
4153
4365
  const current = this.requireThread(thread.id);
4154
- const trimmed = text.trim();
4366
+ const trimmed = text2.trim();
4155
4367
  if (!trimmed || index < 0 || index >= current.queue.length) {
4156
4368
  return current;
4157
4369
  }
@@ -4296,10 +4508,19 @@ var Orchestrator = class {
4296
4508
  attachments: sentAttachments ?? thread.attachments
4297
4509
  }
4298
4510
  );
4299
- const orchestrationReminder = isOrchestratorThread(thread) ? coordinatorTurnReminder({
4300
- parentId: threadId,
4301
- goal: thread.sourceRef || thread.title
4302
- }) : null;
4511
+ const slackInbound = /^(Slack DM|Slack @mention)(?:\n|$)/.test(
4512
+ expandedPrompt.trim()
4513
+ );
4514
+ const orchestrationReminder = isOrchestratorThread(thread) ? [
4515
+ coordinatorTurnReminder({
4516
+ parentId: threadId,
4517
+ goal: thread.sourceRef || thread.title
4518
+ }),
4519
+ slackInbound ? [
4520
+ "This turn is a Slack DM or @mention. Your reply is posted back in Slack \u2014 keep it concise. Sideboard signs it with this Mac's destination name; do not prefix the name yourself.",
4521
+ SLACK_REPLY_FORMATTING
4522
+ ].join("\n") : null
4523
+ ].filter(Boolean).join("\n") : null;
4303
4524
  const artifactReminder = thread.agent !== "brightsy" ? [
4304
4525
  "Sideboard side column (important):",
4305
4526
  "There is no claude.ai Artifact tool here \u2014 that is normal.",
@@ -4339,7 +4560,7 @@ var Orchestrator = class {
4339
4560
  });
4340
4561
  const artifactDirective = isBrightsy ? null : formatArtifactDirective();
4341
4562
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
4342
- const { autoRenameBranchEnabled } = await import("../app-settings-GJVZGU3M.js");
4563
+ const { autoRenameBranchEnabled } = await import("../app-settings-P42Z3VOB.js");
4343
4564
  const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled() ? formatRenameBranchDirective(fresh, {
4344
4565
  customPrompt: settings?.prompts?.renameBranch
4345
4566
  }) : null;
@@ -4359,7 +4580,9 @@ var Orchestrator = class {
4359
4580
  goal: fresh.sourceRef || fresh.title || "Orchestration",
4360
4581
  parentId: fresh.id,
4361
4582
  workspaces: inventory,
4362
- audience: "desktop"
4583
+ audience: /^(Slack DM|Slack @mention)(?:\n|$)/.test(
4584
+ expandedPrompt.trim()
4585
+ ) ? "slack" : "desktop"
4363
4586
  });
4364
4587
  }
4365
4588
  }
@@ -4902,6 +5125,11 @@ var Orchestrator = class {
4902
5125
  return getDiff(thread.worktreePath, thread.repoPath, {
4903
5126
  scope: opts?.scope,
4904
5127
  commitSha: opts?.commitSha,
5128
+ base: opts?.base,
5129
+ includePatches: opts?.includePatches,
5130
+ includeMeta: opts?.includeMeta,
5131
+ includeUntracked: opts?.includeUntracked,
5132
+ path: opts?.path,
4905
5133
  lastTurnBase: this.turnBaselines.get(thread.id) ?? null
4906
5134
  });
4907
5135
  }
@@ -4979,7 +5207,8 @@ var Orchestrator = class {
4979
5207
  isDraft: false,
4980
5208
  reviewDecision: null,
4981
5209
  baseRefName: "",
4982
- headRefName: ""
5210
+ headRefName: "",
5211
+ isInMergeQueue: false
4983
5212
  };
4984
5213
  await this.persistPrMetaAndMaybeArchive(thread, metaLike);
4985
5214
  return { url: metaLike.url, state };
@@ -5029,7 +5258,7 @@ var Orchestrator = class {
5029
5258
  updateThread(thread.id, { title: meta.title });
5030
5259
  }
5031
5260
  }
5032
- const { autoArchiveOnMergeEnabled } = await import("../app-settings-GJVZGU3M.js");
5261
+ const { autoArchiveOnMergeEnabled } = await import("../app-settings-P42Z3VOB.js");
5033
5262
  const latest = this.requireThread(thread.id);
5034
5263
  if (!shouldAutoArchiveOnPrMerge({
5035
5264
  previousPrState: prevState || null,
@@ -5253,7 +5482,7 @@ var Orchestrator = class {
5253
5482
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
5254
5483
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
5255
5484
  try {
5256
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-AIUYUXB7.js");
5485
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-SVQIEVIZ.js");
5257
5486
  await ensureWorkspace2(thread.repoPath);
5258
5487
  } catch {
5259
5488
  }
@@ -5274,7 +5503,7 @@ var Orchestrator = class {
5274
5503
  await runArchiveScript(thread.repoPath, thread.worktreePath);
5275
5504
  } catch {
5276
5505
  }
5277
- const { deleteBranchOnPurgeEnabled } = await import("../app-settings-GJVZGU3M.js");
5506
+ const { deleteBranchOnPurgeEnabled } = await import("../app-settings-P42Z3VOB.js");
5278
5507
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
5279
5508
  await removeWorktree(thread.repoPath, thread.worktreePath, {
5280
5509
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -5295,7 +5524,7 @@ var Orchestrator = class {
5295
5524
  return restored2;
5296
5525
  }
5297
5526
  if (!existsSync13(thread.worktreePath)) {
5298
- const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-WMDJW2BL.js");
5527
+ const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-KWXHMKRN.js");
5299
5528
  const { execa: execa6 } = await import("execa");
5300
5529
  const slug = thread.worktreePath.split("/").pop();
5301
5530
  const dest = thread.worktreePath;
@@ -5367,6 +5596,103 @@ function getOrchestrator() {
5367
5596
  return singleton;
5368
5597
  }
5369
5598
 
5599
+ // src/integrations/linear-oauth.ts
5600
+ import { createHash, randomBytes } from "crypto";
5601
+ import { createServer as createServer2 } from "http";
5602
+
5603
+ // src/integrations/linear-app.ts
5604
+ var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
5605
+ var BAKED_LINEAR_CLIENT_SECRET = "";
5606
+
5607
+ // src/integrations/linear-oauth.ts
5608
+ var LINEAR_OAUTH_PORT = 19848;
5609
+ var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
5610
+ var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
5611
+ var REFRESH_SKEW_MS = 5 * 6e4;
5612
+ function linearOAuthCredentials() {
5613
+ const settings = loadAppSettings();
5614
+ const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
5615
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || BAKED_LINEAR_CLIENT_SECRET.trim();
5616
+ if (!clientId) {
5617
+ throw new Error(
5618
+ `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.`
5619
+ );
5620
+ }
5621
+ return { clientId, clientSecret };
5622
+ }
5623
+ function linearAuthorizationHeader(token) {
5624
+ const t = token.trim();
5625
+ if (!t) return t;
5626
+ if (/^bearer\s+/i.test(t)) return t;
5627
+ if (t.startsWith("lin_api_")) return t;
5628
+ return `Bearer ${t}`;
5629
+ }
5630
+ async function exchangeLinearToken(body) {
5631
+ const res = await fetch(LINEAR_TOKEN, {
5632
+ method: "POST",
5633
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5634
+ body
5635
+ });
5636
+ const data = await res.json().catch(() => ({}));
5637
+ if (!res.ok || data.error || !data.access_token?.trim()) {
5638
+ const detail = data.error_description || data.error || `HTTP ${res.status}`;
5639
+ throw new Error(`Linear OAuth token exchange failed: ${detail}`);
5640
+ }
5641
+ return data;
5642
+ }
5643
+ async function persistLinearTokens(data, viewer) {
5644
+ return saveLinearOAuth({
5645
+ accessToken: data.access_token.trim(),
5646
+ refreshToken: data.refresh_token?.trim(),
5647
+ expiresIn: data.expires_in,
5648
+ viewerName: viewer?.name,
5649
+ organizationName: viewer?.organizationName
5650
+ });
5651
+ }
5652
+ var refreshInFlight = null;
5653
+ async function refreshLinearAccessToken(refreshToken) {
5654
+ const { clientId, clientSecret } = linearOAuthCredentials();
5655
+ const body = new URLSearchParams({
5656
+ grant_type: "refresh_token",
5657
+ refresh_token: refreshToken,
5658
+ client_id: clientId
5659
+ });
5660
+ if (clientSecret) body.set("client_secret", clientSecret);
5661
+ try {
5662
+ const data = await exchangeLinearToken(body);
5663
+ await persistLinearTokens(data);
5664
+ return data.access_token.trim();
5665
+ } catch (err) {
5666
+ const message = err instanceof Error ? err.message : String(err);
5667
+ if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
5668
+ disconnectLinearConnection();
5669
+ throw new Error("Linear sign-in expired \u2014 connect again in Account settings");
5670
+ }
5671
+ throw err;
5672
+ }
5673
+ }
5674
+ async function getLinearAuthToken(settings = loadAppSettings()) {
5675
+ const access = settings.integrations.linearAccessToken?.trim() || "";
5676
+ const refresh = settings.integrations.linearRefreshToken?.trim() || "";
5677
+ const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
5678
+ const apiKey = settings.integrations.linearApiKey?.trim() || "";
5679
+ if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
5680
+ return access;
5681
+ }
5682
+ if (refresh) {
5683
+ if (!refreshInFlight) {
5684
+ refreshInFlight = refreshLinearAccessToken(refresh).finally(() => {
5685
+ refreshInFlight = null;
5686
+ });
5687
+ }
5688
+ return refreshInFlight.catch((err) => {
5689
+ if (apiKey) return apiKey;
5690
+ throw err;
5691
+ });
5692
+ }
5693
+ return apiKey || null;
5694
+ }
5695
+
5370
5696
  // src/integrations/linear.ts
5371
5697
  var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
5372
5698
  var ASSIGNED_ISSUES_QUERY = `
@@ -5389,16 +5715,16 @@ query SideboardAssignedIssues($first: Int!) {
5389
5715
  }
5390
5716
  `;
5391
5717
  async function listLinearIssuesDirect(opts) {
5392
- const apiKey = (opts?.apiKey ?? getLinearApiKey())?.trim();
5718
+ const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
5393
5719
  if (!apiKey) {
5394
- throw new Error("Linear is not connected \u2014 add an API key in Account settings");
5720
+ throw new Error("Linear is not connected \u2014 sign in from Account settings");
5395
5721
  }
5396
5722
  const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
5397
5723
  const res = await fetch(LINEAR_GRAPHQL, {
5398
5724
  method: "POST",
5399
5725
  headers: {
5400
5726
  "Content-Type": "application/json",
5401
- Authorization: apiKey
5727
+ Authorization: linearAuthorizationHeader(apiKey)
5402
5728
  },
5403
5729
  body: JSON.stringify({
5404
5730
  query: ASSIGNED_ISSUES_QUERY,
@@ -5488,6 +5814,640 @@ function mcpArchiveBlockedReason(thread) {
5488
5814
  return null;
5489
5815
  }
5490
5816
 
5817
+ // src/mcp/slack-tools.ts
5818
+ import { z } from "zod";
5819
+
5820
+ // src/slack/api.ts
5821
+ var SLACK_API = "https://slack.com/api";
5822
+ var SlackApiError = class extends Error {
5823
+ constructor(method, slackError) {
5824
+ super(`Slack ${method}: ${slackError}`);
5825
+ this.method = method;
5826
+ this.slackError = slackError;
5827
+ this.name = "SlackApiError";
5828
+ }
5829
+ method;
5830
+ slackError;
5831
+ };
5832
+ async function slackApi(token, method, params, fetchImpl) {
5833
+ const doFetch = fetchImpl ?? fetch;
5834
+ const body = new URLSearchParams();
5835
+ if (params) {
5836
+ for (const [key, value] of Object.entries(params)) {
5837
+ if (value === void 0) continue;
5838
+ body.set(key, String(value));
5839
+ }
5840
+ }
5841
+ const res = await doFetch(`${SLACK_API}/${method}`, {
5842
+ method: "POST",
5843
+ headers: {
5844
+ Authorization: `Bearer ${token}`,
5845
+ "Content-Type": "application/x-www-form-urlencoded"
5846
+ },
5847
+ body
5848
+ });
5849
+ const json = await res.json();
5850
+ if (!json.ok) {
5851
+ throw new SlackApiError(method, json.error || `HTTP ${res.status}`);
5852
+ }
5853
+ return json;
5854
+ }
5855
+
5856
+ // src/slack/destination.ts
5857
+ function isChannelId(raw) {
5858
+ return /^[CGD][A-Z0-9]+$/i.test(raw);
5859
+ }
5860
+ function isUserId(raw) {
5861
+ return /^U[A-Z0-9]+$/i.test(raw);
5862
+ }
5863
+ function isEmail(raw) {
5864
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw);
5865
+ }
5866
+ function normalizeSlackHandle(raw) {
5867
+ let s = raw.trim();
5868
+ const mention = s.match(/^<@([UW][A-Z0-9]+)(?:\|[^>]+)?>$/i);
5869
+ if (mention?.[1]) return mention[1];
5870
+ const channelMention = s.match(/^<#([CGD][A-Z0-9]+)(?:\|[^>]+)?>$/i);
5871
+ if (channelMention?.[1]) return channelMention[1];
5872
+ if (s.startsWith("#") || s.startsWith("@")) s = s.slice(1);
5873
+ return s.trim();
5874
+ }
5875
+ async function listSlackUsers(token, opts) {
5876
+ const query = opts?.query?.trim().toLowerCase() ?? "";
5877
+ const limit = Math.min(200, Math.max(1, opts?.limit ?? 50));
5878
+ const members = [];
5879
+ let cursor;
5880
+ do {
5881
+ const data = await slackApi(
5882
+ token,
5883
+ "users.list",
5884
+ {
5885
+ limit: 200,
5886
+ cursor: cursor || void 0
5887
+ },
5888
+ opts?.fetchImpl
5889
+ );
5890
+ for (const m of data.members ?? []) {
5891
+ if (!m.id || m.deleted || m.is_bot) continue;
5892
+ members.push(m);
5893
+ }
5894
+ cursor = data.response_metadata?.next_cursor?.trim() || void 0;
5895
+ } while (cursor && members.length < 2e3);
5896
+ const mapped = members.map((m) => ({
5897
+ id: m.id,
5898
+ name: m.name ?? m.id,
5899
+ real_name: m.profile?.real_name,
5900
+ display_name: m.profile?.display_name,
5901
+ email: m.profile?.email
5902
+ }));
5903
+ const filtered = query ? mapped.filter((u) => {
5904
+ const hay = [u.name, u.real_name, u.display_name, u.email, u.id].filter(Boolean).join(" ").toLowerCase();
5905
+ return hay.includes(query);
5906
+ }) : mapped;
5907
+ return filtered.slice(0, limit);
5908
+ }
5909
+ async function findUserId(token, handle, fetchImpl) {
5910
+ if (isUserId(handle)) {
5911
+ return { id: handle, name: handle };
5912
+ }
5913
+ if (isEmail(handle)) {
5914
+ try {
5915
+ const data = await slackApi(
5916
+ token,
5917
+ "users.lookupByEmail",
5918
+ { email: handle },
5919
+ fetchImpl
5920
+ );
5921
+ if (data.user?.id) {
5922
+ return {
5923
+ id: data.user.id,
5924
+ name: data.user.name ?? data.user.id,
5925
+ real_name: data.user.profile?.real_name,
5926
+ display_name: data.user.profile?.display_name,
5927
+ email: data.user.profile?.email
5928
+ };
5929
+ }
5930
+ } catch {
5931
+ }
5932
+ }
5933
+ const users = await listSlackUsers(token, { query: handle, limit: 20, fetchImpl });
5934
+ const exact = users.find(
5935
+ (u) => u.name.toLowerCase() === handle.toLowerCase() || u.display_name?.toLowerCase() === handle.toLowerCase() || u.real_name?.toLowerCase() === handle.toLowerCase() || u.email?.toLowerCase() === handle.toLowerCase()
5936
+ );
5937
+ if (exact) return exact;
5938
+ if (users.length === 1) return users[0];
5939
+ if (users.length === 0) {
5940
+ throw new Error(`No Slack user matching "${handle}"`);
5941
+ }
5942
+ throw new Error(
5943
+ `Ambiguous Slack user "${handle}". Matches: ${users.slice(0, 5).map((u) => `${u.name} (${u.id})`).join(", ")}. Pass a user id (U\u2026) or use slack_list_users.`
5944
+ );
5945
+ }
5946
+ async function openDmChannel(token, userId, fetchImpl) {
5947
+ const data = await slackApi(
5948
+ token,
5949
+ "conversations.open",
5950
+ { users: userId },
5951
+ fetchImpl
5952
+ );
5953
+ const id = data.channel?.id?.trim();
5954
+ if (!id) throw new Error(`Could not open Slack DM with user ${userId}`);
5955
+ return id;
5956
+ }
5957
+ async function resolveChannelByName(token, name, fetchImpl) {
5958
+ const data = await slackApi(
5959
+ token,
5960
+ "conversations.list",
5961
+ {
5962
+ types: "public_channel,private_channel",
5963
+ exclude_archived: true,
5964
+ limit: 200
5965
+ },
5966
+ fetchImpl
5967
+ );
5968
+ const match = (data.channels ?? []).find((c) => c.name?.toLowerCase() === name.toLowerCase());
5969
+ if (!match?.id) throw new Error(`No Slack channel named #${name}`);
5970
+ return match.id;
5971
+ }
5972
+ async function resolveSlackDestination(token, destination, opts) {
5973
+ const raw = destination.trim();
5974
+ if (!raw) throw new Error("destination is required");
5975
+ const hadAt = raw.startsWith("@") || /^<@/i.test(raw);
5976
+ const hadHash = raw.startsWith("#") || /^<#/i.test(raw);
5977
+ const handle = normalizeSlackHandle(raw);
5978
+ if (isChannelId(handle)) {
5979
+ return {
5980
+ channelId: handle,
5981
+ kind: handle.toUpperCase().startsWith("D") ? "dm" : "channel",
5982
+ label: handle
5983
+ };
5984
+ }
5985
+ if (isUserId(handle) || hadAt || isEmail(handle)) {
5986
+ const user = await findUserId(token, handle, opts?.fetchImpl);
5987
+ if (opts?.forRead) {
5988
+ const channelId2 = await openDmChannel(token, user.id, opts?.fetchImpl);
5989
+ return {
5990
+ channelId: channelId2,
5991
+ kind: "user",
5992
+ userId: user.id,
5993
+ label: user.display_name || user.real_name || user.name
5994
+ };
5995
+ }
5996
+ const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
5997
+ return {
5998
+ channelId,
5999
+ kind: "user",
6000
+ userId: user.id,
6001
+ label: user.display_name || user.real_name || user.name
6002
+ };
6003
+ }
6004
+ if (hadHash) {
6005
+ const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
6006
+ return { channelId, kind: "channel", label: `#${handle}` };
6007
+ }
6008
+ try {
6009
+ const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
6010
+ return { channelId, kind: "channel", label: `#${handle}` };
6011
+ } catch (channelErr) {
6012
+ try {
6013
+ const user = await findUserId(token, handle, opts?.fetchImpl);
6014
+ const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
6015
+ return {
6016
+ channelId,
6017
+ kind: "user",
6018
+ userId: user.id,
6019
+ label: user.display_name || user.real_name || user.name
6020
+ };
6021
+ } catch {
6022
+ throw channelErr;
6023
+ }
6024
+ }
6025
+ }
6026
+
6027
+ // src/slack/github-link.ts
6028
+ function labelGithubUrl(url) {
6029
+ const raw = url.trim();
6030
+ if (!raw) return null;
6031
+ let parsed;
6032
+ try {
6033
+ parsed = new URL(raw);
6034
+ } catch {
6035
+ return null;
6036
+ }
6037
+ if (!/(^|\.)github\.com$/i.test(parsed.hostname)) return null;
6038
+ const path = parsed.pathname;
6039
+ const hash = parsed.hash || "";
6040
+ const pr = path.match(/\/([^/]+)\/([^/]+)\/pull\/(\d+)/i);
6041
+ if (pr) {
6042
+ const n = pr[3];
6043
+ if (/#discussion_r\d+/i.test(hash) || /#pullrequestreview-\d+/i.test(hash)) {
6044
+ return { kind: "comment", label: `PR #${n} comment`, url: raw };
6045
+ }
6046
+ if (/#issuecomment-\d+/i.test(hash)) {
6047
+ return { kind: "comment", label: `PR #${n} comment`, url: raw };
6048
+ }
6049
+ return { kind: "pr", label: `PR #${n}`, url: raw };
6050
+ }
6051
+ const issueComment = path.match(/\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
6052
+ if (issueComment && /#issuecomment-\d+/i.test(hash)) {
6053
+ return { kind: "comment", label: `Issue #${issueComment[3]} comment`, url: raw };
6054
+ }
6055
+ const blob = path.match(/\/([^/]+)\/([^/]+)\/blob\/[^/]+\/(.+)$/i);
6056
+ if (blob) {
6057
+ const filePath = decodeURIComponent(blob[3]);
6058
+ const fileName = filePath.split("/").pop() || filePath;
6059
+ const line = hash.match(/^#L(\d+)(?:-L(\d+))?$/i);
6060
+ if (line) {
6061
+ const start = line[1];
6062
+ const end = line[2];
6063
+ const loc = end && end !== start ? `${start}-${end}` : start;
6064
+ return { kind: "code", label: `${fileName}:${loc}`, url: raw };
6065
+ }
6066
+ return { kind: "code", label: fileName, url: raw };
6067
+ }
6068
+ return { kind: "other", label: "GitHub", url: raw };
6069
+ }
6070
+ function appendGithubLink(text2, githubUrl) {
6071
+ const body = text2.trimEnd();
6072
+ if (!githubUrl?.trim()) return body;
6073
+ const labeled = labelGithubUrl(githubUrl);
6074
+ if (!labeled) {
6075
+ return body ? `${body}
6076
+ ${githubUrl.trim()}` : githubUrl.trim();
6077
+ }
6078
+ const link = `<${labeled.url}|${labeled.label}>`;
6079
+ return body ? `${body}
6080
+ ${link}` : link;
6081
+ }
6082
+
6083
+ // src/slack/outbound-watch.ts
6084
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
6085
+ import { join as join12 } from "path";
6086
+
6087
+ // src/slack/workspaces.ts
6088
+ import { join as join11 } from "path";
6089
+ function storePath() {
6090
+ return join11(appDataDir(), "slack-workspaces.json");
6091
+ }
6092
+ function readStore() {
6093
+ try {
6094
+ const path = storePath();
6095
+ const wasEncrypted = isSecureFileEncrypted(path);
6096
+ const parsed = readSecureJson(path);
6097
+ const workspaces = Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
6098
+ if (workspaces.length > 0 && !wasEncrypted && resolveVaultKey()) {
6099
+ writeSecureJson(path, { workspaces });
6100
+ }
6101
+ return workspaces;
6102
+ } catch {
6103
+ return [];
6104
+ }
6105
+ }
6106
+ function toInfo(ws) {
6107
+ return {
6108
+ team_id: ws.team_id,
6109
+ team_name: ws.team_name,
6110
+ user_id: ws.user_id,
6111
+ has_bot_token: Boolean(ws.bot_token),
6112
+ has_user_token: Boolean(ws.user_token),
6113
+ connected_at: ws.connected_at
6114
+ };
6115
+ }
6116
+ function listSlackWorkspaces() {
6117
+ return readStore().map(toInfo).sort((a, b) => a.team_name.localeCompare(b.team_name));
6118
+ }
6119
+ function getSlackWorkspace(teamId) {
6120
+ const id = teamId.trim();
6121
+ if (!id) return null;
6122
+ return readStore().find(
6123
+ (ws) => ws.team_id === id || ws.team_name.toLowerCase() === id.toLowerCase()
6124
+ ) ?? null;
6125
+ }
6126
+ function slackTokenFor(ws, kind = "read") {
6127
+ if (kind === "search") {
6128
+ const token2 = ws.user_token?.trim();
6129
+ if (!token2) {
6130
+ throw new Error(
6131
+ `Slack search needs a user token for ${ws.team_name}. Reconnect via Account \u2192 Slack (browser) or paste an xoxp- token.`
6132
+ );
6133
+ }
6134
+ return token2;
6135
+ }
6136
+ const token = (kind === "write" ? ws.bot_token || ws.user_token : ws.user_token || ws.bot_token)?.trim();
6137
+ if (!token) {
6138
+ throw new Error(`Slack workspace ${ws.team_name} has no token`);
6139
+ }
6140
+ return token;
6141
+ }
6142
+ function requireSlackWorkspace(teamId) {
6143
+ const ws = getSlackWorkspace(teamId);
6144
+ if (!ws) {
6145
+ const connected = listSlackWorkspaces();
6146
+ const hint = connected.length === 0 ? "Connect a workspace in Account \u2192 Slack workspaces." : `Connected: ${connected.map((t) => `${t.team_name} (${t.team_id})`).join(", ")}`;
6147
+ throw new Error(`Unknown Slack team_id "${teamId}". ${hint}`);
6148
+ }
6149
+ return ws;
6150
+ }
6151
+
6152
+ // src/slack/outbound-watch.ts
6153
+ var MAX_WATCHES = 40;
6154
+ var WATCH_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6155
+ function storePath2() {
6156
+ return join12(appDataDir(), "slack-outbound-watch.json");
6157
+ }
6158
+ function watchId(teamId, channelId, ts) {
6159
+ return `${teamId}:${channelId}:${ts}`;
6160
+ }
6161
+ function slackArchiveUrl(channelId, ts) {
6162
+ return `https://slack.com/archives/${channelId}/p${ts.replace(".", "")}`;
6163
+ }
6164
+ function readStore2() {
6165
+ const path = storePath2();
6166
+ if (!existsSync14(path)) return [];
6167
+ try {
6168
+ const parsed = isSecureFileEncrypted(path) ? readSecureJson(path) : JSON.parse(readFileSync9(path, "utf8"));
6169
+ return Array.isArray(parsed?.watches) ? parsed.watches : [];
6170
+ } catch {
6171
+ return [];
6172
+ }
6173
+ }
6174
+ function writeStore(watches) {
6175
+ writePrivateFile(storePath2(), `${JSON.stringify({ watches }, null, 2)}
6176
+ `);
6177
+ return watches;
6178
+ }
6179
+ function pruneWatches(watches, nowMs = Date.now()) {
6180
+ const cutoff = nowMs - WATCH_TTL_MS;
6181
+ const kept = watches.filter((w) => {
6182
+ const posted = Date.parse(w.postedAt);
6183
+ return Number.isFinite(posted) ? posted >= cutoff : true;
6184
+ });
6185
+ if (kept.length <= MAX_WATCHES) return kept;
6186
+ return kept.slice().sort((a, b) => b.postedAt.localeCompare(a.postedAt)).slice(0, MAX_WATCHES);
6187
+ }
6188
+ function recordSlackOutboundWatch(input) {
6189
+ const ts = input.ts.trim();
6190
+ const channelId = input.channelId.trim();
6191
+ const teamId = input.teamId.trim();
6192
+ if (!ts || !channelId || !teamId) return null;
6193
+ const owner = input.ownerUserId?.trim();
6194
+ const toUser = input.toUserId?.trim();
6195
+ if (toUser && owner && toUser === owner) return null;
6196
+ const id = watchId(teamId, channelId, ts);
6197
+ const next = {
6198
+ id,
6199
+ teamId,
6200
+ channelId,
6201
+ ts,
6202
+ threadTs: input.threadTs?.trim() || ts,
6203
+ kind: input.kind === "dm" ? "dm" : "channel",
6204
+ toUserId: toUser,
6205
+ toLabel: input.toLabel.trim() || toUser || channelId,
6206
+ ownerUserId: owner,
6207
+ postedAt: (/* @__PURE__ */ new Date()).toISOString(),
6208
+ lastSeenTs: ts,
6209
+ unread: false,
6210
+ permalink: slackArchiveUrl(channelId, ts)
6211
+ };
6212
+ const watches = pruneWatches(readStore2().filter((w) => w.id !== id));
6213
+ watches.unshift(next);
6214
+ writeStore(pruneWatches(watches));
6215
+ return next;
6216
+ }
6217
+
6218
+ // src/mcp/slack-tools.ts
6219
+ function text(payload, isError = false) {
6220
+ return {
6221
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
6222
+ ...isError ? { isError: true } : {}
6223
+ };
6224
+ }
6225
+ function fail(err) {
6226
+ return text(
6227
+ { error: err instanceof Error ? err.message : String(err) },
6228
+ true
6229
+ );
6230
+ }
6231
+ function registerSlackTools(server) {
6232
+ server.tool(
6233
+ "list_teams",
6234
+ "List Slack workspaces connected in Sideboard Account settings. Each row is team_id + name. Pass team_id to slack_list_channels, slack_list_users, slack_search, slack_read, and slack_post.",
6235
+ {},
6236
+ async () => {
6237
+ const teams = listSlackWorkspaces();
6238
+ if (teams.length === 0) {
6239
+ return text({
6240
+ teams: [],
6241
+ hint: "No Slack workspaces connected. Add one in Account \u2192 Slack workspaces (paste xoxb-/xoxp- or browser sign-in)."
6242
+ });
6243
+ }
6244
+ return text({
6245
+ teams: teams.map((t) => ({
6246
+ team_id: t.team_id,
6247
+ name: t.team_name,
6248
+ search: t.has_user_token
6249
+ }))
6250
+ });
6251
+ }
6252
+ );
6253
+ server.tool(
6254
+ "slack_list_channels",
6255
+ "List channels in a connected Slack workspace. Pass team_id from list_teams.",
6256
+ {
6257
+ team_id: z.string(),
6258
+ limit: z.number().optional()
6259
+ },
6260
+ async ({ team_id, limit }) => {
6261
+ try {
6262
+ const ws = requireSlackWorkspace(team_id);
6263
+ const token = slackTokenFor(ws, "read");
6264
+ const data = await slackApi(token, "conversations.list", {
6265
+ types: "public_channel,private_channel",
6266
+ exclude_archived: true,
6267
+ limit: Math.min(200, Math.max(1, limit ?? 100))
6268
+ });
6269
+ return text({
6270
+ team_id: ws.team_id,
6271
+ team_name: ws.team_name,
6272
+ channels: (data.channels ?? []).map((c) => ({
6273
+ id: c.id,
6274
+ name: c.name,
6275
+ private: Boolean(c.is_private),
6276
+ member: Boolean(c.is_member)
6277
+ }))
6278
+ });
6279
+ } catch (err) {
6280
+ return fail(err);
6281
+ }
6282
+ }
6283
+ );
6284
+ server.tool(
6285
+ "slack_list_users",
6286
+ "List or search people in a connected Slack workspace (for DMs / @mentions). Pass team_id from list_teams. Optional query matches name, display name, real name, or email.",
6287
+ {
6288
+ team_id: z.string(),
6289
+ query: z.string().optional(),
6290
+ limit: z.number().optional()
6291
+ },
6292
+ async ({ team_id, query, limit }) => {
6293
+ try {
6294
+ const ws = requireSlackWorkspace(team_id);
6295
+ const token = slackTokenFor(ws, "read");
6296
+ const users = await listSlackUsers(token, { query, limit });
6297
+ return text({
6298
+ team_id: ws.team_id,
6299
+ team_name: ws.team_name,
6300
+ query: query?.trim() || void 0,
6301
+ users
6302
+ });
6303
+ } catch (err) {
6304
+ return fail(err);
6305
+ }
6306
+ }
6307
+ );
6308
+ server.tool(
6309
+ "slack_search",
6310
+ "Search messages in a connected Slack workspace (needs a user token). Pass team_id from list_teams.",
6311
+ {
6312
+ team_id: z.string(),
6313
+ query: z.string(),
6314
+ count: z.number().optional()
6315
+ },
6316
+ async ({ team_id, query, count }) => {
6317
+ try {
6318
+ const ws = requireSlackWorkspace(team_id);
6319
+ const token = slackTokenFor(ws, "search");
6320
+ const data = await slackApi(token, "search.messages", {
6321
+ query,
6322
+ count: Math.min(50, Math.max(1, count ?? 10)),
6323
+ sort: "timestamp"
6324
+ });
6325
+ return text({
6326
+ team_id: ws.team_id,
6327
+ team_name: ws.team_name,
6328
+ query,
6329
+ matches: (data.messages?.matches ?? []).map((m) => ({
6330
+ channel: m.channel?.name || m.channel?.id,
6331
+ user: m.username || m.user,
6332
+ ts: m.ts,
6333
+ text: m.text,
6334
+ permalink: m.permalink
6335
+ }))
6336
+ });
6337
+ } catch (err) {
6338
+ return fail(err);
6339
+ }
6340
+ }
6341
+ );
6342
+ server.tool(
6343
+ "slack_read",
6344
+ "Read recent messages in a Slack channel or DM, or a thread when ts is set. Pass team_id from list_teams and #channel, @user, or a C\u2026/D\u2026/U\u2026 id.",
6345
+ {
6346
+ team_id: z.string(),
6347
+ channel: z.string(),
6348
+ ts: z.string().optional(),
6349
+ limit: z.number().optional()
6350
+ },
6351
+ async ({ team_id, channel, ts, limit }) => {
6352
+ try {
6353
+ const ws = requireSlackWorkspace(team_id);
6354
+ const token = slackTokenFor(ws, "read");
6355
+ const dest = await resolveSlackDestination(token, channel, { forRead: true });
6356
+ const n = Math.min(100, Math.max(1, limit ?? 20));
6357
+ if (ts?.trim()) {
6358
+ const data2 = await slackApi(token, "conversations.replies", {
6359
+ channel: dest.channelId,
6360
+ ts: ts.trim(),
6361
+ limit: n
6362
+ });
6363
+ return text({
6364
+ team_id: ws.team_id,
6365
+ channel: dest.channelId,
6366
+ label: dest.label,
6367
+ thread_ts: ts.trim(),
6368
+ messages: data2.messages ?? []
6369
+ });
6370
+ }
6371
+ const data = await slackApi(token, "conversations.history", {
6372
+ channel: dest.channelId,
6373
+ limit: n
6374
+ });
6375
+ return text({
6376
+ team_id: ws.team_id,
6377
+ channel: dest.channelId,
6378
+ label: dest.label,
6379
+ messages: data.messages ?? []
6380
+ });
6381
+ } catch (err) {
6382
+ return fail(err);
6383
+ }
6384
+ }
6385
+ );
6386
+ server.tool(
6387
+ "slack_post",
6388
+ "Post a message to a Slack channel or DM (as the Sideboard bot). Pass team_id from list_teams. Use to or channel for #name, @user, or C\u2026/D\u2026/U\u2026 ids. Optional github_url appends a PR / code / comment link. Only notify when the user asks. Thread with thread_ts when set.",
6389
+ {
6390
+ team_id: z.string(),
6391
+ channel: z.string().optional(),
6392
+ to: z.string().optional(),
6393
+ text: z.string(),
6394
+ github_url: z.string().optional(),
6395
+ thread_ts: z.string().optional()
6396
+ },
6397
+ async ({ team_id, channel, to, text: message, github_url, thread_ts }) => {
6398
+ try {
6399
+ const destRaw = (to ?? channel)?.trim();
6400
+ if (!destRaw) {
6401
+ return fail(new Error("slack_post needs `to` or `channel` (#name, @user, or Slack id)"));
6402
+ }
6403
+ const ws = requireSlackWorkspace(team_id);
6404
+ const token = slackTokenFor(ws, "write");
6405
+ const dest = await resolveSlackDestination(token, destRaw);
6406
+ const body = appendGithubLink(message, github_url);
6407
+ const data = await slackApi(
6408
+ token,
6409
+ "chat.postMessage",
6410
+ {
6411
+ channel: dest.channelId,
6412
+ text: body,
6413
+ thread_ts: thread_ts?.trim() || void 0,
6414
+ unfurl_links: true,
6415
+ unfurl_media: true
6416
+ }
6417
+ );
6418
+ const postedTs = data.ts?.trim();
6419
+ const postedChannel = (data.channel || dest.channelId).trim();
6420
+ if (postedTs && postedChannel) {
6421
+ try {
6422
+ recordSlackOutboundWatch({
6423
+ teamId: ws.team_id,
6424
+ channelId: postedChannel,
6425
+ ts: postedTs,
6426
+ threadTs: thread_ts?.trim() || postedTs,
6427
+ kind: dest.kind === "channel" ? "channel" : "dm",
6428
+ toUserId: dest.userId,
6429
+ toLabel: dest.label,
6430
+ ownerUserId: ws.user_id
6431
+ });
6432
+ } catch {
6433
+ }
6434
+ }
6435
+ return text({
6436
+ ok: true,
6437
+ team_id: ws.team_id,
6438
+ channel: postedChannel,
6439
+ label: dest.label,
6440
+ kind: dest.kind,
6441
+ user_id: dest.userId,
6442
+ ts: postedTs
6443
+ });
6444
+ } catch (err) {
6445
+ return fail(err);
6446
+ }
6447
+ }
6448
+ );
6449
+ }
6450
+
5491
6451
  // src/mcp/server.ts
5492
6452
  var MAX_ORCH_THREADS = 5;
5493
6453
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -5511,7 +6471,7 @@ function withTimeout(promise, ms, label) {
5511
6471
  async function startMcpServer() {
5512
6472
  const orch = getOrchestrator();
5513
6473
  try {
5514
- const { maxConcurrentAgents } = await import("../app-settings-GJVZGU3M.js");
6474
+ const { maxConcurrentAgents } = await import("../app-settings-P42Z3VOB.js");
5515
6475
  orch.setMaxConcurrent(maxConcurrentAgents());
5516
6476
  } catch {
5517
6477
  }
@@ -5564,7 +6524,7 @@ async function startMcpServer() {
5564
6524
  server.tool(
5565
6525
  "get_thread",
5566
6526
  "Get a compact thread summary by id/ref",
5567
- { ref: z.string() },
6527
+ { ref: z2.string() },
5568
6528
  async ({ ref }) => {
5569
6529
  const t = orch.getThread(ref);
5570
6530
  if (!t) {
@@ -5593,14 +6553,14 @@ async function startMcpServer() {
5593
6553
  "present_artifact",
5594
6554
  "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.",
5595
6555
  {
5596
- title: z.string().describe("Short title shown in the artifact pane header"),
5597
- type: z.enum(["html", "svg", "markdown", "react"]).describe(
6556
+ title: z2.string().describe("Short title shown in the artifact pane header"),
6557
+ type: z2.enum(["html", "svg", "markdown", "react"]).describe(
5598
6558
  "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
5599
6559
  ),
5600
- content: z.string().describe(
6560
+ content: z2.string().describe(
5601
6561
  "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
5602
6562
  ),
5603
- artifact_id: z.string().optional().describe("Stable id when updating the same artifact across turns")
6563
+ artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
5604
6564
  },
5605
6565
  async ({ title, type, artifact_id }) => {
5606
6566
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5618,15 +6578,15 @@ async function startMcpServer() {
5618
6578
  "ask_user",
5619
6579
  "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.",
5620
6580
  {
5621
- questions: z.array(
5622
- z.object({
5623
- question: z.string().describe("Full question text ending with ?"),
5624
- header: z.string().max(24).optional().describe("Short label shown above the question"),
5625
- multiSelect: z.boolean().optional().describe("Allow selecting multiple options"),
5626
- options: z.array(
5627
- z.object({
5628
- label: z.string(),
5629
- description: z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
6581
+ questions: z2.array(
6582
+ z2.object({
6583
+ question: z2.string().describe("Full question text ending with ?"),
6584
+ header: z2.string().max(24).optional().describe("Short label shown above the question"),
6585
+ multiSelect: z2.boolean().optional().describe("Allow selecting multiple options"),
6586
+ options: z2.array(
6587
+ z2.object({
6588
+ label: z2.string(),
6589
+ description: z2.string().optional().describe("What this option means / when to choose it (strongly preferred)")
5630
6590
  })
5631
6591
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
5632
6592
  })
@@ -5645,9 +6605,9 @@ async function startMcpServer() {
5645
6605
  "present_plan",
5646
6606
  "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.",
5647
6607
  {
5648
- title: z.string().optional().describe("Short plan title (defaults to Plan)"),
5649
- content: z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
5650
- thread_id: z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
6608
+ title: z2.string().optional().describe("Short plan title (defaults to Plan)"),
6609
+ content: z2.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
6610
+ thread_id: z2.string().optional().describe("Sideboard thread id when cwd is not the worktree")
5651
6611
  },
5652
6612
  async ({ title, content, thread_id }) => {
5653
6613
  const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
@@ -5668,17 +6628,17 @@ async function startMcpServer() {
5668
6628
  );
5669
6629
  server.tool(
5670
6630
  "present_schema",
5671
- "Open Sideboard\u2019s schema-driven CMS side column (filterable table and/or form). Pass JSON Schema + schemaUi (Brightsy extensions supported). Use datasource=brightsy with resource_id (record type UUID) when logged into Brightsy; use datasource=inline with embedded resource/records for any other source.",
6631
+ "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.",
5672
6632
  {
5673
- title: z.string().describe("Pane title"),
5674
- mode: z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
5675
- datasource: z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
5676
- resource_id: z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
5677
- record_id: z.string().optional().describe("Record id when opening form mode"),
5678
- resource: z.record(z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
5679
- record: z.record(z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
5680
- records: z.array(z.record(z.unknown())).optional().describe("Inline records for table mode"),
5681
- pane_id: z.string().optional().describe("Stable pane id across updates")
6633
+ title: z2.string().describe("Pane title"),
6634
+ mode: z2.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
6635
+ datasource: z2.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
6636
+ resource_id: z2.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
6637
+ record_id: z2.string().optional().describe("Record id when opening form mode"),
6638
+ resource: z2.record(z2.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
6639
+ record: z2.record(z2.unknown()).optional().describe("Inline record { id, data, published_at? }"),
6640
+ records: z2.array(z2.record(z2.unknown())).optional().describe("Inline records for table mode"),
6641
+ pane_id: z2.string().optional().describe("Stable pane id across updates")
5682
6642
  },
5683
6643
  async (args) => {
5684
6644
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5699,10 +6659,10 @@ async function startMcpServer() {
5699
6659
  "present_files",
5700
6660
  "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.",
5701
6661
  {
5702
- title: z.string().optional().describe("Pane title (default: Files)"),
5703
- datasource: z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
5704
- path: z.string().optional().describe("Initial folder path (e.g. public)"),
5705
- pane_id: z.string().optional().describe("Stable pane id across updates")
6662
+ title: z2.string().optional().describe("Pane title (default: Files)"),
6663
+ datasource: z2.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
6664
+ path: z2.string().optional().describe("Initial folder path (e.g. public)"),
6665
+ pane_id: z2.string().optional().describe("Stable pane id across updates")
5706
6666
  },
5707
6667
  async (args) => {
5708
6668
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5717,23 +6677,62 @@ async function startMcpServer() {
5717
6677
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
5718
6678
  }
5719
6679
  );
6680
+ registerSlackTools(server);
5720
6681
  if (sideboardMcpProfile() !== "worktree") {
5721
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-GJVZGU3M.js");
6682
+ const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-BEJIYPJ7.js");
6683
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-P42Z3VOB.js");
5722
6684
  const accountDefaults = resolveThreadDefaults();
5723
6685
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
6686
+ server.tool(
6687
+ "set_caffeinate",
6688
+ "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.",
6689
+ {
6690
+ enabled: z2.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
6691
+ },
6692
+ async ({ enabled }) => {
6693
+ const state = setCaffeinateHold(enabled);
6694
+ if (enabled && !state.held) {
6695
+ return {
6696
+ content: [
6697
+ {
6698
+ type: "text",
6699
+ text: JSON.stringify({
6700
+ ...state,
6701
+ ok: false,
6702
+ message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
6703
+ })
6704
+ }
6705
+ ],
6706
+ isError: true
6707
+ };
6708
+ }
6709
+ return {
6710
+ content: [
6711
+ {
6712
+ type: "text",
6713
+ text: JSON.stringify({
6714
+ ...getCaffeinateHold(),
6715
+ ok: true,
6716
+ 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)."
6717
+ })
6718
+ }
6719
+ ]
6720
+ };
6721
+ }
6722
+ );
5724
6723
  server.tool(
5725
6724
  "create_thread",
5726
6725
  `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.`,
5727
6726
  {
5728
- sourceType: z.enum(["branch", "pr", "ticket"]),
5729
- sourceRef: z.string(),
5730
- agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
5731
- model: z.string().nullable().optional().describe(
6727
+ sourceType: z2.enum(["branch", "pr", "ticket"]),
6728
+ sourceRef: z2.string(),
6729
+ agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
6730
+ model: z2.string().nullable().optional().describe(
5732
6731
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
5733
6732
  ),
5734
- repoPath: z.string(),
5735
- title: z.string().optional(),
5736
- parentThreadId: z.string().optional().describe(
6733
+ repoPath: z2.string(),
6734
+ title: z2.string().optional(),
6735
+ parentThreadId: z2.string().optional().describe(
5737
6736
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
5738
6737
  )
5739
6738
  },
@@ -5840,9 +6839,9 @@ async function startMcpServer() {
5840
6839
  "send_to_thread",
5841
6840
  "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.",
5842
6841
  {
5843
- ref: z.string(),
5844
- prompt: z.string(),
5845
- force_stop: z.boolean().optional()
6842
+ ref: z2.string(),
6843
+ prompt: z2.string(),
6844
+ force_stop: z2.boolean().optional()
5846
6845
  },
5847
6846
  async ({ ref, prompt, force_stop }) => {
5848
6847
  if (force_stop) {
@@ -5871,8 +6870,8 @@ async function startMcpServer() {
5871
6870
  "wait_for_turn",
5872
6871
  "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
5873
6872
  {
5874
- ref: z.string(),
5875
- timeoutMs: z.number().optional()
6873
+ ref: z2.string(),
6874
+ timeoutMs: z2.number().optional()
5876
6875
  },
5877
6876
  async ({ ref, timeoutMs }) => {
5878
6877
  const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
@@ -5894,7 +6893,7 @@ async function startMcpServer() {
5894
6893
  server.tool(
5895
6894
  "get_turn_result",
5896
6895
  "Final assistant message only (not full transcript)",
5897
- { ref: z.string() },
6896
+ { ref: z2.string() },
5898
6897
  async ({ ref }) => {
5899
6898
  const result = orch.getTurnResult(ref);
5900
6899
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
@@ -5904,8 +6903,8 @@ async function startMcpServer() {
5904
6903
  "stop_thread",
5905
6904
  "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.",
5906
6905
  {
5907
- ref: z.string(),
5908
- force: z.boolean().optional()
6906
+ ref: z2.string(),
6907
+ force: z2.boolean().optional()
5909
6908
  },
5910
6909
  async ({ ref, force }) => {
5911
6910
  const t = orch.getThread(ref);
@@ -5934,8 +6933,8 @@ async function startMcpServer() {
5934
6933
  );
5935
6934
  server.tool(
5936
6935
  "archive_thread",
5937
- "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Cannot archive the cloud coordinator \u2014 use the Sideboard UI for that.",
5938
- { ref: z.string() },
6936
+ "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators open PRs only by asking the worktree agent.",
6937
+ { ref: z2.string() },
5939
6938
  async ({ ref }) => {
5940
6939
  const t = orch.getThread(ref);
5941
6940
  if (!t) {
@@ -5968,7 +6967,7 @@ async function startMcpServer() {
5968
6967
  server.tool(
5969
6968
  "restore_thread",
5970
6969
  "Restore an archived thread (recreates worktree from branch when needed)",
5971
- { ref: z.string() },
6970
+ { ref: z2.string() },
5972
6971
  async ({ ref }) => {
5973
6972
  try {
5974
6973
  const restored = await orch.restore(ref);
@@ -5994,8 +6993,8 @@ async function startMcpServer() {
5994
6993
  "get_diff",
5995
6994
  "Compact diff summary (capped hunks, paginated)",
5996
6995
  {
5997
- ref: z.string(),
5998
- maxFiles: z.number().optional()
6996
+ ref: z2.string(),
6997
+ maxFiles: z2.number().optional()
5999
6998
  },
6000
6999
  async ({ ref, maxFiles }) => {
6001
7000
  const summary = await orch.diffSummary(ref);
@@ -6015,7 +7014,7 @@ async function startMcpServer() {
6015
7014
  server.tool(
6016
7015
  "request_review",
6017
7016
  '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.',
6018
- { ref: z.string().describe("Worktree thread id/ref to review") },
7017
+ { ref: z2.string().describe("Worktree thread id/ref to review") },
6019
7018
  async ({ ref }) => {
6020
7019
  try {
6021
7020
  const tab = await orch.requestReview(ref);
@@ -6040,7 +7039,7 @@ async function startMcpServer() {
6040
7039
  }
6041
7040
  }
6042
7041
  );
6043
- const agentEnum = z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7042
+ const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
6044
7043
  server.tool(
6045
7044
  "list_models",
6046
7045
  "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.",
@@ -6063,11 +7062,11 @@ async function startMcpServer() {
6063
7062
  "fork_worktree",
6064
7063
  "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.",
6065
7064
  {
6066
- ref: z.string().describe("Worktree thread id/ref to fork"),
6067
- through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7065
+ ref: z2.string().describe("Worktree thread id/ref to fork"),
7066
+ through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6068
7067
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6069
- model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6070
- title: z.string().optional()
7068
+ model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7069
+ title: z2.string().optional()
6071
7070
  },
6072
7071
  async ({ ref, through_index, agent, model, title }) => {
6073
7072
  try {
@@ -6106,13 +7105,13 @@ async function startMcpServer() {
6106
7105
  );
6107
7106
  server.tool(
6108
7107
  "fork_chat",
6109
- "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). Remote coordinators 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.",
7108
+ "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.",
6110
7109
  {
6111
- ref: z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
6112
- through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7110
+ ref: z2.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
7111
+ through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6113
7112
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6114
- model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6115
- title: z.string().optional()
7113
+ model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7114
+ title: z2.string().optional()
6116
7115
  },
6117
7116
  async ({ ref, through_index, agent, model, title }) => {
6118
7117
  try {
@@ -6158,8 +7157,8 @@ async function startMcpServer() {
6158
7157
  "run_dev_script",
6159
7158
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
6160
7159
  {
6161
- ref: z.string(),
6162
- name: z.string().optional()
7160
+ ref: z2.string(),
7161
+ name: z2.string().optional()
6163
7162
  },
6164
7163
  async ({ ref, name }) => {
6165
7164
  const result = await orch.startDev(ref, name);
@@ -6181,7 +7180,7 @@ async function startMcpServer() {
6181
7180
  server.tool(
6182
7181
  "list_run_scripts",
6183
7182
  "List named run scripts available for a thread",
6184
- { ref: z.string() },
7183
+ { ref: z2.string() },
6185
7184
  async ({ ref }) => {
6186
7185
  const scripts = orch.listThreadRunScripts(ref);
6187
7186
  const active = orch.getActiveRuns(ref);
@@ -6199,8 +7198,8 @@ async function startMcpServer() {
6199
7198
  "stop_dev_script",
6200
7199
  "Stop a running script for a thread (all scripts if name omitted)",
6201
7200
  {
6202
- ref: z.string(),
6203
- name: z.string().optional()
7201
+ ref: z2.string(),
7202
+ name: z2.string().optional()
6204
7203
  },
6205
7204
  async ({ ref, name }) => {
6206
7205
  orch.stopDev(ref, name);
@@ -6210,7 +7209,7 @@ async function startMcpServer() {
6210
7209
  server.tool(
6211
7210
  "run_setup",
6212
7211
  "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
6213
- { ref: z.string() },
7212
+ { ref: z2.string() },
6214
7213
  async ({ ref }) => {
6215
7214
  const result = await orch.runSetup(ref);
6216
7215
  return {
@@ -6221,7 +7220,7 @@ async function startMcpServer() {
6221
7220
  server.tool(
6222
7221
  "add_workspace",
6223
7222
  "Register a git repo as a Sideboard workspace",
6224
- { repoPath: z.string() },
7223
+ { repoPath: z2.string() },
6225
7224
  async ({ repoPath }) => {
6226
7225
  const ws = await orch.addWorkspace(repoPath);
6227
7226
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -6230,7 +7229,7 @@ async function startMcpServer() {
6230
7229
  server.tool(
6231
7230
  "remove_workspace",
6232
7231
  "Unregister a Sideboard workspace (does not archive threads)",
6233
- { repoPath: z.string() },
7232
+ { repoPath: z2.string() },
6234
7233
  async ({ repoPath }) => {
6235
7234
  orch.removeWorkspace(repoPath);
6236
7235
  return { content: [{ type: "text", text: "ok" }] };
@@ -6240,12 +7239,12 @@ async function startMcpServer() {
6240
7239
  "fanout",
6241
7240
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
6242
7241
  {
6243
- prompt: z.string(),
6244
- agents: z.array(z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
6245
- repoPath: z.string(),
6246
- sourceType: z.enum(["branch", "pr", "ticket"]).optional(),
6247
- sourceRef: z.string().optional(),
6248
- title: z.string().optional()
7242
+ prompt: z2.string(),
7243
+ agents: z2.array(z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
7244
+ repoPath: z2.string(),
7245
+ sourceType: z2.enum(["branch", "pr", "ticket"]).optional(),
7246
+ sourceRef: z2.string().optional(),
7247
+ title: z2.string().optional()
6249
7248
  },
6250
7249
  async (args) => {
6251
7250
  const threads = await orch.bestOfN(args);
@@ -6272,8 +7271,8 @@ async function startMcpServer() {
6272
7271
  "list_branches",
6273
7272
  "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).",
6274
7273
  {
6275
- repoPath: z.string(),
6276
- unmergedOnly: z.boolean().optional()
7274
+ repoPath: z2.string(),
7275
+ unmergedOnly: z2.boolean().optional()
6277
7276
  },
6278
7277
  async ({ repoPath, unmergedOnly }) => {
6279
7278
  const root = await resolveRepoRoot(repoPath);
@@ -6293,7 +7292,7 @@ async function startMcpServer() {
6293
7292
  server.tool(
6294
7293
  "list_prs",
6295
7294
  "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.",
6296
- { repoPath: z.string() },
7295
+ { repoPath: z2.string() },
6297
7296
  async ({ repoPath }) => {
6298
7297
  const root = await resolveRepoRoot(repoPath);
6299
7298
  const prs = await listPrs(root);
@@ -6312,7 +7311,7 @@ async function startMcpServer() {
6312
7311
  server.tool(
6313
7312
  "get_pr_stack",
6314
7313
  "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.",
6315
- { ref: z.string() },
7314
+ { ref: z2.string() },
6316
7315
  async ({ ref }) => {
6317
7316
  const stack = await orch.getPrStack(ref);
6318
7317
  return {
@@ -6324,8 +7323,8 @@ async function startMcpServer() {
6324
7323
  "open_pr_stack_layers",
6325
7324
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
6326
7325
  {
6327
- ref: z.string(),
6328
- layer: z.number().int().positive().optional()
7326
+ ref: z2.string(),
7327
+ layer: z2.number().int().positive().optional()
6329
7328
  },
6330
7329
  async ({ ref, layer }) => {
6331
7330
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -6359,9 +7358,9 @@ async function startMcpServer() {
6359
7358
  "add_stack_layer",
6360
7359
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
6361
7360
  {
6362
- ref: z.string(),
6363
- branchName: z.string(),
6364
- title: z.string().optional()
7361
+ ref: z2.string(),
7362
+ branchName: z2.string(),
7363
+ title: z2.string().optional()
6365
7364
  },
6366
7365
  async ({ ref, branchName, title }) => {
6367
7366
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -6390,11 +7389,11 @@ async function startMcpServer() {
6390
7389
  "create_pr_stack",
6391
7390
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
6392
7391
  {
6393
- repoPath: z.string(),
6394
- branches: z.array(z.string()).min(1),
6395
- agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
6396
- base: z.string().optional(),
6397
- title: z.string().optional()
7392
+ repoPath: z2.string(),
7393
+ branches: z2.array(z2.string()).min(1),
7394
+ agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
7395
+ base: z2.string().optional(),
7396
+ title: z2.string().optional()
6398
7397
  },
6399
7398
  async (args) => {
6400
7399
  const result = await orch.createPrStack({
@@ -6432,7 +7431,7 @@ async function startMcpServer() {
6432
7431
  server.tool(
6433
7432
  "list_issues",
6434
7433
  "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.",
6435
- { repoPath: z.string() },
7434
+ { repoPath: z2.string() },
6436
7435
  async ({ repoPath }) => {
6437
7436
  const root = await resolveRepoRoot(repoPath);
6438
7437
  const result = await listIssues(root);
@@ -6443,8 +7442,8 @@ async function startMcpServer() {
6443
7442
  "list_linear_issues",
6444
7443
  "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
6445
7444
  {
6446
- agent: z.enum(["claude", "codex", "opencode"]),
6447
- repoPath: z.string()
7445
+ agent: z2.enum(["claude", "codex", "opencode"]),
7446
+ repoPath: z2.string()
6448
7447
  },
6449
7448
  async ({ agent, repoPath }) => {
6450
7449
  const issues = await listLinearIssues(agent, repoPath);