@sideboard-ai/core 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{agents-LUB3Q773.js → agents-ON6RKKND.js} +3 -3
  2. package/dist/{agents-3P4N6KHC.js → agents-YKSS6VBO.js} +3 -3
  3. package/dist/{chunk-RQI4TOXK.js → chunk-6QTZVJ7A.js} +7 -5
  4. package/dist/{chunk-AJ6ROGD7.js → chunk-7LNGIP3X.js} +20 -2
  5. package/dist/chunk-B3SJXYIJ.js +24 -0
  6. package/dist/{chunk-ILQK4P5R.js → chunk-BXJ76RHF.js} +4 -4
  7. package/dist/{chunk-SS34TVSY.js → chunk-D3METLRW.js} +7 -5
  8. package/dist/{chunk-FV6FN6V5.js → chunk-DZFH2KLT.js} +340 -22
  9. package/dist/chunk-FKOIHGKV.js +21 -0
  10. package/dist/{chunk-3NAS4MWH.js → chunk-GNML24AW.js} +2 -2
  11. package/dist/{chunk-E35APBHV.js → chunk-H6GGDLYS.js} +1 -1
  12. package/dist/{chunk-EOYDCKQC.js → chunk-HLEX5AQ6.js} +4 -0
  13. package/dist/{chunk-ZQCQIWIP.js → chunk-LRLKJM3O.js} +2 -1
  14. package/dist/chunk-N5PM7HGQ.js +103 -0
  15. package/dist/chunk-QTUESPAW.js +101 -0
  16. package/dist/{chunk-O6W3P7V3.js → chunk-TSRXOSVD.js} +4 -0
  17. package/dist/{chunk-GZXBYHJC.js → chunk-UEAHMGHW.js} +2 -1
  18. package/dist/{chunk-ZVV5EEQN.js → chunk-XOU6HNQJ.js} +305 -22
  19. package/dist/{chunk-5WYEJ3F3.js → chunk-XX5BB7NV.js} +3 -3
  20. package/dist/{chunk-V4JRXYYU.js → chunk-YDXQ72MD.js} +2 -2
  21. package/dist/{chunk-DF5VQQKA.js → chunk-YOWIYAVA.js} +3 -3
  22. package/dist/{chunk-ISY4EGF6.js → chunk-ZNM4MAN4.js} +20 -2
  23. package/dist/{connected-teams-UBXHZGO4.js → connected-teams-2ANNZCP5.js} +2 -2
  24. package/dist/{connected-teams-GF52Q7LB.js → connected-teams-ONC4666A.js} +2 -2
  25. package/dist/{coordinator-prompt-VG4BZ5JL.js → coordinator-prompt-6FXVTSFN.js} +5 -4
  26. package/dist/{coordinator-prompt-SPGDT7J5.js → coordinator-prompt-S6JZD5EF.js} +5 -4
  27. package/dist/{global-workspace-OJF4ENH4.js → global-workspace-EV4G2WMQ.js} +6 -5
  28. package/dist/{global-workspace-KSR3E63K.js → global-workspace-MSX2K27Y.js} +6 -5
  29. package/dist/index.cjs +1369 -155
  30. package/dist/index.d.cts +397 -14
  31. package/dist/index.d.ts +397 -14
  32. package/dist/index.js +811 -90
  33. package/dist/mcp/run-stdio.cjs +1136 -147
  34. package/dist/mcp/run-stdio.js +635 -79
  35. package/dist/plan-file-6O7G4VPQ.js +23 -0
  36. package/dist/plan-file-PHVKUAEE.js +25 -0
  37. package/dist/{run-HMRSRG3U.js → run-H3IMNOYN.js} +3 -1
  38. package/dist/run-ZJKYHXGN.js +12 -0
  39. package/dist/{thread-store-XICUWFNM.js → thread-store-GHOADGL2.js} +1 -1
  40. package/dist/{thread-store-OV2X6PYO.js → thread-store-UJIGMI5J.js} +1 -1
  41. package/dist/{workspaces-OZE7LRDO.js → workspaces-3RQQZQRO.js} +7 -6
  42. package/dist/{workspaces-UJX7JIGH.js → workspaces-AYTBR6KQ.js} +7 -6
  43. package/dist/{worktree-XG5PCLZY.js → worktree-5KEQWSAF.js} +8 -3
  44. package/dist/{worktree-TEDAJ57S.js → worktree-RWGL7FUV.js} +8 -3
  45. package/package.json +1 -1
  46. package/dist/run-LF6E5IKL.js +0 -10
