@sideboard-ai/core 0.1.50 → 0.1.52

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 (46) hide show
  1. package/dist/{agents-LUB3Q773.js → agents-ON6RKKND.js} +3 -3
  2. package/dist/{agents-3P4N6KHC.js → agents-YKSS6VBO.js} +3 -3
  3. package/dist/{chunk-RQI4TOXK.js → chunk-6QTZVJ7A.js} +7 -5
  4. package/dist/{chunk-AJ6ROGD7.js → chunk-7LNGIP3X.js} +20 -2
  5. package/dist/chunk-B3SJXYIJ.js +24 -0
  6. package/dist/{chunk-ILQK4P5R.js → chunk-BXJ76RHF.js} +4 -4
  7. package/dist/{chunk-SS34TVSY.js → chunk-D3METLRW.js} +7 -5
  8. package/dist/{chunk-FV6FN6V5.js → chunk-DZFH2KLT.js} +340 -22
  9. package/dist/chunk-FKOIHGKV.js +21 -0
  10. package/dist/{chunk-3NAS4MWH.js → chunk-GNML24AW.js} +2 -2
  11. package/dist/{chunk-E35APBHV.js → chunk-H6GGDLYS.js} +1 -1
  12. package/dist/{chunk-EOYDCKQC.js → chunk-HLEX5AQ6.js} +4 -0
  13. package/dist/{chunk-ZQCQIWIP.js → chunk-LRLKJM3O.js} +2 -1
  14. package/dist/chunk-N5PM7HGQ.js +103 -0
  15. package/dist/chunk-QTUESPAW.js +101 -0
  16. package/dist/{chunk-O6W3P7V3.js → chunk-TSRXOSVD.js} +4 -0
  17. package/dist/{chunk-GZXBYHJC.js → chunk-UEAHMGHW.js} +2 -1
  18. package/dist/{chunk-ZVV5EEQN.js → chunk-XOU6HNQJ.js} +305 -22
  19. package/dist/{chunk-5WYEJ3F3.js → chunk-XX5BB7NV.js} +3 -3
  20. package/dist/{chunk-V4JRXYYU.js → chunk-YDXQ72MD.js} +2 -2
  21. package/dist/{chunk-DF5VQQKA.js → chunk-YOWIYAVA.js} +3 -3
  22. package/dist/{chunk-ISY4EGF6.js → chunk-ZNM4MAN4.js} +20 -2
  23. package/dist/{connected-teams-UBXHZGO4.js → connected-teams-2ANNZCP5.js} +2 -2
  24. package/dist/{connected-teams-GF52Q7LB.js → connected-teams-ONC4666A.js} +2 -2
  25. package/dist/{coordinator-prompt-VG4BZ5JL.js → coordinator-prompt-6FXVTSFN.js} +5 -4
  26. package/dist/{coordinator-prompt-SPGDT7J5.js → coordinator-prompt-S6JZD5EF.js} +5 -4
  27. package/dist/{global-workspace-OJF4ENH4.js → global-workspace-EV4G2WMQ.js} +6 -5
  28. package/dist/{global-workspace-KSR3E63K.js → global-workspace-MSX2K27Y.js} +6 -5
  29. package/dist/index.cjs +1369 -155
  30. package/dist/index.d.cts +397 -14
  31. package/dist/index.d.ts +397 -14
  32. package/dist/index.js +811 -90
  33. package/dist/mcp/run-stdio.cjs +1136 -147
  34. package/dist/mcp/run-stdio.js +635 -79
  35. package/dist/plan-file-6O7G4VPQ.js +23 -0
  36. package/dist/plan-file-PHVKUAEE.js +25 -0
  37. package/dist/{run-HMRSRG3U.js → run-H3IMNOYN.js} +3 -1
  38. package/dist/run-ZJKYHXGN.js +12 -0
  39. package/dist/{thread-store-XICUWFNM.js → thread-store-GHOADGL2.js} +1 -1
  40. package/dist/{thread-store-OV2X6PYO.js → thread-store-UJIGMI5J.js} +1 -1
  41. package/dist/{workspaces-OZE7LRDO.js → workspaces-3RQQZQRO.js} +7 -6
  42. package/dist/{workspaces-UJX7JIGH.js → workspaces-AYTBR6KQ.js} +7 -6
  43. package/dist/{worktree-XG5PCLZY.js → worktree-5KEQWSAF.js} +8 -3
  44. package/dist/{worktree-TEDAJ57S.js → worktree-RWGL7FUV.js} +8 -3
  45. package/package.json +1 -1
  46. package/dist/run-LF6E5IKL.js +0 -10
@@ -15,14 +15,19 @@ import {
15
15
  pushTurnStderr,
16
16
  resolveQuotaFallbackAgent,
17
17
  summarizeTurnStderr
18
- } from "../chunk-RQI4TOXK.js";
19
- import "../chunk-E35APBHV.js";
18
+ } from "../chunk-6QTZVJ7A.js";
19
+ import "../chunk-H6GGDLYS.js";
20
20
  import {
21
21
  addWorkspace,
22
22
  ensureWorkspace,
23
23
  removeWorkspace,
24
24
  syncWorkspacesFromThreads
25
- } from "../chunk-V4JRXYYU.js";
25
+ } from "../chunk-YDXQ72MD.js";
26
+ import {
27
+ extractPresentedPlan,
28
+ readPlanFile,
29
+ writePlanFile
30
+ } from "../chunk-N5PM7HGQ.js";
26
31
  import {
27
32
  childEnvWithAppSettings,
28
33
  getLinearApiKey,
@@ -41,7 +46,7 @@ import {
41
46
  isGlobalThread,
42
47
  isOrchestratorThread,
43
48
  orchestratorSessionPoisonedByBuiltins
44
- } from "../chunk-DF5VQQKA.js";
49
+ } from "../chunk-YOWIYAVA.js";
45
50
  import {
46
51
  assertOrchestratorCapableAgent
47
52
  } from "../chunk-S2LL5HA4.js";
