@sideboard-ai/core 0.1.51 → 0.1.53

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 (40) hide show
  1. package/dist/agents/cursor-runner.cjs +43 -4
  2. package/dist/agents/cursor-runner.js +45 -6
  3. package/dist/{agents-HIEJL3UV.js → agents-KP7UJEHJ.js} +1 -1
  4. package/dist/{agents-5ROTZNCX.js → agents-KYACODJ3.js} +2 -2
  5. package/dist/{chunk-5ZPSH7VI.js → chunk-A6HVEMIB.js} +16 -7
  6. package/dist/chunk-B3SJXYIJ.js +24 -0
  7. package/dist/{chunk-7EUWSBWR.js → chunk-BZST4HMJ.js} +21 -6
  8. package/dist/{chunk-ZH5QZ4CR.js → chunk-DZFH2KLT.js} +280 -7
  9. package/dist/chunk-FKOIHGKV.js +21 -0
  10. package/dist/{chunk-E4VVEKAM.js → chunk-GNML24AW.js} +2 -2
  11. package/dist/{chunk-EOYDCKQC.js → chunk-HLEX5AQ6.js} +4 -0
  12. package/dist/{chunk-K3WMKGFY.js → chunk-LRLKJM3O.js} +2 -1
  13. package/dist/chunk-N5PM7HGQ.js +103 -0
  14. package/dist/chunk-QTUESPAW.js +101 -0
  15. package/dist/{chunk-O6W3P7V3.js → chunk-TSRXOSVD.js} +4 -0
  16. package/dist/{chunk-F4Q3IM6V.js → chunk-UEAHMGHW.js} +2 -1
  17. package/dist/{chunk-J5JTEJ5O.js → chunk-VG22SETP.js} +6 -0
  18. package/dist/{chunk-XRSAGVRW.js → chunk-XOU6HNQJ.js} +245 -7
  19. package/dist/{chunk-O5DOO7DP.js → chunk-XX5BB7NV.js} +3 -3
  20. package/dist/{chunk-QN7XNQAT.js → chunk-YDXQ72MD.js} +2 -2
  21. package/dist/{chunk-YFJ4FG2P.js → chunk-YOWIYAVA.js} +3 -3
  22. package/dist/{coordinator-prompt-7HHJRO7B.js → coordinator-prompt-6FXVTSFN.js} +4 -3
  23. package/dist/{coordinator-prompt-WD7FAMA2.js → coordinator-prompt-S6JZD5EF.js} +4 -3
  24. package/dist/{global-workspace-OJEPGDXA.js → global-workspace-EV4G2WMQ.js} +5 -4
  25. package/dist/{global-workspace-ECYN2MKL.js → global-workspace-MSX2K27Y.js} +5 -4
  26. package/dist/index.cjs +1383 -149
  27. package/dist/index.d.cts +389 -15
  28. package/dist/index.d.ts +389 -15
  29. package/dist/index.js +883 -91
  30. package/dist/mcp/run-stdio.cjs +1154 -141
  31. package/dist/mcp/run-stdio.js +709 -79
  32. package/dist/plan-file-6O7G4VPQ.js +23 -0
  33. package/dist/plan-file-PHVKUAEE.js +25 -0
  34. package/dist/{thread-store-XICUWFNM.js → thread-store-GHOADGL2.js} +1 -1
  35. package/dist/{thread-store-OV2X6PYO.js → thread-store-UJIGMI5J.js} +1 -1
  36. package/dist/{workspaces-MUU7RGVV.js → workspaces-3RQQZQRO.js} +6 -5
  37. package/dist/{workspaces-ZWOOFZUV.js → workspaces-AYTBR6KQ.js} +6 -5
  38. package/dist/{worktree-DVNDMWZ7.js → worktree-5KEQWSAF.js} +5 -2
  39. package/dist/{worktree-GDV56MX4.js → worktree-RWGL7FUV.js} +5 -2
  40. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -916,6 +916,8 @@ function normalizeThread(raw) {
916
916
  agentPid: raw.agentPid ?? null,
917
917
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
918
918
  prTitle: raw.prTitle ?? null,
919
+ stackId: raw.stackId ?? null,
920
+ stackLayer: raw.stackLayer ?? null,
919
921
  userSetTitle: Boolean(raw.userSetTitle),
920
922
  activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
921
923
  quotaResumeAt: raw.quotaResumeAt ?? null,
@@ -940,6 +942,8 @@ function createEmptyThread(partial) {
940
942
  activeRuns: partial.activeRuns ?? [],
941
943
  prUrl: partial.prUrl ?? null,
942
944
  prTitle: partial.prTitle ?? null,
945
+ stackId: partial.stackId ?? null,
946
+ stackLayer: partial.stackLayer ?? null,
943
947
  userSetTitle: partial.userSetTitle ?? false,
944
948
  messages: partial.messages ?? [],
945
949
  attachments: partial.attachments ?? [],
@@ -2196,6 +2200,234 @@ var init_pr_gates = __esm({
2196
2200
  }
2197
2201
  });
2198
2202
 
2203
+ // src/git/stack.ts
2204
+ async function detectGhStack(cwd) {
2205
+ const now = Date.now();
2206
+ if (cachedStatus && now - cachedStatus.at < STATUS_TTL_MS) {
2207
+ return cachedStatus.status;
2208
+ }
2209
+ const probe = await gh(["stack", "view", "--help"], cwd, { reject: false });
2210
+ if (probe.exitCode === 0) {
2211
+ const status2 = { available: true };
2212
+ cachedStatus = { at: now, status: status2 };
2213
+ return status2;
2214
+ }
2215
+ const err = `${probe.stderr}
2216
+ ${probe.stdout}`;
2217
+ const reason = /official extension|extension install|github\/gh-stack/i.test(err) ? "Install with: gh extension install github/gh-stack" : err.trim() || "gh stack is not available";
2218
+ const status = { available: false, reason };
2219
+ cachedStatus = { at: now, status };
2220
+ return status;
2221
+ }
2222
+ function resetGhStackDetectCache() {
2223
+ cachedStatus = null;
2224
+ }
2225
+ function str(v) {
2226
+ return typeof v === "string" ? v : v == null ? "" : String(v);
2227
+ }
2228
+ function num(v) {
2229
+ if (typeof v === "number" && Number.isFinite(v)) return v;
2230
+ if (typeof v === "string" && v.trim() && Number.isFinite(Number(v))) {
2231
+ return Number(v);
2232
+ }
2233
+ return null;
2234
+ }
2235
+ function parseGhStackViewJson(raw) {
2236
+ let data;
2237
+ try {
2238
+ data = JSON.parse(raw);
2239
+ } catch {
2240
+ return null;
2241
+ }
2242
+ if (!Array.isArray(data.branches) || data.branches.length === 0) return null;
2243
+ const trunk = str(data.trunk) || "main";
2244
+ const currentBranch2 = str(data.currentBranch);
2245
+ const stackNumber = num(data.stackNumber) ?? num(data.number);
2246
+ const layers = [];
2247
+ for (let i = 0; i < data.branches.length; i++) {
2248
+ const b = data.branches[i];
2249
+ if (!b || typeof b !== "object") continue;
2250
+ const name = str(b.name);
2251
+ if (!name) continue;
2252
+ const pr = b.pr && typeof b.pr === "object" ? b.pr : null;
2253
+ layers.push({
2254
+ position: i + 1,
2255
+ branchName: name,
2256
+ headSha: str(b.head) || void 0,
2257
+ baseSha: str(b.base) || void 0,
2258
+ isCurrent: Boolean(b.isCurrent) || name === currentBranch2,
2259
+ isMerged: Boolean(b.isMerged),
2260
+ isQueued: Boolean(b.isQueued),
2261
+ needsRebase: Boolean(b.needsRebase),
2262
+ prNumber: pr ? num(pr.number) : null,
2263
+ prUrl: pr && str(pr.url) ? str(pr.url) : null,
2264
+ prState: pr && str(pr.state) ? str(pr.state).toUpperCase() : null,
2265
+ title: pr && str(pr.title) ? str(pr.title) : void 0
2266
+ });
2267
+ }
2268
+ if (!layers.length) return null;
2269
+ let currentIndex = layers.findIndex((l) => l.isCurrent);
2270
+ if (currentIndex < 0 && currentBranch2) {
2271
+ currentIndex = layers.findIndex((l) => l.branchName === currentBranch2);
2272
+ }
2273
+ const { readyToMerge, blockedReason } = stackMergeReadiness(layers, currentIndex);
2274
+ return {
2275
+ stackNumber,
2276
+ trunk,
2277
+ currentBranch: currentBranch2 || layers[currentIndex]?.branchName || layers[0].branchName,
2278
+ layers,
2279
+ currentIndex,
2280
+ readyToMerge,
2281
+ blockedReason
2282
+ };
2283
+ }
2284
+ function stackMergeReadiness(layers, throughIndex) {
2285
+ if (throughIndex < 0 || throughIndex >= layers.length) {
2286
+ return { readyToMerge: false, blockedReason: "Not on a stack layer" };
2287
+ }
2288
+ for (let i = 0; i <= throughIndex; i++) {
2289
+ const layer = layers[i];
2290
+ if (layer.isMerged) continue;
2291
+ if (!layer.prNumber) {
2292
+ return {
2293
+ readyToMerge: false,
2294
+ blockedReason: `Layer ${layer.branchName} has no pull request yet`
2295
+ };
2296
+ }
2297
+ if (layer.needsRebase) {
2298
+ return {
2299
+ readyToMerge: false,
2300
+ blockedReason: `PR #${layer.prNumber} needs rebase`
2301
+ };
2302
+ }
2303
+ const state = (layer.prState ?? "").toUpperCase();
2304
+ if (state && state !== "OPEN" && state !== "QUEUED") {
2305
+ return {
2306
+ readyToMerge: false,
2307
+ blockedReason: `PR #${layer.prNumber} is ${state}`
2308
+ };
2309
+ }
2310
+ }
2311
+ return { readyToMerge: true, blockedReason: null };
2312
+ }
2313
+ async function getPrStack(cwd) {
2314
+ const status = await detectGhStack(cwd);
2315
+ if (!status.available) return null;
2316
+ const result = await gh(["stack", "view", "--json"], cwd, { reject: false });
2317
+ if (result.exitCode === 2) return null;
2318
+ if (result.exitCode !== 0) {
2319
+ if (/not in a stack|no stack/i.test(`${result.stderr}
2320
+ ${result.stdout}`)) {
2321
+ return null;
2322
+ }
2323
+ if (result.exitCode === 9) return null;
2324
+ return null;
2325
+ }
2326
+ const json = result.stdout.trim();
2327
+ if (!json) return null;
2328
+ return parseGhStackViewJson(json);
2329
+ }
2330
+ async function isInPrStack(cwd) {
2331
+ return Boolean(await getPrStack(cwd));
2332
+ }
2333
+ async function mergePrStack(cwd, opts) {
2334
+ const status = await detectGhStack(cwd);
2335
+ if (!status.available) {
2336
+ throw new Error(status.reason);
2337
+ }
2338
+ const method = opts.method ?? "squash";
2339
+ const methodFlag = method === "rebase" ? "--rebase" : method === "merge" ? "--merge" : "--squash";
2340
+ const args = [
2341
+ "stack",
2342
+ "merge",
2343
+ String(opts.through),
2344
+ "--yes",
2345
+ methodFlag
2346
+ ];
2347
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
2348
+ if (exitCode !== 0) {
2349
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack merge failed");
2350
+ }
2351
+ return { stdout };
2352
+ }
2353
+ async function initPrStack(cwd, branches, opts) {
2354
+ if (!branches.length) throw new Error("initPrStack requires at least one branch name");
2355
+ const status = await detectGhStack(cwd);
2356
+ if (!status.available) throw new Error(status.reason);
2357
+ const args = ["stack", "init"];
2358
+ if (opts?.base) args.push("--base", opts.base);
2359
+ args.push(...branches);
2360
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
2361
+ if (exitCode !== 0) {
2362
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack init failed");
2363
+ }
2364
+ }
2365
+ async function addPrStackLayer(cwd, branchName) {
2366
+ if (!branchName.trim()) throw new Error("branch name required");
2367
+ const status = await detectGhStack(cwd);
2368
+ if (!status.available) throw new Error(status.reason);
2369
+ const { exitCode, stderr, stdout } = await gh(
2370
+ ["stack", "add", branchName.trim()],
2371
+ cwd,
2372
+ { reject: false }
2373
+ );
2374
+ if (exitCode !== 0) {
2375
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack add failed");
2376
+ }
2377
+ }
2378
+ async function submitPrStack(cwd, opts) {
2379
+ const status = await detectGhStack(cwd);
2380
+ if (!status.available) throw new Error(status.reason);
2381
+ const args = ["stack", "submit", "--auto"];
2382
+ if (opts?.open) args.push("--open");
2383
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
2384
+ if (exitCode !== 0) {
2385
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack submit failed");
2386
+ }
2387
+ }
2388
+ async function checkoutPrStackLayer(cwd, target) {
2389
+ const status = await detectGhStack(cwd);
2390
+ if (!status.available) throw new Error(status.reason);
2391
+ const { exitCode, stderr, stdout } = await gh(
2392
+ ["stack", "checkout", String(target)],
2393
+ cwd,
2394
+ { reject: false }
2395
+ );
2396
+ if (exitCode !== 0) {
2397
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack checkout failed");
2398
+ }
2399
+ }
2400
+ var cachedStatus, STATUS_TTL_MS;
2401
+ var init_stack = __esm({
2402
+ "src/git/stack.ts"() {
2403
+ "use strict";
2404
+ init_run();
2405
+ cachedStatus = null;
2406
+ STATUS_TTL_MS = 6e4;
2407
+ }
2408
+ });
2409
+
2410
+ // src/paths/workspace-scratch.ts
2411
+ function attachmentsGitignoreBody() {
2412
+ return ATTACHMENTS_GITIGNORE;
2413
+ }
2414
+ function isWorkspaceScratchPath(relativePath) {
2415
+ const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
2416
+ return p === ATTACHMENTS_DIR || p.startsWith(`${ATTACHMENTS_DIR}/`) || p === LEGACY_ATTACHMENTS_DIR || p.startsWith(`${LEGACY_ATTACHMENTS_DIR}/`) || p === ".context" || p.startsWith(".context/");
2417
+ }
2418
+ var ATTACHMENTS_DIR, LEGACY_ATTACHMENTS_DIR, ATTACHMENTS_GITIGNORE;
2419
+ var init_workspace_scratch = __esm({
2420
+ "src/paths/workspace-scratch.ts"() {
2421
+ "use strict";
2422
+ ATTACHMENTS_DIR = ".context/attachments";
2423
+ LEGACY_ATTACHMENTS_DIR = ".sideboard/attachments";
2424
+ ATTACHMENTS_GITIGNORE = `# Sideboard / workspace attachments (local only)
2425
+ *
2426
+ !.gitignore
2427
+ `;
2428
+ }
2429
+ });
2430
+
2199
2431
  // src/git/worktree.ts
2200
2432
  var worktree_exports = {};
2201
2433
  __export(worktree_exports, {
@@ -2205,6 +2437,7 @@ __export(worktree_exports, {
2205
2437
  branchDisplayLabel: () => branchDisplayLabel,
2206
2438
  collectTakenTeamSlugs: () => collectTakenTeamSlugs,
2207
2439
  commitAll: () => commitAll,
2440
+ createExistingBranchWorktree: () => createExistingBranchWorktree,
2208
2441
  createOrUpdatePr: () => createOrUpdatePr,
2209
2442
  createThreadWorktree: () => createThreadWorktree,
2210
2443
  currentBranch: () => currentBranch,
@@ -2918,6 +3151,52 @@ ${add.stdout}`;
2918
3151
  await ensureGhPreferOrigin(worktreePath);
2919
3152
  return { branchName, worktreePath };
2920
3153
  }
3154
+ async function createExistingBranchWorktree(opts) {
3155
+ const branchName = opts.branchName.trim();
3156
+ if (!branchName) throw new Error("branch name required");
3157
+ const worktreePath = (0, import_node_path6.join)(worktreesRoot(opts.repoPath), opts.slug);
3158
+ if ((0, import_node_fs5.existsSync)(worktreePath)) {
3159
+ throw new Error(`Worktree already exists at ${worktreePath}`);
3160
+ }
3161
+ await ensureGhPreferOrigin(opts.repoPath);
3162
+ await git(["fetch", "origin", "--prune"], opts.repoPath, { reject: false });
3163
+ if (!branchName.startsWith("origin/") && !branchName.startsWith("refs/")) {
3164
+ await git(["fetch", "origin", branchName], opts.repoPath, { reject: false });
3165
+ }
3166
+ const existing = await listWorktrees(opts.repoPath);
3167
+ const already = existing.find((w) => w.branch === branchName);
3168
+ if (already?.path) {
3169
+ throw new Error(
3170
+ `Branch ${branchName} is already checked out at ${already.path}`
3171
+ );
3172
+ }
3173
+ const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
3174
+ const add = await git(
3175
+ ["worktree", "add", worktreePath, startPoint],
3176
+ opts.repoPath,
3177
+ { reject: false }
3178
+ );
3179
+ if (add.exitCode !== 0) {
3180
+ const retry = await git(
3181
+ ["worktree", "add", worktreePath, branchName],
3182
+ opts.repoPath,
3183
+ { reject: false }
3184
+ );
3185
+ if (retry.exitCode !== 0) {
3186
+ throw new Error(
3187
+ `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
3188
+ );
3189
+ }
3190
+ }
3191
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
3192
+ reject: false
3193
+ });
3194
+ if (head.stdout.trim() === "HEAD" || head.stdout.trim() !== branchName) {
3195
+ await git(["checkout", "-B", branchName], worktreePath, { reject: false });
3196
+ }
3197
+ await ensureGhPreferOrigin(worktreePath);
3198
+ return { branchName, worktreePath };
3199
+ }
2921
3200
  async function removeWorktree(repoPath, worktreePath, opts) {
2922
3201
  await git(["worktree", "remove", "--force", worktreePath], repoPath, {
2923
3202
  reject: false
@@ -2957,8 +3236,7 @@ async function isDirty(worktreePath) {
2957
3236
  return false;
2958
3237
  }
2959
3238
  function isSideboardScratchPath(relativePath) {
2960
- const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
2961
- return p === ".sideboard/attachments" || p.startsWith(".sideboard/attachments/");
3239
+ return isWorkspaceScratchPath(relativePath);
2962
3240
  }
2963
3241
  function porcelainStatusPath(line) {
2964
3242
  const rest = line.length >= 3 ? line.slice(3) : "";
@@ -2986,7 +3264,7 @@ async function pushBranch(worktreePath, branchName) {
2986
3264
  }
2987
3265
  async function mergePr(cwd, selector, opts) {
2988
3266
  const slug = await resolveGithubRepoSlug(cwd);
2989
- const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft"];
3267
+ const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft,number"];
2990
3268
  if (slug) viewArgs.push("--repo", slug);
2991
3269
  const before = await gh(viewArgs, cwd, { reject: false });
2992
3270
  if (before.exitCode !== 0 || !before.stdout.trim()) {
@@ -2994,16 +3272,29 @@ async function mergePr(cwd, selector, opts) {
2994
3272
  }
2995
3273
  let url = "";
2996
3274
  let isDraft = false;
3275
+ let prNumber = null;
2997
3276
  try {
2998
3277
  const parsed = JSON.parse(before.stdout);
2999
3278
  url = String(parsed.url ?? "");
3000
3279
  isDraft = Boolean(parsed.isDraft);
3280
+ prNumber = typeof parsed.number === "number" && Number.isFinite(parsed.number) ? parsed.number : null;
3001
3281
  if (String(parsed.state ?? "").toUpperCase() === "MERGED") {
3002
3282
  return { url, state: "MERGED" };
3003
3283
  }
3004
3284
  } catch {
3005
3285
  throw new Error("Could not parse pull request details");
3006
3286
  }
3287
+ const stack = await getPrStack(cwd);
3288
+ const stackLayer = stack && prNumber != null ? stack.layers.find((l) => l.prNumber === prNumber) : null;
3289
+ if (stack && stackLayer && prNumber != null) {
3290
+ const throughIndex = stack.layers.findIndex((l) => l.prNumber === prNumber);
3291
+ const gate = stackMergeReadiness(stack.layers, throughIndex);
3292
+ if (!gate.readyToMerge) {
3293
+ throw new Error(gate.blockedReason || "Stack is not ready to merge");
3294
+ }
3295
+ await mergePrStack(cwd, { through: prNumber, method: opts?.method ?? "squash" });
3296
+ return { url, state: "MERGED" };
3297
+ }
3007
3298
  if (isDraft) {
3008
3299
  const readyArgs = ["pr", "ready", selector];
3009
3300
  if (slug) readyArgs.push("--repo", slug);
@@ -3025,10 +3316,10 @@ async function mergePr(cwd, selector, opts) {
3025
3316
  const after = await gh(viewArgs, cwd, { reject: false });
3026
3317
  if (after.exitCode === 0 && after.stdout.trim()) {
3027
3318
  try {
3028
- const parsed = JSON.parse(after.stdout);
3319
+ const parsed = after.stdout ? JSON.parse(after.stdout) : null;
3029
3320
  return {
3030
- url: String(parsed.url ?? url),
3031
- state: String(parsed.state ?? "MERGED")
3321
+ url: String(parsed?.url ?? url),
3322
+ state: String(parsed?.state ?? "MERGED")
3032
3323
  };
3033
3324
  } catch {
3034
3325
  }
@@ -3176,7 +3467,9 @@ var init_worktree = __esm({
3176
3467
  init_gh_errors();
3177
3468
  init_run();
3178
3469
  init_pr_gates();
3470
+ init_stack();
3179
3471
  init_teams();
3472
+ init_workspace_scratch();
3180
3473
  init_worktree_labels();
3181
3474
  }
3182
3475
  });
@@ -3302,6 +3595,7 @@ var init_coordinator_prompt = __esm({
3302
3595
  "Discover:",
3303
3596
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3304
3597
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3598
+ "- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
3305
3599
  "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3306
3600
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3307
3601
  "Workspaces:",
@@ -4022,6 +4316,11 @@ function summarizeTurnStderr(tail, maxChars = 500) {
4022
4316
  if (joined.length <= maxChars) return joined;
4023
4317
  return joined.slice(joined.length - maxChars);
4024
4318
  }
4319
+ function looksLikeInvalidAgentSession(text) {
4320
+ const lower = text.trim().toLowerCase();
4321
+ if (!lower) return false;
4322
+ return /session not found/.test(lower) || /no conversation found/.test(lower) || /conversation .+ not found/.test(lower) || /thread .+ not found/.test(lower) || /unknown session/.test(lower) || /invalid session/.test(lower) || /session .+ (missing|expired|deleted|gone)/.test(lower) || /cannot resume/.test(lower) || /failed to (load|resume|open) session/.test(lower);
4323
+ }
4025
4324
  function looksLikeAgentFailureMessage(text) {
4026
4325
  const lower = text.trim().toLowerCase();
4027
4326
  if (!lower) return false;
@@ -4713,7 +5012,9 @@ var init_injected_mcp = __esm({
4713
5012
  SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS = [
4714
5013
  "mcp__sideboard__present_artifact",
4715
5014
  "mcp__sideboard__present_schema",
4716
- "mcp__sideboard__present_files"
5015
+ "mcp__sideboard__present_files",
5016
+ "mcp__sideboard__ask_user",
5017
+ "mcp__sideboard__present_plan"
4717
5018
  ];
4718
5019
  BRIGHTSY_MCP_ALLOWED_TOOLS = [
4719
5020
  "mcp__brightsy",
@@ -4756,7 +5057,7 @@ var PLAN_MODE_INSTRUCTION;
4756
5057
  var init_types = __esm({
4757
5058
  "src/agents/types.ts"() {
4758
5059
  "use strict";
4759
- PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or explicitly asks you to implement). Analyze the codebase, search and read files as needed, and produce or refine a clear implementation plan. Do not modify, create, or delete any files. Do not exit plan mode on your own.";
5060
+ PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or Approves / Hands off the plan). Analyze the codebase, search and read files as needed, and produce or refine a clear implementation plan. Do not modify, create, or delete any project files except via Sideboard MCP present_plan (writes .context/attachments/plan.md). When you need a clarifying decision (approach forks, auth choice, scope): (1) first write a short chat message that explains the decision and what each option means (tradeoffs, when to pick it) \u2014 do not leave the user staring at bare labels; (2) then call Sideboard MCP ask_user with the same options, including a description on every option. Sideboard shows questions in the composer and mirrors them in chat. After ask_user, wait for the user's next message with their answers before finalizing the plan. When the plan is ready for approval: (1) call present_plan with the full markdown plan (title + content) so Sideboard saves .context/attachments/plan.md and shows it in chat for Approve / Hand off / Copy; (2) Claude should also call ExitPlanMode after present_plan. Do not skip present_plan \u2014 the plan must be a markdown file, not only chat prose.";
4760
5061
  }
4761
5062
  });
4762
5063
 
@@ -5828,6 +6129,7 @@ var init_opencode = __esm({
5828
6129
  }
5829
6130
  },
5830
6131
  async resolveSessionId(worktreePath, cached) {
6132
+ const cachedId = cached?.trim() || null;
5831
6133
  const listed = await run(
5832
6134
  "opencode",
5833
6135
  ["session", "list", "--format", "json"],
@@ -5839,15 +6141,21 @@ var init_opencode = __esm({
5839
6141
  if (Array.isArray(sessions) && sessions.length > 0) {
5840
6142
  const norm = (p) => p.replace(/\/+$/, "");
5841
6143
  const wt = norm(worktreePath);
5842
- const match = sessions.find(
6144
+ const forWorktree = sessions.filter(
5843
6145
  (s) => s.directory && norm(s.directory) === wt || s.path && norm(s.path) === wt
5844
6146
  );
5845
- if (match?.id) return match.id;
6147
+ if (cachedId && forWorktree.some((s) => s.id === cachedId)) {
6148
+ return cachedId;
6149
+ }
6150
+ if (cachedId && sessions.some((s) => s.id === cachedId)) {
6151
+ return cachedId;
6152
+ }
6153
+ return null;
5846
6154
  }
5847
6155
  } catch {
5848
6156
  }
5849
6157
  }
5850
- return cached;
6158
+ return cachedId;
5851
6159
  },
5852
6160
  async buildAttach(thread) {
5853
6161
  const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
@@ -6345,6 +6653,116 @@ var init_agents = __esm({
6345
6653
  }
6346
6654
  });
6347
6655
 
6656
+ // src/plan/plan-present.ts
6657
+ function asRecord2(v) {
6658
+ return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
6659
+ }
6660
+ function isPresentPlanToolName(name) {
6661
+ if (!name) return false;
6662
+ return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
6663
+ }
6664
+ function extractPresentedPlan(parts) {
6665
+ if (!parts?.length) return null;
6666
+ for (let i = parts.length - 1; i >= 0; i--) {
6667
+ const p = parts[i];
6668
+ if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
6669
+ const input = asRecord2(p.input) ?? {};
6670
+ const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
6671
+ if (!content.trim()) continue;
6672
+ const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
6673
+ const path = typeof input.path === "string" && input.path.trim() ? input.path.trim() : PLAN_FILE_REL;
6674
+ return { title, content: content.trim(), path, source: "present_plan" };
6675
+ }
6676
+ return null;
6677
+ }
6678
+ function resolvePlanMarkdown(opts) {
6679
+ const fromTool = extractPresentedPlan(opts.parts);
6680
+ if (fromTool) return fromTool;
6681
+ const file = opts.fileContent?.trim();
6682
+ if (file) {
6683
+ return {
6684
+ title: "Plan",
6685
+ content: file,
6686
+ path: PLAN_FILE_REL,
6687
+ source: "exit_plan"
6688
+ };
6689
+ }
6690
+ const text = opts.text?.trim();
6691
+ if (text && text.length >= 80) {
6692
+ return {
6693
+ title: "Plan",
6694
+ content: text,
6695
+ path: PLAN_FILE_REL,
6696
+ source: "text"
6697
+ };
6698
+ }
6699
+ return null;
6700
+ }
6701
+ var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
6702
+ var init_plan_present = __esm({
6703
+ "src/plan/plan-present.ts"() {
6704
+ "use strict";
6705
+ init_workspace_scratch();
6706
+ PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
6707
+ PLAN_FILE_NAME = "plan.md";
6708
+ LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
6709
+ }
6710
+ });
6711
+
6712
+ // src/plan/plan-file.ts
6713
+ var plan_file_exports = {};
6714
+ __export(plan_file_exports, {
6715
+ LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
6716
+ PLAN_FILE_NAME: () => PLAN_FILE_NAME,
6717
+ PLAN_FILE_REL: () => PLAN_FILE_REL,
6718
+ extractPresentedPlan: () => extractPresentedPlan,
6719
+ isPresentPlanToolName: () => isPresentPlanToolName,
6720
+ planFileAbs: () => planFileAbs,
6721
+ readPlanFile: () => readPlanFile,
6722
+ resolvePlanMarkdown: () => resolvePlanMarkdown,
6723
+ writePlanFile: () => writePlanFile
6724
+ });
6725
+ function ensureAttachmentsGitignore2(worktreePath) {
6726
+ const gitignoreAbs = (0, import_node_path24.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
6727
+ if ((0, import_node_fs26.existsSync)(gitignoreAbs)) return;
6728
+ (0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(gitignoreAbs), { recursive: true });
6729
+ (0, import_node_fs26.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
6730
+ }
6731
+ function planFileAbs(worktreePath) {
6732
+ return (0, import_node_path24.join)(worktreePath, PLAN_FILE_REL);
6733
+ }
6734
+ function readTextIfPresent2(abs) {
6735
+ if (!(0, import_node_fs26.existsSync)(abs)) return null;
6736
+ try {
6737
+ const content = (0, import_node_fs26.readFileSync)(abs, "utf8");
6738
+ return content.trim() ? content : null;
6739
+ } catch {
6740
+ return null;
6741
+ }
6742
+ }
6743
+ function readPlanFile(worktreePath) {
6744
+ return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path24.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path24.join)(worktreePath, LEGACY_PLAN_FILE_REL));
6745
+ }
6746
+ function writePlanFile(worktreePath, content) {
6747
+ ensureAttachmentsGitignore2(worktreePath);
6748
+ const abs = planFileAbs(worktreePath);
6749
+ (0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(abs), { recursive: true });
6750
+ const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
6751
+ (0, import_node_fs26.writeFileSync)(abs, body, "utf8");
6752
+ return PLAN_FILE_REL;
6753
+ }
6754
+ var import_node_fs26, import_node_path24;
6755
+ var init_plan_file = __esm({
6756
+ "src/plan/plan-file.ts"() {
6757
+ "use strict";
6758
+ import_node_fs26 = require("fs");
6759
+ import_node_path24 = require("path");
6760
+ init_workspace_scratch();
6761
+ init_plan_present();
6762
+ init_plan_present();
6763
+ }
6764
+ });
6765
+
6348
6766
  // src/agents/cursor-recover.ts
6349
6767
  var cursor_recover_exports = {};
6350
6768
  __export(cursor_recover_exports, {
@@ -6353,10 +6771,10 @@ __export(cursor_recover_exports, {
6353
6771
  function recoverFinishedCursorRun(opts) {
6354
6772
  const agentId = opts.agentId.trim();
6355
6773
  if (!agentId) return null;
6356
- const runsPath = (0, import_node_path24.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6357
- if (!(0, import_node_fs25.existsSync)(runsPath)) return null;
6774
+ const runsPath = (0, import_node_path25.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6775
+ if (!(0, import_node_fs27.existsSync)(runsPath)) return null;
6358
6776
  try {
6359
- const lines = (0, import_node_fs25.readFileSync)(runsPath, "utf8").split("\n");
6777
+ const lines = (0, import_node_fs27.readFileSync)(runsPath, "utf8").split("\n");
6360
6778
  let best = null;
6361
6779
  for (const line of lines) {
6362
6780
  const trimmed = line.trim();
@@ -6382,12 +6800,12 @@ function recoverFinishedCursorRun(opts) {
6382
6800
  return null;
6383
6801
  }
6384
6802
  }
6385
- var import_node_fs25, import_node_path24;
6803
+ var import_node_fs27, import_node_path25;
6386
6804
  var init_cursor_recover = __esm({
6387
6805
  "src/agents/cursor-recover.ts"() {
6388
6806
  "use strict";
6389
- import_node_fs25 = require("fs");
6390
- import_node_path24 = require("path");
6807
+ import_node_fs27 = require("fs");
6808
+ import_node_path25 = require("path");
6391
6809
  init_paths();
6392
6810
  }
6393
6811
  });
@@ -6426,6 +6844,7 @@ var init_title = __esm({
6426
6844
  // src/index.ts
6427
6845
  var index_exports = {};
6428
6846
  __export(index_exports, {
6847
+ ATTACHMENTS_DIR: () => ATTACHMENTS_DIR,
6429
6848
  BRIGHTSY_MCP_ALLOWED_TOOLS: () => BRIGHTSY_MCP_ALLOWED_TOOLS,
6430
6849
  BrightsySideboardApi: () => BrightsySideboardApi,
6431
6850
  CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
@@ -6441,11 +6860,16 @@ __export(index_exports, {
6441
6860
  FAMOUS_SOCCER_TEAMS: () => FAMOUS_SOCCER_TEAMS,
6442
6861
  GLOBAL_WORKSPACE_ID: () => GLOBAL_WORKSPACE_ID,
6443
6862
  HARNESS_ENV_KEYS: () => HARNESS_ENV_KEYS,
6863
+ LEGACY_ATTACHMENTS_DIR: () => LEGACY_ATTACHMENTS_DIR,
6864
+ LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
6865
+ LEGACY_REVIEW_REQUEST_PATH: () => LEGACY_REVIEW_REQUEST_PATH,
6444
6866
  MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS: () => MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS,
6445
6867
  ORCHESTRATOR_AGENT_KINDS: () => ORCHESTRATOR_AGENT_KINDS,
6446
6868
  Orchestrator: () => Orchestrator,
6447
6869
  PASTE_ATTACH_MIN_CHARS: () => PASTE_ATTACH_MIN_CHARS,
6448
6870
  PASTE_ATTACH_MIN_LINES: () => PASTE_ATTACH_MIN_LINES,
6871
+ PLAN_FILE_NAME: () => PLAN_FILE_NAME,
6872
+ PLAN_FILE_REL: () => PLAN_FILE_REL,
6449
6873
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
6450
6874
  REPO_REVIEW_NAME: () => REPO_REVIEW_NAME,
6451
6875
  REPO_REVIEW_PATH: () => REPO_REVIEW_PATH,
@@ -6455,6 +6879,8 @@ __export(index_exports, {
6455
6879
  SIDEBOARD_FORCE_STOP: () => SIDEBOARD_FORCE_STOP,
6456
6880
  SIDEBOARD_MCP_ALLOWED_TOOLS: () => SIDEBOARD_MCP_ALLOWED_TOOLS,
6457
6881
  THINKING_EFFORTS: () => THINKING_EFFORTS,
6882
+ addPrStackLayer: () => addPrStackLayer,
6883
+ addStackLayerFromThread: () => addStackLayerFromThread,
6458
6884
  addWorkspace: () => addWorkspace,
6459
6885
  adoptThread: () => adoptThread,
6460
6886
  allAdapters: () => allAdapters,
@@ -6473,6 +6899,7 @@ __export(index_exports, {
6473
6899
  attachmentFromAbsolutePath: () => attachmentFromAbsolutePath,
6474
6900
  attachmentsFromBuffers: () => attachmentsFromBuffers,
6475
6901
  attachmentsFromWorktreePaths: () => attachmentsFromWorktreePaths,
6902
+ attachmentsGitignoreBody: () => attachmentsGitignoreBody,
6476
6903
  autoCleanupOrphansEnabled: () => autoCleanupOrphansEnabled,
6477
6904
  autoRenameBranchEnabled: () => autoRenameBranchEnabled,
6478
6905
  autoRunAfterSetupEnabled: () => autoRunAfterSetupEnabled,
@@ -6495,6 +6922,7 @@ __export(index_exports, {
6495
6922
  caffeinateWhileRunningEnabled: () => caffeinateWhileRunningEnabled,
6496
6923
  captureLoginEnv: () => captureLoginEnv,
6497
6924
  captureTurnBaseline: () => captureTurnBaseline,
6925
+ checkoutPrStackLayer: () => checkoutPrStackLayer,
6498
6926
  childEnvWithAppSettings: () => childEnvWithAppSettings,
6499
6927
  claudeAdapter: () => claudeAdapter,
6500
6928
  claudeChromeEnabled: () => claudeChromeEnabled,
@@ -6514,8 +6942,10 @@ __export(index_exports, {
6514
6942
  countCacheControlBlocks: () => countCacheControlBlocks,
6515
6943
  createChatTab: () => createChatTab,
6516
6944
  createEmptyThread: () => createEmptyThread,
6945
+ createExistingBranchWorktree: () => createExistingBranchWorktree,
6517
6946
  createGlobalChat: () => createGlobalChat,
6518
6947
  createOrUpdatePr: () => createOrUpdatePr,
6948
+ createPrStack: () => createPrStack,
6519
6949
  createThread: () => createThread,
6520
6950
  createThreadWorktree: () => createThreadWorktree,
6521
6951
  currentBranch: () => currentBranch,
@@ -6525,6 +6955,7 @@ __export(index_exports, {
6525
6955
  deleteBranchOnPurgeEnabled: () => deleteBranchOnPurgeEnabled,
6526
6956
  deleteThreadRecord: () => deleteThreadRecord,
6527
6957
  detectAgents: () => detectAgents,
6958
+ detectGhStack: () => detectGhStack,
6528
6959
  detectLocalMergeConflicts: () => detectLocalMergeConflicts,
6529
6960
  disconnectBrightsyTeam: () => disconnectBrightsyTeam,
6530
6961
  discoverSkills: () => discoverSkills,
@@ -6540,12 +6971,15 @@ __export(index_exports, {
6540
6971
  estimateThreadChars: () => estimateThreadChars,
6541
6972
  expandComposerPrompt: () => expandComposerPrompt,
6542
6973
  extractGhErrorDetail: () => extractGhErrorDetail,
6974
+ extractPendingPlanQuestions: () => extractPendingPlanQuestions,
6975
+ extractPresentedPlan: () => extractPresentedPlan,
6543
6976
  extractiveSummary: () => extractiveSummary,
6544
6977
  fetchPrHead: () => fetchPrHead,
6545
6978
  finalizeParts: () => finalizeParts,
6546
6979
  findInvalidCacheControlTtlOrder: () => findInvalidCacheControlTtlOrder,
6547
6980
  findOrphanWorktrees: () => findOrphanWorktrees,
6548
6981
  findThreadByRef: () => findThreadByRef,
6982
+ findThreadForStackLayer: () => findThreadForStackLayer,
6549
6983
  flattenTurnInput: () => flattenTurnInput,
6550
6984
  forkChatTab: () => forkChatTab,
6551
6985
  forkMessageSlice: () => forkMessageSlice,
@@ -6556,6 +6990,8 @@ __export(index_exports, {
6556
6990
  formatGhLandError: () => formatGhLandError,
6557
6991
  formatIpcInvokeError: () => formatIpcInvokeError,
6558
6992
  formatMessagesAsTranscript: () => formatMessagesAsTranscript,
6993
+ formatPlanQuestionAnswers: () => formatPlanQuestionAnswers,
6994
+ formatPlanQuestionsForChat: () => formatPlanQuestionsForChat,
6559
6995
  formatRateLimitResetHint: () => formatRateLimitResetHint,
6560
6996
  formatRenameBranchDirective: () => formatRenameBranchDirective,
6561
6997
  formatTranscriptMarkdown: () => formatTranscriptMarkdown,
@@ -6579,6 +7015,7 @@ __export(index_exports, {
6579
7015
  getPrChecks: () => getPrChecks,
6580
7016
  getPrDetails: () => getPrDetails,
6581
7017
  getPrMeta: () => getPrMeta,
7018
+ getPrStack: () => getPrStack,
6582
7019
  getRepoSetupInfo: () => getRepoSetupInfo,
6583
7020
  getRunMode: () => getRunMode,
6584
7021
  getRunScript: () => getRunScript,
@@ -6595,9 +7032,12 @@ __export(index_exports, {
6595
7032
  healOrchestrationSoccerTitles: () => healOrchestrationSoccerTitles,
6596
7033
  importConductorWorkspace: () => importConductorWorkspace,
6597
7034
  importConductorWorkspaceAsync: () => importConductorWorkspaceAsync,
7035
+ initPrStack: () => initPrStack,
7036
+ initStackFromThread: () => initStackFromThread,
6598
7037
  initializeGitRepository: () => initializeGitRepository,
6599
7038
  inspectGitWorktree: () => inspectGitWorktree,
6600
7039
  installAgent: () => installAgent,
7040
+ isAskUserToolName: () => isAskUserToolName,
6601
7041
  isBrightsyConnected: () => isBrightsyConnected,
6602
7042
  isBrightsyNdjsonLine: () => isBrightsyNdjsonLine,
6603
7043
  isCloudCoordinatorThread: () => isCloudCoordinatorThread,
@@ -6607,14 +7047,17 @@ __export(index_exports, {
6607
7047
  isGlobalRepoPath: () => isGlobalRepoPath,
6608
7048
  isGlobalThread: () => isGlobalThread,
6609
7049
  isImageFilePath: () => isImageFilePath,
7050
+ isInPrStack: () => isInPrStack,
6610
7051
  isLinearConnected: () => isLinearConnected,
6611
7052
  isOrchestratorCapableAgent: () => isOrchestratorCapableAgent,
6612
7053
  isOrchestratorThread: () => isOrchestratorThread,
6613
7054
  isPidAlive: () => isPidAlive,
6614
7055
  isPlaceholderBranch: () => isPlaceholderBranch,
7056
+ isPresentPlanToolName: () => isPresentPlanToolName,
6615
7057
  isSessionQuotaLimit: () => isSessionQuotaLimit,
6616
7058
  isSideboardScratchPath: () => isSideboardScratchPath,
6617
7059
  isThinkingEffort: () => isThinkingEffort,
7060
+ isWorkspaceScratchPath: () => isWorkspaceScratchPath,
6618
7061
  listAgentSetupInfo: () => listAgentSetupInfo,
6619
7062
  listBranchCommits: () => listBranchCommits,
6620
7063
  listBranches: () => listBranches,
@@ -6651,6 +7094,7 @@ __export(index_exports, {
6651
7094
  mcpAllowTools: () => mcpAllowTools,
6652
7095
  mcpAuthWarnings: () => mcpAuthWarnings,
6653
7096
  mergePr: () => mergePr,
7097
+ mergePrStack: () => mergePrStack,
6654
7098
  mergeUsage: () => mergeUsage,
6655
7099
  nextPastedTextName: () => nextPastedTextName,
6656
7100
  nextThinkingEffort: () => nextThinkingEffort,
@@ -6660,6 +7104,8 @@ __export(index_exports, {
6660
7104
  normalizeTurnInput: () => normalizeTurnInput,
6661
7105
  normalizeWorktreePath: () => normalizeWorktreePath,
6662
7106
  openInSystemTerminal: () => openInSystemTerminal,
7107
+ openPrStackLayers: () => openPrStackLayers,
7108
+ openStackLayer: () => openStackLayer,
6663
7109
  opencodeAdapter: () => opencodeAdapter,
6664
7110
  orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
6665
7111
  orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
@@ -6668,15 +7114,19 @@ __export(index_exports, {
6668
7114
  originGhRepoEnv: () => originGhRepoEnv,
6669
7115
  parseCursorRunnerLine: () => parseCursorRunnerLine,
6670
7116
  parseForceStopMessage: () => parseForceStopMessage,
7117
+ parseGhStackViewJson: () => parseGhStackViewJson,
6671
7118
  parseGithubSlugFromRemoteUrl: () => parseGithubSlugFromRemoteUrl,
6672
7119
  parseMcpList: () => parseMcpList,
7120
+ parsePlanQuestionsInput: () => parsePlanQuestionsInput,
6673
7121
  parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
6674
7122
  partsToAssistantText: () => partsToAssistantText,
6675
7123
  pastedTextStats: () => pastedTextStats,
6676
7124
  permissionMode: () => permissionMode,
7125
+ planFileAbs: () => planFileAbs,
6677
7126
  previewLand: () => previewLand,
6678
7127
  pushBranch: () => pushBranch,
6679
7128
  readExistingReviewRequestFile: () => readExistingReviewRequestFile,
7129
+ readPlanFile: () => readPlanFile,
6680
7130
  readSkillBody: () => readSkillBody,
6681
7131
  readThread: () => readThread,
6682
7132
  readWorktreeFile: () => readWorktreeFile,
@@ -6688,6 +7138,7 @@ __export(index_exports, {
6688
7138
  repoSlug: () => repoSlug,
6689
7139
  requestReview: () => requestReview,
6690
7140
  requireAgent: () => requireAgent,
7141
+ resetGhStackDetectCache: () => resetGhStackDetectCache,
6691
7142
  resolveClaudeExecutable: () => resolveClaudeExecutable,
6692
7143
  resolveConductorCursorAgentId: () => resolveConductorCursorAgentId,
6693
7144
  resolveCursorModelId: () => resolveCursorModelId,
@@ -6697,6 +7148,7 @@ __export(index_exports, {
6697
7148
  resolveFilesToCopy: () => resolveFilesToCopy,
6698
7149
  resolveGhAuthToken: () => resolveGhAuthToken,
6699
7150
  resolveGithubRepoSlug: () => resolveGithubRepoSlug,
7151
+ resolvePlanMarkdown: () => resolvePlanMarkdown,
6700
7152
  resolvePrSelector: () => resolvePrSelector,
6701
7153
  resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent,
6702
7154
  resolveRepoRoot: () => resolveRepoRoot,
@@ -6724,12 +7176,16 @@ __export(index_exports, {
6724
7176
  slugify: () => slugify,
6725
7177
  spawnAgentTurn: () => spawnAgentTurn,
6726
7178
  splitForCompaction: () => splitForCompaction,
7179
+ stackAgentDefaultsFrom: () => stackAgentDefaultsFrom,
7180
+ stackIdFrom: () => stackIdFrom,
7181
+ stackMergeReadiness: () => stackMergeReadiness,
6727
7182
  stageAbsolutePathsAsAttachments: () => stageAbsolutePathsAsAttachments,
6728
7183
  stageBuffersAsAttachments: () => stageBuffersAsAttachments,
6729
7184
  startDevServer: () => startDevServer,
6730
7185
  startMcpServer: () => startMcpServer,
6731
7186
  startOrchestration: () => startOrchestration,
6732
7187
  stripBrightsyNdjsonNoise: () => stripBrightsyNdjsonNoise,
7188
+ submitPrStack: () => submitPrStack,
6733
7189
  suggestSlug: () => suggestSlug,
6734
7190
  summarizeConversation: () => summarizeConversation,
6735
7191
  switchBrightsyAccount: () => switchBrightsyAccount,
@@ -6765,6 +7221,7 @@ __export(index_exports, {
6765
7221
  worktreeNameFromPath: () => worktreeNameFromPath,
6766
7222
  worktreesRoot: () => worktreesRoot,
6767
7223
  writeInjectedMcpConfig: () => writeInjectedMcpConfig,
7224
+ writePlanFile: () => writePlanFile,
6768
7225
  writeThread: () => writeThread,
6769
7226
  writeWorktreeFile: () => writeWorktreeFile
6770
7227
  });
@@ -6778,6 +7235,7 @@ init_global_workspace();
6778
7235
  init_run();
6779
7236
  init_gh_errors();
6780
7237
  init_worktree();
7238
+ init_stack();
6781
7239
 
6782
7240
  // src/integrations/github.ts
6783
7241
  init_run();
@@ -6974,18 +7432,18 @@ function asRecord(input) {
6974
7432
  }
6975
7433
  return void 0;
6976
7434
  }
6977
- function str(v) {
7435
+ function str2(v) {
6978
7436
  return typeof v === "string" && v.trim() ? v : void 0;
6979
7437
  }
6980
7438
  function toolDetail(name, input) {
6981
7439
  if (!input) return void 0;
6982
- const command = str(input.command) ?? str(input.cmd);
7440
+ const command = str2(input.command) ?? str2(input.cmd);
6983
7441
  if (command) return command;
6984
- const path = str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
7442
+ const path = str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
6985
7443
  if (path) return path;
6986
- const pattern = str(input.pattern) ?? str(input.glob) ?? str(input.glob_pattern);
7444
+ const pattern = str2(input.pattern) ?? str2(input.glob) ?? str2(input.glob_pattern);
6987
7445
  if (pattern) return pattern;
6988
- const query = str(input.query) ?? str(input.prompt);
7446
+ const query = str2(input.query) ?? str2(input.prompt);
6989
7447
  if (query) return query.length > 80 ? `${query.slice(0, 77)}\u2026` : query;
6990
7448
  try {
6991
7449
  const raw = JSON.stringify(input);
@@ -6998,23 +7456,26 @@ function toolDescription(name, input) {
6998
7456
  const n = name.replace(/^mcp__/, "").replace(/__/g, " \xB7 ");
6999
7457
  if (/^get_record_types$|recordTypes/i.test(name)) return "List record types";
7000
7458
  if (/connectedAgentRequest/i.test(name)) {
7001
- return str(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
7459
+ return str2(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
7002
7460
  }
7003
7461
  if (/present_artifact$/i.test(name)) {
7004
- return str(input?.title) ? `Present ${str(input?.title)}` : "Present artifact";
7462
+ return str2(input?.title) ? `Present ${str2(input?.title)}` : "Present artifact";
7463
+ }
7464
+ if (/present_plan$/i.test(name)) {
7465
+ return str2(input?.title) ? `Plan ${str2(input?.title)}` : "Present plan";
7005
7466
  }
7006
7467
  if (/present_schema$/i.test(name)) {
7007
- return str(input?.title) ? `Schema ${str(input?.title)}` : "Present schema";
7468
+ return str2(input?.title) ? `Schema ${str2(input?.title)}` : "Present schema";
7008
7469
  }
7009
7470
  if (/present_files$/i.test(name)) {
7010
- return str(input?.title) ? `Files ${str(input?.title)}` : "Present files";
7471
+ return str2(input?.title) ? `Files ${str2(input?.title)}` : "Present files";
7011
7472
  }
7012
7473
  if (/^(create|update)_artifact$/i.test(name.replace(/^mcp__[^_]+__/, ""))) {
7013
- return str(input?.title) ? `Artifact ${str(input?.title)}` : "Artifact";
7474
+ return str2(input?.title) ? `Artifact ${str2(input?.title)}` : "Artifact";
7014
7475
  }
7015
7476
  if (!input) return n;
7016
- if (/bash|shell|terminal/i.test(name) && str(input.command)) {
7017
- const cmd = str(input.command);
7477
+ if (/bash|shell|terminal/i.test(name) && str2(input.command)) {
7478
+ const cmd = str2(input.command);
7018
7479
  if (/git\s+fetch/i.test(cmd)) return "Fetch latest from origin and check status";
7019
7480
  if (/git\s+status/i.test(cmd)) return "Check git status";
7020
7481
  if (/git\s+log/i.test(cmd)) return "Inspect recent commits";
@@ -7023,11 +7484,11 @@ function toolDescription(name, input) {
7023
7484
  return "Run shell command";
7024
7485
  }
7025
7486
  if (/edit|write|apply/i.test(name)) {
7026
- const path = str(input.file_path) ?? str(input.path);
7487
+ const path = str2(input.file_path) ?? str2(input.path);
7027
7488
  return path ? `Edit ${path.split("/").pop()}` : "Edit file";
7028
7489
  }
7029
7490
  if (/read/i.test(name)) {
7030
- const path = str(input.file_path) ?? str(input.path);
7491
+ const path = str2(input.file_path) ?? str2(input.path);
7031
7492
  return path ? `Read ${path.split("/").pop()}` : "Read file";
7032
7493
  }
7033
7494
  if (/grep/i.test(name)) return "Search files";
@@ -7036,7 +7497,7 @@ function toolDescription(name, input) {
7036
7497
  }
7037
7498
  function toolFilePath(input) {
7038
7499
  if (!input) return void 0;
7039
- return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
7500
+ return str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
7040
7501
  }
7041
7502
  function countLines(text) {
7042
7503
  if (!text) return 0;
@@ -7044,8 +7505,8 @@ function countLines(text) {
7044
7505
  }
7045
7506
  function diffFromInput(input) {
7046
7507
  if (!input) return {};
7047
- const oldS = str(input.old_string) ?? str(input.oldString);
7048
- const newS = str(input.new_string) ?? str(input.newString) ?? str(input.content);
7508
+ const oldS = str2(input.old_string) ?? str2(input.oldString);
7509
+ const newS = str2(input.new_string) ?? str2(input.newString) ?? str2(input.content);
7049
7510
  if (oldS != null || newS != null) {
7050
7511
  return {
7051
7512
  additions: countLines(newS),
@@ -8212,33 +8673,33 @@ async function getDiff(worktreePath, repoPath, opts) {
8212
8673
  let combinedDiff = "";
8213
8674
  let labelBase = base;
8214
8675
  if (scope === "staged") {
8215
- const [ns, num, diff] = await Promise.all([
8676
+ const [ns, num2, diff] = await Promise.all([
8216
8677
  git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
8217
8678
  git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
8218
8679
  git(["diff", "--cached"], worktreePath, { reject: false })
8219
8680
  ]);
8220
8681
  nameStatus = ns.stdout;
8221
- numstat = num.stdout;
8682
+ numstat = num2.stdout;
8222
8683
  combinedDiff = diff.stdout;
8223
8684
  labelBase = "staged";
8224
8685
  } else if (scope === "unstaged") {
8225
- const [ns, num, diff] = await Promise.all([
8686
+ const [ns, num2, diff] = await Promise.all([
8226
8687
  git(["diff", "--name-status"], worktreePath, { reject: false }),
8227
8688
  git(["diff", "--numstat"], worktreePath, { reject: false }),
8228
8689
  git(["diff"], worktreePath, { reject: false })
8229
8690
  ]);
8230
8691
  nameStatus = ns.stdout;
8231
- numstat = num.stdout;
8692
+ numstat = num2.stdout;
8232
8693
  combinedDiff = diff.stdout;
8233
8694
  labelBase = "unstaged";
8234
8695
  } else if (scope === "uncommitted") {
8235
- const [ns, num, diff] = await Promise.all([
8696
+ const [ns, num2, diff] = await Promise.all([
8236
8697
  git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
8237
8698
  git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
8238
8699
  git(["diff", "HEAD"], worktreePath, { reject: false })
8239
8700
  ]);
8240
8701
  nameStatus = ns.stdout;
8241
- numstat = num.stdout;
8702
+ numstat = num2.stdout;
8242
8703
  combinedDiff = diff.stdout;
8243
8704
  labelBase = "HEAD";
8244
8705
  } else if (scope === "last_turn") {
@@ -8257,34 +8718,34 @@ async function getDiff(worktreePath, repoPath, opts) {
8257
8718
  scopeStats
8258
8719
  };
8259
8720
  }
8260
- const [ns, num, diff] = await Promise.all([
8721
+ const [ns, num2, diff] = await Promise.all([
8261
8722
  git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
8262
8723
  git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
8263
8724
  git(["diff", lastTurnBase], worktreePath, { reject: false })
8264
8725
  ]);
8265
8726
  nameStatus = ns.stdout;
8266
- numstat = num.stdout;
8727
+ numstat = num2.stdout;
8267
8728
  combinedDiff = diff.stdout;
8268
8729
  labelBase = "last turn";
8269
8730
  } else if (scope === "commits" && commitSha) {
8270
8731
  const range = `${commitSha}^!`;
8271
- const [ns, num, diff] = await Promise.all([
8732
+ const [ns, num2, diff] = await Promise.all([
8272
8733
  git(["diff", "--name-status", range], worktreePath, { reject: false }),
8273
8734
  git(["diff", "--numstat", range], worktreePath, { reject: false }),
8274
8735
  git(["diff", range], worktreePath, { reject: false })
8275
8736
  ]);
8276
8737
  nameStatus = ns.stdout;
8277
- numstat = num.stdout;
8738
+ numstat = num2.stdout;
8278
8739
  combinedDiff = diff.stdout;
8279
8740
  labelBase = commitSha.slice(0, 7);
8280
8741
  } else {
8281
- const [ns, num, diff] = await Promise.all([
8742
+ const [ns, num2, diff] = await Promise.all([
8282
8743
  git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
8283
8744
  git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
8284
8745
  git(["diff", mergeBase], worktreePath, { reject: false })
8285
8746
  ]);
8286
8747
  nameStatus = ns.stdout;
8287
- numstat = num.stdout;
8748
+ numstat = num2.stdout;
8288
8749
  combinedDiff = diff.stdout;
8289
8750
  labelBase = base;
8290
8751
  }
@@ -8717,6 +9178,7 @@ function buildDiffCommentAttachment(input) {
8717
9178
  var import_node_fs19 = require("fs");
8718
9179
  var import_node_path19 = require("path");
8719
9180
  var import_node_crypto2 = require("crypto");
9181
+ init_workspace_scratch();
8720
9182
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
8721
9183
  "png",
8722
9184
  "jpg",
@@ -8737,11 +9199,6 @@ var IMAGE_MIME_BY_EXT = {
8737
9199
  bmp: "image/bmp",
8738
9200
  ico: "image/x-icon"
8739
9201
  };
8740
- var ATTACHMENTS_DIR = ".sideboard/attachments";
8741
- var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
8742
- *
8743
- !.gitignore
8744
- `;
8745
9202
  var MAX_INLINE_BYTES = 4e5;
8746
9203
  var MAX_PREVIEW_BYTES = 5e6;
8747
9204
  function fileExtension(filePath) {
@@ -8759,7 +9216,7 @@ function ensureAttachmentsDir(worktreePath) {
8759
9216
  (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
8760
9217
  const gi = (0, import_node_path19.join)(dir, ".gitignore");
8761
9218
  if (!(0, import_node_fs19.existsSync)(gi)) {
8762
- (0, import_node_fs19.writeFileSync)(gi, ATTACHMENTS_GITIGNORE, "utf8");
9219
+ (0, import_node_fs19.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
8763
9220
  }
8764
9221
  return dir;
8765
9222
  }
@@ -9419,7 +9876,7 @@ var import_node_fs20 = require("fs");
9419
9876
  init_worktree();
9420
9877
  init_thread_store();
9421
9878
  init_workspaces();
9422
- async function createThread(input, onSetupLine) {
9879
+ async function createThread(input, _onSetupLine) {
9423
9880
  await requireAgent(input.agent);
9424
9881
  const repoPath = await resolveRepoRoot(input.repoPath);
9425
9882
  if (!(0, import_node_fs20.existsSync)(repoPath)) {
@@ -9429,10 +9886,10 @@ async function createThread(input, onSetupLine) {
9429
9886
  let sourceIsFork = false;
9430
9887
  let prUrl = null;
9431
9888
  if (input.sourceType === "pr") {
9432
- const num = Number(input.sourceRef.replace(/^#/, ""));
9433
- if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
9434
- const pr = await getPr(repoPath, num);
9435
- if (!pr) throw new Error(`PR #${num} not found`);
9889
+ const num2 = Number(input.sourceRef.replace(/^#/, ""));
9890
+ if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
9891
+ const pr = await getPr(repoPath, num2);
9892
+ if (!pr) throw new Error(`PR #${num2} not found`);
9436
9893
  sourceIsFork = pr.isCrossRepository;
9437
9894
  prUrl = pr.url;
9438
9895
  const localFetchBranch = `sideboard-pr-${pr.number}`;
@@ -9477,22 +9934,6 @@ async function createThread(input, onSetupLine) {
9477
9934
  });
9478
9935
  writeThread(thread);
9479
9936
  await ensureWorkspace(repoPath);
9480
- try {
9481
- let setup = await runSetupScript(repoPath, worktreePath, onSetupLine);
9482
- if (!setup.ran) {
9483
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onSetupLine);
9484
- }
9485
- if (setup.ran && setup.exitCode !== 0 && setup.exitCode !== null) {
9486
- updateThread(thread.id, {
9487
- lastError: `Setup exited ${setup.exitCode} (thread is still usable)`
9488
- });
9489
- }
9490
- } catch (err) {
9491
- const message = err instanceof Error ? err.message : String(err);
9492
- updateThread(thread.id, {
9493
- lastError: `Setup failed: ${message}`
9494
- });
9495
- }
9496
9937
  return readThread(thread.id) ?? thread;
9497
9938
  }
9498
9939
  async function listLinearIssues(agent, repoPath) {
@@ -9545,7 +9986,9 @@ function worktreeBindingFrom(from) {
9545
9986
  sourceIsFork: from.sourceIsFork,
9546
9987
  parentThreadId: from.parentThreadId,
9547
9988
  prUrl: from.prUrl,
9548
- prTitle: from.prTitle
9989
+ prTitle: from.prTitle,
9990
+ stackId: from.stackId,
9991
+ stackLayer: from.stackLayer
9549
9992
  };
9550
9993
  }
9551
9994
  function threadsSharingWorktree(worktreePath) {
@@ -9660,9 +10103,308 @@ async function forkThreadWorktree(input, onSetupLine) {
9660
10103
  return thread;
9661
10104
  }
9662
10105
 
10106
+ // src/threads/stack-layers.ts
10107
+ var import_node_fs21 = require("fs");
10108
+ init_run();
10109
+ init_stack();
10110
+ init_worktree();
10111
+ init_thread_store();
10112
+ init_workspaces();
10113
+ function stackIdFrom(stack) {
10114
+ if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
10115
+ const key = stack.layers.map((l) => l.branchName).join("|");
10116
+ if (!key) return null;
10117
+ return `gh-stack-local-${hashShort(key)}`;
10118
+ }
10119
+ function hashShort(s) {
10120
+ let h = 0;
10121
+ for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
10122
+ return Math.abs(h).toString(36);
10123
+ }
10124
+ function requireThread3(idOrRef) {
10125
+ const thread = findThreadByRef(idOrRef) ?? readThread(idOrRef);
10126
+ if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
10127
+ return thread;
10128
+ }
10129
+ function sanitizeSlugPart(name) {
10130
+ return name.trim().replace(/^refs\/heads\//, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "layer";
10131
+ }
10132
+ function layerSlug(stackId, layer) {
10133
+ const stackPart = stackId.replace(/^gh-stack-/, "s");
10134
+ return sanitizeSlugPart(`${stackPart}-L${layer.position}-${layer.branchName}`);
10135
+ }
10136
+ function findThreadForStackLayer(repoPath, stackId, layer) {
10137
+ const threads = listThreads({ includeArchived: false }).filter(
10138
+ (t) => t.repoPath === repoPath && t.stackId === stackId
10139
+ );
10140
+ const byLayer = threads.find((t) => t.stackLayer === layer.position);
10141
+ if (byLayer) return byLayer;
10142
+ return threads.find((t) => t.branchName === layer.branchName) ?? listThreads({ includeArchived: false }).find(
10143
+ (t) => t.repoPath === repoPath && t.branchName === layer.branchName
10144
+ ) ?? null;
10145
+ }
10146
+ async function openStackLayer(input, _onSetupLine) {
10147
+ await requireAgent(input.agent);
10148
+ const repoPath = await resolveRepoRoot(input.repoPath);
10149
+ const stackId = stackIdFrom(input.stack);
10150
+ if (!stackId) throw new Error("Cannot open stack layer without a stack id");
10151
+ const existing = findThreadForStackLayer(repoPath, stackId, input.layer);
10152
+ if (existing) {
10153
+ const patch = {};
10154
+ if (existing.stackId !== stackId) patch.stackId = stackId;
10155
+ if (existing.stackLayer !== input.layer.position) {
10156
+ patch.stackLayer = input.layer.position;
10157
+ }
10158
+ if (input.layer.prUrl && existing.prUrl !== input.layer.prUrl) {
10159
+ patch.prUrl = input.layer.prUrl;
10160
+ }
10161
+ if (input.layer.title && existing.prTitle !== input.layer.title) {
10162
+ patch.prTitle = input.layer.title;
10163
+ }
10164
+ if (Object.keys(patch).length > 0) updateThread(existing.id, patch);
10165
+ return {
10166
+ thread: readThread(existing.id) ?? existing,
10167
+ createdWorktree: false
10168
+ };
10169
+ }
10170
+ let worktreePath = null;
10171
+ let branchName = input.layer.branchName;
10172
+ let createdWorktree = false;
10173
+ const trees = await listWorktrees(repoPath);
10174
+ const checkedOut = trees.find((w) => w.branch === branchName);
10175
+ if (checkedOut?.path && (0, import_node_fs21.existsSync)(checkedOut.path)) {
10176
+ if (input.reuseExistingWorktree !== false) {
10177
+ worktreePath = checkedOut.path;
10178
+ } else {
10179
+ throw new Error(
10180
+ `Branch ${branchName} is already checked out at ${checkedOut.path}`
10181
+ );
10182
+ }
10183
+ }
10184
+ if (!worktreePath) {
10185
+ const slug = layerSlug(stackId, input.layer);
10186
+ const created = await createExistingBranchWorktree({
10187
+ repoPath,
10188
+ branchName,
10189
+ slug
10190
+ });
10191
+ worktreePath = created.worktreePath;
10192
+ branchName = created.branchName;
10193
+ copyConfiguredFiles(repoPath, worktreePath);
10194
+ createdWorktree = true;
10195
+ }
10196
+ const title = input.layer.title?.trim() || (input.layer.prNumber != null ? `PR #${input.layer.prNumber}` : input.layer.branchName);
10197
+ const thread = createEmptyThread({
10198
+ title,
10199
+ userSetTitle: Boolean(input.layer.title?.trim()),
10200
+ sourceType: input.layer.prNumber != null ? "pr" : "branch",
10201
+ sourceRef: input.layer.prNumber != null ? String(input.layer.prNumber) : input.layer.branchName,
10202
+ branchName,
10203
+ worktreePath,
10204
+ repoPath,
10205
+ agent: input.agent,
10206
+ autonomy: input.autonomy ?? "default",
10207
+ model: input.model ?? null,
10208
+ effort: input.effort ?? "high",
10209
+ fast: Boolean(input.fast),
10210
+ planMode: Boolean(input.planMode),
10211
+ parentThreadId: input.parentThreadId ?? null,
10212
+ status: "idle",
10213
+ prUrl: input.layer.prUrl,
10214
+ prTitle: input.layer.title ?? null,
10215
+ stackId,
10216
+ stackLayer: input.layer.position
10217
+ });
10218
+ writeThread(thread);
10219
+ await ensureWorkspace(repoPath);
10220
+ return { thread: readThread(thread.id) ?? thread, createdWorktree };
10221
+ }
10222
+ async function openPrStackLayers(input, onSetupLine) {
10223
+ const from = requireThread3(input.threadRef);
10224
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
10225
+ throw new Error("Thread has no worktree");
10226
+ }
10227
+ const stack = await getPrStack(from.worktreePath);
10228
+ if (!stack) throw new Error("Current branch is not part of a GitHub PR stack");
10229
+ const layers = input.layer != null ? stack.layers.filter((l) => l.position === input.layer) : stack.layers;
10230
+ if (!layers.length) {
10231
+ throw new Error(
10232
+ input.layer != null ? `No stack layer at position ${input.layer}` : "Stack has no layers"
10233
+ );
10234
+ }
10235
+ const threads = [];
10236
+ const createdThreadIds = [];
10237
+ for (const layer of layers) {
10238
+ const { thread, createdWorktree } = await openStackLayer(
10239
+ {
10240
+ repoPath: from.repoPath,
10241
+ stack,
10242
+ layer,
10243
+ agent: from.agent,
10244
+ autonomy: from.autonomy,
10245
+ model: from.model,
10246
+ effort: from.effort,
10247
+ fast: from.fast,
10248
+ planMode: from.planMode,
10249
+ parentThreadId: from.id
10250
+ },
10251
+ onSetupLine
10252
+ );
10253
+ threads.push(thread);
10254
+ if (createdWorktree) createdThreadIds.push(thread.id);
10255
+ }
10256
+ const stackId = stackIdFrom(stack);
10257
+ const current = stack.layers[stack.currentIndex];
10258
+ if (stackId) {
10259
+ updateThread(from.id, {
10260
+ stackId,
10261
+ stackLayer: current?.position ?? from.stackLayer
10262
+ });
10263
+ }
10264
+ return { stack, threads, createdThreadIds };
10265
+ }
10266
+ async function addStackLayerFromThread(input, onSetupLine) {
10267
+ const from = requireThread3(input.threadRef);
10268
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
10269
+ throw new Error("Thread has no worktree");
10270
+ }
10271
+ const status = await detectGhStack(from.worktreePath);
10272
+ if (!status.available) throw new Error(status.reason);
10273
+ await addPrStackLayer(from.worktreePath, input.branchName);
10274
+ const stack = await getPrStack(from.worktreePath);
10275
+ if (!stack) throw new Error("Stack not found after adding layer");
10276
+ const layer = stack.layers.find((l) => l.branchName === input.branchName.trim()) ?? stack.layers[stack.layers.length - 1];
10277
+ if (!layer) throw new Error("New stack layer not found");
10278
+ if (input.title?.trim()) {
10279
+ layer.title = input.title.trim();
10280
+ }
10281
+ const thread = await openStackLayer(
10282
+ {
10283
+ repoPath: from.repoPath,
10284
+ stack,
10285
+ layer,
10286
+ agent: from.agent,
10287
+ autonomy: from.autonomy,
10288
+ model: from.model,
10289
+ effort: from.effort,
10290
+ fast: from.fast,
10291
+ planMode: from.planMode,
10292
+ parentThreadId: from.id
10293
+ },
10294
+ onSetupLine
10295
+ );
10296
+ return { stack, thread: thread.thread, createdWorktree: thread.createdWorktree };
10297
+ }
10298
+ async function initStackFromThread(input, onSetupLine) {
10299
+ const from = requireThread3(input.threadRef);
10300
+ if (!from.worktreePath?.trim() || !from.branchName?.trim() || !from.repoPath?.trim()) {
10301
+ throw new Error("Thread has no worktree/branch");
10302
+ }
10303
+ const status = await detectGhStack(from.worktreePath);
10304
+ if (!status.available) throw new Error(status.reason);
10305
+ const branches = [
10306
+ from.branchName,
10307
+ ...(input.additionalBranches ?? []).map((b) => b.trim()).filter(Boolean)
10308
+ ];
10309
+ await initPrStack(from.worktreePath, branches, {
10310
+ base: input.base ?? await resolveDefaultBranch(from.repoPath)
10311
+ });
10312
+ return openPrStackLayers({ threadRef: from.id }, onSetupLine);
10313
+ }
10314
+ async function createPrStack(input, onSetupLine) {
10315
+ await requireAgent(input.agent);
10316
+ const repoPath = await resolveRepoRoot(input.repoPath);
10317
+ if (!(0, import_node_fs21.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
10318
+ if (!input.branches.length) throw new Error("At least one branch name required");
10319
+ const status = await detectGhStack(repoPath);
10320
+ if (!status.available) throw new Error(status.reason);
10321
+ const team = allocateTeamSlug(repoPath);
10322
+ const base = input.base ?? await resolveDefaultBranch(repoPath);
10323
+ const bootstrap = await createThreadWorktree({
10324
+ repoPath,
10325
+ sourceRef: base,
10326
+ slug: `${team.slug}-stack-init`
10327
+ });
10328
+ copyConfiguredFiles(repoPath, bootstrap.worktreePath);
10329
+ try {
10330
+ await initPrStack(bootstrap.worktreePath, input.branches, { base });
10331
+ const bottom = input.branches[0];
10332
+ const co = await git(["checkout", bottom], bootstrap.worktreePath, {
10333
+ reject: false
10334
+ });
10335
+ if (co.exitCode !== 0) {
10336
+ throw new Error(
10337
+ co.stderr.trim() || co.stdout.trim() || `Could not check out ${bottom} after stack init`
10338
+ );
10339
+ }
10340
+ } catch (err) {
10341
+ try {
10342
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
10343
+ deleteBranch: bootstrap.branchName
10344
+ });
10345
+ } catch {
10346
+ }
10347
+ throw err;
10348
+ }
10349
+ const stack = await getPrStack(bootstrap.worktreePath);
10350
+ if (!stack) {
10351
+ try {
10352
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
10353
+ deleteBranch: bootstrap.branchName
10354
+ });
10355
+ } catch {
10356
+ }
10357
+ throw new Error("Stack init succeeded but gh stack view returned no stack");
10358
+ }
10359
+ const threads = [];
10360
+ const createdThreadIds = [];
10361
+ for (const layer of stack.layers) {
10362
+ const title = layer.position === 1 && input.title?.trim() ? input.title.trim() : layer.title;
10363
+ const opened = await openStackLayer(
10364
+ {
10365
+ repoPath,
10366
+ stack,
10367
+ layer: title ? { ...layer, title } : layer,
10368
+ agent: input.agent,
10369
+ autonomy: input.autonomy,
10370
+ model: input.model,
10371
+ effort: input.effort,
10372
+ fast: input.fast,
10373
+ planMode: input.planMode,
10374
+ reuseExistingWorktree: true
10375
+ },
10376
+ onSetupLine
10377
+ );
10378
+ threads.push(opened.thread);
10379
+ if (opened.createdWorktree || opened.thread.worktreePath === bootstrap.worktreePath) {
10380
+ createdThreadIds.push(opened.thread.id);
10381
+ }
10382
+ }
10383
+ const claimed = new Set(threads.map((t) => t.worktreePath));
10384
+ if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs21.existsSync)(bootstrap.worktreePath)) {
10385
+ try {
10386
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
10387
+ deleteBranch: bootstrap.branchName
10388
+ });
10389
+ } catch {
10390
+ }
10391
+ }
10392
+ return { stack, threads, createdThreadIds };
10393
+ }
10394
+ function stackAgentDefaultsFrom(input) {
10395
+ return {
10396
+ agent: input.agent,
10397
+ autonomy: input.autonomy,
10398
+ model: input.model,
10399
+ effort: input.effort,
10400
+ fast: input.fast,
10401
+ planMode: input.planMode
10402
+ };
10403
+ }
10404
+
9663
10405
  // src/threads/adopt.ts
9664
10406
  var import_node_child_process = require("child_process");
9665
- var import_node_fs21 = require("fs");
10407
+ var import_node_fs22 = require("fs");
9666
10408
  var import_node_os9 = require("os");
9667
10409
  var import_node_path20 = require("path");
9668
10410
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
@@ -9687,21 +10429,21 @@ function mapAgentType(raw) {
9687
10429
  return null;
9688
10430
  }
9689
10431
  function resolveConductorCursorAgentId(workspacePath) {
9690
- if (!workspacePath || !(0, import_node_fs21.existsSync)(CURSOR_SDK_STORE)) return null;
10432
+ if (!workspacePath || !(0, import_node_fs22.existsSync)(CURSOR_SDK_STORE)) return null;
9691
10433
  const normalized = workspacePath.replace(/\/$/, "");
9692
10434
  let best = null;
9693
10435
  let hashes;
9694
10436
  try {
9695
- hashes = (0, import_node_fs21.readdirSync)(CURSOR_SDK_STORE);
10437
+ hashes = (0, import_node_fs22.readdirSync)(CURSOR_SDK_STORE);
9696
10438
  } catch {
9697
10439
  return null;
9698
10440
  }
9699
10441
  for (const hash of hashes) {
9700
10442
  const agentsFile = (0, import_node_path20.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
9701
- if (!(0, import_node_fs21.existsSync)(agentsFile)) continue;
10443
+ if (!(0, import_node_fs22.existsSync)(agentsFile)) continue;
9702
10444
  let text;
9703
10445
  try {
9704
- text = (0, import_node_fs21.readFileSync)(agentsFile, "utf8");
10446
+ text = (0, import_node_fs22.readFileSync)(agentsFile, "utf8");
9705
10447
  } catch {
9706
10448
  continue;
9707
10449
  }
@@ -9725,7 +10467,7 @@ function resolveConductorCursorAgentId(workspacePath) {
9725
10467
  return best?.agentId ?? null;
9726
10468
  }
9727
10469
  async function adoptThread(input) {
9728
- if (!(0, import_node_fs21.existsSync)(input.worktreePath)) {
10470
+ if (!(0, import_node_fs22.existsSync)(input.worktreePath)) {
9729
10471
  throw new Error(`Worktree not found: ${input.worktreePath}`);
9730
10472
  }
9731
10473
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -9752,18 +10494,18 @@ function conductorDbPath() {
9752
10494
  return CONDUCTOR_DB;
9753
10495
  }
9754
10496
  function listConductorWorkspaces() {
9755
- if (!(0, import_node_fs21.existsSync)(CONDUCTOR_DB)) {
10497
+ if (!(0, import_node_fs22.existsSync)(CONDUCTOR_DB)) {
9756
10498
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
9757
10499
  }
9758
- const tmp = (0, import_node_fs21.mkdtempSync)((0, import_node_path20.join)((0, import_node_os9.tmpdir)(), "sideboard-conductor-"));
10500
+ const tmp = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os9.tmpdir)(), "sideboard-conductor-"));
9759
10501
  const snapshot = (0, import_node_path20.join)(tmp, "conductor.db");
9760
10502
  try {
9761
- (0, import_node_fs21.copyFileSync)(CONDUCTOR_DB, snapshot);
10503
+ (0, import_node_fs22.copyFileSync)(CONDUCTOR_DB, snapshot);
9762
10504
  for (const suffix of ["-wal", "-shm"]) {
9763
10505
  const src = `${CONDUCTOR_DB}${suffix}`;
9764
- if ((0, import_node_fs21.existsSync)(src)) {
10506
+ if ((0, import_node_fs22.existsSync)(src)) {
9765
10507
  try {
9766
- (0, import_node_fs21.copyFileSync)(src, `${snapshot}${suffix}`);
10508
+ (0, import_node_fs22.copyFileSync)(src, `${snapshot}${suffix}`);
9767
10509
  } catch {
9768
10510
  }
9769
10511
  }
@@ -9839,22 +10581,22 @@ function listConductorWorkspaces() {
9839
10581
  db.close();
9840
10582
  }
9841
10583
  } finally {
9842
- (0, import_node_fs21.rmSync)(tmp, { recursive: true, force: true });
10584
+ (0, import_node_fs22.rmSync)(tmp, { recursive: true, force: true });
9843
10585
  }
9844
10586
  }
9845
10587
  function importConductorWorkspace(workspaceId) {
9846
- if (!(0, import_node_fs21.existsSync)(CONDUCTOR_DB)) {
10588
+ if (!(0, import_node_fs22.existsSync)(CONDUCTOR_DB)) {
9847
10589
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
9848
10590
  }
9849
- const tmp = (0, import_node_fs21.mkdtempSync)((0, import_node_path20.join)((0, import_node_os9.tmpdir)(), "sideboard-conductor-"));
10591
+ const tmp = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os9.tmpdir)(), "sideboard-conductor-"));
9850
10592
  const snapshot = (0, import_node_path20.join)(tmp, "conductor.db");
9851
10593
  try {
9852
- (0, import_node_fs21.copyFileSync)(CONDUCTOR_DB, snapshot);
10594
+ (0, import_node_fs22.copyFileSync)(CONDUCTOR_DB, snapshot);
9853
10595
  for (const suffix of ["-wal", "-shm"]) {
9854
10596
  const src = `${CONDUCTOR_DB}${suffix}`;
9855
- if ((0, import_node_fs21.existsSync)(src)) {
10597
+ if ((0, import_node_fs22.existsSync)(src)) {
9856
10598
  try {
9857
- (0, import_node_fs21.copyFileSync)(src, `${snapshot}${suffix}`);
10599
+ (0, import_node_fs22.copyFileSync)(src, `${snapshot}${suffix}`);
9858
10600
  } catch {
9859
10601
  }
9860
10602
  }
@@ -9872,7 +10614,7 @@ function importConductorWorkspace(workspaceId) {
9872
10614
  ).get(workspaceId);
9873
10615
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
9874
10616
  const worktreePath = String(row.workspacePath);
9875
- if (!(0, import_node_fs21.existsSync)(worktreePath)) {
10617
+ if (!(0, import_node_fs22.existsSync)(worktreePath)) {
9876
10618
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
9877
10619
  }
9878
10620
  let sessionId = null;
@@ -9935,7 +10677,7 @@ function importConductorWorkspace(workspaceId) {
9935
10677
  db.close();
9936
10678
  }
9937
10679
  } finally {
9938
- (0, import_node_fs21.rmSync)(tmp, { recursive: true, force: true });
10680
+ (0, import_node_fs22.rmSync)(tmp, { recursive: true, force: true });
9939
10681
  }
9940
10682
  }
9941
10683
  async function importConductorWorkspaceAsync(workspaceId) {
@@ -9944,13 +10686,14 @@ async function importConductorWorkspaceAsync(workspaceId) {
9944
10686
 
9945
10687
  // src/orchestrator/orchestrator.ts
9946
10688
  var import_node_events = require("events");
9947
- var import_node_fs26 = require("fs");
10689
+ var import_node_fs28 = require("fs");
9948
10690
  init_error_detail();
9949
10691
  init_agents();
9950
10692
  init_worktree();
10693
+ init_stack();
9951
10694
 
9952
10695
  // src/git/orphan-cleanup.ts
9953
- var import_node_fs22 = require("fs");
10696
+ var import_node_fs23 = require("fs");
9954
10697
  var import_node_path21 = require("path");
9955
10698
  init_worktree();
9956
10699
  init_thread_store();
@@ -9966,9 +10709,9 @@ async function findOrphanWorktrees(repoPaths) {
9966
10709
  repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
9967
10710
  );
9968
10711
  const homeRoot = sideboardWorkspacesDir();
9969
- if ((0, import_node_fs22.existsSync)(homeRoot)) {
10712
+ if ((0, import_node_fs23.existsSync)(homeRoot)) {
9970
10713
  try {
9971
- for (const entry of (0, import_node_fs22.readdirSync)(homeRoot, { withFileTypes: true })) {
10714
+ for (const entry of (0, import_node_fs23.readdirSync)(homeRoot, { withFileTypes: true })) {
9972
10715
  if (!entry.isDirectory()) continue;
9973
10716
  void entry;
9974
10717
  }
@@ -9978,7 +10721,7 @@ async function findOrphanWorktrees(repoPaths) {
9978
10721
  const orphans = [];
9979
10722
  const seen = /* @__PURE__ */ new Set();
9980
10723
  for (const repoPath of repos) {
9981
- if (!repoPath || !(0, import_node_fs22.existsSync)(repoPath)) continue;
10724
+ if (!repoPath || !(0, import_node_fs23.existsSync)(repoPath)) continue;
9982
10725
  try {
9983
10726
  const wts = await listWorktrees(repoPath);
9984
10727
  for (const wt of wts) {
@@ -9989,7 +10732,7 @@ async function findOrphanWorktrees(repoPaths) {
9989
10732
  seen.add(path);
9990
10733
  let mtimeMs = 0;
9991
10734
  try {
9992
- mtimeMs = (0, import_node_fs22.statSync)(path).mtimeMs;
10735
+ mtimeMs = (0, import_node_fs23.statSync)(path).mtimeMs;
9993
10736
  } catch {
9994
10737
  mtimeMs = 0;
9995
10738
  }
@@ -9999,16 +10742,16 @@ async function findOrphanWorktrees(repoPaths) {
9999
10742
  }
10000
10743
  try {
10001
10744
  const root = worktreesRoot(repoPath);
10002
- if ((0, import_node_fs22.existsSync)(root)) {
10003
- for (const entry of (0, import_node_fs22.readdirSync)(root, { withFileTypes: true })) {
10745
+ if ((0, import_node_fs23.existsSync)(root)) {
10746
+ for (const entry of (0, import_node_fs23.readdirSync)(root, { withFileTypes: true })) {
10004
10747
  if (!entry.isDirectory()) continue;
10005
10748
  const path = (0, import_node_path21.join)(root, entry.name).replace(/\/$/, "");
10006
10749
  if (known.has(path) || seen.has(path)) continue;
10007
- if (!(0, import_node_fs22.existsSync)((0, import_node_path21.join)(path, ".git"))) continue;
10750
+ if (!(0, import_node_fs23.existsSync)((0, import_node_path21.join)(path, ".git"))) continue;
10008
10751
  seen.add(path);
10009
10752
  let mtimeMs = 0;
10010
10753
  try {
10011
- mtimeMs = (0, import_node_fs22.statSync)(path).mtimeMs;
10754
+ mtimeMs = (0, import_node_fs23.statSync)(path).mtimeMs;
10012
10755
  } catch {
10013
10756
  mtimeMs = Date.now();
10014
10757
  }
@@ -10154,7 +10897,7 @@ async function applyThreadIntoMain(thread, opts) {
10154
10897
  }
10155
10898
 
10156
10899
  // src/git/clone-repo.ts
10157
- var import_node_fs23 = require("fs");
10900
+ var import_node_fs24 = require("fs");
10158
10901
  var import_node_path22 = require("path");
10159
10902
  var import_execa6 = require("execa");
10160
10903
  init_paths();
@@ -10170,7 +10913,7 @@ async function cloneRepoIntoSideboard(opts) {
10170
10913
  }
10171
10914
  name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
10172
10915
  const dest = (0, import_node_path22.join)(sideboardReposDir(), name);
10173
- if ((0, import_node_fs23.existsSync)(dest)) {
10916
+ if ((0, import_node_fs24.existsSync)(dest)) {
10174
10917
  const repoPath2 = await resolveRepoRoot(dest);
10175
10918
  const workspace2 = await ensureWorkspace(repoPath2);
10176
10919
  return { repoPath: repoPath2, workspace: workspace2 };
@@ -10192,7 +10935,7 @@ init_orchestrator_capable();
10192
10935
 
10193
10936
  // src/review/request-review.ts
10194
10937
  var import_node_crypto5 = require("crypto");
10195
- var import_node_fs24 = require("fs");
10938
+ var import_node_fs25 = require("fs");
10196
10939
  var import_node_path23 = require("path");
10197
10940
  init_global_workspace();
10198
10941
  init_thread_store();
@@ -10317,15 +11060,13 @@ File: src/client/frontends/desktop/core/UserData.ts
10317
11060
  `;
10318
11061
 
10319
11062
  // src/review/request-review.ts
11063
+ init_workspace_scratch();
10320
11064
  var REPO_REVIEW_PATH = ".sideboard/review.md";
10321
11065
  var REPO_REVIEW_NAME = "review.md";
10322
- var REVIEW_REQUEST_PATH = ".sideboard/attachments/Review request.md";
11066
+ var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
11067
+ var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
10323
11068
  var REVIEW_REQUEST_NAME = "Review request.md";
10324
11069
  var REVIEW_REQUEST_PREFILL = "Review.";
10325
- var ATTACHMENTS_GITIGNORE2 = `# Sideboard review / composer attachments (local only)
10326
- *
10327
- !.gitignore
10328
- `;
10329
11070
  var LEGACY_REVIEW_TEMPLATE_MARKERS = [
10330
11071
  "You are acting as a reviewer for a proposed code change made by another engineer.",
10331
11072
  "HOW MANY FINDINGS TO RETURN:"
@@ -10339,19 +11080,19 @@ function shouldRefreshReviewRequestTemplate(content) {
10339
11080
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
10340
11081
  }
10341
11082
  function readTextIfPresent(abs) {
10342
- if (!(0, import_node_fs24.existsSync)(abs)) return null;
11083
+ if (!(0, import_node_fs25.existsSync)(abs)) return null;
10343
11084
  try {
10344
- const content = (0, import_node_fs24.readFileSync)(abs, "utf8");
11085
+ const content = (0, import_node_fs25.readFileSync)(abs, "utf8");
10345
11086
  return content.trim() ? content : null;
10346
11087
  } catch {
10347
11088
  return null;
10348
11089
  }
10349
11090
  }
10350
11091
  function ensureAttachmentsGitignore(worktreePath) {
10351
- const gitignoreAbs = (0, import_node_path23.join)(worktreePath, ".sideboard", "attachments", ".gitignore");
10352
- if ((0, import_node_fs24.existsSync)(gitignoreAbs)) return;
10353
- (0, import_node_fs24.mkdirSync)((0, import_node_path23.dirname)(gitignoreAbs), { recursive: true });
10354
- (0, import_node_fs24.writeFileSync)(gitignoreAbs, ATTACHMENTS_GITIGNORE2, "utf8");
11092
+ const gitignoreAbs = (0, import_node_path23.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
11093
+ if ((0, import_node_fs25.existsSync)(gitignoreAbs)) return;
11094
+ (0, import_node_fs25.mkdirSync)((0, import_node_path23.dirname)(gitignoreAbs), { recursive: true });
11095
+ (0, import_node_fs25.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
10355
11096
  }
10356
11097
  function resolveReviewGuidelines(worktreePath) {
10357
11098
  const repoAbs = (0, import_node_path23.join)(worktreePath, REPO_REVIEW_PATH);
@@ -10374,9 +11115,19 @@ function resolveReviewGuidelines(worktreePath) {
10374
11115
  source: "local"
10375
11116
  };
10376
11117
  }
11118
+ const legacyAbs = (0, import_node_path23.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
11119
+ const legacyContent = readTextIfPresent(legacyAbs);
11120
+ if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
11121
+ return {
11122
+ path: LEGACY_REVIEW_REQUEST_PATH,
11123
+ name: REVIEW_REQUEST_NAME,
11124
+ content: legacyContent,
11125
+ source: "local"
11126
+ };
11127
+ }
10377
11128
  ensureAttachmentsGitignore(worktreePath);
10378
- (0, import_node_fs24.mkdirSync)((0, import_node_path23.dirname)(localAbs), { recursive: true });
10379
- (0, import_node_fs24.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
11129
+ (0, import_node_fs25.mkdirSync)((0, import_node_path23.dirname)(localAbs), { recursive: true });
11130
+ (0, import_node_fs25.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
10380
11131
  return {
10381
11132
  path: REVIEW_REQUEST_PATH,
10382
11133
  name: REVIEW_REQUEST_NAME,
@@ -10396,17 +11147,18 @@ function ensureReviewRequestFile(worktreePath) {
10396
11147
  };
10397
11148
  }
10398
11149
  const localAbs = (0, import_node_path23.join)(worktreePath, REVIEW_REQUEST_PATH);
10399
- const localContent = readTextIfPresent(localAbs);
11150
+ const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent((0, import_node_path23.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
10400
11151
  if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
11152
+ const path = (0, import_node_fs25.existsSync)(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
10401
11153
  return {
10402
- path: REVIEW_REQUEST_PATH,
11154
+ path,
10403
11155
  name: REVIEW_REQUEST_NAME,
10404
11156
  content: localContent,
10405
11157
  source: "local"
10406
11158
  };
10407
11159
  }
10408
- (0, import_node_fs24.mkdirSync)((0, import_node_path23.dirname)(repoAbs), { recursive: true });
10409
- (0, import_node_fs24.writeFileSync)(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
11160
+ (0, import_node_fs25.mkdirSync)((0, import_node_path23.dirname)(repoAbs), { recursive: true });
11161
+ (0, import_node_fs25.writeFileSync)(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
10410
11162
  return {
10411
11163
  path: REPO_REVIEW_PATH,
10412
11164
  name: REPO_REVIEW_NAME,
@@ -10426,7 +11178,7 @@ function buildReviewRequestAttachment(content, opts) {
10426
11178
  };
10427
11179
  }
10428
11180
  function readExistingReviewRequestFile(worktreePath) {
10429
- return readTextIfPresent((0, import_node_path23.join)(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent((0, import_node_path23.join)(worktreePath, REVIEW_REQUEST_PATH));
11181
+ return readTextIfPresent((0, import_node_path23.join)(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent((0, import_node_path23.join)(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent((0, import_node_path23.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
10430
11182
  }
10431
11183
  async function requestReview(threadRef, send) {
10432
11184
  const from = findThreadByRef(threadRef);
@@ -10565,6 +11317,7 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
10565
11317
 
10566
11318
  // src/orchestrator/orchestrator.ts
10567
11319
  init_types();
11320
+ init_plan_file();
10568
11321
  init_settings();
10569
11322
 
10570
11323
  // src/threads/sync-branch.ts
@@ -10684,7 +11437,7 @@ var Orchestrator = class {
10684
11437
  }
10685
11438
  continue;
10686
11439
  }
10687
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
11440
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
10688
11441
  setStatus(thread.id, "broken", "Worktree missing on disk");
10689
11442
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
10690
11443
  continue;
@@ -10829,26 +11582,43 @@ var Orchestrator = class {
10829
11582
  return findThreadByRef(idOrRef) ?? readThread(idOrRef);
10830
11583
  }
10831
11584
  async createThread(input) {
10832
- let thread = await createThread(input, (line) => {
10833
- this.emit({
10834
- type: "turn_output",
10835
- threadId: "pending",
10836
- event: { type: "stdout", data: line }
10837
- });
10838
- });
11585
+ const thread = await createThread(input);
10839
11586
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
11587
+ void this.finishCreateThread(thread.id, input.prompt?.trim() || void 0);
11588
+ return thread;
11589
+ }
11590
+ async finishCreateThread(threadId, prompt) {
11591
+ await this.runSetupAfterCreate(threadId);
10840
11592
  const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
10841
11593
  if (autoRunAfterSetupEnabled2()) {
10842
11594
  try {
10843
- await this.startDev(thread.id);
11595
+ await this.startDev(threadId);
10844
11596
  } catch {
10845
11597
  }
10846
11598
  }
10847
- const prompt = input.prompt?.trim();
10848
11599
  if (prompt) {
10849
- thread = await this.send(thread.id, prompt);
11600
+ try {
11601
+ await this.send(threadId, prompt);
11602
+ } catch (err) {
11603
+ const message = err instanceof Error ? err.message : String(err);
11604
+ updateThread(threadId, {
11605
+ lastError: `First prompt failed: ${message}`
11606
+ });
11607
+ }
11608
+ }
11609
+ }
11610
+ /** Run workspace setup after a new worktree is created (no-op if none configured). */
11611
+ async runSetupAfterCreate(threadId) {
11612
+ try {
11613
+ await this.runSetup(threadId);
11614
+ } catch (err) {
11615
+ const message = err instanceof Error ? err.message : String(err);
11616
+ if (/no setup script/i.test(message)) return;
11617
+ if (/already running/i.test(message)) return;
11618
+ updateThread(threadId, {
11619
+ lastError: `Setup failed: ${message}`
11620
+ });
10850
11621
  }
10851
- return thread;
10852
11622
  }
10853
11623
  listWorkspaces() {
10854
11624
  const fromThreads = listThreads({ includeArchived: false }).map((t) => t.repoPath);
@@ -11181,8 +11951,73 @@ var Orchestrator = class {
11181
11951
  }
11182
11952
  }
11183
11953
  }
11184
- const lastStderr = summarizeTurnStderr(stderrTail);
11185
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
11954
+ let lastStderr = summarizeTurnStderr(stderrTail);
11955
+ let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
11956
+ if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && looksLikeInvalidAgentSession(detail) && this.requireThread(threadId).sessionId && this.requireThread(threadId).agent !== "cursor" && this.requireThread(threadId).agent !== "brightsy") {
11957
+ updateThread(threadId, { sessionId: null });
11958
+ pushTurnStderr(
11959
+ stderrTail,
11960
+ "Agent session missing \u2014 starting a fresh session"
11961
+ );
11962
+ this.emit({
11963
+ type: "turn_output",
11964
+ threadId,
11965
+ event: {
11966
+ type: "stderr",
11967
+ data: "Agent session missing \u2014 starting a fresh session"
11968
+ }
11969
+ });
11970
+ const retryThread = this.requireThread(threadId);
11971
+ const prior = retryThread.messages.slice(0, -1);
11972
+ const retrySeed = buildSessionSeed(prior);
11973
+ const retryInstructions = retryThread.agent === "claude" ? null : formatAgentInstructions(
11974
+ loadAgentInstructions(retryThread.worktreePath, retryThread.agent)
11975
+ );
11976
+ const retryPrefix = [
11977
+ coordinatorDirective,
11978
+ worktreeDirective,
11979
+ artifactDirective,
11980
+ renameBranchDirective,
11981
+ retryInstructions,
11982
+ retrySeed
11983
+ ].filter(Boolean).join("\n\n---\n\n");
11984
+ const retryHandle = await spawnAgentTurn(
11985
+ retryThread,
11986
+ { cachedPrefix: retryPrefix, prompt: agentPrompt },
11987
+ (event) => {
11988
+ this.emit({ type: "turn_output", threadId, event });
11989
+ if (event.type === "session_id") {
11990
+ updateThread(threadId, { sessionId: event.data });
11991
+ }
11992
+ if (event.type === "stderr" && typeof event.data === "string") {
11993
+ pushTurnStderr(stderrTail, event.data);
11994
+ }
11995
+ }
11996
+ );
11997
+ this.activeTurns.set(threadId, retryHandle);
11998
+ if (typeof retryHandle.pid === "number" && retryHandle.pid > 0) {
11999
+ updateThread(threadId, { agentPid: retryHandle.pid });
12000
+ }
12001
+ this.processes.set(`${threadId}:agent`, {
12002
+ kind: "agent",
12003
+ pid: retryHandle.pid,
12004
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
12005
+ kill: retryHandle.kill
12006
+ });
12007
+ if (this.stoppedTurns.has(threadId)) {
12008
+ retryHandle.kill();
12009
+ }
12010
+ const retryResult = await retryHandle.done;
12011
+ if (retryResult.sessionId) {
12012
+ updateThread(threadId, { sessionId: retryResult.sessionId });
12013
+ }
12014
+ assistantText = retryResult.assistantText.trim();
12015
+ parts = retryResult.parts;
12016
+ usage = retryResult.usage ?? void 0;
12017
+ exitCode = retryResult.exitCode;
12018
+ lastStderr = summarizeTurnStderr(stderrTail);
12019
+ detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
12020
+ }
11186
12021
  let chatText = assistantText;
11187
12022
  if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
11188
12023
  chatText = humanizeAgentFailDetail(detail);
@@ -11198,6 +12033,19 @@ var Orchestrator = class {
11198
12033
  });
11199
12034
  }
11200
12035
  const afterTurn = this.requireThread(threadId);
12036
+ if (afterTurn.planMode && afterTurn.worktreePath?.trim()) {
12037
+ const presented = extractPresentedPlan(parts);
12038
+ const exited = parts.some(
12039
+ (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
12040
+ );
12041
+ if (presented?.content) {
12042
+ writePlanFile(afterTurn.worktreePath, presented.content);
12043
+ } else if (exited || chatText && chatText.trim().length >= 400) {
12044
+ if (!readPlanFile(afterTurn.worktreePath) && chatText?.trim()) {
12045
+ writePlanFile(afterTurn.worktreePath, chatText.trim());
12046
+ }
12047
+ }
12048
+ }
11201
12049
  if (afterTurn.planMode && afterTurn.agent === "claude" && parts.some(
11202
12050
  (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
11203
12051
  )) {
@@ -11679,6 +12527,82 @@ var Orchestrator = class {
11679
12527
  }
11680
12528
  return meta;
11681
12529
  }
12530
+ async getPrStack(threadRef) {
12531
+ const thread = this.requireThread(threadRef);
12532
+ if (!thread.worktreePath?.trim()) return null;
12533
+ const stack = await getPrStack(thread.worktreePath);
12534
+ if (!stack) return null;
12535
+ const current = stack.currentIndex >= 0 ? stack.layers[stack.currentIndex] : null;
12536
+ const patch = {};
12537
+ if (stack.stackNumber != null) {
12538
+ const id = `gh-stack-${stack.stackNumber}`;
12539
+ if (thread.stackId !== id) patch.stackId = id;
12540
+ }
12541
+ if (current?.position != null && thread.stackLayer !== current.position) {
12542
+ patch.stackLayer = current.position;
12543
+ }
12544
+ if (current?.prUrl && current.prUrl !== thread.prUrl) patch.prUrl = current.prUrl;
12545
+ if (current?.title && current.title !== thread.prTitle) patch.prTitle = current.title;
12546
+ if (current?.branchName && current.branchName !== thread.branchName) {
12547
+ patch.branchName = current.branchName;
12548
+ }
12549
+ if (Object.keys(patch).length > 0) updateThread(thread.id, patch);
12550
+ return stack;
12551
+ }
12552
+ /** Open worktrees for all (or one) stack layers discovered from a thread. */
12553
+ async openPrStackLayers(threadRef, opts) {
12554
+ const result = await openPrStackLayers({ threadRef, layer: opts?.layer });
12555
+ for (const t of result.threads) {
12556
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
12557
+ }
12558
+ for (const id of result.createdThreadIds) {
12559
+ await this.runSetupAfterCreate(id);
12560
+ }
12561
+ return { stack: result.stack, threads: result.threads };
12562
+ }
12563
+ /** Add a branch on top of the thread's stack and open its worktree. */
12564
+ async addStackLayer(threadRef, branchName, opts) {
12565
+ const result = await addStackLayerFromThread({
12566
+ threadRef,
12567
+ branchName,
12568
+ title: opts?.title
12569
+ });
12570
+ this.emit({
12571
+ type: "status_changed",
12572
+ threadId: result.thread.id,
12573
+ status: result.thread.status
12574
+ });
12575
+ if (result.createdWorktree) {
12576
+ await this.runSetupAfterCreate(result.thread.id);
12577
+ }
12578
+ return { stack: result.stack, thread: result.thread };
12579
+ }
12580
+ /** Initialize a stack from the current thread branch (optional extra layers). */
12581
+ async initStackFromThread(threadRef, opts) {
12582
+ const result = await initStackFromThread({
12583
+ threadRef,
12584
+ additionalBranches: opts?.additionalBranches,
12585
+ base: opts?.base
12586
+ });
12587
+ for (const t of result.threads) {
12588
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
12589
+ }
12590
+ for (const id of result.createdThreadIds) {
12591
+ await this.runSetupAfterCreate(id);
12592
+ }
12593
+ return { stack: result.stack, threads: result.threads };
12594
+ }
12595
+ /** Create a new multi-layer stack with one worktree per layer. */
12596
+ async createPrStack(input) {
12597
+ const result = await createPrStack(input);
12598
+ for (const t of result.threads) {
12599
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
12600
+ }
12601
+ for (const id of result.createdThreadIds) {
12602
+ await this.runSetupAfterCreate(id);
12603
+ }
12604
+ return { stack: result.stack, threads: result.threads };
12605
+ }
11682
12606
  async getPrDetails(threadRef) {
11683
12607
  const { thread, selector, cwd } = await this.withPrSelector(threadRef);
11684
12608
  if (!selector) return null;
@@ -11741,14 +12665,9 @@ var Orchestrator = class {
11741
12665
  return forkChatTab(input);
11742
12666
  }
11743
12667
  async forkThreadWorktree(input) {
11744
- const thread = await forkThreadWorktree(input, (line) => {
11745
- this.emit({
11746
- type: "turn_output",
11747
- threadId: "pending",
11748
- event: { type: "stdout", data: line }
11749
- });
11750
- });
12668
+ const thread = await forkThreadWorktree(input);
11751
12669
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
12670
+ await this.runSetupAfterCreate(thread.id);
11752
12671
  return thread;
11753
12672
  }
11754
12673
  renameThread(threadRef, title) {
@@ -11762,7 +12681,7 @@ var Orchestrator = class {
11762
12681
  }
11763
12682
  /**
11764
12683
  * Stage OS / worktree files into composer attachments (copies external files
11765
- * into `.sideboard/attachments/` so agents can Read images and binaries).
12684
+ * into `.context/attachments/` so agents can Read images and binaries).
11766
12685
  */
11767
12686
  attachComposerFiles(threadRef, opts) {
11768
12687
  const thread = this.requireThread(threadRef);
@@ -11836,7 +12755,7 @@ var Orchestrator = class {
11836
12755
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
11837
12756
  return setStatus(thread.id, "idle");
11838
12757
  }
11839
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
12758
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
11840
12759
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
11841
12760
  const { execa: execa7 } = await import("execa");
11842
12761
  const slug = thread.worktreePath.split("/").pop();
@@ -11944,13 +12863,116 @@ async function startOrchestration(opts) {
11944
12863
  }
11945
12864
 
11946
12865
  // src/index.ts
12866
+ init_workspace_scratch();
12867
+
12868
+ // src/plan/ask-user.ts
12869
+ function asRecord3(v) {
12870
+ return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
12871
+ }
12872
+ function parseOptions(raw) {
12873
+ if (!Array.isArray(raw)) return [];
12874
+ const out = [];
12875
+ for (const item of raw) {
12876
+ if (typeof item === "string" && item.trim()) {
12877
+ out.push({ label: item.trim() });
12878
+ continue;
12879
+ }
12880
+ const o = asRecord3(item);
12881
+ if (!o) continue;
12882
+ const label = typeof o.label === "string" ? o.label.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.title === "string" ? o.title.trim() : "";
12883
+ if (!label) continue;
12884
+ const description = typeof o.description === "string" && o.description.trim() ? o.description.trim() : void 0;
12885
+ out.push({ label, description });
12886
+ }
12887
+ return out;
12888
+ }
12889
+ function parseOneQuestion(raw) {
12890
+ const o = asRecord3(raw);
12891
+ if (!o) return null;
12892
+ const question = typeof o.question === "string" ? o.question.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.prompt === "string" ? o.prompt.trim() : "";
12893
+ if (!question) return null;
12894
+ const options = parseOptions(o.options);
12895
+ if (options.length < 1) return null;
12896
+ const header = typeof o.header === "string" && o.header.trim() ? o.header.trim() : void 0;
12897
+ const multiSelect = Boolean(o.multiSelect ?? o.multi_select);
12898
+ return { question, header, multiSelect, options };
12899
+ }
12900
+ function parsePlanQuestionsInput(input) {
12901
+ const root = asRecord3(input);
12902
+ if (!root) return [];
12903
+ const list = Array.isArray(root.questions) ? root.questions : Array.isArray(root.question) ? root.question : root.question || root.prompt ? [root] : [];
12904
+ const out = [];
12905
+ for (const item of list) {
12906
+ const q = parseOneQuestion(item);
12907
+ if (q) out.push(q);
12908
+ }
12909
+ return out;
12910
+ }
12911
+ var ASK_USER_TOOL_RE = /^(AskUserQuestion|ask_user|mcp__sideboard__ask_user)$/i;
12912
+ function isAskUserToolName(name) {
12913
+ if (!name) return false;
12914
+ const base = name.replace(/^mcp__sideboard__/i, "");
12915
+ return ASK_USER_TOOL_RE.test(name) || /^ask_user$/i.test(base);
12916
+ }
12917
+ function extractPendingPlanQuestions(parts) {
12918
+ if (!parts?.length) return null;
12919
+ for (let i = parts.length - 1; i >= 0; i--) {
12920
+ const p = parts[i];
12921
+ if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
12922
+ const questions = parsePlanQuestionsInput(p.input);
12923
+ if (!questions.length) continue;
12924
+ return {
12925
+ id: p.id || `ask-${i}`,
12926
+ questions,
12927
+ source: p.name || "ask_user"
12928
+ };
12929
+ }
12930
+ return null;
12931
+ }
12932
+ function formatPlanQuestionAnswers(questions, answers) {
12933
+ const lines = ["Answers to your questions:"];
12934
+ for (let i = 0; i < questions.length; i++) {
12935
+ const q = questions[i];
12936
+ const a = answers.find((x) => x.questionIndex === i);
12937
+ const header = q.header ? `**${q.header}** \u2014 ` : "";
12938
+ const parts = [];
12939
+ if (a?.selected.length) parts.push(a.selected.join(", "));
12940
+ if (a?.other?.trim()) parts.push(a.other.trim());
12941
+ const body = parts.length ? parts.join(" \xB7 ") : "(no answer)";
12942
+ lines.push(`${i + 1}. ${header}${q.question}`);
12943
+ lines.push(` \u2192 ${body}`);
12944
+ }
12945
+ return lines.join("\n");
12946
+ }
12947
+ function formatPlanQuestionsForChat(questions) {
12948
+ const lines = ["### Questions for you", ""];
12949
+ for (let i = 0; i < questions.length; i++) {
12950
+ const q = questions[i];
12951
+ const header = q.header ? `**${q.header}** \u2014 ` : "";
12952
+ lines.push(`${i + 1}. ${header}${q.question}`);
12953
+ for (let oi = 0; oi < q.options.length; oi++) {
12954
+ const opt = q.options[oi];
12955
+ const desc = opt.description?.trim();
12956
+ lines.push(
12957
+ desc ? ` - **${opt.label}** \u2014 ${desc}` : ` - **${opt.label}**`
12958
+ );
12959
+ }
12960
+ lines.push("");
12961
+ }
12962
+ lines.push("_Answer in the composer below._");
12963
+ return lines.join("\n").trimEnd();
12964
+ }
12965
+
12966
+ // src/index.ts
12967
+ init_plan_present();
12968
+ init_plan_file();
11947
12969
  init_coordinator_prompt();
11948
12970
 
11949
12971
  // src/mcp/server.ts
11950
12972
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
11951
12973
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
11952
12974
  var import_zod = require("zod");
11953
- var import_node_path25 = require("path");
12975
+ var import_node_path26 = require("path");
11954
12976
  init_worktree();
11955
12977
  init_global_workspace();
11956
12978
  init_list_models();
@@ -11999,7 +13021,7 @@ async function startMcpServer() {
11999
13021
  async () => {
12000
13022
  const threads = orch.getThreads(true);
12001
13023
  const lines = threads.map((t) => {
12002
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path25.basename)(t.repoPath) || t.repoPath;
13024
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path26.basename)(t.repoPath) || t.repoPath;
12003
13025
  return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
12004
13026
  });
12005
13027
  return {
@@ -12061,6 +13083,59 @@ async function startMcpServer() {
12061
13083
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
12062
13084
  }
12063
13085
  );
13086
+ server.tool(
13087
+ "ask_user",
13088
+ "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.",
13089
+ {
13090
+ questions: import_zod.z.array(
13091
+ import_zod.z.object({
13092
+ question: import_zod.z.string().describe("Full question text ending with ?"),
13093
+ header: import_zod.z.string().max(24).optional().describe("Short label shown above the question"),
13094
+ multiSelect: import_zod.z.boolean().optional().describe("Allow selecting multiple options"),
13095
+ options: import_zod.z.array(
13096
+ import_zod.z.object({
13097
+ label: import_zod.z.string(),
13098
+ description: import_zod.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
13099
+ })
13100
+ ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
13101
+ })
13102
+ ).min(1).max(4).describe("1\u20134 questions")
13103
+ },
13104
+ async ({ questions }) => {
13105
+ const payload = {
13106
+ ok: true,
13107
+ questions,
13108
+ message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
13109
+ };
13110
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
13111
+ }
13112
+ );
13113
+ server.tool(
13114
+ "present_plan",
13115
+ "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.",
13116
+ {
13117
+ title: import_zod.z.string().optional().describe("Short plan title (defaults to Plan)"),
13118
+ content: import_zod.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
13119
+ thread_id: import_zod.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
13120
+ },
13121
+ async ({ title, content, thread_id }) => {
13122
+ const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
13123
+ let root = process.cwd();
13124
+ if (thread_id?.trim()) {
13125
+ const t = orch.getThread(thread_id.trim());
13126
+ if (t?.worktreePath?.trim()) root = t.worktreePath;
13127
+ }
13128
+ const path = writePlanFile2(root, content);
13129
+ const payload = {
13130
+ ok: true,
13131
+ path,
13132
+ title: title?.trim() || "Plan",
13133
+ content,
13134
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
13135
+ };
13136
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
13137
+ }
13138
+ );
12064
13139
  server.tool(
12065
13140
  "present_schema",
12066
13141
  "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.",
@@ -12639,6 +13714,126 @@ async function startMcpServer() {
12639
13714
  };
12640
13715
  }
12641
13716
  );
13717
+ server.tool(
13718
+ "get_pr_stack",
13719
+ "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.",
13720
+ { ref: import_zod.z.string() },
13721
+ async ({ ref }) => {
13722
+ const stack = await orch.getPrStack(ref);
13723
+ return {
13724
+ content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
13725
+ };
13726
+ }
13727
+ );
13728
+ server.tool(
13729
+ "open_pr_stack_layers",
13730
+ "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
13731
+ {
13732
+ ref: import_zod.z.string(),
13733
+ layer: import_zod.z.number().int().positive().optional()
13734
+ },
13735
+ async ({ ref, layer }) => {
13736
+ const result = await orch.openPrStackLayers(ref, { layer });
13737
+ return {
13738
+ content: [
13739
+ {
13740
+ type: "text",
13741
+ text: JSON.stringify(
13742
+ {
13743
+ stackNumber: result.stack.stackNumber,
13744
+ trunk: result.stack.trunk,
13745
+ threads: result.threads.map((t) => ({
13746
+ id: t.id,
13747
+ title: t.title,
13748
+ branchName: t.branchName,
13749
+ stackLayer: t.stackLayer,
13750
+ worktreePath: t.worktreePath,
13751
+ prUrl: t.prUrl,
13752
+ link: `sideboard://thread/${t.id}`
13753
+ }))
13754
+ },
13755
+ null,
13756
+ 2
13757
+ )
13758
+ }
13759
+ ]
13760
+ };
13761
+ }
13762
+ );
13763
+ server.tool(
13764
+ "add_stack_layer",
13765
+ "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
13766
+ {
13767
+ ref: import_zod.z.string(),
13768
+ branchName: import_zod.z.string(),
13769
+ title: import_zod.z.string().optional()
13770
+ },
13771
+ async ({ ref, branchName, title }) => {
13772
+ const result = await orch.addStackLayer(ref, branchName, { title });
13773
+ return {
13774
+ content: [
13775
+ {
13776
+ type: "text",
13777
+ text: JSON.stringify(
13778
+ {
13779
+ id: result.thread.id,
13780
+ title: result.thread.title,
13781
+ branchName: result.thread.branchName,
13782
+ stackLayer: result.thread.stackLayer,
13783
+ worktreePath: result.thread.worktreePath,
13784
+ link: `sideboard://thread/${result.thread.id}`
13785
+ },
13786
+ null,
13787
+ 2
13788
+ )
13789
+ }
13790
+ ]
13791
+ };
13792
+ }
13793
+ );
13794
+ server.tool(
13795
+ "create_pr_stack",
13796
+ "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
13797
+ {
13798
+ repoPath: import_zod.z.string(),
13799
+ branches: import_zod.z.array(import_zod.z.string()).min(1),
13800
+ agent: import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
13801
+ base: import_zod.z.string().optional(),
13802
+ title: import_zod.z.string().optional()
13803
+ },
13804
+ async (args) => {
13805
+ const result = await orch.createPrStack({
13806
+ repoPath: args.repoPath,
13807
+ branches: args.branches,
13808
+ agent: args.agent,
13809
+ base: args.base,
13810
+ title: args.title
13811
+ });
13812
+ return {
13813
+ content: [
13814
+ {
13815
+ type: "text",
13816
+ text: JSON.stringify(
13817
+ {
13818
+ stackNumber: result.stack.stackNumber,
13819
+ trunk: result.stack.trunk,
13820
+ threads: result.threads.map((t) => ({
13821
+ id: t.id,
13822
+ title: t.title,
13823
+ branchName: t.branchName,
13824
+ stackLayer: t.stackLayer,
13825
+ worktreePath: t.worktreePath,
13826
+ link: `sideboard://thread/${t.id}`
13827
+ }))
13828
+ },
13829
+ null,
13830
+ 2
13831
+ )
13832
+ }
13833
+ ]
13834
+ };
13835
+ }
13836
+ );
12642
13837
  server.tool(
12643
13838
  "list_issues",
12644
13839
  "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.",
@@ -13008,6 +14203,7 @@ init_connected_teams();
13008
14203
  init_injected_mcp();
13009
14204
  // Annotate the CommonJS export names for ESM import in node:
13010
14205
  0 && (module.exports = {
14206
+ ATTACHMENTS_DIR,
13011
14207
  BRIGHTSY_MCP_ALLOWED_TOOLS,
13012
14208
  BrightsySideboardApi,
13013
14209
  CLAUDE_MODEL_CATALOG,
@@ -13023,11 +14219,16 @@ init_injected_mcp();
13023
14219
  FAMOUS_SOCCER_TEAMS,
13024
14220
  GLOBAL_WORKSPACE_ID,
13025
14221
  HARNESS_ENV_KEYS,
14222
+ LEGACY_ATTACHMENTS_DIR,
14223
+ LEGACY_PLAN_FILE_REL,
14224
+ LEGACY_REVIEW_REQUEST_PATH,
13026
14225
  MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS,
13027
14226
  ORCHESTRATOR_AGENT_KINDS,
13028
14227
  Orchestrator,
13029
14228
  PASTE_ATTACH_MIN_CHARS,
13030
14229
  PASTE_ATTACH_MIN_LINES,
14230
+ PLAN_FILE_NAME,
14231
+ PLAN_FILE_REL,
13031
14232
  PLAN_MODE_INSTRUCTION,
13032
14233
  REPO_REVIEW_NAME,
13033
14234
  REPO_REVIEW_PATH,
@@ -13037,6 +14238,8 @@ init_injected_mcp();
13037
14238
  SIDEBOARD_FORCE_STOP,
13038
14239
  SIDEBOARD_MCP_ALLOWED_TOOLS,
13039
14240
  THINKING_EFFORTS,
14241
+ addPrStackLayer,
14242
+ addStackLayerFromThread,
13040
14243
  addWorkspace,
13041
14244
  adoptThread,
13042
14245
  allAdapters,
@@ -13055,6 +14258,7 @@ init_injected_mcp();
13055
14258
  attachmentFromAbsolutePath,
13056
14259
  attachmentsFromBuffers,
13057
14260
  attachmentsFromWorktreePaths,
14261
+ attachmentsGitignoreBody,
13058
14262
  autoCleanupOrphansEnabled,
13059
14263
  autoRenameBranchEnabled,
13060
14264
  autoRunAfterSetupEnabled,
@@ -13077,6 +14281,7 @@ init_injected_mcp();
13077
14281
  caffeinateWhileRunningEnabled,
13078
14282
  captureLoginEnv,
13079
14283
  captureTurnBaseline,
14284
+ checkoutPrStackLayer,
13080
14285
  childEnvWithAppSettings,
13081
14286
  claudeAdapter,
13082
14287
  claudeChromeEnabled,
@@ -13096,8 +14301,10 @@ init_injected_mcp();
13096
14301
  countCacheControlBlocks,
13097
14302
  createChatTab,
13098
14303
  createEmptyThread,
14304
+ createExistingBranchWorktree,
13099
14305
  createGlobalChat,
13100
14306
  createOrUpdatePr,
14307
+ createPrStack,
13101
14308
  createThread,
13102
14309
  createThreadWorktree,
13103
14310
  currentBranch,
@@ -13107,6 +14314,7 @@ init_injected_mcp();
13107
14314
  deleteBranchOnPurgeEnabled,
13108
14315
  deleteThreadRecord,
13109
14316
  detectAgents,
14317
+ detectGhStack,
13110
14318
  detectLocalMergeConflicts,
13111
14319
  disconnectBrightsyTeam,
13112
14320
  discoverSkills,
@@ -13122,12 +14330,15 @@ init_injected_mcp();
13122
14330
  estimateThreadChars,
13123
14331
  expandComposerPrompt,
13124
14332
  extractGhErrorDetail,
14333
+ extractPendingPlanQuestions,
14334
+ extractPresentedPlan,
13125
14335
  extractiveSummary,
13126
14336
  fetchPrHead,
13127
14337
  finalizeParts,
13128
14338
  findInvalidCacheControlTtlOrder,
13129
14339
  findOrphanWorktrees,
13130
14340
  findThreadByRef,
14341
+ findThreadForStackLayer,
13131
14342
  flattenTurnInput,
13132
14343
  forkChatTab,
13133
14344
  forkMessageSlice,
@@ -13138,6 +14349,8 @@ init_injected_mcp();
13138
14349
  formatGhLandError,
13139
14350
  formatIpcInvokeError,
13140
14351
  formatMessagesAsTranscript,
14352
+ formatPlanQuestionAnswers,
14353
+ formatPlanQuestionsForChat,
13141
14354
  formatRateLimitResetHint,
13142
14355
  formatRenameBranchDirective,
13143
14356
  formatTranscriptMarkdown,
@@ -13161,6 +14374,7 @@ init_injected_mcp();
13161
14374
  getPrChecks,
13162
14375
  getPrDetails,
13163
14376
  getPrMeta,
14377
+ getPrStack,
13164
14378
  getRepoSetupInfo,
13165
14379
  getRunMode,
13166
14380
  getRunScript,
@@ -13177,9 +14391,12 @@ init_injected_mcp();
13177
14391
  healOrchestrationSoccerTitles,
13178
14392
  importConductorWorkspace,
13179
14393
  importConductorWorkspaceAsync,
14394
+ initPrStack,
14395
+ initStackFromThread,
13180
14396
  initializeGitRepository,
13181
14397
  inspectGitWorktree,
13182
14398
  installAgent,
14399
+ isAskUserToolName,
13183
14400
  isBrightsyConnected,
13184
14401
  isBrightsyNdjsonLine,
13185
14402
  isCloudCoordinatorThread,
@@ -13189,14 +14406,17 @@ init_injected_mcp();
13189
14406
  isGlobalRepoPath,
13190
14407
  isGlobalThread,
13191
14408
  isImageFilePath,
14409
+ isInPrStack,
13192
14410
  isLinearConnected,
13193
14411
  isOrchestratorCapableAgent,
13194
14412
  isOrchestratorThread,
13195
14413
  isPidAlive,
13196
14414
  isPlaceholderBranch,
14415
+ isPresentPlanToolName,
13197
14416
  isSessionQuotaLimit,
13198
14417
  isSideboardScratchPath,
13199
14418
  isThinkingEffort,
14419
+ isWorkspaceScratchPath,
13200
14420
  listAgentSetupInfo,
13201
14421
  listBranchCommits,
13202
14422
  listBranches,
@@ -13233,6 +14453,7 @@ init_injected_mcp();
13233
14453
  mcpAllowTools,
13234
14454
  mcpAuthWarnings,
13235
14455
  mergePr,
14456
+ mergePrStack,
13236
14457
  mergeUsage,
13237
14458
  nextPastedTextName,
13238
14459
  nextThinkingEffort,
@@ -13242,6 +14463,8 @@ init_injected_mcp();
13242
14463
  normalizeTurnInput,
13243
14464
  normalizeWorktreePath,
13244
14465
  openInSystemTerminal,
14466
+ openPrStackLayers,
14467
+ openStackLayer,
13245
14468
  opencodeAdapter,
13246
14469
  orchestrationQuotaFallbackAgent,
13247
14470
  orchestrationQuotaOnLimit,
@@ -13250,15 +14473,19 @@ init_injected_mcp();
13250
14473
  originGhRepoEnv,
13251
14474
  parseCursorRunnerLine,
13252
14475
  parseForceStopMessage,
14476
+ parseGhStackViewJson,
13253
14477
  parseGithubSlugFromRemoteUrl,
13254
14478
  parseMcpList,
14479
+ parsePlanQuestionsInput,
13255
14480
  parseSessionQuotaResetAt,
13256
14481
  partsToAssistantText,
13257
14482
  pastedTextStats,
13258
14483
  permissionMode,
14484
+ planFileAbs,
13259
14485
  previewLand,
13260
14486
  pushBranch,
13261
14487
  readExistingReviewRequestFile,
14488
+ readPlanFile,
13262
14489
  readSkillBody,
13263
14490
  readThread,
13264
14491
  readWorktreeFile,
@@ -13270,6 +14497,7 @@ init_injected_mcp();
13270
14497
  repoSlug,
13271
14498
  requestReview,
13272
14499
  requireAgent,
14500
+ resetGhStackDetectCache,
13273
14501
  resolveClaudeExecutable,
13274
14502
  resolveConductorCursorAgentId,
13275
14503
  resolveCursorModelId,
@@ -13279,6 +14507,7 @@ init_injected_mcp();
13279
14507
  resolveFilesToCopy,
13280
14508
  resolveGhAuthToken,
13281
14509
  resolveGithubRepoSlug,
14510
+ resolvePlanMarkdown,
13282
14511
  resolvePrSelector,
13283
14512
  resolveQuotaFallbackAgent,
13284
14513
  resolveRepoRoot,
@@ -13306,12 +14535,16 @@ init_injected_mcp();
13306
14535
  slugify,
13307
14536
  spawnAgentTurn,
13308
14537
  splitForCompaction,
14538
+ stackAgentDefaultsFrom,
14539
+ stackIdFrom,
14540
+ stackMergeReadiness,
13309
14541
  stageAbsolutePathsAsAttachments,
13310
14542
  stageBuffersAsAttachments,
13311
14543
  startDevServer,
13312
14544
  startMcpServer,
13313
14545
  startOrchestration,
13314
14546
  stripBrightsyNdjsonNoise,
14547
+ submitPrStack,
13315
14548
  suggestSlug,
13316
14549
  summarizeConversation,
13317
14550
  switchBrightsyAccount,
@@ -13347,6 +14580,7 @@ init_injected_mcp();
13347
14580
  worktreeNameFromPath,
13348
14581
  worktreesRoot,
13349
14582
  writeInjectedMcpConfig,
14583
+ writePlanFile,
13350
14584
  writeThread,
13351
14585
  writeWorktreeFile
13352
14586
  });