@@ -481,6 +481,8 @@ function normalizeThread(raw) {
481
481
  agentPid: raw.agentPid ?? null,
482
482
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
483
483
  prTitle: raw.prTitle ?? null,
484
+ stackId: raw.stackId ?? null,
485
+ stackLayer: raw.stackLayer ?? null,
484
486
  userSetTitle: Boolean(raw.userSetTitle),
485
487
  activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
486
488
  quotaResumeAt: raw.quotaResumeAt ?? null,
@@ -505,6 +507,8 @@ function createEmptyThread(partial) {
505
507
  activeRuns: partial.activeRuns ?? [],
506
508
  prUrl: partial.prUrl ?? null,
507
509
  prTitle: partial.prTitle ?? null,
510
+ stackId: partial.stackId ?? null,
511
+ stackLayer: partial.stackLayer ?? null,
508
512
  userSetTitle: partial.userSetTitle ?? false,
509
513
  messages: partial.messages ?? [],
510
514
  attachments: partial.attachments ?? [],
@@ -1536,6 +1540,7 @@ var run_exports = {};
1536
1540
  __export(run_exports, {
1537
1541
  gh: () => gh,
1538
1542
  git: () => git,
1543
+ resolveGhAuthToken: () => resolveGhAuthToken,
1539
1544
  run: () => run
1540
1545
  });
1541
1546
  async function run(file, args, opts) {
@@ -1564,11 +1569,28 @@ async function run(file, args, opts) {
1564
1569
  }
1565
1570
  }
1566
1571
  async function git(args, cwd, opts) {
1567
- return run("git", args, { cwd, reject: opts?.reject });
1572
+ const prefix = [];
1573
+ if (opts?.config) {
1574
+ for (const [key, value] of Object.entries(opts.config)) {
1575
+ if (!key) continue;
1576
+ prefix.push("-c", `${key}=${value}`);
1577
+ }
1578
+ }
1579
+ return run("git", [...prefix, ...args], {
1580
+ cwd,
1581
+ reject: opts?.reject,
1582
+ env: opts?.env
1583
+ });
1568
1584
  }
1569
1585
  async function gh(args, cwd, opts) {
1570
1586
  return run("gh", args, { cwd, reject: opts?.reject });
1571
1587
  }
1588
+ async function resolveGhAuthToken(cwd) {
1589
+ const result = await gh(["auth", "token"], cwd, { reject: false });
1590
+ if (result.exitCode !== 0) return null;
1591
+ const token = result.stdout.trim();
1592
+ return token || null;
1593
+ }
1572
1594
  var import_execa;
1573
1595
  var init_run = __esm({
1574
1596
  "src/git/run.ts"() {
@@ -1670,6 +1692,206 @@ var init_pr_gates = __esm({
1670
1692
  }
1671
1693
  });
1672
1694
 
1695
+ // src/git/stack.ts
1696
+ async function detectGhStack(cwd) {
1697
+ const now = Date.now();
1698
+ if (cachedStatus && now - cachedStatus.at < STATUS_TTL_MS) {
1699
+ return cachedStatus.status;
1700
+ }
1701
+ const probe = await gh(["stack", "view", "--help"], cwd, { reject: false });
1702
+ if (probe.exitCode === 0) {
1703
+ const status2 = { available: true };
1704
+ cachedStatus = { at: now, status: status2 };
1705
+ return status2;
1706
+ }
1707
+ const err = `${probe.stderr}
1708
+ ${probe.stdout}`;
1709
+ 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";
1710
+ const status = { available: false, reason };
1711
+ cachedStatus = { at: now, status };
1712
+ return status;
1713
+ }
1714
+ function str(v) {
1715
+ return typeof v === "string" ? v : v == null ? "" : String(v);
1716
+ }
1717
+ function num(v) {
1718
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1719
+ if (typeof v === "string" && v.trim() && Number.isFinite(Number(v))) {
1720
+ return Number(v);
1721
+ }
1722
+ return null;
1723
+ }
1724
+ function parseGhStackViewJson(raw) {
1725
+ let data;
1726
+ try {
1727
+ data = JSON.parse(raw);
1728
+ } catch {
1729
+ return null;
1730
+ }
1731
+ if (!Array.isArray(data.branches) || data.branches.length === 0) return null;
1732
+ const trunk = str(data.trunk) || "main";
1733
+ const currentBranch2 = str(data.currentBranch);
1734
+ const stackNumber = num(data.stackNumber) ?? num(data.number);
1735
+ const layers = [];
1736
+ for (let i = 0; i < data.branches.length; i++) {
1737
+ const b = data.branches[i];
1738
+ if (!b || typeof b !== "object") continue;
1739
+ const name = str(b.name);
1740
+ if (!name) continue;
1741
+ const pr = b.pr && typeof b.pr === "object" ? b.pr : null;
1742
+ layers.push({
1743
+ position: i + 1,
1744
+ branchName: name,
1745
+ headSha: str(b.head) || void 0,
1746
+ baseSha: str(b.base) || void 0,
1747
+ isCurrent: Boolean(b.isCurrent) || name === currentBranch2,
1748
+ isMerged: Boolean(b.isMerged),
1749
+ isQueued: Boolean(b.isQueued),
1750
+ needsRebase: Boolean(b.needsRebase),
1751
+ prNumber: pr ? num(pr.number) : null,
1752
+ prUrl: pr && str(pr.url) ? str(pr.url) : null,
1753
+ prState: pr && str(pr.state) ? str(pr.state).toUpperCase() : null,
1754
+ title: pr && str(pr.title) ? str(pr.title) : void 0
1755
+ });
1756
+ }
1757
+ if (!layers.length) return null;
1758
+ let currentIndex = layers.findIndex((l) => l.isCurrent);
1759
+ if (currentIndex < 0 && currentBranch2) {
1760
+ currentIndex = layers.findIndex((l) => l.branchName === currentBranch2);
1761
+ }
1762
+ const { readyToMerge, blockedReason } = stackMergeReadiness(layers, currentIndex);
1763
+ return {
1764
+ stackNumber,
1765
+ trunk,
1766
+ currentBranch: currentBranch2 || layers[currentIndex]?.branchName || layers[0].branchName,
1767
+ layers,
1768
+ currentIndex,
1769
+ readyToMerge,
1770
+ blockedReason
1771
+ };
1772
+ }
1773
+ function stackMergeReadiness(layers, throughIndex) {
1774
+ if (throughIndex < 0 || throughIndex >= layers.length) {
1775
+ return { readyToMerge: false, blockedReason: "Not on a stack layer" };
1776
+ }
1777
+ for (let i = 0; i <= throughIndex; i++) {
1778
+ const layer = layers[i];
1779
+ if (layer.isMerged) continue;
1780
+ if (!layer.prNumber) {
1781
+ return {
1782
+ readyToMerge: false,
1783
+ blockedReason: `Layer ${layer.branchName} has no pull request yet`
1784
+ };
1785
+ }
1786
+ if (layer.needsRebase) {
1787
+ return {
1788
+ readyToMerge: false,
1789
+ blockedReason: `PR #${layer.prNumber} needs rebase`
1790
+ };
1791
+ }
1792
+ const state = (layer.prState ?? "").toUpperCase();
1793
+ if (state && state !== "OPEN" && state !== "QUEUED") {
1794
+ return {
1795
+ readyToMerge: false,
1796
+ blockedReason: `PR #${layer.prNumber} is ${state}`
1797
+ };
1798
+ }
1799
+ }
1800
+ return { readyToMerge: true, blockedReason: null };
1801
+ }
1802
+ async function getPrStack(cwd) {
1803
+ const status = await detectGhStack(cwd);
1804
+ if (!status.available) return null;
1805
+ const result = await gh(["stack", "view", "--json"], cwd, { reject: false });
1806
+ if (result.exitCode === 2) return null;
1807
+ if (result.exitCode !== 0) {
1808
+ if (/not in a stack|no stack/i.test(`${result.stderr}
1809
+ ${result.stdout}`)) {
1810
+ return null;
1811
+ }
1812
+ if (result.exitCode === 9) return null;
1813
+ return null;
1814
+ }
1815
+ const json = result.stdout.trim();
1816
+ if (!json) return null;
1817
+ return parseGhStackViewJson(json);
1818
+ }
1819
+ async function mergePrStack(cwd, opts) {
1820
+ const status = await detectGhStack(cwd);
1821
+ if (!status.available) {
1822
+ throw new Error(status.reason);
1823
+ }
1824
+ const method = opts.method ?? "squash";
1825
+ const methodFlag = method === "rebase" ? "--rebase" : method === "merge" ? "--merge" : "--squash";
1826
+ const args = [
1827
+ "stack",
1828
+ "merge",
1829
+ String(opts.through),
1830
+ "--yes",
1831
+ methodFlag
1832
+ ];
1833
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
1834
+ if (exitCode !== 0) {
1835
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack merge failed");
1836
+ }
1837
+ return { stdout };
1838
+ }
1839
+ async function initPrStack(cwd, branches, opts) {
1840
+ if (!branches.length) throw new Error("initPrStack requires at least one branch name");
1841
+ const status = await detectGhStack(cwd);
1842
+ if (!status.available) throw new Error(status.reason);
1843
+ const args = ["stack", "init"];
1844
+ if (opts?.base) args.push("--base", opts.base);
1845
+ args.push(...branches);
1846
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
1847
+ if (exitCode !== 0) {
1848
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack init failed");
1849
+ }
1850
+ }
1851
+ async function addPrStackLayer(cwd, branchName) {
1852
+ if (!branchName.trim()) throw new Error("branch name required");
1853
+ const status = await detectGhStack(cwd);
1854
+ if (!status.available) throw new Error(status.reason);
1855
+ const { exitCode, stderr, stdout } = await gh(
1856
+ ["stack", "add", branchName.trim()],
1857
+ cwd,
1858
+ { reject: false }
1859
+ );
1860
+ if (exitCode !== 0) {
1861
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack add failed");
1862
+ }
1863
+ }
1864
+ var cachedStatus, STATUS_TTL_MS;
1865
+ var init_stack = __esm({
1866
+ "src/git/stack.ts"() {
1867
+ "use strict";
1868
+ init_run();
1869
+ cachedStatus = null;
1870
+ STATUS_TTL_MS = 6e4;
1871
+ }
1872
+ });
1873
+
1874
+ // src/paths/workspace-scratch.ts
1875
+ function attachmentsGitignoreBody() {
1876
+ return ATTACHMENTS_GITIGNORE;
1877
+ }
1878
+ function isWorkspaceScratchPath(relativePath) {
1879
+ const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
1880
+ return p === ATTACHMENTS_DIR || p.startsWith(`${ATTACHMENTS_DIR}/`) || p === LEGACY_ATTACHMENTS_DIR || p.startsWith(`${LEGACY_ATTACHMENTS_DIR}/`) || p === ".context" || p.startsWith(".context/");
1881
+ }
1882
+ var ATTACHMENTS_DIR, LEGACY_ATTACHMENTS_DIR, ATTACHMENTS_GITIGNORE;
1883
+ var init_workspace_scratch = __esm({
1884
+ "src/paths/workspace-scratch.ts"() {
1885
+ "use strict";
1886
+ ATTACHMENTS_DIR = ".context/attachments";
1887
+ LEGACY_ATTACHMENTS_DIR = ".sideboard/attachments";
1888
+ ATTACHMENTS_GITIGNORE = `# Sideboard / workspace attachments (local only)
1889
+ *
1890
+ !.gitignore
1891
+ `;
1892
+ }
1893
+ });
1894
+
1673
1895
  // src/git/worktree.ts
1674
1896
  var worktree_exports = {};
1675
1897
  __export(worktree_exports, {
@@ -1679,6 +1901,7 @@ __export(worktree_exports, {
1679
1901
  branchDisplayLabel: () => branchDisplayLabel,
1680
1902
  collectTakenTeamSlugs: () => collectTakenTeamSlugs,
1681
1903
  commitAll: () => commitAll,
1904
+ createExistingBranchWorktree: () => createExistingBranchWorktree,
1682
1905
  createOrUpdatePr: () => createOrUpdatePr,
1683
1906
  createThreadWorktree: () => createThreadWorktree,
1684
1907
  currentBranch: () => currentBranch,
@@ -1693,6 +1916,7 @@ __export(worktree_exports, {
1693
1916
  ghRepoSelectArgs: () => ghRepoSelectArgs,
1694
1917
  isDirty: () => isDirty,
1695
1918
  isPlaceholderBranch: () => isPlaceholderBranch,
1919
+ isSideboardScratchPath: () => isSideboardScratchPath,
1696
1920
  listBranches: () => listBranches,
1697
1921
  listPrs: () => listPrs,
1698
1922
  listWorktrees: () => listWorktrees,
@@ -2222,18 +2446,31 @@ async function fetchPrHead(repoPath, number, localBranch) {
2222
2446
  const slug = await resolveGithubRepoSlug(repoPath);
2223
2447
  const refspec = `+pull/${number}/head:${localBranch}`;
2224
2448
  const errors = [];
2225
- const tryFetch = async (remote) => {
2226
- const result = await git(["fetch", remote, refspec], repoPath, {
2227
- reject: false
2228
- });
2449
+ const httpsRemote = slug ? `https://github.com/${slug}.git` : null;
2450
+ const tryFetch = async (remote, opts) => {
2451
+ const label = opts?.ghAuth ? `${remote} (gh auth)` : remote;
2452
+ const gitOpts = { reject: false };
2453
+ if (opts?.ghAuth) {
2454
+ const token = await resolveGhAuthToken(repoPath);
2455
+ if (!token) {
2456
+ errors.push(`${label}: gh auth token unavailable`);
2457
+ return false;
2458
+ }
2459
+ gitOpts.env = { GIT_TERMINAL_PROMPT: "0" };
2460
+ gitOpts.config = {
2461
+ "http.extraHeader": `AUTHORIZATION: bearer ${token}`
2462
+ };
2463
+ }
2464
+ const result = await git(["fetch", remote, refspec], repoPath, gitOpts);
2229
2465
  if (result.exitCode === 0) return true;
2230
2466
  const detail = (result.stderr || result.stdout).trim();
2231
- if (detail) errors.push(`${remote}: ${detail}`);
2467
+ if (detail) errors.push(`${label}: ${detail}`);
2232
2468
  return false;
2233
2469
  };
2234
2470
  let fetched = await tryFetch("origin");
2235
- if (!fetched && slug) {
2236
- fetched = await tryFetch(`https://github.com/${slug}.git`);
2471
+ if (!fetched && httpsRemote) {
2472
+ fetched = await tryFetch(httpsRemote);
2473
+ if (!fetched) fetched = await tryFetch(httpsRemote, { ghAuth: true });
2237
2474
  }
2238
2475
  const localOk = async () => {
2239
2476
  const verify = await git(["rev-parse", "--verify", localBranch], repoPath, {
@@ -2262,18 +2499,28 @@ async function fetchPrHead(repoPath, number, localBranch) {
2262
2499
  }
2263
2500
  }
2264
2501
  if (oid) {
2265
- const ensureOid = async (remote) => {
2266
- const got = await git(["fetch", remote, oid], repoPath, {
2267
- reject: false
2268
- });
2502
+ const ensureOid = async (remote, opts) => {
2503
+ const gitOpts = { reject: false };
2504
+ if (opts?.ghAuth) {
2505
+ const token = await resolveGhAuthToken(repoPath);
2506
+ if (!token) return false;
2507
+ gitOpts.env = { GIT_TERMINAL_PROMPT: "0" };
2508
+ gitOpts.config = {
2509
+ "http.extraHeader": `AUTHORIZATION: bearer ${token}`
2510
+ };
2511
+ }
2512
+ const got = await git(["fetch", remote, oid], repoPath, gitOpts);
2269
2513
  return got.exitCode === 0;
2270
2514
  };
2271
2515
  let haveObject = (await git(["cat-file", "-e", `${oid}^{commit}`], repoPath, {
2272
2516
  reject: false
2273
2517
  })).exitCode === 0;
2274
2518
  if (!haveObject) haveObject = await ensureOid("origin");
2275
- if (!haveObject && slug) {
2276
- haveObject = await ensureOid(`https://github.com/${slug}.git`);
2519
+ if (!haveObject && httpsRemote) {
2520
+ haveObject = await ensureOid(httpsRemote);
2521
+ if (!haveObject) {
2522
+ haveObject = await ensureOid(httpsRemote, { ghAuth: true });
2523
+ }
2277
2524
  }
2278
2525
  if (haveObject) {
2279
2526
  const branched = await git(["branch", "-f", localBranch, oid], repoPath, {
@@ -2368,6 +2615,52 @@ ${add.stdout}`;
2368
2615
  await ensureGhPreferOrigin(worktreePath);
2369
2616
  return { branchName, worktreePath };
2370
2617
  }
2618
+ async function createExistingBranchWorktree(opts) {
2619
+ const branchName = opts.branchName.trim();
2620
+ if (!branchName) throw new Error("branch name required");
2621
+ const worktreePath = (0, import_node_path5.join)(worktreesRoot(opts.repoPath), opts.slug);
2622
+ if ((0, import_node_fs4.existsSync)(worktreePath)) {
2623
+ throw new Error(`Worktree already exists at ${worktreePath}`);
2624
+ }
2625
+ await ensureGhPreferOrigin(opts.repoPath);
2626
+ await git(["fetch", "origin", "--prune"], opts.repoPath, { reject: false });
2627
+ if (!branchName.startsWith("origin/") && !branchName.startsWith("refs/")) {
2628
+ await git(["fetch", "origin", branchName], opts.repoPath, { reject: false });
2629
+ }
2630
+ const existing = await listWorktrees(opts.repoPath);
2631
+ const already = existing.find((w) => w.branch === branchName);
2632
+ if (already?.path) {
2633
+ throw new Error(
2634
+ `Branch ${branchName} is already checked out at ${already.path}`
2635
+ );
2636
+ }
2637
+ const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
2638
+ const add = await git(
2639
+ ["worktree", "add", worktreePath, startPoint],
2640
+ opts.repoPath,
2641
+ { reject: false }
2642
+ );
2643
+ if (add.exitCode !== 0) {
2644
+ const retry = await git(
2645
+ ["worktree", "add", worktreePath, branchName],
2646
+ opts.repoPath,
2647
+ { reject: false }
2648
+ );
2649
+ if (retry.exitCode !== 0) {
2650
+ throw new Error(
2651
+ `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
2652
+ );
2653
+ }
2654
+ }
2655
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
2656
+ reject: false
2657
+ });
2658
+ if (head.stdout.trim() === "HEAD" || head.stdout.trim() !== branchName) {
2659
+ await git(["checkout", "-B", branchName], worktreePath, { reject: false });
2660
+ }
2661
+ await ensureGhPreferOrigin(worktreePath);
2662
+ return { branchName, worktreePath };
2663
+ }
2371
2664
  async function removeWorktree(repoPath, worktreePath, opts) {
2372
2665
  await git(["worktree", "remove", "--force", worktreePath], repoPath, {
2373
2666
  reject: false
@@ -2397,8 +2690,27 @@ async function listWorktrees(repoPath) {
2397
2690
  return entries;
2398
2691
  }
2399
2692
  async function isDirty(worktreePath) {
2400
- const { stdout } = await git(["status", "--porcelain"], worktreePath);
2401
- return stdout.trim().length > 0;
2693
+ const { stdout } = await git(["status", "--porcelain", "-uall"], worktreePath);
2694
+ for (const line of stdout.split("\n")) {
2695
+ if (!line.trim()) continue;
2696
+ const rel = porcelainStatusPath(line);
2697
+ if (!rel || isSideboardScratchPath(rel)) continue;
2698
+ return true;
2699
+ }
2700
+ return false;
2701
+ }
2702
+ function isSideboardScratchPath(relativePath) {
2703
+ return isWorkspaceScratchPath(relativePath);
2704
+ }
2705
+ function porcelainStatusPath(line) {
2706
+ const rest = line.length >= 3 ? line.slice(3) : "";
2707
+ if (!rest) return "";
2708
+ const arrow = rest.lastIndexOf(" -> ");
2709
+ const raw = arrow >= 0 ? rest.slice(arrow + 4) : rest;
2710
+ if (raw.startsWith('"') && raw.endsWith('"')) {
2711
+ return raw.slice(1, -1).replace(/\\([\\"])/g, "$1");
2712
+ }
2713
+ return raw;
2402
2714
  }
2403
2715
  async function currentBranch(worktreePath) {
2404
2716
  const { stdout } = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
@@ -2416,7 +2728,7 @@ async function pushBranch(worktreePath, branchName) {
2416
2728
  }
2417
2729
  async function mergePr(cwd, selector, opts) {
2418
2730
  const slug = await resolveGithubRepoSlug(cwd);
2419
- const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft"];
2731
+ const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft,number"];
2420
2732
  if (slug) viewArgs.push("--repo", slug);
2421
2733
  const before = await gh(viewArgs, cwd, { reject: false });
2422
2734
  if (before.exitCode !== 0 || !before.stdout.trim()) {
@@ -2424,16 +2736,29 @@ async function mergePr(cwd, selector, opts) {
2424
2736
  }
2425
2737
  let url = "";
2426
2738
  let isDraft = false;
2739
+ let prNumber = null;
2427
2740
  try {
2428
2741
  const parsed = JSON.parse(before.stdout);
2429
2742
  url = String(parsed.url ?? "");
2430
2743
  isDraft = Boolean(parsed.isDraft);
2744
+ prNumber = typeof parsed.number === "number" && Number.isFinite(parsed.number) ? parsed.number : null;
2431
2745
  if (String(parsed.state ?? "").toUpperCase() === "MERGED") {
2432
2746
  return { url, state: "MERGED" };
2433
2747
  }
2434
2748
  } catch {
2435
2749
  throw new Error("Could not parse pull request details");
2436
2750
  }
2751
+ const stack = await getPrStack(cwd);
2752
+ const stackLayer = stack && prNumber != null ? stack.layers.find((l) => l.prNumber === prNumber) : null;
2753
+ if (stack && stackLayer && prNumber != null) {
2754
+ const throughIndex = stack.layers.findIndex((l) => l.prNumber === prNumber);
2755
+ const gate = stackMergeReadiness(stack.layers, throughIndex);
2756
+ if (!gate.readyToMerge) {
2757
+ throw new Error(gate.blockedReason || "Stack is not ready to merge");
2758
+ }
2759
+ await mergePrStack(cwd, { through: prNumber, method: opts?.method ?? "squash" });
2760
+ return { url, state: "MERGED" };
2761
+ }
2437
2762
  if (isDraft) {
2438
2763
  const readyArgs = ["pr", "ready", selector];
2439
2764
  if (slug) readyArgs.push("--repo", slug);
@@ -2455,10 +2780,10 @@ async function mergePr(cwd, selector, opts) {
2455
2780
  const after = await gh(viewArgs, cwd, { reject: false });
2456
2781
  if (after.exitCode === 0 && after.stdout.trim()) {
2457
2782
  try {
2458
- const parsed = JSON.parse(after.stdout);
2783
+ const parsed = after.stdout ? JSON.parse(after.stdout) : null;
2459
2784
  return {
2460
- url: String(parsed.url ?? url),
2461
- state: String(parsed.state ?? "MERGED")
2785
+ url: String(parsed?.url ?? url),
2786
+ state: String(parsed?.state ?? "MERGED")
2462
2787
  };
2463
2788
  } catch {
2464
2789
  }
@@ -2606,7 +2931,9 @@ var init_worktree = __esm({
2606
2931
  init_gh_errors();
2607
2932
  init_run();
2608
2933
  init_pr_gates();
2934
+ init_stack();
2609
2935
  init_teams();
2936
+ init_workspace_scratch();
2610
2937
  init_worktree_labels();
2611
2938
  }
2612
2939
  });
@@ -3284,6 +3611,7 @@ var init_coordinator_prompt = __esm({
3284
3611
  "Discover:",
3285
3612
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3286
3613
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3614
+ "- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
3287
3615
  "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3288
3616
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3289
3617
  "Workspaces:",
@@ -4269,7 +4597,9 @@ var init_injected_mcp = __esm({
4269
4597
  SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS = [
4270
4598
  "mcp__sideboard__present_artifact",
4271
4599
  "mcp__sideboard__present_schema",
4272
- "mcp__sideboard__present_files"
4600
+ "mcp__sideboard__present_files",
4601
+ "mcp__sideboard__ask_user",
4602
+ "mcp__sideboard__present_plan"
4273
4603
  ];
4274
4604
  brightsyMcpCommandCache = null;
4275
4605
  }
@@ -4308,7 +4638,7 @@ var PLAN_MODE_INSTRUCTION;
4308
4638
  var init_types = __esm({
4309
4639
  "src/agents/types.ts"() {
4310
4640
  "use strict";
4311
- 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.";
4641
+ 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.";
4312
4642
  }
4313
4643
  });
4314
4644
 
@@ -6016,6 +6346,116 @@ var init_workspaces = __esm({
6016
6346
  }
6017
6347
  });
6018
6348
 
6349
+ // src/plan/plan-present.ts
6350
+ function asRecord2(v) {
6351
+ return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
6352
+ }
6353
+ function isPresentPlanToolName(name) {
6354
+ if (!name) return false;
6355
+ return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
6356
+ }
6357
+ function extractPresentedPlan(parts) {
6358
+ if (!parts?.length) return null;
6359
+ for (let i = parts.length - 1; i >= 0; i--) {
6360
+ const p = parts[i];
6361
+ if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
6362
+ const input = asRecord2(p.input) ?? {};
6363
+ const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
6364
+ if (!content.trim()) continue;
6365
+ const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
6366
+ const path = typeof input.path === "string" && input.path.trim() ? input.path.trim() : PLAN_FILE_REL;
6367
+ return { title, content: content.trim(), path, source: "present_plan" };
6368
+ }
6369
+ return null;
6370
+ }
6371
+ function resolvePlanMarkdown(opts) {
6372
+ const fromTool = extractPresentedPlan(opts.parts);
6373
+ if (fromTool) return fromTool;
6374
+ const file = opts.fileContent?.trim();
6375
+ if (file) {
6376
+ return {
6377
+ title: "Plan",
6378
+ content: file,
6379
+ path: PLAN_FILE_REL,
6380
+ source: "exit_plan"
6381
+ };
6382
+ }
6383
+ const text = opts.text?.trim();
6384
+ if (text && text.length >= 80) {
6385
+ return {
6386
+ title: "Plan",
6387
+ content: text,
6388
+ path: PLAN_FILE_REL,
6389
+ source: "text"
6390
+ };
6391
+ }
6392
+ return null;
6393
+ }
6394
+ var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
6395
+ var init_plan_present = __esm({
6396
+ "src/plan/plan-present.ts"() {
6397
+ "use strict";
6398
+ init_workspace_scratch();
6399
+ PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
6400
+ PLAN_FILE_NAME = "plan.md";
6401
+ LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
6402
+ }
6403
+ });
6404
+
6405
+ // src/plan/plan-file.ts
6406
+ var plan_file_exports = {};
6407
+ __export(plan_file_exports, {
6408
+ LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
6409
+ PLAN_FILE_NAME: () => PLAN_FILE_NAME,
6410
+ PLAN_FILE_REL: () => PLAN_FILE_REL,
6411
+ extractPresentedPlan: () => extractPresentedPlan,
6412
+ isPresentPlanToolName: () => isPresentPlanToolName,
6413
+ planFileAbs: () => planFileAbs,
6414
+ readPlanFile: () => readPlanFile,
6415
+ resolvePlanMarkdown: () => resolvePlanMarkdown,
6416
+ writePlanFile: () => writePlanFile
6417
+ });
6418
+ function ensureAttachmentsGitignore2(worktreePath) {
6419
+ const gitignoreAbs = (0, import_node_path24.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
6420
+ if ((0, import_node_fs26.existsSync)(gitignoreAbs)) return;
6421
+ (0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(gitignoreAbs), { recursive: true });
6422
+ (0, import_node_fs26.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
6423
+ }
6424
+ function planFileAbs(worktreePath) {
6425
+ return (0, import_node_path24.join)(worktreePath, PLAN_FILE_REL);
6426
+ }
6427
+ function readTextIfPresent2(abs) {
6428
+ if (!(0, import_node_fs26.existsSync)(abs)) return null;
6429
+ try {
6430
+ const content = (0, import_node_fs26.readFileSync)(abs, "utf8");
6431
+ return content.trim() ? content : null;
6432
+ } catch {
6433
+ return null;
6434
+ }
6435
+ }
6436
+ function readPlanFile(worktreePath) {
6437
+ 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));
6438
+ }
6439
+ function writePlanFile(worktreePath, content) {
6440
+ ensureAttachmentsGitignore2(worktreePath);
6441
+ const abs = planFileAbs(worktreePath);
6442
+ (0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(abs), { recursive: true });
6443
+ const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
6444
+ (0, import_node_fs26.writeFileSync)(abs, body, "utf8");
6445
+ return PLAN_FILE_REL;
6446
+ }
6447
+ var import_node_fs26, import_node_path24;
6448
+ var init_plan_file = __esm({
6449
+ "src/plan/plan-file.ts"() {
6450
+ "use strict";
6451
+ import_node_fs26 = require("fs");
6452
+ import_node_path24 = require("path");
6453
+ init_workspace_scratch();
6454
+ init_plan_present();
6455
+ init_plan_present();
6456
+ }
6457
+ });
6458
+
6019
6459
  // src/agents/cursor-recover.ts
6020
6460
  var cursor_recover_exports = {};
6021
6461
  __export(cursor_recover_exports, {
@@ -6024,10 +6464,10 @@ __export(cursor_recover_exports, {
6024
6464
  function recoverFinishedCursorRun(opts) {
6025
6465
  const agentId = opts.agentId.trim();
6026
6466
  if (!agentId) return null;
6027
- const runsPath = (0, import_node_path24.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6028
- if (!(0, import_node_fs25.existsSync)(runsPath)) return null;
6467
+ const runsPath = (0, import_node_path25.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6468
+ if (!(0, import_node_fs27.existsSync)(runsPath)) return null;
6029
6469
  try {
6030
- const lines = (0, import_node_fs25.readFileSync)(runsPath, "utf8").split("\n");
6470
+ const lines = (0, import_node_fs27.readFileSync)(runsPath, "utf8").split("\n");
6031
6471
  let best = null;
6032
6472
  for (const line of lines) {
6033
6473
  const trimmed = line.trim();
@@ -6053,12 +6493,12 @@ function recoverFinishedCursorRun(opts) {
6053
6493
  return null;
6054
6494
  }
6055
6495
  }
6056
- var import_node_fs25, import_node_path24;
6496
+ var import_node_fs27, import_node_path25;
6057
6497
  var init_cursor_recover = __esm({
6058
6498
  "src/agents/cursor-recover.ts"() {
6059
6499
  "use strict";
6060
- import_node_fs25 = require("fs");
6061
- import_node_path24 = require("path");
6500
+ import_node_fs27 = require("fs");
6501
+ import_node_path25 = require("path");
6062
6502
  init_paths();
6063
6503
  }
6064
6504
  });
@@ -6067,11 +6507,11 @@ var init_cursor_recover = __esm({
6067
6507
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
6068
6508
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6069
6509
  var import_zod = require("zod");
6070
- var import_node_path25 = require("path");
6510
+ var import_node_path26 = require("path");
6071
6511
 
6072
6512
  // src/orchestrator/orchestrator.ts
6073
6513
  var import_node_events = require("events");
6074
- var import_node_fs26 = require("fs");
6514
+ var import_node_fs28 = require("fs");
6075
6515
  init_error_detail();
6076
6516
 
6077
6517
  // src/agents/spawn.ts
@@ -6091,18 +6531,18 @@ function asRecord(input) {
6091
6531
  }
6092
6532
  return void 0;
6093
6533
  }
6094
- function str(v) {
6534
+ function str2(v) {
6095
6535
  return typeof v === "string" && v.trim() ? v : void 0;
6096
6536
  }
6097
6537
  function toolDetail(name, input) {
6098
6538
  if (!input) return void 0;
6099
- const command = str(input.command) ?? str(input.cmd);
6539
+ const command = str2(input.command) ?? str2(input.cmd);
6100
6540
  if (command) return command;
6101
- const path = str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
6541
+ const path = str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
6102
6542
  if (path) return path;
6103
- const pattern = str(input.pattern) ?? str(input.glob) ?? str(input.glob_pattern);
6543
+ const pattern = str2(input.pattern) ?? str2(input.glob) ?? str2(input.glob_pattern);
6104
6544
  if (pattern) return pattern;
6105
- const query = str(input.query) ?? str(input.prompt);
6545
+ const query = str2(input.query) ?? str2(input.prompt);
6106
6546
  if (query) return query.length > 80 ? `${query.slice(0, 77)}\u2026` : query;
6107
6547
  try {
6108
6548
  const raw = JSON.stringify(input);
@@ -6115,23 +6555,26 @@ function toolDescription(name, input) {
6115
6555
  const n = name.replace(/^mcp__/, "").replace(/__/g, " \xB7 ");
6116
6556
  if (/^get_record_types$|recordTypes/i.test(name)) return "List record types";
6117
6557
  if (/connectedAgentRequest/i.test(name)) {
6118
- return str(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
6558
+ return str2(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
6119
6559
  }
6120
6560
  if (/present_artifact$/i.test(name)) {
6121
- return str(input?.title) ? `Present ${str(input?.title)}` : "Present artifact";
6561
+ return str2(input?.title) ? `Present ${str2(input?.title)}` : "Present artifact";
6562
+ }
6563
+ if (/present_plan$/i.test(name)) {
6564
+ return str2(input?.title) ? `Plan ${str2(input?.title)}` : "Present plan";
6122
6565
  }
6123
6566
  if (/present_schema$/i.test(name)) {
6124
- return str(input?.title) ? `Schema ${str(input?.title)}` : "Present schema";
6567
+ return str2(input?.title) ? `Schema ${str2(input?.title)}` : "Present schema";
6125
6568
  }
6126
6569
  if (/present_files$/i.test(name)) {
6127
- return str(input?.title) ? `Files ${str(input?.title)}` : "Present files";
6570
+ return str2(input?.title) ? `Files ${str2(input?.title)}` : "Present files";
6128
6571
  }
6129
6572
  if (/^(create|update)_artifact$/i.test(name.replace(/^mcp__[^_]+__/, ""))) {
6130
- return str(input?.title) ? `Artifact ${str(input?.title)}` : "Artifact";
6573
+ return str2(input?.title) ? `Artifact ${str2(input?.title)}` : "Artifact";
6131
6574
  }
6132
6575
  if (!input) return n;
6133
- if (/bash|shell|terminal/i.test(name) && str(input.command)) {
6134
- const cmd = str(input.command);
6576
+ if (/bash|shell|terminal/i.test(name) && str2(input.command)) {
6577
+ const cmd = str2(input.command);
6135
6578
  if (/git\s+fetch/i.test(cmd)) return "Fetch latest from origin and check status";
6136
6579
  if (/git\s+status/i.test(cmd)) return "Check git status";
6137
6580
  if (/git\s+log/i.test(cmd)) return "Inspect recent commits";
@@ -6140,11 +6583,11 @@ function toolDescription(name, input) {
6140
6583
  return "Run shell command";
6141
6584
  }
6142
6585
  if (/edit|write|apply/i.test(name)) {
6143
- const path = str(input.file_path) ?? str(input.path);
6586
+ const path = str2(input.file_path) ?? str2(input.path);
6144
6587
  return path ? `Edit ${path.split("/").pop()}` : "Edit file";
6145
6588
  }
6146
6589
  if (/read/i.test(name)) {
6147
- const path = str(input.file_path) ?? str(input.path);
6590
+ const path = str2(input.file_path) ?? str2(input.path);
6148
6591
  return path ? `Read ${path.split("/").pop()}` : "Read file";
6149
6592
  }
6150
6593
  if (/grep/i.test(name)) return "Search files";
@@ -6153,7 +6596,7 @@ function toolDescription(name, input) {
6153
6596
  }
6154
6597
  function toolFilePath(input) {
6155
6598
  if (!input) return void 0;
6156
- return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
6599
+ return str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
6157
6600
  }
6158
6601
  function countLines(text) {
6159
6602
  if (!text) return 0;
@@ -6161,8 +6604,8 @@ function countLines(text) {
6161
6604
  }
6162
6605
  function diffFromInput(input) {
6163
6606
  if (!input) return {};
6164
- const oldS = str(input.old_string) ?? str(input.oldString);
6165
- const newS = str(input.new_string) ?? str(input.newString) ?? str(input.content);
6607
+ const oldS = str2(input.old_string) ?? str2(input.oldString);
6608
+ const newS = str2(input.new_string) ?? str2(input.newString) ?? str2(input.content);
6166
6609
  if (oldS != null || newS != null) {
6167
6610
  return {
6168
6611
  additions: countLines(newS),
@@ -6424,6 +6867,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
6424
6867
  // src/orchestrator/orchestrator.ts
6425
6868
  init_agents();
6426
6869
  init_worktree();
6870
+ init_stack();
6427
6871
 
6428
6872
  // src/hook/conductor.ts
6429
6873
  var import_node_fs13 = require("fs");
@@ -7059,7 +7503,7 @@ async function requireAgent(agent, opts) {
7059
7503
  init_worktree();
7060
7504
  init_thread_store();
7061
7505
  init_workspaces();
7062
- async function createThread(input, onSetupLine) {
7506
+ async function createThread(input, _onSetupLine) {
7063
7507
  await requireAgent(input.agent);
7064
7508
  const repoPath = await resolveRepoRoot(input.repoPath);
7065
7509
  if (!(0, import_node_fs18.existsSync)(repoPath)) {
@@ -7069,10 +7513,10 @@ async function createThread(input, onSetupLine) {
7069
7513
  let sourceIsFork = false;
7070
7514
  let prUrl = null;
7071
7515
  if (input.sourceType === "pr") {
7072
- const num = Number(input.sourceRef.replace(/^#/, ""));
7073
- if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
7074
- const pr = await getPr(repoPath, num);
7075
- if (!pr) throw new Error(`PR #${num} not found`);
7516
+ const num2 = Number(input.sourceRef.replace(/^#/, ""));
7517
+ if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
7518
+ const pr = await getPr(repoPath, num2);
7519
+ if (!pr) throw new Error(`PR #${num2} not found`);
7076
7520
  sourceIsFork = pr.isCrossRepository;
7077
7521
  prUrl = pr.url;
7078
7522
  const localFetchBranch = `sideboard-pr-${pr.number}`;
@@ -7117,22 +7561,6 @@ async function createThread(input, onSetupLine) {
7117
7561
  });
7118
7562
  writeThread(thread);
7119
7563
  await ensureWorkspace(repoPath);
7120
- try {
7121
- let setup = await runSetupScript(repoPath, worktreePath, onSetupLine);
7122
- if (!setup.ran) {
7123
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onSetupLine);
7124
- }
7125
- if (setup.ran && setup.exitCode !== 0 && setup.exitCode !== null) {
7126
- updateThread(thread.id, {
7127
- lastError: `Setup exited ${setup.exitCode} (thread is still usable)`
7128
- });
7129
- }
7130
- } catch (err) {
7131
- const message = err instanceof Error ? err.message : String(err);
7132
- updateThread(thread.id, {
7133
- lastError: `Setup failed: ${message}`
7134
- });
7135
- }
7136
7564
  return readThread(thread.id) ?? thread;
7137
7565
  }
7138
7566
  async function listLinearIssues(agent, repoPath) {
@@ -7464,7 +7892,9 @@ function worktreeBindingFrom(from) {
7464
7892
  sourceIsFork: from.sourceIsFork,
7465
7893
  parentThreadId: from.parentThreadId,
7466
7894
  prUrl: from.prUrl,
7467
- prTitle: from.prTitle
7895
+ prTitle: from.prTitle,
7896
+ stackId: from.stackId,
7897
+ stackLayer: from.stackLayer
7468
7898
  };
7469
7899
  }
7470
7900
  function threadsSharingWorktree(worktreePath) {
@@ -7662,15 +8092,13 @@ File: src/client/frontends/desktop/core/UserData.ts
7662
8092
  `;
7663
8093
 
7664
8094
  // src/review/request-review.ts
8095
+ init_workspace_scratch();
7665
8096
  var REPO_REVIEW_PATH = ".sideboard/review.md";
7666
8097
  var REPO_REVIEW_NAME = "review.md";
7667
- var REVIEW_REQUEST_PATH = ".sideboard/attachments/Review request.md";
8098
+ var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
8099
+ var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
7668
8100
  var REVIEW_REQUEST_NAME = "Review request.md";
7669
8101
  var REVIEW_REQUEST_PREFILL = "Review.";
7670
- var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
7671
- *
7672
- !.gitignore
7673
- `;
7674
8102
  var LEGACY_REVIEW_TEMPLATE_MARKERS = [
7675
8103
  "You are acting as a reviewer for a proposed code change made by another engineer.",
7676
8104
  "HOW MANY FINDINGS TO RETURN:"
@@ -7693,10 +8121,10 @@ function readTextIfPresent(abs) {
7693
8121
  }
7694
8122
  }
7695
8123
  function ensureAttachmentsGitignore(worktreePath) {
7696
- const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ".sideboard", "attachments", ".gitignore");
8124
+ const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
7697
8125
  if ((0, import_node_fs19.existsSync)(gitignoreAbs)) return;
7698
8126
  (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(gitignoreAbs), { recursive: true });
7699
- (0, import_node_fs19.writeFileSync)(gitignoreAbs, ATTACHMENTS_GITIGNORE, "utf8");
8127
+ (0, import_node_fs19.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
7700
8128
  }
7701
8129
  function resolveReviewGuidelines(worktreePath) {
7702
8130
  const repoAbs = (0, import_node_path18.join)(worktreePath, REPO_REVIEW_PATH);
@@ -7719,6 +8147,16 @@ function resolveReviewGuidelines(worktreePath) {
7719
8147
  source: "local"
7720
8148
  };
7721
8149
  }
8150
+ const legacyAbs = (0, import_node_path18.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
8151
+ const legacyContent = readTextIfPresent(legacyAbs);
8152
+ if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
8153
+ return {
8154
+ path: LEGACY_REVIEW_REQUEST_PATH,
8155
+ name: REVIEW_REQUEST_NAME,
8156
+ content: legacyContent,
8157
+ source: "local"
8158
+ };
8159
+ }
7722
8160
  ensureAttachmentsGitignore(worktreePath);
7723
8161
  (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(localAbs), { recursive: true });
7724
8162
  (0, import_node_fs19.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
@@ -8198,16 +8636,305 @@ async function importConductorWorkspaceAsync(workspaceId) {
8198
8636
  return importConductorWorkspace(workspaceId);
8199
8637
  }
8200
8638
 
8639
+ // src/threads/stack-layers.ts
8640
+ var import_node_fs21 = require("fs");
8641
+ init_run();
8642
+ init_stack();
8643
+ init_worktree();
8644
+ init_thread_store();
8645
+ init_workspaces();
8646
+ function stackIdFrom(stack) {
8647
+ if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
8648
+ const key = stack.layers.map((l) => l.branchName).join("|");
8649
+ if (!key) return null;
8650
+ return `gh-stack-local-${hashShort(key)}`;
8651
+ }
8652
+ function hashShort(s) {
8653
+ let h = 0;
8654
+ for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
8655
+ return Math.abs(h).toString(36);
8656
+ }
8657
+ function requireThread3(idOrRef) {
8658
+ const thread = findThreadByRef(idOrRef) ?? readThread(idOrRef);
8659
+ if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
8660
+ return thread;
8661
+ }
8662
+ function sanitizeSlugPart(name) {
8663
+ return name.trim().replace(/^refs\/heads\//, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "layer";
8664
+ }
8665
+ function layerSlug(stackId, layer) {
8666
+ const stackPart = stackId.replace(/^gh-stack-/, "s");
8667
+ return sanitizeSlugPart(`${stackPart}-L${layer.position}-${layer.branchName}`);
8668
+ }
8669
+ function findThreadForStackLayer(repoPath, stackId, layer) {
8670
+ const threads = listThreads({ includeArchived: false }).filter(
8671
+ (t) => t.repoPath === repoPath && t.stackId === stackId
8672
+ );
8673
+ const byLayer = threads.find((t) => t.stackLayer === layer.position);
8674
+ if (byLayer) return byLayer;
8675
+ return threads.find((t) => t.branchName === layer.branchName) ?? listThreads({ includeArchived: false }).find(
8676
+ (t) => t.repoPath === repoPath && t.branchName === layer.branchName
8677
+ ) ?? null;
8678
+ }
8679
+ async function openStackLayer(input, _onSetupLine) {
8680
+ await requireAgent(input.agent);
8681
+ const repoPath = await resolveRepoRoot(input.repoPath);
8682
+ const stackId = stackIdFrom(input.stack);
8683
+ if (!stackId) throw new Error("Cannot open stack layer without a stack id");
8684
+ const existing = findThreadForStackLayer(repoPath, stackId, input.layer);
8685
+ if (existing) {
8686
+ const patch = {};
8687
+ if (existing.stackId !== stackId) patch.stackId = stackId;
8688
+ if (existing.stackLayer !== input.layer.position) {
8689
+ patch.stackLayer = input.layer.position;
8690
+ }
8691
+ if (input.layer.prUrl && existing.prUrl !== input.layer.prUrl) {
8692
+ patch.prUrl = input.layer.prUrl;
8693
+ }
8694
+ if (input.layer.title && existing.prTitle !== input.layer.title) {
8695
+ patch.prTitle = input.layer.title;
8696
+ }
8697
+ if (Object.keys(patch).length > 0) updateThread(existing.id, patch);
8698
+ return {
8699
+ thread: readThread(existing.id) ?? existing,
8700
+ createdWorktree: false
8701
+ };
8702
+ }
8703
+ let worktreePath = null;
8704
+ let branchName = input.layer.branchName;
8705
+ let createdWorktree = false;
8706
+ const trees = await listWorktrees(repoPath);
8707
+ const checkedOut = trees.find((w) => w.branch === branchName);
8708
+ if (checkedOut?.path && (0, import_node_fs21.existsSync)(checkedOut.path)) {
8709
+ if (input.reuseExistingWorktree !== false) {
8710
+ worktreePath = checkedOut.path;
8711
+ } else {
8712
+ throw new Error(
8713
+ `Branch ${branchName} is already checked out at ${checkedOut.path}`
8714
+ );
8715
+ }
8716
+ }
8717
+ if (!worktreePath) {
8718
+ const slug = layerSlug(stackId, input.layer);
8719
+ const created = await createExistingBranchWorktree({
8720
+ repoPath,
8721
+ branchName,
8722
+ slug
8723
+ });
8724
+ worktreePath = created.worktreePath;
8725
+ branchName = created.branchName;
8726
+ copyConfiguredFiles(repoPath, worktreePath);
8727
+ createdWorktree = true;
8728
+ }
8729
+ const title = input.layer.title?.trim() || (input.layer.prNumber != null ? `PR #${input.layer.prNumber}` : input.layer.branchName);
8730
+ const thread = createEmptyThread({
8731
+ title,
8732
+ userSetTitle: Boolean(input.layer.title?.trim()),
8733
+ sourceType: input.layer.prNumber != null ? "pr" : "branch",
8734
+ sourceRef: input.layer.prNumber != null ? String(input.layer.prNumber) : input.layer.branchName,
8735
+ branchName,
8736
+ worktreePath,
8737
+ repoPath,
8738
+ agent: input.agent,
8739
+ autonomy: input.autonomy ?? "default",
8740
+ model: input.model ?? null,
8741
+ effort: input.effort ?? "high",
8742
+ fast: Boolean(input.fast),
8743
+ planMode: Boolean(input.planMode),
8744
+ parentThreadId: input.parentThreadId ?? null,
8745
+ status: "idle",
8746
+ prUrl: input.layer.prUrl,
8747
+ prTitle: input.layer.title ?? null,
8748
+ stackId,
8749
+ stackLayer: input.layer.position
8750
+ });
8751
+ writeThread(thread);
8752
+ await ensureWorkspace(repoPath);
8753
+ return { thread: readThread(thread.id) ?? thread, createdWorktree };
8754
+ }
8755
+ async function openPrStackLayers(input, onSetupLine) {
8756
+ const from = requireThread3(input.threadRef);
8757
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
8758
+ throw new Error("Thread has no worktree");
8759
+ }
8760
+ const stack = await getPrStack(from.worktreePath);
8761
+ if (!stack) throw new Error("Current branch is not part of a GitHub PR stack");
8762
+ const layers = input.layer != null ? stack.layers.filter((l) => l.position === input.layer) : stack.layers;
8763
+ if (!layers.length) {
8764
+ throw new Error(
8765
+ input.layer != null ? `No stack layer at position ${input.layer}` : "Stack has no layers"
8766
+ );
8767
+ }
8768
+ const threads = [];
8769
+ const createdThreadIds = [];
8770
+ for (const layer of layers) {
8771
+ const { thread, createdWorktree } = await openStackLayer(
8772
+ {
8773
+ repoPath: from.repoPath,
8774
+ stack,
8775
+ layer,
8776
+ agent: from.agent,
8777
+ autonomy: from.autonomy,
8778
+ model: from.model,
8779
+ effort: from.effort,
8780
+ fast: from.fast,
8781
+ planMode: from.planMode,
8782
+ parentThreadId: from.id
8783
+ },
8784
+ onSetupLine
8785
+ );
8786
+ threads.push(thread);
8787
+ if (createdWorktree) createdThreadIds.push(thread.id);
8788
+ }
8789
+ const stackId = stackIdFrom(stack);
8790
+ const current = stack.layers[stack.currentIndex];
8791
+ if (stackId) {
8792
+ updateThread(from.id, {
8793
+ stackId,
8794
+ stackLayer: current?.position ?? from.stackLayer
8795
+ });
8796
+ }
8797
+ return { stack, threads, createdThreadIds };
8798
+ }
8799
+ async function addStackLayerFromThread(input, onSetupLine) {
8800
+ const from = requireThread3(input.threadRef);
8801
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
8802
+ throw new Error("Thread has no worktree");
8803
+ }
8804
+ const status = await detectGhStack(from.worktreePath);
8805
+ if (!status.available) throw new Error(status.reason);
8806
+ await addPrStackLayer(from.worktreePath, input.branchName);
8807
+ const stack = await getPrStack(from.worktreePath);
8808
+ if (!stack) throw new Error("Stack not found after adding layer");
8809
+ const layer = stack.layers.find((l) => l.branchName === input.branchName.trim()) ?? stack.layers[stack.layers.length - 1];
8810
+ if (!layer) throw new Error("New stack layer not found");
8811
+ if (input.title?.trim()) {
8812
+ layer.title = input.title.trim();
8813
+ }
8814
+ const thread = await openStackLayer(
8815
+ {
8816
+ repoPath: from.repoPath,
8817
+ stack,
8818
+ layer,
8819
+ agent: from.agent,
8820
+ autonomy: from.autonomy,
8821
+ model: from.model,
8822
+ effort: from.effort,
8823
+ fast: from.fast,
8824
+ planMode: from.planMode,
8825
+ parentThreadId: from.id
8826
+ },
8827
+ onSetupLine
8828
+ );
8829
+ return { stack, thread: thread.thread, createdWorktree: thread.createdWorktree };
8830
+ }
8831
+ async function initStackFromThread(input, onSetupLine) {
8832
+ const from = requireThread3(input.threadRef);
8833
+ if (!from.worktreePath?.trim() || !from.branchName?.trim() || !from.repoPath?.trim()) {
8834
+ throw new Error("Thread has no worktree/branch");
8835
+ }
8836
+ const status = await detectGhStack(from.worktreePath);
8837
+ if (!status.available) throw new Error(status.reason);
8838
+ const branches = [
8839
+ from.branchName,
8840
+ ...(input.additionalBranches ?? []).map((b) => b.trim()).filter(Boolean)
8841
+ ];
8842
+ await initPrStack(from.worktreePath, branches, {
8843
+ base: input.base ?? await resolveDefaultBranch(from.repoPath)
8844
+ });
8845
+ return openPrStackLayers({ threadRef: from.id }, onSetupLine);
8846
+ }
8847
+ async function createPrStack(input, onSetupLine) {
8848
+ await requireAgent(input.agent);
8849
+ const repoPath = await resolveRepoRoot(input.repoPath);
8850
+ if (!(0, import_node_fs21.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
8851
+ if (!input.branches.length) throw new Error("At least one branch name required");
8852
+ const status = await detectGhStack(repoPath);
8853
+ if (!status.available) throw new Error(status.reason);
8854
+ const team = allocateTeamSlug(repoPath);
8855
+ const base = input.base ?? await resolveDefaultBranch(repoPath);
8856
+ const bootstrap = await createThreadWorktree({
8857
+ repoPath,
8858
+ sourceRef: base,
8859
+ slug: `${team.slug}-stack-init`
8860
+ });
8861
+ copyConfiguredFiles(repoPath, bootstrap.worktreePath);
8862
+ try {
8863
+ await initPrStack(bootstrap.worktreePath, input.branches, { base });
8864
+ const bottom = input.branches[0];
8865
+ const co = await git(["checkout", bottom], bootstrap.worktreePath, {
8866
+ reject: false
8867
+ });
8868
+ if (co.exitCode !== 0) {
8869
+ throw new Error(
8870
+ co.stderr.trim() || co.stdout.trim() || `Could not check out ${bottom} after stack init`
8871
+ );
8872
+ }
8873
+ } catch (err) {
8874
+ try {
8875
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
8876
+ deleteBranch: bootstrap.branchName
8877
+ });
8878
+ } catch {
8879
+ }
8880
+ throw err;
8881
+ }
8882
+ const stack = await getPrStack(bootstrap.worktreePath);
8883
+ if (!stack) {
8884
+ try {
8885
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
8886
+ deleteBranch: bootstrap.branchName
8887
+ });
8888
+ } catch {
8889
+ }
8890
+ throw new Error("Stack init succeeded but gh stack view returned no stack");
8891
+ }
8892
+ const threads = [];
8893
+ const createdThreadIds = [];
8894
+ for (const layer of stack.layers) {
8895
+ const title = layer.position === 1 && input.title?.trim() ? input.title.trim() : layer.title;
8896
+ const opened = await openStackLayer(
8897
+ {
8898
+ repoPath,
8899
+ stack,
8900
+ layer: title ? { ...layer, title } : layer,
8901
+ agent: input.agent,
8902
+ autonomy: input.autonomy,
8903
+ model: input.model,
8904
+ effort: input.effort,
8905
+ fast: input.fast,
8906
+ planMode: input.planMode,
8907
+ reuseExistingWorktree: true
8908
+ },
8909
+ onSetupLine
8910
+ );
8911
+ threads.push(opened.thread);
8912
+ if (opened.createdWorktree || opened.thread.worktreePath === bootstrap.worktreePath) {
8913
+ createdThreadIds.push(opened.thread.id);
8914
+ }
8915
+ }
8916
+ const claimed = new Set(threads.map((t) => t.worktreePath));
8917
+ if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs21.existsSync)(bootstrap.worktreePath)) {
8918
+ try {
8919
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
8920
+ deleteBranch: bootstrap.branchName
8921
+ });
8922
+ } catch {
8923
+ }
8924
+ }
8925
+ return { stack, threads, createdThreadIds };
8926
+ }
8927
+
8201
8928
  // src/land/land.ts
8202
8929
  init_worktree();
8203
8930
 
8204
8931
  // src/diff/diff.ts
8205
- var import_node_fs21 = require("fs");
8932
+ var import_node_fs22 = require("fs");
8206
8933
  var import_node_path20 = require("path");
8207
8934
  init_run();
8208
8935
  init_worktree();
8209
8936
  async function inspectGitWorktree(worktreePath) {
8210
- if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) return "missing_worktree";
8937
+ if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) return "missing_worktree";
8211
8938
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
8212
8939
  reject: false
8213
8940
  });
@@ -8215,7 +8942,7 @@ async function inspectGitWorktree(worktreePath) {
8215
8942
  return "ok";
8216
8943
  }
8217
8944
  async function initializeGitRepository(worktreePath) {
8218
- if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) {
8945
+ if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) {
8219
8946
  throw new Error("Worktree not found");
8220
8947
  }
8221
8948
  const status = await inspectGitWorktree(worktreePath);
@@ -8353,7 +9080,7 @@ async function listUntrackedPaths(worktreePath) {
8353
9080
  worktreePath,
8354
9081
  { reject: false }
8355
9082
  );
8356
- return stdout.split("\0").filter(Boolean);
9083
+ return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
8357
9084
  }
8358
9085
  async function listUntrackedFiles(worktreePath, maxHunk) {
8359
9086
  const paths = await listUntrackedPaths(worktreePath);
@@ -8511,33 +9238,33 @@ async function getDiff(worktreePath, repoPath, opts) {
8511
9238
  let combinedDiff = "";
8512
9239
  let labelBase = base;
8513
9240
  if (scope === "staged") {
8514
- const [ns, num, diff] = await Promise.all([
9241
+ const [ns, num2, diff] = await Promise.all([
8515
9242
  git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
8516
9243
  git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
8517
9244
  git(["diff", "--cached"], worktreePath, { reject: false })
8518
9245
  ]);
8519
9246
  nameStatus = ns.stdout;
8520
- numstat = num.stdout;
9247
+ numstat = num2.stdout;
8521
9248
  combinedDiff = diff.stdout;
8522
9249
  labelBase = "staged";
8523
9250
  } else if (scope === "unstaged") {
8524
- const [ns, num, diff] = await Promise.all([
9251
+ const [ns, num2, diff] = await Promise.all([
8525
9252
  git(["diff", "--name-status"], worktreePath, { reject: false }),
8526
9253
  git(["diff", "--numstat"], worktreePath, { reject: false }),
8527
9254
  git(["diff"], worktreePath, { reject: false })
8528
9255
  ]);
8529
9256
  nameStatus = ns.stdout;
8530
- numstat = num.stdout;
9257
+ numstat = num2.stdout;
8531
9258
  combinedDiff = diff.stdout;
8532
9259
  labelBase = "unstaged";
8533
9260
  } else if (scope === "uncommitted") {
8534
- const [ns, num, diff] = await Promise.all([
9261
+ const [ns, num2, diff] = await Promise.all([
8535
9262
  git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
8536
9263
  git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
8537
9264
  git(["diff", "HEAD"], worktreePath, { reject: false })
8538
9265
  ]);
8539
9266
  nameStatus = ns.stdout;
8540
- numstat = num.stdout;
9267
+ numstat = num2.stdout;
8541
9268
  combinedDiff = diff.stdout;
8542
9269
  labelBase = "HEAD";
8543
9270
  } else if (scope === "last_turn") {
@@ -8556,34 +9283,34 @@ async function getDiff(worktreePath, repoPath, opts) {
8556
9283
  scopeStats
8557
9284
  };
8558
9285
  }
8559
- const [ns, num, diff] = await Promise.all([
9286
+ const [ns, num2, diff] = await Promise.all([
8560
9287
  git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
8561
9288
  git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
8562
9289
  git(["diff", lastTurnBase], worktreePath, { reject: false })
8563
9290
  ]);
8564
9291
  nameStatus = ns.stdout;
8565
- numstat = num.stdout;
9292
+ numstat = num2.stdout;
8566
9293
  combinedDiff = diff.stdout;
8567
9294
  labelBase = "last turn";
8568
9295
  } else if (scope === "commits" && commitSha) {
8569
9296
  const range = `${commitSha}^!`;
8570
- const [ns, num, diff] = await Promise.all([
9297
+ const [ns, num2, diff] = await Promise.all([
8571
9298
  git(["diff", "--name-status", range], worktreePath, { reject: false }),
8572
9299
  git(["diff", "--numstat", range], worktreePath, { reject: false }),
8573
9300
  git(["diff", range], worktreePath, { reject: false })
8574
9301
  ]);
8575
9302
  nameStatus = ns.stdout;
8576
- numstat = num.stdout;
9303
+ numstat = num2.stdout;
8577
9304
  combinedDiff = diff.stdout;
8578
9305
  labelBase = commitSha.slice(0, 7);
8579
9306
  } else {
8580
- const [ns, num, diff] = await Promise.all([
9307
+ const [ns, num2, diff] = await Promise.all([
8581
9308
  git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
8582
9309
  git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
8583
9310
  git(["diff", mergeBase], worktreePath, { reject: false })
8584
9311
  ]);
8585
9312
  nameStatus = ns.stdout;
8586
- numstat = num.stdout;
9313
+ numstat = num2.stdout;
8587
9314
  combinedDiff = diff.stdout;
8588
9315
  labelBase = base;
8589
9316
  }
@@ -8653,7 +9380,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
8653
9380
  assertSafeRelativePath(relativePath);
8654
9381
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
8655
9382
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8656
- const st = (0, import_node_fs21.statSync)(abs);
9383
+ const st = (0, import_node_fs22.statSync)(abs);
8657
9384
  if (!st.isFile()) {
8658
9385
  throw new Error(`Not a file: ${relativePath}`);
8659
9386
  }
@@ -8662,7 +9389,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
8662
9389
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
8663
9390
  );
8664
9391
  }
8665
- const buf = (0, import_node_fs21.readFileSync)(abs);
9392
+ const buf = (0, import_node_fs22.readFileSync)(abs);
8666
9393
  return {
8667
9394
  path: relativePath,
8668
9395
  contentBase64: buf.toString("base64"),
@@ -8673,11 +9400,11 @@ function readWorktreeFile(worktreePath, relativePath, opts) {
8673
9400
  assertSafeRelativePath(relativePath);
8674
9401
  const maxBytes = opts?.maxBytes ?? 2e5;
8675
9402
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8676
- const st = (0, import_node_fs21.statSync)(abs);
9403
+ const st = (0, import_node_fs22.statSync)(abs);
8677
9404
  if (!st.isFile()) {
8678
9405
  throw new Error(`Not a file: ${relativePath}`);
8679
9406
  }
8680
- const buf = (0, import_node_fs21.readFileSync)(abs);
9407
+ const buf = (0, import_node_fs22.readFileSync)(abs);
8681
9408
  if (isImageRelativePath(relativePath)) {
8682
9409
  const maxImageBytes = Math.max(maxBytes, 15e6);
8683
9410
  const truncated2 = buf.length > maxImageBytes;
@@ -8721,8 +9448,8 @@ function assertSafeRelativePath(relativePath) {
8721
9448
  function writeWorktreeFile(worktreePath, relativePath, content) {
8722
9449
  assertSafeRelativePath(relativePath);
8723
9450
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8724
- (0, import_node_fs21.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
8725
- (0, import_node_fs21.writeFileSync)(abs, content, "utf8");
9451
+ (0, import_node_fs22.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
9452
+ (0, import_node_fs22.writeFileSync)(abs, content, "utf8");
8726
9453
  return { path: relativePath };
8727
9454
  }
8728
9455
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -8901,7 +9628,7 @@ async function confirmLand(thread, opts) {
8901
9628
  }
8902
9629
 
8903
9630
  // src/skills/discover.ts
8904
- var import_node_fs22 = require("fs");
9631
+ var import_node_fs23 = require("fs");
8905
9632
  var import_node_os9 = require("os");
8906
9633
  var import_node_path21 = require("path");
8907
9634
  function toCommand(name) {
@@ -8935,7 +9662,7 @@ function parseFrontmatter(content) {
8935
9662
  }
8936
9663
  function readSkill(skillMd, source) {
8937
9664
  try {
8938
- const content = (0, import_node_fs22.readFileSync)(skillMd, "utf8");
9665
+ const content = (0, import_node_fs23.readFileSync)(skillMd, "utf8");
8939
9666
  const { name: fmName, description } = parseFrontmatter(content);
8940
9667
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
8941
9668
  const name = fmName || dirName;
@@ -8954,19 +9681,19 @@ function readSkill(skillMd, source) {
8954
9681
  }
8955
9682
  }
8956
9683
  function scanSkillsDir(dir, source, out) {
8957
- if (!(0, import_node_fs22.existsSync)(dir)) return;
9684
+ if (!(0, import_node_fs23.existsSync)(dir)) return;
8958
9685
  let entries;
8959
9686
  try {
8960
- entries = (0, import_node_fs22.readdirSync)(dir);
9687
+ entries = (0, import_node_fs23.readdirSync)(dir);
8961
9688
  } catch {
8962
9689
  return;
8963
9690
  }
8964
9691
  for (const entry of entries) {
8965
9692
  if (entry.startsWith(".")) continue;
8966
9693
  const skillMd = (0, import_node_path21.join)(dir, entry, "SKILL.md");
8967
- if (!(0, import_node_fs22.existsSync)(skillMd)) continue;
9694
+ if (!(0, import_node_fs23.existsSync)(skillMd)) continue;
8968
9695
  try {
8969
- if (!(0, import_node_fs22.statSync)(skillMd).isFile()) continue;
9696
+ if (!(0, import_node_fs23.statSync)(skillMd).isFile()) continue;
8970
9697
  } catch {
8971
9698
  continue;
8972
9699
  }
@@ -8975,12 +9702,12 @@ function scanSkillsDir(dir, source, out) {
8975
9702
  }
8976
9703
  }
8977
9704
  function scanClaudePluginSkills(pluginsRoot, out) {
8978
- if (!(0, import_node_fs22.existsSync)(pluginsRoot)) return;
9705
+ if (!(0, import_node_fs23.existsSync)(pluginsRoot)) return;
8979
9706
  const walk = (dir, depth, lookingForSkillsDir) => {
8980
9707
  if (depth > 7) return;
8981
9708
  let entries;
8982
9709
  try {
8983
- entries = (0, import_node_fs22.readdirSync)(dir);
9710
+ entries = (0, import_node_fs23.readdirSync)(dir);
8984
9711
  } catch {
8985
9712
  return;
8986
9713
  }
@@ -8992,7 +9719,7 @@ function scanClaudePluginSkills(pluginsRoot, out) {
8992
9719
  if (entry === "node_modules" || entry === ".git") continue;
8993
9720
  const full = (0, import_node_path21.join)(dir, entry);
8994
9721
  try {
8995
- if (!(0, import_node_fs22.statSync)(full).isDirectory()) continue;
9722
+ if (!(0, import_node_fs23.statSync)(full).isDirectory()) continue;
8996
9723
  } catch {
8997
9724
  continue;
8998
9725
  }
@@ -9032,7 +9759,7 @@ function discoverSkills(worktreePath) {
9032
9759
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
9033
9760
  }
9034
9761
  function readSkillBody(skillPath, maxChars = 12e3) {
9035
- const raw = (0, import_node_fs22.readFileSync)(skillPath, "utf8");
9762
+ const raw = (0, import_node_fs23.readFileSync)(skillPath, "utf8");
9036
9763
  if (raw.startsWith("---")) {
9037
9764
  const end = raw.indexOf("\n---", 3);
9038
9765
  if (end >= 0) {
@@ -9125,9 +9852,10 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
9125
9852
  }
9126
9853
 
9127
9854
  // src/composer/stage-files.ts
9128
- var import_node_fs23 = require("fs");
9855
+ var import_node_fs24 = require("fs");
9129
9856
  var import_node_path22 = require("path");
9130
9857
  var import_node_crypto5 = require("crypto");
9858
+ init_workspace_scratch();
9131
9859
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
9132
9860
  "png",
9133
9861
  "jpg",
@@ -9148,11 +9876,6 @@ var IMAGE_MIME_BY_EXT = {
9148
9876
  bmp: "image/bmp",
9149
9877
  ico: "image/x-icon"
9150
9878
  };
9151
- var ATTACHMENTS_DIR = ".sideboard/attachments";
9152
- var ATTACHMENTS_GITIGNORE2 = `# Sideboard review / composer attachments (local only)
9153
- *
9154
- !.gitignore
9155
- `;
9156
9879
  var MAX_INLINE_BYTES = 4e5;
9157
9880
  var MAX_PREVIEW_BYTES = 5e6;
9158
9881
  function fileExtension(filePath) {
@@ -9167,21 +9890,21 @@ function imageMimeType(filePath) {
9167
9890
  }
9168
9891
  function ensureAttachmentsDir(worktreePath) {
9169
9892
  const dir = (0, import_node_path22.join)(worktreePath, ATTACHMENTS_DIR);
9170
- (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
9893
+ (0, import_node_fs24.mkdirSync)(dir, { recursive: true });
9171
9894
  const gi = (0, import_node_path22.join)(dir, ".gitignore");
9172
- if (!(0, import_node_fs23.existsSync)(gi)) {
9173
- (0, import_node_fs23.writeFileSync)(gi, ATTACHMENTS_GITIGNORE2, "utf8");
9895
+ if (!(0, import_node_fs24.existsSync)(gi)) {
9896
+ (0, import_node_fs24.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
9174
9897
  }
9175
9898
  return dir;
9176
9899
  }
9177
9900
  function uniqueAttachmentName(dir, originalName) {
9178
9901
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
9179
- if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
9902
+ if (!(0, import_node_fs24.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
9180
9903
  const ext = (0, import_node_path22.extname)(safe);
9181
9904
  const stem = ext ? safe.slice(0, -ext.length) : safe;
9182
9905
  for (let i = 1; i < 1e4; i++) {
9183
9906
  const candidate = `${stem}-${i}${ext}`;
9184
- if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
9907
+ if (!(0, import_node_fs24.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
9185
9908
  }
9186
9909
  return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
9187
9910
  }
@@ -9239,13 +9962,13 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
9239
9962
  for (const abs of absolutePaths) {
9240
9963
  const originalName = (0, import_node_path22.basename)(abs);
9241
9964
  try {
9242
- const st = (0, import_node_fs23.statSync)(abs);
9965
+ const st = (0, import_node_fs24.statSync)(abs);
9243
9966
  if (!st.isFile()) continue;
9244
9967
  const name = uniqueAttachmentName(dir, originalName);
9245
9968
  const destAbs = (0, import_node_path22.join)(dir, name);
9246
- (0, import_node_fs23.copyFileSync)(abs, destAbs);
9969
+ (0, import_node_fs24.copyFileSync)(abs, destAbs);
9247
9970
  const rel = `${ATTACHMENTS_DIR}/${name}`;
9248
- const buf = (0, import_node_fs23.readFileSync)(destAbs);
9971
+ const buf = (0, import_node_fs24.readFileSync)(destAbs);
9249
9972
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
9250
9973
  } catch (err) {
9251
9974
  out.push({
@@ -9268,7 +9991,7 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
9268
9991
  const buf = Buffer.from(item.dataBase64, "base64");
9269
9992
  const name = uniqueAttachmentName(dir, originalName);
9270
9993
  const destAbs = (0, import_node_path22.join)(dir, name);
9271
- (0, import_node_fs23.writeFileSync)(destAbs, buf);
9994
+ (0, import_node_fs24.writeFileSync)(destAbs, buf);
9272
9995
  const rel = `${ATTACHMENTS_DIR}/${name}`;
9273
9996
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
9274
9997
  } catch (err) {
@@ -9297,9 +10020,9 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
9297
10020
  const name = (0, import_node_path22.basename)(rel);
9298
10021
  try {
9299
10022
  const abs = (0, import_node_path22.join)(worktreePath, rel);
9300
- const st = (0, import_node_fs23.statSync)(abs);
10023
+ const st = (0, import_node_fs24.statSync)(abs);
9301
10024
  if (!st.isFile()) continue;
9302
- const buf = (0, import_node_fs23.readFileSync)(abs);
10025
+ const buf = (0, import_node_fs24.readFileSync)(abs);
9303
10026
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
9304
10027
  } catch (err) {
9305
10028
  out.push({
@@ -9314,7 +10037,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
9314
10037
  }
9315
10038
 
9316
10039
  // src/agents/instructions.ts
9317
- var import_node_fs24 = require("fs");
10040
+ var import_node_fs25 = require("fs");
9318
10041
  var import_node_path23 = require("path");
9319
10042
  init_worktree_labels();
9320
10043
  function normPath2(p) {
@@ -9455,10 +10178,10 @@ function loadAgentInstructions(worktreePath, agent) {
9455
10178
  for (const rel of candidates) {
9456
10179
  if (seen.has(rel)) continue;
9457
10180
  const abs = (0, import_node_path23.join)(worktreePath, rel);
9458
- if (!(0, import_node_fs24.existsSync)(abs)) continue;
10181
+ if (!(0, import_node_fs25.existsSync)(abs)) continue;
9459
10182
  try {
9460
- if (!(0, import_node_fs24.statSync)(abs).isFile()) continue;
9461
- let content = (0, import_node_fs24.readFileSync)(abs, "utf8");
10183
+ if (!(0, import_node_fs25.statSync)(abs).isFile()) continue;
10184
+ let content = (0, import_node_fs25.readFileSync)(abs, "utf8");
9462
10185
  if (!content.trim()) continue;
9463
10186
  if (content.length > MAX_CHARS_PER_FILE) {
9464
10187
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -9491,6 +10214,7 @@ function formatAgentInstructions(files) {
9491
10214
 
9492
10215
  // src/orchestrator/orchestrator.ts
9493
10216
  init_types();
10217
+ init_plan_file();
9494
10218
  init_settings();
9495
10219
 
9496
10220
  // src/threads/sync-branch.ts
@@ -9610,7 +10334,7 @@ var Orchestrator = class {
9610
10334
  }
9611
10335
  continue;
9612
10336
  }
9613
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
10337
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
9614
10338
  setStatus(thread.id, "broken", "Worktree missing on disk");
9615
10339
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
9616
10340
  continue;
@@ -9755,14 +10479,9 @@ var Orchestrator = class {
9755
10479
  return findThreadByRef(idOrRef) ?? readThread(idOrRef);
9756
10480
  }
9757
10481
  async createThread(input) {
9758
- let thread = await createThread(input, (line) => {
9759
- this.emit({
9760
- type: "turn_output",
9761
- threadId: "pending",
9762
- event: { type: "stdout", data: line }
9763
- });
9764
- });
10482
+ let thread = await createThread(input);
9765
10483
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
10484
+ await this.runSetupAfterCreate(thread.id);
9766
10485
  const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
9767
10486
  if (autoRunAfterSetupEnabled2()) {
9768
10487
  try {
@@ -9776,6 +10495,19 @@ var Orchestrator = class {
9776
10495
  }
9777
10496
  return thread;
9778
10497
  }
10498
+ /** Run workspace setup after a new worktree is created (no-op if none configured). */
10499
+ async runSetupAfterCreate(threadId) {
10500
+ try {
10501
+ await this.runSetup(threadId);
10502
+ } catch (err) {
10503
+ const message = err instanceof Error ? err.message : String(err);
10504
+ if (/no setup script/i.test(message)) return;
10505
+ if (/already running/i.test(message)) return;
10506
+ updateThread(threadId, {
10507
+ lastError: `Setup failed: ${message}`
10508
+ });
10509
+ }
10510
+ }
9779
10511
  listWorkspaces() {
9780
10512
  const fromThreads = listThreads({ includeArchived: false }).map((t) => t.repoPath);
9781
10513
  return syncWorkspacesFromThreads(fromThreads);
@@ -10124,6 +10856,19 @@ var Orchestrator = class {
10124
10856
  });
10125
10857
  }
10126
10858
  const afterTurn = this.requireThread(threadId);
10859
+ if (afterTurn.planMode && afterTurn.worktreePath?.trim()) {
10860
+ const presented = extractPresentedPlan(parts);
10861
+ const exited = parts.some(
10862
+ (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
10863
+ );
10864
+ if (presented?.content) {
10865
+ writePlanFile(afterTurn.worktreePath, presented.content);
10866
+ } else if (exited || chatText && chatText.trim().length >= 400) {
10867
+ if (!readPlanFile(afterTurn.worktreePath) && chatText?.trim()) {
10868
+ writePlanFile(afterTurn.worktreePath, chatText.trim());
10869
+ }
10870
+ }
10871
+ }
10127
10872
  if (afterTurn.planMode && afterTurn.agent === "claude" && parts.some(
10128
10873
  (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
10129
10874
  )) {
@@ -10605,6 +11350,82 @@ var Orchestrator = class {
10605
11350
  }
10606
11351
  return meta;
10607
11352
  }
11353
+ async getPrStack(threadRef) {
11354
+ const thread = this.requireThread(threadRef);
11355
+ if (!thread.worktreePath?.trim()) return null;
11356
+ const stack = await getPrStack(thread.worktreePath);
11357
+ if (!stack) return null;
11358
+ const current = stack.currentIndex >= 0 ? stack.layers[stack.currentIndex] : null;
11359
+ const patch = {};
11360
+ if (stack.stackNumber != null) {
11361
+ const id = `gh-stack-${stack.stackNumber}`;
11362
+ if (thread.stackId !== id) patch.stackId = id;
11363
+ }
11364
+ if (current?.position != null && thread.stackLayer !== current.position) {
11365
+ patch.stackLayer = current.position;
11366
+ }
11367
+ if (current?.prUrl && current.prUrl !== thread.prUrl) patch.prUrl = current.prUrl;
11368
+ if (current?.title && current.title !== thread.prTitle) patch.prTitle = current.title;
11369
+ if (current?.branchName && current.branchName !== thread.branchName) {
11370
+ patch.branchName = current.branchName;
11371
+ }
11372
+ if (Object.keys(patch).length > 0) updateThread(thread.id, patch);
11373
+ return stack;
11374
+ }
11375
+ /** Open worktrees for all (or one) stack layers discovered from a thread. */
11376
+ async openPrStackLayers(threadRef, opts) {
11377
+ const result = await openPrStackLayers({ threadRef, layer: opts?.layer });
11378
+ for (const t of result.threads) {
11379
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
11380
+ }
11381
+ for (const id of result.createdThreadIds) {
11382
+ await this.runSetupAfterCreate(id);
11383
+ }
11384
+ return { stack: result.stack, threads: result.threads };
11385
+ }
11386
+ /** Add a branch on top of the thread's stack and open its worktree. */
11387
+ async addStackLayer(threadRef, branchName, opts) {
11388
+ const result = await addStackLayerFromThread({
11389
+ threadRef,
11390
+ branchName,
11391
+ title: opts?.title
11392
+ });
11393
+ this.emit({
11394
+ type: "status_changed",
11395
+ threadId: result.thread.id,
11396
+ status: result.thread.status
11397
+ });
11398
+ if (result.createdWorktree) {
11399
+ await this.runSetupAfterCreate(result.thread.id);
11400
+ }
11401
+ return { stack: result.stack, thread: result.thread };
11402
+ }
11403
+ /** Initialize a stack from the current thread branch (optional extra layers). */
11404
+ async initStackFromThread(threadRef, opts) {
11405
+ const result = await initStackFromThread({
11406
+ threadRef,
11407
+ additionalBranches: opts?.additionalBranches,
11408
+ base: opts?.base
11409
+ });
11410
+ for (const t of result.threads) {
11411
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
11412
+ }
11413
+ for (const id of result.createdThreadIds) {
11414
+ await this.runSetupAfterCreate(id);
11415
+ }
11416
+ return { stack: result.stack, threads: result.threads };
11417
+ }
11418
+ /** Create a new multi-layer stack with one worktree per layer. */
11419
+ async createPrStack(input) {
11420
+ const result = await createPrStack(input);
11421
+ for (const t of result.threads) {
11422
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
11423
+ }
11424
+ for (const id of result.createdThreadIds) {
11425
+ await this.runSetupAfterCreate(id);
11426
+ }
11427
+ return { stack: result.stack, threads: result.threads };
11428
+ }
10608
11429
  async getPrDetails(threadRef) {
10609
11430
  const { thread, selector, cwd } = await this.withPrSelector(threadRef);
10610
11431
  if (!selector) return null;
@@ -10667,14 +11488,9 @@ var Orchestrator = class {
10667
11488
  return forkChatTab(input);
10668
11489
  }
10669
11490
  async forkThreadWorktree(input) {
10670
- const thread = await forkThreadWorktree(input, (line) => {
10671
- this.emit({
10672
- type: "turn_output",
10673
- threadId: "pending",
10674
- event: { type: "stdout", data: line }
10675
- });
10676
- });
11491
+ const thread = await forkThreadWorktree(input);
10677
11492
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
11493
+ await this.runSetupAfterCreate(thread.id);
10678
11494
  return thread;
10679
11495
  }
10680
11496
  renameThread(threadRef, title) {
@@ -10688,7 +11504,7 @@ var Orchestrator = class {
10688
11504
  }
10689
11505
  /**
10690
11506
  * Stage OS / worktree files into composer attachments (copies external files
10691
- * into `.sideboard/attachments/` so agents can Read images and binaries).
11507
+ * into `.context/attachments/` so agents can Read images and binaries).
10692
11508
  */
10693
11509
  attachComposerFiles(threadRef, opts) {
10694
11510
  const thread = this.requireThread(threadRef);
@@ -10762,7 +11578,7 @@ var Orchestrator = class {
10762
11578
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
10763
11579
  return setStatus(thread.id, "idle");
10764
11580
  }
10765
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
11581
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
10766
11582
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
10767
11583
  const { execa: execa7 } = await import("execa");
10768
11584
  const slug = thread.worktreePath.split("/").pop();
@@ -10981,7 +11797,7 @@ async function startMcpServer() {
10981
11797
  async () => {
10982
11798
  const threads = orch.getThreads(true);
10983
11799
  const lines = threads.map((t) => {
10984
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path25.basename)(t.repoPath) || t.repoPath;
11800
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path26.basename)(t.repoPath) || t.repoPath;
10985
11801
  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}` : ""}`;
10986
11802
  });
10987
11803
  return {
@@ -11043,6 +11859,59 @@ async function startMcpServer() {
11043
11859
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
11044
11860
  }
11045
11861
  );
11862
+ server.tool(
11863
+ "ask_user",
11864
+ "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.",
11865
+ {
11866
+ questions: import_zod.z.array(
11867
+ import_zod.z.object({
11868
+ question: import_zod.z.string().describe("Full question text ending with ?"),
11869
+ header: import_zod.z.string().max(24).optional().describe("Short label shown above the question"),
11870
+ multiSelect: import_zod.z.boolean().optional().describe("Allow selecting multiple options"),
11871
+ options: import_zod.z.array(
11872
+ import_zod.z.object({
11873
+ label: import_zod.z.string(),
11874
+ description: import_zod.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
11875
+ })
11876
+ ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
11877
+ })
11878
+ ).min(1).max(4).describe("1\u20134 questions")
11879
+ },
11880
+ async ({ questions }) => {
11881
+ const payload = {
11882
+ ok: true,
11883
+ questions,
11884
+ message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
11885
+ };
11886
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
11887
+ }
11888
+ );
11889
+ server.tool(
11890
+ "present_plan",
11891
+ "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.",
11892
+ {
11893
+ title: import_zod.z.string().optional().describe("Short plan title (defaults to Plan)"),
11894
+ content: import_zod.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
11895
+ thread_id: import_zod.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
11896
+ },
11897
+ async ({ title, content, thread_id }) => {
11898
+ const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
11899
+ let root = process.cwd();
11900
+ if (thread_id?.trim()) {
11901
+ const t = orch.getThread(thread_id.trim());
11902
+ if (t?.worktreePath?.trim()) root = t.worktreePath;
11903
+ }
11904
+ const path = writePlanFile2(root, content);
11905
+ const payload = {
11906
+ ok: true,
11907
+ path,
11908
+ title: title?.trim() || "Plan",
11909
+ content,
11910
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
11911
+ };
11912
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
11913
+ }
11914
+ );
11046
11915
  server.tool(
11047
11916
  "present_schema",
11048
11917
  "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.",
@@ -11621,6 +12490,126 @@ async function startMcpServer() {
11621
12490
  };
11622
12491
  }
11623
12492
  );
12493
+ server.tool(
12494
+ "get_pr_stack",
12495
+ "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.",
12496
+ { ref: import_zod.z.string() },
12497
+ async ({ ref }) => {
12498
+ const stack = await orch.getPrStack(ref);
12499
+ return {
12500
+ content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
12501
+ };
12502
+ }
12503
+ );
12504
+ server.tool(
12505
+ "open_pr_stack_layers",
12506
+ "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
12507
+ {
12508
+ ref: import_zod.z.string(),
12509
+ layer: import_zod.z.number().int().positive().optional()
12510
+ },
12511
+ async ({ ref, layer }) => {
12512
+ const result = await orch.openPrStackLayers(ref, { layer });
12513
+ return {
12514
+ content: [
12515
+ {
12516
+ type: "text",
12517
+ text: JSON.stringify(
12518
+ {
12519
+ stackNumber: result.stack.stackNumber,
12520
+ trunk: result.stack.trunk,
12521
+ threads: result.threads.map((t) => ({
12522
+ id: t.id,
12523
+ title: t.title,
12524
+ branchName: t.branchName,
12525
+ stackLayer: t.stackLayer,
12526
+ worktreePath: t.worktreePath,
12527
+ prUrl: t.prUrl,
12528
+ link: `sideboard://thread/${t.id}`
12529
+ }))
12530
+ },
12531
+ null,
12532
+ 2
12533
+ )
12534
+ }
12535
+ ]
12536
+ };
12537
+ }
12538
+ );
12539
+ server.tool(
12540
+ "add_stack_layer",
12541
+ "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
12542
+ {
12543
+ ref: import_zod.z.string(),
12544
+ branchName: import_zod.z.string(),
12545
+ title: import_zod.z.string().optional()
12546
+ },
12547
+ async ({ ref, branchName, title }) => {
12548
+ const result = await orch.addStackLayer(ref, branchName, { title });
12549
+ return {
12550
+ content: [
12551
+ {
12552
+ type: "text",
12553
+ text: JSON.stringify(
12554
+ {
12555
+ id: result.thread.id,
12556
+ title: result.thread.title,
12557
+ branchName: result.thread.branchName,
12558
+ stackLayer: result.thread.stackLayer,
12559
+ worktreePath: result.thread.worktreePath,
12560
+ link: `sideboard://thread/${result.thread.id}`
12561
+ },
12562
+ null,
12563
+ 2
12564
+ )
12565
+ }
12566
+ ]
12567
+ };
12568
+ }
12569
+ );
12570
+ server.tool(
12571
+ "create_pr_stack",
12572
+ "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
12573
+ {
12574
+ repoPath: import_zod.z.string(),
12575
+ branches: import_zod.z.array(import_zod.z.string()).min(1),
12576
+ agent: import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
12577
+ base: import_zod.z.string().optional(),
12578
+ title: import_zod.z.string().optional()
12579
+ },
12580
+ async (args) => {
12581
+ const result = await orch.createPrStack({
12582
+ repoPath: args.repoPath,
12583
+ branches: args.branches,
12584
+ agent: args.agent,
12585
+ base: args.base,
12586
+ title: args.title
12587
+ });
12588
+ return {
12589
+ content: [
12590
+ {
12591
+ type: "text",
12592
+ text: JSON.stringify(
12593
+ {
12594
+ stackNumber: result.stack.stackNumber,
12595
+ trunk: result.stack.trunk,
12596
+ threads: result.threads.map((t) => ({
12597
+ id: t.id,
12598
+ title: t.title,
12599
+ branchName: t.branchName,
12600
+ stackLayer: t.stackLayer,
12601
+ worktreePath: t.worktreePath,
12602
+ link: `sideboard://thread/${t.id}`
12603
+ }))
12604
+ },
12605
+ null,
12606
+ 2
12607
+ )
12608
+ }
12609
+ ]
12610
+ };
12611
+ }
12612
+ );
11624
12613
  server.tool(
11625
12614
  "list_issues",
11626
12615
  "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.",