@@ -50,21 +55,27 @@ import {
50
55
  coordinatorTurnReminder,
51
56
  enrichWorkspacesWithGithub,
52
57
  ensureGlobalCoordinatorCwd
53
- } from "../chunk-GZXBYHJC.js";
58
+ } from "../chunk-UEAHMGHW.js";
54
59
  import {
60
+ addPrStackLayer,
55
61
  allocateTeamName,
56
62
  allocateTeamSlug,
57
63
  commitAll,
64
+ createExistingBranchWorktree,
58
65
  createOrUpdatePr,
59
66
  createThreadWorktree,
67
+ detectGhStack,
60
68
  fetchPrHead,
61
69
  formatGhLandError,
62
70
  getPr,
63
71
  getPrChecks,
64
72
  getPrDetails,
65
73
  getPrMeta,
74
+ getPrStack,
75
+ initPrStack,
66
76
  isDirty,
67
77
  isPlaceholderBranch,
78
+ isSideboardScratchPath,
68
79
  listBranches,
69
80
  listPrs,
70
81
  listWorktrees,
@@ -81,7 +92,12 @@ import {
81
92
  takenSlugsFromThread,
82
93
  threadDisplayLabel,
83
94
  worktreeNameFromPath
84
- } from "../chunk-ZVV5EEQN.js";
95
+ } from "../chunk-XOU6HNQJ.js";
96
+ import {
97
+ ATTACHMENTS_DIR,
98
+ LEGACY_ATTACHMENTS_DIR,
99
+ attachmentsGitignoreBody
100
+ } from "../chunk-B3SJXYIJ.js";
85
101
  import {
86
102
  appendMessage,
87
103
  createEmptyThread,
@@ -93,13 +109,13 @@ import {
93
109
  updateThread,
94
110
  withThreadLock,
95
111
  writeThread
96
- } from "../chunk-EOYDCKQC.js";
112
+ } from "../chunk-HLEX5AQ6.js";
97
113
  import {
98
114
  ensureAgentPath,
99
115
  gh,
100
116
  git,
101
117
  run
102
- } from "../chunk-ISY4EGF6.js";
118
+ } from "../chunk-ZNM4MAN4.js";
103
119
  import "../chunk-KGZBYWZ3.js";
104
120
  import {
105
121
  loadRepoSettings,
@@ -118,7 +134,7 @@ import { basename as basename4 } from "path";
118
134
 
119
135
  // src/orchestrator/orchestrator.ts
120
136
  import { EventEmitter } from "events";
121
- import { existsSync as existsSync12 } from "fs";
137
+ import { existsSync as existsSync13 } from "fs";
122
138
 
123
139
  // src/agents/spawn.ts
124
140
  import { createInterface } from "readline";
@@ -160,6 +176,9 @@ function toolDescription(name, input) {
160
176
  if (/present_artifact$/i.test(name)) {
161
177
  return str(input?.title) ? `Present ${str(input?.title)}` : "Present artifact";
162
178
  }
179
+ if (/present_plan$/i.test(name)) {
180
+ return str(input?.title) ? `Plan ${str(input?.title)}` : "Present plan";
181
+ }
163
182
  if (/present_schema$/i.test(name)) {
164
183
  return str(input?.title) ? `Schema ${str(input?.title)}` : "Present schema";
165
184
  }
@@ -356,9 +375,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
356
375
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
357
376
  );
358
377
  }
359
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-OJF4ENH4.js");
378
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-EV4G2WMQ.js");
360
379
  if (isGlobalThread2(thread)) {
361
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-VG4BZ5JL.js");
380
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-6FXVTSFN.js");
362
381
  ensureGlobalCoordinatorCwd2();
363
382
  }
364
383
  if (isOrchestratorThread(thread)) {
@@ -1082,7 +1101,7 @@ async function requireAgent(agent, opts) {
1082
1101
  }
1083
1102
 
1084
1103
  // src/threads/create.ts
1085
- async function createThread(input, onSetupLine) {
1104
+ async function createThread(input, _onSetupLine) {
1086
1105
  await requireAgent(input.agent);
1087
1106
  const repoPath = await resolveRepoRoot(input.repoPath);
1088
1107
  if (!existsSync5(repoPath)) {
@@ -1140,26 +1159,10 @@ async function createThread(input, onSetupLine) {
1140
1159
  });
1141
1160
  writeThread(thread);
1142
1161
  await ensureWorkspace(repoPath);
1143
- try {
1144
- let setup = await runSetupScript(repoPath, worktreePath, onSetupLine);
1145
- if (!setup.ran) {
1146
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onSetupLine);
1147
- }
1148
- if (setup.ran && setup.exitCode !== 0 && setup.exitCode !== null) {
1149
- updateThread(thread.id, {
1150
- lastError: `Setup exited ${setup.exitCode} (thread is still usable)`
1151
- });
1152
- }
1153
- } catch (err) {
1154
- const message = err instanceof Error ? err.message : String(err);
1155
- updateThread(thread.id, {
1156
- lastError: `Setup failed: ${message}`
1157
- });
1158
- }
1159
1162
  return readThread(thread.id) ?? thread;
1160
1163
  }
1161
1164
  async function listLinearIssues(agent, repoPath) {
1162
- const { getAdapter: getAdapter2 } = await import("../agents-LUB3Q773.js");
1165
+ const { getAdapter: getAdapter2 } = await import("../agents-ON6RKKND.js");
1163
1166
  await requireAgent(agent, { requireLinear: true });
1164
1167
  const adapter = getAdapter2(agent);
1165
1168
  if (!adapter.listLinearIssues) {
@@ -1476,7 +1479,9 @@ function worktreeBindingFrom(from) {
1476
1479
  sourceIsFork: from.sourceIsFork,
1477
1480
  parentThreadId: from.parentThreadId,
1478
1481
  prUrl: from.prUrl,
1479
- prTitle: from.prTitle
1482
+ prTitle: from.prTitle,
1483
+ stackId: from.stackId,
1484
+ stackLayer: from.stackLayer
1480
1485
  };
1481
1486
  }
1482
1487
  function threadsSharingWorktree(worktreePath) {
@@ -1674,13 +1679,10 @@ File: src/client/frontends/desktop/core/UserData.ts
1674
1679
  // src/review/request-review.ts
1675
1680
  var REPO_REVIEW_PATH = ".sideboard/review.md";
1676
1681
  var REPO_REVIEW_NAME = "review.md";
1677
- var REVIEW_REQUEST_PATH = ".sideboard/attachments/Review request.md";
1682
+ var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
1683
+ var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
1678
1684
  var REVIEW_REQUEST_NAME = "Review request.md";
1679
1685
  var REVIEW_REQUEST_PREFILL = "Review.";
1680
- var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
1681
- *
1682
- !.gitignore
1683
- `;
1684
1686
  var LEGACY_REVIEW_TEMPLATE_MARKERS = [
1685
1687
  "You are acting as a reviewer for a proposed code change made by another engineer.",
1686
1688
  "HOW MANY FINDINGS TO RETURN:"
@@ -1703,10 +1705,10 @@ function readTextIfPresent(abs) {
1703
1705
  }
1704
1706
  }
1705
1707
  function ensureAttachmentsGitignore(worktreePath) {
1706
- const gitignoreAbs = join5(worktreePath, ".sideboard", "attachments", ".gitignore");
1708
+ const gitignoreAbs = join5(worktreePath, ATTACHMENTS_DIR, ".gitignore");
1707
1709
  if (existsSync6(gitignoreAbs)) return;
1708
1710
  mkdirSync2(dirname2(gitignoreAbs), { recursive: true });
1709
- writeFileSync(gitignoreAbs, ATTACHMENTS_GITIGNORE, "utf8");
1711
+ writeFileSync(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
1710
1712
  }
1711
1713
  function resolveReviewGuidelines(worktreePath) {
1712
1714
  const repoAbs = join5(worktreePath, REPO_REVIEW_PATH);
@@ -1729,6 +1731,16 @@ function resolveReviewGuidelines(worktreePath) {
1729
1731
  source: "local"
1730
1732
  };
1731
1733
  }
1734
+ const legacyAbs = join5(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
1735
+ const legacyContent = readTextIfPresent(legacyAbs);
1736
+ if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
1737
+ return {
1738
+ path: LEGACY_REVIEW_REQUEST_PATH,
1739
+ name: REVIEW_REQUEST_NAME,
1740
+ content: legacyContent,
1741
+ source: "local"
1742
+ };
1743
+ }
1732
1744
  ensureAttachmentsGitignore(worktreePath);
1733
1745
  mkdirSync2(dirname2(localAbs), { recursive: true });
1734
1746
  writeFileSync(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
@@ -1997,7 +2009,7 @@ async function adoptThread(input) {
1997
2009
  throw new Error(`Worktree not found: ${input.worktreePath}`);
1998
2010
  }
1999
2011
  const repoPath = await resolveRepoRoot(input.worktreePath);
2000
- const { stdout: branch } = await import("../run-HMRSRG3U.js").then(
2012
+ const { stdout: branch } = await import("../run-H3IMNOYN.js").then(
2001
2013
  ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], input.worktreePath)
2002
2014
  );
2003
2015
  const thread = createEmptyThread({
@@ -2012,7 +2024,7 @@ async function adoptThread(input) {
2012
2024
  messages: input.messages ?? []
2013
2025
  });
2014
2026
  writeThread(thread);
2015
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-OZE7LRDO.js");
2027
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-3RQQZQRO.js");
2016
2028
  await ensureWorkspace2(repoPath);
2017
2029
  return thread;
2018
2030
  }
@@ -2207,11 +2219,295 @@ async function importConductorWorkspaceAsync(workspaceId) {
2207
2219
  return importConductorWorkspace(workspaceId);
2208
2220
  }
2209
2221
 
2222
+ // src/threads/stack-layers.ts
2223
+ import { existsSync as existsSync8 } from "fs";
2224
+ function stackIdFrom(stack) {
2225
+ if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
2226
+ const key = stack.layers.map((l) => l.branchName).join("|");
2227
+ if (!key) return null;
2228
+ return `gh-stack-local-${hashShort(key)}`;
2229
+ }
2230
+ function hashShort(s) {
2231
+ let h = 0;
2232
+ for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
2233
+ return Math.abs(h).toString(36);
2234
+ }
2235
+ function requireThread3(idOrRef) {
2236
+ const thread = findThreadByRef(idOrRef) ?? readThread(idOrRef);
2237
+ if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
2238
+ return thread;
2239
+ }
2240
+ function sanitizeSlugPart(name) {
2241
+ return name.trim().replace(/^refs\/heads\//, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "layer";
2242
+ }
2243
+ function layerSlug(stackId, layer) {
2244
+ const stackPart = stackId.replace(/^gh-stack-/, "s");
2245
+ return sanitizeSlugPart(`${stackPart}-L${layer.position}-${layer.branchName}`);
2246
+ }
2247
+ function findThreadForStackLayer(repoPath, stackId, layer) {
2248
+ const threads = listThreads({ includeArchived: false }).filter(
2249
+ (t) => t.repoPath === repoPath && t.stackId === stackId
2250
+ );
2251
+ const byLayer = threads.find((t) => t.stackLayer === layer.position);
2252
+ if (byLayer) return byLayer;
2253
+ return threads.find((t) => t.branchName === layer.branchName) ?? listThreads({ includeArchived: false }).find(
2254
+ (t) => t.repoPath === repoPath && t.branchName === layer.branchName
2255
+ ) ?? null;
2256
+ }
2257
+ async function openStackLayer(input, _onSetupLine) {
2258
+ await requireAgent(input.agent);
2259
+ const repoPath = await resolveRepoRoot(input.repoPath);
2260
+ const stackId = stackIdFrom(input.stack);
2261
+ if (!stackId) throw new Error("Cannot open stack layer without a stack id");
2262
+ const existing = findThreadForStackLayer(repoPath, stackId, input.layer);
2263
+ if (existing) {
2264
+ const patch = {};
2265
+ if (existing.stackId !== stackId) patch.stackId = stackId;
2266
+ if (existing.stackLayer !== input.layer.position) {
2267
+ patch.stackLayer = input.layer.position;
2268
+ }
2269
+ if (input.layer.prUrl && existing.prUrl !== input.layer.prUrl) {
2270
+ patch.prUrl = input.layer.prUrl;
2271
+ }
2272
+ if (input.layer.title && existing.prTitle !== input.layer.title) {
2273
+ patch.prTitle = input.layer.title;
2274
+ }
2275
+ if (Object.keys(patch).length > 0) updateThread(existing.id, patch);
2276
+ return {
2277
+ thread: readThread(existing.id) ?? existing,
2278
+ createdWorktree: false
2279
+ };
2280
+ }
2281
+ let worktreePath = null;
2282
+ let branchName = input.layer.branchName;
2283
+ let createdWorktree = false;
2284
+ const trees = await listWorktrees(repoPath);
2285
+ const checkedOut = trees.find((w) => w.branch === branchName);
2286
+ if (checkedOut?.path && existsSync8(checkedOut.path)) {
2287
+ if (input.reuseExistingWorktree !== false) {
2288
+ worktreePath = checkedOut.path;
2289
+ } else {
2290
+ throw new Error(
2291
+ `Branch ${branchName} is already checked out at ${checkedOut.path}`
2292
+ );
2293
+ }
2294
+ }
2295
+ if (!worktreePath) {
2296
+ const slug = layerSlug(stackId, input.layer);
2297
+ const created = await createExistingBranchWorktree({
2298
+ repoPath,
2299
+ branchName,
2300
+ slug
2301
+ });
2302
+ worktreePath = created.worktreePath;
2303
+ branchName = created.branchName;
2304
+ copyConfiguredFiles(repoPath, worktreePath);
2305
+ createdWorktree = true;
2306
+ }
2307
+ const title = input.layer.title?.trim() || (input.layer.prNumber != null ? `PR #${input.layer.prNumber}` : input.layer.branchName);
2308
+ const thread = createEmptyThread({
2309
+ title,
2310
+ userSetTitle: Boolean(input.layer.title?.trim()),
2311
+ sourceType: input.layer.prNumber != null ? "pr" : "branch",
2312
+ sourceRef: input.layer.prNumber != null ? String(input.layer.prNumber) : input.layer.branchName,
2313
+ branchName,
2314
+ worktreePath,
2315
+ repoPath,
2316
+ agent: input.agent,
2317
+ autonomy: input.autonomy ?? "default",
2318
+ model: input.model ?? null,
2319
+ effort: input.effort ?? "high",
2320
+ fast: Boolean(input.fast),
2321
+ planMode: Boolean(input.planMode),
2322
+ parentThreadId: input.parentThreadId ?? null,
2323
+ status: "idle",
2324
+ prUrl: input.layer.prUrl,
2325
+ prTitle: input.layer.title ?? null,
2326
+ stackId,
2327
+ stackLayer: input.layer.position
2328
+ });
2329
+ writeThread(thread);
2330
+ await ensureWorkspace(repoPath);
2331
+ return { thread: readThread(thread.id) ?? thread, createdWorktree };
2332
+ }
2333
+ async function openPrStackLayers(input, onSetupLine) {
2334
+ const from = requireThread3(input.threadRef);
2335
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
2336
+ throw new Error("Thread has no worktree");
2337
+ }
2338
+ const stack = await getPrStack(from.worktreePath);
2339
+ if (!stack) throw new Error("Current branch is not part of a GitHub PR stack");
2340
+ const layers = input.layer != null ? stack.layers.filter((l) => l.position === input.layer) : stack.layers;
2341
+ if (!layers.length) {
2342
+ throw new Error(
2343
+ input.layer != null ? `No stack layer at position ${input.layer}` : "Stack has no layers"
2344
+ );
2345
+ }
2346
+ const threads = [];
2347
+ const createdThreadIds = [];
2348
+ for (const layer of layers) {
2349
+ const { thread, createdWorktree } = await openStackLayer(
2350
+ {
2351
+ repoPath: from.repoPath,
2352
+ stack,
2353
+ layer,
2354
+ agent: from.agent,
2355
+ autonomy: from.autonomy,
2356
+ model: from.model,
2357
+ effort: from.effort,
2358
+ fast: from.fast,
2359
+ planMode: from.planMode,
2360
+ parentThreadId: from.id
2361
+ },
2362
+ onSetupLine
2363
+ );
2364
+ threads.push(thread);
2365
+ if (createdWorktree) createdThreadIds.push(thread.id);
2366
+ }
2367
+ const stackId = stackIdFrom(stack);
2368
+ const current = stack.layers[stack.currentIndex];
2369
+ if (stackId) {
2370
+ updateThread(from.id, {
2371
+ stackId,
2372
+ stackLayer: current?.position ?? from.stackLayer
2373
+ });
2374
+ }
2375
+ return { stack, threads, createdThreadIds };
2376
+ }
2377
+ async function addStackLayerFromThread(input, onSetupLine) {
2378
+ const from = requireThread3(input.threadRef);
2379
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
2380
+ throw new Error("Thread has no worktree");
2381
+ }
2382
+ const status = await detectGhStack(from.worktreePath);
2383
+ if (!status.available) throw new Error(status.reason);
2384
+ await addPrStackLayer(from.worktreePath, input.branchName);
2385
+ const stack = await getPrStack(from.worktreePath);
2386
+ if (!stack) throw new Error("Stack not found after adding layer");
2387
+ const layer = stack.layers.find((l) => l.branchName === input.branchName.trim()) ?? stack.layers[stack.layers.length - 1];
2388
+ if (!layer) throw new Error("New stack layer not found");
2389
+ if (input.title?.trim()) {
2390
+ layer.title = input.title.trim();
2391
+ }
2392
+ const thread = await openStackLayer(
2393
+ {
2394
+ repoPath: from.repoPath,
2395
+ stack,
2396
+ layer,
2397
+ agent: from.agent,
2398
+ autonomy: from.autonomy,
2399
+ model: from.model,
2400
+ effort: from.effort,
2401
+ fast: from.fast,
2402
+ planMode: from.planMode,
2403
+ parentThreadId: from.id
2404
+ },
2405
+ onSetupLine
2406
+ );
2407
+ return { stack, thread: thread.thread, createdWorktree: thread.createdWorktree };
2408
+ }
2409
+ async function initStackFromThread(input, onSetupLine) {
2410
+ const from = requireThread3(input.threadRef);
2411
+ if (!from.worktreePath?.trim() || !from.branchName?.trim() || !from.repoPath?.trim()) {
2412
+ throw new Error("Thread has no worktree/branch");
2413
+ }
2414
+ const status = await detectGhStack(from.worktreePath);
2415
+ if (!status.available) throw new Error(status.reason);
2416
+ const branches = [
2417
+ from.branchName,
2418
+ ...(input.additionalBranches ?? []).map((b) => b.trim()).filter(Boolean)
2419
+ ];
2420
+ await initPrStack(from.worktreePath, branches, {
2421
+ base: input.base ?? await resolveDefaultBranch(from.repoPath)
2422
+ });
2423
+ return openPrStackLayers({ threadRef: from.id }, onSetupLine);
2424
+ }
2425
+ async function createPrStack(input, onSetupLine) {
2426
+ await requireAgent(input.agent);
2427
+ const repoPath = await resolveRepoRoot(input.repoPath);
2428
+ if (!existsSync8(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
2429
+ if (!input.branches.length) throw new Error("At least one branch name required");
2430
+ const status = await detectGhStack(repoPath);
2431
+ if (!status.available) throw new Error(status.reason);
2432
+ const team = allocateTeamSlug(repoPath);
2433
+ const base = input.base ?? await resolveDefaultBranch(repoPath);
2434
+ const bootstrap = await createThreadWorktree({
2435
+ repoPath,
2436
+ sourceRef: base,
2437
+ slug: `${team.slug}-stack-init`
2438
+ });
2439
+ copyConfiguredFiles(repoPath, bootstrap.worktreePath);
2440
+ try {
2441
+ await initPrStack(bootstrap.worktreePath, input.branches, { base });
2442
+ const bottom = input.branches[0];
2443
+ const co = await git(["checkout", bottom], bootstrap.worktreePath, {
2444
+ reject: false
2445
+ });
2446
+ if (co.exitCode !== 0) {
2447
+ throw new Error(
2448
+ co.stderr.trim() || co.stdout.trim() || `Could not check out ${bottom} after stack init`
2449
+ );
2450
+ }
2451
+ } catch (err) {
2452
+ try {
2453
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
2454
+ deleteBranch: bootstrap.branchName
2455
+ });
2456
+ } catch {
2457
+ }
2458
+ throw err;
2459
+ }
2460
+ const stack = await getPrStack(bootstrap.worktreePath);
2461
+ if (!stack) {
2462
+ try {
2463
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
2464
+ deleteBranch: bootstrap.branchName
2465
+ });
2466
+ } catch {
2467
+ }
2468
+ throw new Error("Stack init succeeded but gh stack view returned no stack");
2469
+ }
2470
+ const threads = [];
2471
+ const createdThreadIds = [];
2472
+ for (const layer of stack.layers) {
2473
+ const title = layer.position === 1 && input.title?.trim() ? input.title.trim() : layer.title;
2474
+ const opened = await openStackLayer(
2475
+ {
2476
+ repoPath,
2477
+ stack,
2478
+ layer: title ? { ...layer, title } : layer,
2479
+ agent: input.agent,
2480
+ autonomy: input.autonomy,
2481
+ model: input.model,
2482
+ effort: input.effort,
2483
+ fast: input.fast,
2484
+ planMode: input.planMode,
2485
+ reuseExistingWorktree: true
2486
+ },
2487
+ onSetupLine
2488
+ );
2489
+ threads.push(opened.thread);
2490
+ if (opened.createdWorktree || opened.thread.worktreePath === bootstrap.worktreePath) {
2491
+ createdThreadIds.push(opened.thread.id);
2492
+ }
2493
+ }
2494
+ const claimed = new Set(threads.map((t) => t.worktreePath));
2495
+ if (!claimed.has(bootstrap.worktreePath) && existsSync8(bootstrap.worktreePath)) {
2496
+ try {
2497
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
2498
+ deleteBranch: bootstrap.branchName
2499
+ });
2500
+ } catch {
2501
+ }
2502
+ }
2503
+ return { stack, threads, createdThreadIds };
2504
+ }
2505
+
2210
2506
  // src/diff/diff.ts
2211
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
2507
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
2212
2508
  import { dirname as dirname3, join as join7 } from "path";
2213
2509
  async function inspectGitWorktree(worktreePath) {
2214
- if (!worktreePath || !existsSync8(worktreePath)) return "missing_worktree";
2510
+ if (!worktreePath || !existsSync9(worktreePath)) return "missing_worktree";
2215
2511
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
2216
2512
  reject: false
2217
2513
  });
@@ -2219,7 +2515,7 @@ async function inspectGitWorktree(worktreePath) {
2219
2515
  return "ok";
2220
2516
  }
2221
2517
  async function initializeGitRepository(worktreePath) {
2222
- if (!worktreePath || !existsSync8(worktreePath)) {
2518
+ if (!worktreePath || !existsSync9(worktreePath)) {
2223
2519
  throw new Error("Worktree not found");
2224
2520
  }
2225
2521
  const status = await inspectGitWorktree(worktreePath);
@@ -2357,7 +2653,7 @@ async function listUntrackedPaths(worktreePath) {
2357
2653
  worktreePath,
2358
2654
  { reject: false }
2359
2655
  );
2360
- return stdout.split("\0").filter(Boolean);
2656
+ return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
2361
2657
  }
2362
2658
  async function listUntrackedFiles(worktreePath, maxHunk) {
2363
2659
  const paths = await listUntrackedPaths(worktreePath);
@@ -2834,7 +3130,7 @@ async function previewLand(thread) {
2834
3130
  const target = await resolveDefaultBranch(thread.repoPath);
2835
3131
  if (thread.branchName === target || thread.branchName === "main" || thread.branchName === "master") {
2836
3132
  }
2837
- const { stdout: head } = await import("../run-HMRSRG3U.js").then(
3133
+ const { stdout: head } = await import("../run-H3IMNOYN.js").then(
2838
3134
  ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], thread.worktreePath)
2839
3135
  );
2840
3136
  const current = head.trim();
@@ -2873,7 +3169,7 @@ async function confirmLand(thread, opts) {
2873
3169
  if (preview.dirty) {
2874
3170
  committed = await commitAll(thread.worktreePath, meta.commitMessage);
2875
3171
  }
2876
- const { git: git2 } = await import("../run-HMRSRG3U.js");
3172
+ const { git: git2 } = await import("../run-H3IMNOYN.js");
2877
3173
  const { stdout: headOut } = await git2(
2878
3174
  ["rev-parse", "--abbrev-ref", "HEAD"],
2879
3175
  thread.worktreePath,
@@ -2903,7 +3199,7 @@ async function confirmLand(thread, opts) {
2903
3199
  }
2904
3200
 
2905
3201
  // src/skills/discover.ts
2906
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
3202
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
2907
3203
  import { homedir } from "os";
2908
3204
  import { join as join8 } from "path";
2909
3205
  function toCommand(name) {
@@ -2956,7 +3252,7 @@ function readSkill(skillMd, source) {
2956
3252
  }
2957
3253
  }
2958
3254
  function scanSkillsDir(dir, source, out) {
2959
- if (!existsSync9(dir)) return;
3255
+ if (!existsSync10(dir)) return;
2960
3256
  let entries;
2961
3257
  try {
2962
3258
  entries = readdirSync4(dir);
@@ -2966,7 +3262,7 @@ function scanSkillsDir(dir, source, out) {
2966
3262
  for (const entry of entries) {
2967
3263
  if (entry.startsWith(".")) continue;
2968
3264
  const skillMd = join8(dir, entry, "SKILL.md");
2969
- if (!existsSync9(skillMd)) continue;
3265
+ if (!existsSync10(skillMd)) continue;
2970
3266
  try {
2971
3267
  if (!statSync3(skillMd).isFile()) continue;
2972
3268
  } catch {
@@ -2977,7 +3273,7 @@ function scanSkillsDir(dir, source, out) {
2977
3273
  }
2978
3274
  }
2979
3275
  function scanClaudePluginSkills(pluginsRoot, out) {
2980
- if (!existsSync9(pluginsRoot)) return;
3276
+ if (!existsSync10(pluginsRoot)) return;
2981
3277
  const walk = (dir, depth, lookingForSkillsDir) => {
2982
3278
  if (depth > 7) return;
2983
3279
  let entries;
@@ -3127,7 +3423,7 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
3127
3423
  }
3128
3424
 
3129
3425
  // src/composer/stage-files.ts
3130
- import { copyFileSync as copyFileSync3, existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync7, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
3426
+ import { copyFileSync as copyFileSync3, existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync7, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
3131
3427
  import { basename as basename3, extname, join as join9 } from "path";
3132
3428
  import { randomUUID as randomUUID4 } from "crypto";
3133
3429
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
@@ -3150,11 +3446,6 @@ var IMAGE_MIME_BY_EXT = {
3150
3446
  bmp: "image/bmp",
3151
3447
  ico: "image/x-icon"
3152
3448
  };
3153
- var ATTACHMENTS_DIR = ".sideboard/attachments";
3154
- var ATTACHMENTS_GITIGNORE2 = `# Sideboard review / composer attachments (local only)
3155
- *
3156
- !.gitignore
3157
- `;
3158
3449
  var MAX_INLINE_BYTES = 4e5;
3159
3450
  var MAX_PREVIEW_BYTES = 5e6;
3160
3451
  function fileExtension(filePath) {
@@ -3171,19 +3462,19 @@ function ensureAttachmentsDir(worktreePath) {
3171
3462
  const dir = join9(worktreePath, ATTACHMENTS_DIR);
3172
3463
  mkdirSync4(dir, { recursive: true });
3173
3464
  const gi = join9(dir, ".gitignore");
3174
- if (!existsSync10(gi)) {
3175
- writeFileSync3(gi, ATTACHMENTS_GITIGNORE2, "utf8");
3465
+ if (!existsSync11(gi)) {
3466
+ writeFileSync3(gi, attachmentsGitignoreBody(), "utf8");
3176
3467
  }
3177
3468
  return dir;
3178
3469
  }
3179
3470
  function uniqueAttachmentName(dir, originalName) {
3180
3471
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
3181
- if (!existsSync10(join9(dir, safe))) return safe;
3472
+ if (!existsSync11(join9(dir, safe))) return safe;
3182
3473
  const ext = extname(safe);
3183
3474
  const stem = ext ? safe.slice(0, -ext.length) : safe;
3184
3475
  for (let i = 1; i < 1e4; i++) {
3185
3476
  const candidate = `${stem}-${i}${ext}`;
3186
- if (!existsSync10(join9(dir, candidate))) return candidate;
3477
+ if (!existsSync11(join9(dir, candidate))) return candidate;
3187
3478
  }
3188
3479
  return `${stem}-${randomUUID4()}${ext}`;
3189
3480
  }
@@ -3316,7 +3607,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
3316
3607
  }
3317
3608
 
3318
3609
  // src/agents/instructions.ts
3319
- import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
3610
+ import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
3320
3611
  import { join as join10 } from "path";
3321
3612
  function normPath(p) {
3322
3613
  return p.replace(/\/+$/, "");
@@ -3456,7 +3747,7 @@ function loadAgentInstructions(worktreePath, agent) {
3456
3747
  for (const rel of candidates) {
3457
3748
  if (seen.has(rel)) continue;
3458
3749
  const abs = join10(worktreePath, rel);
3459
- if (!existsSync11(abs)) continue;
3750
+ if (!existsSync12(abs)) continue;
3460
3751
  try {
3461
3752
  if (!statSync5(abs).isFile()) continue;
3462
3753
  let content = readFileSync8(abs, "utf8");
@@ -3601,7 +3892,7 @@ var Orchestrator = class {
3601
3892
  }
3602
3893
  continue;
3603
3894
  }
3604
- if (!existsSync12(thread.worktreePath)) {
3895
+ if (!existsSync13(thread.worktreePath)) {
3605
3896
  setStatus(thread.id, "broken", "Worktree missing on disk");
3606
3897
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
3607
3898
  continue;
@@ -3746,14 +4037,9 @@ var Orchestrator = class {
3746
4037
  return findThreadByRef(idOrRef) ?? readThread(idOrRef);
3747
4038
  }
3748
4039
  async createThread(input) {
3749
- let thread = await createThread(input, (line) => {
3750
- this.emit({
3751
- type: "turn_output",
3752
- threadId: "pending",
3753
- event: { type: "stdout", data: line }
3754
- });
3755
- });
4040
+ let thread = await createThread(input);
3756
4041
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
4042
+ await this.runSetupAfterCreate(thread.id);
3757
4043
  const { autoRunAfterSetupEnabled } = await import("../app-settings-7XVDQJ7F.js");
3758
4044
  if (autoRunAfterSetupEnabled()) {
3759
4045
  try {
@@ -3767,6 +4053,19 @@ var Orchestrator = class {
3767
4053
  }
3768
4054
  return thread;
3769
4055
  }
4056
+ /** Run workspace setup after a new worktree is created (no-op if none configured). */
4057
+ async runSetupAfterCreate(threadId) {
4058
+ try {
4059
+ await this.runSetup(threadId);
4060
+ } catch (err) {
4061
+ const message = err instanceof Error ? err.message : String(err);
4062
+ if (/no setup script/i.test(message)) return;
4063
+ if (/already running/i.test(message)) return;
4064
+ updateThread(threadId, {
4065
+ lastError: `Setup failed: ${message}`
4066
+ });
4067
+ }
4068
+ }
3770
4069
  listWorkspaces() {
3771
4070
  const fromThreads = listThreads({ includeArchived: false }).map((t) => t.repoPath);
3772
4071
  return syncWorkspacesFromThreads(fromThreads);
@@ -4115,6 +4414,19 @@ var Orchestrator = class {
4115
4414
  });
4116
4415
  }
4117
4416
  const afterTurn = this.requireThread(threadId);
4417
+ if (afterTurn.planMode && afterTurn.worktreePath?.trim()) {
4418
+ const presented = extractPresentedPlan(parts);
4419
+ const exited = parts.some(
4420
+ (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
4421
+ );
4422
+ if (presented?.content) {
4423
+ writePlanFile(afterTurn.worktreePath, presented.content);
4424
+ } else if (exited || chatText && chatText.trim().length >= 400) {
4425
+ if (!readPlanFile(afterTurn.worktreePath) && chatText?.trim()) {
4426
+ writePlanFile(afterTurn.worktreePath, chatText.trim());
4427
+ }
4428
+ }
4429
+ }
4118
4430
  if (afterTurn.planMode && afterTurn.agent === "claude" && parts.some(
4119
4431
  (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
4120
4432
  )) {
@@ -4596,6 +4908,82 @@ var Orchestrator = class {
4596
4908
  }
4597
4909
  return meta;
4598
4910
  }
4911
+ async getPrStack(threadRef) {
4912
+ const thread = this.requireThread(threadRef);
4913
+ if (!thread.worktreePath?.trim()) return null;
4914
+ const stack = await getPrStack(thread.worktreePath);
4915
+ if (!stack) return null;
4916
+ const current = stack.currentIndex >= 0 ? stack.layers[stack.currentIndex] : null;
4917
+ const patch = {};
4918
+ if (stack.stackNumber != null) {
4919
+ const id = `gh-stack-${stack.stackNumber}`;
4920
+ if (thread.stackId !== id) patch.stackId = id;
4921
+ }
4922
+ if (current?.position != null && thread.stackLayer !== current.position) {
4923
+ patch.stackLayer = current.position;
4924
+ }
4925
+ if (current?.prUrl && current.prUrl !== thread.prUrl) patch.prUrl = current.prUrl;
4926
+ if (current?.title && current.title !== thread.prTitle) patch.prTitle = current.title;
4927
+ if (current?.branchName && current.branchName !== thread.branchName) {
4928
+ patch.branchName = current.branchName;
4929
+ }
4930
+ if (Object.keys(patch).length > 0) updateThread(thread.id, patch);
4931
+ return stack;
4932
+ }
4933
+ /** Open worktrees for all (or one) stack layers discovered from a thread. */
4934
+ async openPrStackLayers(threadRef, opts) {
4935
+ const result = await openPrStackLayers({ threadRef, layer: opts?.layer });
4936
+ for (const t of result.threads) {
4937
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
4938
+ }
4939
+ for (const id of result.createdThreadIds) {
4940
+ await this.runSetupAfterCreate(id);
4941
+ }
4942
+ return { stack: result.stack, threads: result.threads };
4943
+ }
4944
+ /** Add a branch on top of the thread's stack and open its worktree. */
4945
+ async addStackLayer(threadRef, branchName, opts) {
4946
+ const result = await addStackLayerFromThread({
4947
+ threadRef,
4948
+ branchName,
4949
+ title: opts?.title
4950
+ });
4951
+ this.emit({
4952
+ type: "status_changed",
4953
+ threadId: result.thread.id,
4954
+ status: result.thread.status
4955
+ });
4956
+ if (result.createdWorktree) {
4957
+ await this.runSetupAfterCreate(result.thread.id);
4958
+ }
4959
+ return { stack: result.stack, thread: result.thread };
4960
+ }
4961
+ /** Initialize a stack from the current thread branch (optional extra layers). */
4962
+ async initStackFromThread(threadRef, opts) {
4963
+ const result = await initStackFromThread({
4964
+ threadRef,
4965
+ additionalBranches: opts?.additionalBranches,
4966
+ base: opts?.base
4967
+ });
4968
+ for (const t of result.threads) {
4969
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
4970
+ }
4971
+ for (const id of result.createdThreadIds) {
4972
+ await this.runSetupAfterCreate(id);
4973
+ }
4974
+ return { stack: result.stack, threads: result.threads };
4975
+ }
4976
+ /** Create a new multi-layer stack with one worktree per layer. */
4977
+ async createPrStack(input) {
4978
+ const result = await createPrStack(input);
4979
+ for (const t of result.threads) {
4980
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
4981
+ }
4982
+ for (const id of result.createdThreadIds) {
4983
+ await this.runSetupAfterCreate(id);
4984
+ }
4985
+ return { stack: result.stack, threads: result.threads };
4986
+ }
4599
4987
  async getPrDetails(threadRef) {
4600
4988
  const { thread, selector, cwd } = await this.withPrSelector(threadRef);
4601
4989
  if (!selector) return null;
@@ -4658,14 +5046,9 @@ var Orchestrator = class {
4658
5046
  return forkChatTab(input);
4659
5047
  }
4660
5048
  async forkThreadWorktree(input) {
4661
- const thread = await forkThreadWorktree(input, (line) => {
4662
- this.emit({
4663
- type: "turn_output",
4664
- threadId: "pending",
4665
- event: { type: "stdout", data: line }
4666
- });
4667
- });
5049
+ const thread = await forkThreadWorktree(input);
4668
5050
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
5051
+ await this.runSetupAfterCreate(thread.id);
4669
5052
  return thread;
4670
5053
  }
4671
5054
  renameThread(threadRef, title) {
@@ -4679,7 +5062,7 @@ var Orchestrator = class {
4679
5062
  }
4680
5063
  /**
4681
5064
  * Stage OS / worktree files into composer attachments (copies external files
4682
- * into `.sideboard/attachments/` so agents can Read images and binaries).
5065
+ * into `.context/attachments/` so agents can Read images and binaries).
4683
5066
  */
4684
5067
  attachComposerFiles(threadRef, opts) {
4685
5068
  const thread = this.requireThread(threadRef);
@@ -4753,8 +5136,8 @@ var Orchestrator = class {
4753
5136
  updateThread(thread.id, { worktreePath: globalAgentCwd() });
4754
5137
  return setStatus(thread.id, "idle");
4755
5138
  }
4756
- if (!existsSync12(thread.worktreePath)) {
4757
- const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-XG5PCLZY.js");
5139
+ if (!existsSync13(thread.worktreePath)) {
5140
+ const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-5KEQWSAF.js");
4758
5141
  const { execa: execa6 } = await import("execa");
4759
5142
  const slug = thread.worktreePath.split("/").pop();
4760
5143
  const dest = thread.worktreePath;
@@ -5020,6 +5403,59 @@ async function startMcpServer() {
5020
5403
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
5021
5404
  }
5022
5405
  );
5406
+ server.tool(
5407
+ "ask_user",
5408
+ "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.",
5409
+ {
5410
+ questions: z.array(
5411
+ z.object({
5412
+ question: z.string().describe("Full question text ending with ?"),
5413
+ header: z.string().max(24).optional().describe("Short label shown above the question"),
5414
+ multiSelect: z.boolean().optional().describe("Allow selecting multiple options"),
5415
+ options: z.array(
5416
+ z.object({
5417
+ label: z.string(),
5418
+ description: z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
5419
+ })
5420
+ ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
5421
+ })
5422
+ ).min(1).max(4).describe("1\u20134 questions")
5423
+ },
5424
+ async ({ questions }) => {
5425
+ const payload = {
5426
+ ok: true,
5427
+ questions,
5428
+ message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
5429
+ };
5430
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
5431
+ }
5432
+ );
5433
+ server.tool(
5434
+ "present_plan",
5435
+ "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.",
5436
+ {
5437
+ title: z.string().optional().describe("Short plan title (defaults to Plan)"),
5438
+ content: z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
5439
+ thread_id: z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
5440
+ },
5441
+ async ({ title, content, thread_id }) => {
5442
+ const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
5443
+ let root = process.cwd();
5444
+ if (thread_id?.trim()) {
5445
+ const t = orch.getThread(thread_id.trim());
5446
+ if (t?.worktreePath?.trim()) root = t.worktreePath;
5447
+ }
5448
+ const path = writePlanFile2(root, content);
5449
+ const payload = {
5450
+ ok: true,
5451
+ path,
5452
+ title: title?.trim() || "Plan",
5453
+ content,
5454
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
5455
+ };
5456
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
5457
+ }
5458
+ );
5023
5459
  server.tool(
5024
5460
  "present_schema",
5025
5461
  "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.",
@@ -5598,6 +6034,126 @@ async function startMcpServer() {
5598
6034
  };
5599
6035
  }
5600
6036
  );
6037
+ server.tool(
6038
+ "get_pr_stack",
6039
+ "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.",
6040
+ { ref: z.string() },
6041
+ async ({ ref }) => {
6042
+ const stack = await orch.getPrStack(ref);
6043
+ return {
6044
+ content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
6045
+ };
6046
+ }
6047
+ );
6048
+ server.tool(
6049
+ "open_pr_stack_layers",
6050
+ "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
6051
+ {
6052
+ ref: z.string(),
6053
+ layer: z.number().int().positive().optional()
6054
+ },
6055
+ async ({ ref, layer }) => {
6056
+ const result = await orch.openPrStackLayers(ref, { layer });
6057
+ return {
6058
+ content: [
6059
+ {
6060
+ type: "text",
6061
+ text: JSON.stringify(
6062
+ {
6063
+ stackNumber: result.stack.stackNumber,
6064
+ trunk: result.stack.trunk,
6065
+ threads: result.threads.map((t) => ({
6066
+ id: t.id,
6067
+ title: t.title,
6068
+ branchName: t.branchName,
6069
+ stackLayer: t.stackLayer,
6070
+ worktreePath: t.worktreePath,
6071
+ prUrl: t.prUrl,
6072
+ link: `sideboard://thread/${t.id}`
6073
+ }))
6074
+ },
6075
+ null,
6076
+ 2
6077
+ )
6078
+ }
6079
+ ]
6080
+ };
6081
+ }
6082
+ );
6083
+ server.tool(
6084
+ "add_stack_layer",
6085
+ "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
6086
+ {
6087
+ ref: z.string(),
6088
+ branchName: z.string(),
6089
+ title: z.string().optional()
6090
+ },
6091
+ async ({ ref, branchName, title }) => {
6092
+ const result = await orch.addStackLayer(ref, branchName, { title });
6093
+ return {
6094
+ content: [
6095
+ {
6096
+ type: "text",
6097
+ text: JSON.stringify(
6098
+ {
6099
+ id: result.thread.id,
6100
+ title: result.thread.title,
6101
+ branchName: result.thread.branchName,
6102
+ stackLayer: result.thread.stackLayer,
6103
+ worktreePath: result.thread.worktreePath,
6104
+ link: `sideboard://thread/${result.thread.id}`
6105
+ },
6106
+ null,
6107
+ 2
6108
+ )
6109
+ }
6110
+ ]
6111
+ };
6112
+ }
6113
+ );
6114
+ server.tool(
6115
+ "create_pr_stack",
6116
+ "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
6117
+ {
6118
+ repoPath: z.string(),
6119
+ branches: z.array(z.string()).min(1),
6120
+ agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
6121
+ base: z.string().optional(),
6122
+ title: z.string().optional()
6123
+ },
6124
+ async (args) => {
6125
+ const result = await orch.createPrStack({
6126
+ repoPath: args.repoPath,
6127
+ branches: args.branches,
6128
+ agent: args.agent,
6129
+ base: args.base,
6130
+ title: args.title
6131
+ });
6132
+ return {
6133
+ content: [
6134
+ {
6135
+ type: "text",
6136
+ text: JSON.stringify(
6137
+ {
6138
+ stackNumber: result.stack.stackNumber,
6139
+ trunk: result.stack.trunk,
6140
+ threads: result.threads.map((t) => ({
6141
+ id: t.id,
6142
+ title: t.title,
6143
+ branchName: t.branchName,
6144
+ stackLayer: t.stackLayer,
6145
+ worktreePath: t.worktreePath,
6146
+ link: `sideboard://thread/${t.id}`
6147
+ }))
6148
+ },
6149
+ null,
6150
+ 2
6151
+ )
6152
+ }
6153
+ ]
6154
+ };
6155
+ }
6156
+ );
5601
6157
  server.tool(
5602
6158
  "list_issues",
5603
6159
  "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.",