@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
@@ -92,6 +92,11 @@ function summarizeTurnStderr(tail, maxChars = 500) {
92
92
  if (joined.length <= maxChars) return joined;
93
93
  return joined.slice(joined.length - maxChars);
94
94
  }
95
+ function looksLikeInvalidAgentSession(text) {
96
+ const lower = text.trim().toLowerCase();
97
+ if (!lower) return false;
98
+ 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);
99
+ }
95
100
  function looksLikeAgentFailureMessage(text) {
96
101
  const lower = text.trim().toLowerCase();
97
102
  if (!lower) return false;
@@ -481,6 +486,8 @@ function normalizeThread(raw) {
481
486
  agentPid: raw.agentPid ?? null,
482
487
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
483
488
  prTitle: raw.prTitle ?? null,
489
+ stackId: raw.stackId ?? null,
490
+ stackLayer: raw.stackLayer ?? null,
484
491
  userSetTitle: Boolean(raw.userSetTitle),
485
492
  activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
486
493
  quotaResumeAt: raw.quotaResumeAt ?? null,
@@ -505,6 +512,8 @@ function createEmptyThread(partial) {
505
512
  activeRuns: partial.activeRuns ?? [],
506
513
  prUrl: partial.prUrl ?? null,
507
514
  prTitle: partial.prTitle ?? null,
515
+ stackId: partial.stackId ?? null,
516
+ stackLayer: partial.stackLayer ?? null,
508
517
  userSetTitle: partial.userSetTitle ?? false,
509
518
  messages: partial.messages ?? [],
510
519
  attachments: partial.attachments ?? [],
@@ -1688,6 +1697,206 @@ var init_pr_gates = __esm({
1688
1697
  }
1689
1698
  });
1690
1699
 
1700
+ // src/git/stack.ts
1701
+ async function detectGhStack(cwd) {
1702
+ const now = Date.now();
1703
+ if (cachedStatus && now - cachedStatus.at < STATUS_TTL_MS) {
1704
+ return cachedStatus.status;
1705
+ }
1706
+ const probe = await gh(["stack", "view", "--help"], cwd, { reject: false });
1707
+ if (probe.exitCode === 0) {
1708
+ const status2 = { available: true };
1709
+ cachedStatus = { at: now, status: status2 };
1710
+ return status2;
1711
+ }
1712
+ const err = `${probe.stderr}
1713
+ ${probe.stdout}`;
1714
+ 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";
1715
+ const status = { available: false, reason };
1716
+ cachedStatus = { at: now, status };
1717
+ return status;
1718
+ }
1719
+ function str(v) {
1720
+ return typeof v === "string" ? v : v == null ? "" : String(v);
1721
+ }
1722
+ function num(v) {
1723
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1724
+ if (typeof v === "string" && v.trim() && Number.isFinite(Number(v))) {
1725
+ return Number(v);
1726
+ }
1727
+ return null;
1728
+ }
1729
+ function parseGhStackViewJson(raw) {
1730
+ let data;
1731
+ try {
1732
+ data = JSON.parse(raw);
1733
+ } catch {
1734
+ return null;
1735
+ }
1736
+ if (!Array.isArray(data.branches) || data.branches.length === 0) return null;
1737
+ const trunk = str(data.trunk) || "main";
1738
+ const currentBranch2 = str(data.currentBranch);
1739
+ const stackNumber = num(data.stackNumber) ?? num(data.number);
1740
+ const layers = [];
1741
+ for (let i = 0; i < data.branches.length; i++) {
1742
+ const b = data.branches[i];
1743
+ if (!b || typeof b !== "object") continue;
1744
+ const name = str(b.name);
1745
+ if (!name) continue;
1746
+ const pr = b.pr && typeof b.pr === "object" ? b.pr : null;
1747
+ layers.push({
1748
+ position: i + 1,
1749
+ branchName: name,
1750
+ headSha: str(b.head) || void 0,
1751
+ baseSha: str(b.base) || void 0,
1752
+ isCurrent: Boolean(b.isCurrent) || name === currentBranch2,
1753
+ isMerged: Boolean(b.isMerged),
1754
+ isQueued: Boolean(b.isQueued),
1755
+ needsRebase: Boolean(b.needsRebase),
1756
+ prNumber: pr ? num(pr.number) : null,
1757
+ prUrl: pr && str(pr.url) ? str(pr.url) : null,
1758
+ prState: pr && str(pr.state) ? str(pr.state).toUpperCase() : null,
1759
+ title: pr && str(pr.title) ? str(pr.title) : void 0
1760
+ });
1761
+ }
1762
+ if (!layers.length) return null;
1763
+ let currentIndex = layers.findIndex((l) => l.isCurrent);
1764
+ if (currentIndex < 0 && currentBranch2) {
1765
+ currentIndex = layers.findIndex((l) => l.branchName === currentBranch2);
1766
+ }
1767
+ const { readyToMerge, blockedReason } = stackMergeReadiness(layers, currentIndex);
1768
+ return {
1769
+ stackNumber,
1770
+ trunk,
1771
+ currentBranch: currentBranch2 || layers[currentIndex]?.branchName || layers[0].branchName,
1772
+ layers,
1773
+ currentIndex,
1774
+ readyToMerge,
1775
+ blockedReason
1776
+ };
1777
+ }
1778
+ function stackMergeReadiness(layers, throughIndex) {
1779
+ if (throughIndex < 0 || throughIndex >= layers.length) {
1780
+ return { readyToMerge: false, blockedReason: "Not on a stack layer" };
1781
+ }
1782
+ for (let i = 0; i <= throughIndex; i++) {
1783
+ const layer = layers[i];
1784
+ if (layer.isMerged) continue;
1785
+ if (!layer.prNumber) {
1786
+ return {
1787
+ readyToMerge: false,
1788
+ blockedReason: `Layer ${layer.branchName} has no pull request yet`
1789
+ };
1790
+ }
1791
+ if (layer.needsRebase) {
1792
+ return {
1793
+ readyToMerge: false,
1794
+ blockedReason: `PR #${layer.prNumber} needs rebase`
1795
+ };
1796
+ }
1797
+ const state = (layer.prState ?? "").toUpperCase();
1798
+ if (state && state !== "OPEN" && state !== "QUEUED") {
1799
+ return {
1800
+ readyToMerge: false,
1801
+ blockedReason: `PR #${layer.prNumber} is ${state}`
1802
+ };
1803
+ }
1804
+ }
1805
+ return { readyToMerge: true, blockedReason: null };
1806
+ }
1807
+ async function getPrStack(cwd) {
1808
+ const status = await detectGhStack(cwd);
1809
+ if (!status.available) return null;
1810
+ const result = await gh(["stack", "view", "--json"], cwd, { reject: false });
1811
+ if (result.exitCode === 2) return null;
1812
+ if (result.exitCode !== 0) {
1813
+ if (/not in a stack|no stack/i.test(`${result.stderr}
1814
+ ${result.stdout}`)) {
1815
+ return null;
1816
+ }
1817
+ if (result.exitCode === 9) return null;
1818
+ return null;
1819
+ }
1820
+ const json = result.stdout.trim();
1821
+ if (!json) return null;
1822
+ return parseGhStackViewJson(json);
1823
+ }
1824
+ async function mergePrStack(cwd, opts) {
1825
+ const status = await detectGhStack(cwd);
1826
+ if (!status.available) {
1827
+ throw new Error(status.reason);
1828
+ }
1829
+ const method = opts.method ?? "squash";
1830
+ const methodFlag = method === "rebase" ? "--rebase" : method === "merge" ? "--merge" : "--squash";
1831
+ const args = [
1832
+ "stack",
1833
+ "merge",
1834
+ String(opts.through),
1835
+ "--yes",
1836
+ methodFlag
1837
+ ];
1838
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
1839
+ if (exitCode !== 0) {
1840
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack merge failed");
1841
+ }
1842
+ return { stdout };
1843
+ }
1844
+ async function initPrStack(cwd, branches, opts) {
1845
+ if (!branches.length) throw new Error("initPrStack requires at least one branch name");
1846
+ const status = await detectGhStack(cwd);
1847
+ if (!status.available) throw new Error(status.reason);
1848
+ const args = ["stack", "init"];
1849
+ if (opts?.base) args.push("--base", opts.base);
1850
+ args.push(...branches);
1851
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
1852
+ if (exitCode !== 0) {
1853
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack init failed");
1854
+ }
1855
+ }
1856
+ async function addPrStackLayer(cwd, branchName) {
1857
+ if (!branchName.trim()) throw new Error("branch name required");
1858
+ const status = await detectGhStack(cwd);
1859
+ if (!status.available) throw new Error(status.reason);
1860
+ const { exitCode, stderr, stdout } = await gh(
1861
+ ["stack", "add", branchName.trim()],
1862
+ cwd,
1863
+ { reject: false }
1864
+ );
1865
+ if (exitCode !== 0) {
1866
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack add failed");
1867
+ }
1868
+ }
1869
+ var cachedStatus, STATUS_TTL_MS;
1870
+ var init_stack = __esm({
1871
+ "src/git/stack.ts"() {
1872
+ "use strict";
1873
+ init_run();
1874
+ cachedStatus = null;
1875
+ STATUS_TTL_MS = 6e4;
1876
+ }
1877
+ });
1878
+
1879
+ // src/paths/workspace-scratch.ts
1880
+ function attachmentsGitignoreBody() {
1881
+ return ATTACHMENTS_GITIGNORE;
1882
+ }
1883
+ function isWorkspaceScratchPath(relativePath) {
1884
+ const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
1885
+ return p === ATTACHMENTS_DIR || p.startsWith(`${ATTACHMENTS_DIR}/`) || p === LEGACY_ATTACHMENTS_DIR || p.startsWith(`${LEGACY_ATTACHMENTS_DIR}/`) || p === ".context" || p.startsWith(".context/");
1886
+ }
1887
+ var ATTACHMENTS_DIR, LEGACY_ATTACHMENTS_DIR, ATTACHMENTS_GITIGNORE;
1888
+ var init_workspace_scratch = __esm({
1889
+ "src/paths/workspace-scratch.ts"() {
1890
+ "use strict";
1891
+ ATTACHMENTS_DIR = ".context/attachments";
1892
+ LEGACY_ATTACHMENTS_DIR = ".sideboard/attachments";
1893
+ ATTACHMENTS_GITIGNORE = `# Sideboard / workspace attachments (local only)
1894
+ *
1895
+ !.gitignore
1896
+ `;
1897
+ }
1898
+ });
1899
+
1691
1900
  // src/git/worktree.ts
1692
1901
  var worktree_exports = {};
1693
1902
  __export(worktree_exports, {
@@ -1697,6 +1906,7 @@ __export(worktree_exports, {
1697
1906
  branchDisplayLabel: () => branchDisplayLabel,
1698
1907
  collectTakenTeamSlugs: () => collectTakenTeamSlugs,
1699
1908
  commitAll: () => commitAll,
1909
+ createExistingBranchWorktree: () => createExistingBranchWorktree,
1700
1910
  createOrUpdatePr: () => createOrUpdatePr,
1701
1911
  createThreadWorktree: () => createThreadWorktree,
1702
1912
  currentBranch: () => currentBranch,
@@ -2410,6 +2620,52 @@ ${add.stdout}`;
2410
2620
  await ensureGhPreferOrigin(worktreePath);
2411
2621
  return { branchName, worktreePath };
2412
2622
  }
2623
+ async function createExistingBranchWorktree(opts) {
2624
+ const branchName = opts.branchName.trim();
2625
+ if (!branchName) throw new Error("branch name required");
2626
+ const worktreePath = (0, import_node_path5.join)(worktreesRoot(opts.repoPath), opts.slug);
2627
+ if ((0, import_node_fs4.existsSync)(worktreePath)) {
2628
+ throw new Error(`Worktree already exists at ${worktreePath}`);
2629
+ }
2630
+ await ensureGhPreferOrigin(opts.repoPath);
2631
+ await git(["fetch", "origin", "--prune"], opts.repoPath, { reject: false });
2632
+ if (!branchName.startsWith("origin/") && !branchName.startsWith("refs/")) {
2633
+ await git(["fetch", "origin", branchName], opts.repoPath, { reject: false });
2634
+ }
2635
+ const existing = await listWorktrees(opts.repoPath);
2636
+ const already = existing.find((w) => w.branch === branchName);
2637
+ if (already?.path) {
2638
+ throw new Error(
2639
+ `Branch ${branchName} is already checked out at ${already.path}`
2640
+ );
2641
+ }
2642
+ const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
2643
+ const add = await git(
2644
+ ["worktree", "add", worktreePath, startPoint],
2645
+ opts.repoPath,
2646
+ { reject: false }
2647
+ );
2648
+ if (add.exitCode !== 0) {
2649
+ const retry = await git(
2650
+ ["worktree", "add", worktreePath, branchName],
2651
+ opts.repoPath,
2652
+ { reject: false }
2653
+ );
2654
+ if (retry.exitCode !== 0) {
2655
+ throw new Error(
2656
+ `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
2657
+ );
2658
+ }
2659
+ }
2660
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
2661
+ reject: false
2662
+ });
2663
+ if (head.stdout.trim() === "HEAD" || head.stdout.trim() !== branchName) {
2664
+ await git(["checkout", "-B", branchName], worktreePath, { reject: false });
2665
+ }
2666
+ await ensureGhPreferOrigin(worktreePath);
2667
+ return { branchName, worktreePath };
2668
+ }
2413
2669
  async function removeWorktree(repoPath, worktreePath, opts) {
2414
2670
  await git(["worktree", "remove", "--force", worktreePath], repoPath, {
2415
2671
  reject: false
@@ -2449,8 +2705,7 @@ async function isDirty(worktreePath) {
2449
2705
  return false;
2450
2706
  }
2451
2707
  function isSideboardScratchPath(relativePath) {
2452
- const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
2453
- return p === ".sideboard/attachments" || p.startsWith(".sideboard/attachments/");
2708
+ return isWorkspaceScratchPath(relativePath);
2454
2709
  }
2455
2710
  function porcelainStatusPath(line) {
2456
2711
  const rest = line.length >= 3 ? line.slice(3) : "";
@@ -2478,7 +2733,7 @@ async function pushBranch(worktreePath, branchName) {
2478
2733
  }
2479
2734
  async function mergePr(cwd, selector, opts) {
2480
2735
  const slug = await resolveGithubRepoSlug(cwd);
2481
- const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft"];
2736
+ const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft,number"];
2482
2737
  if (slug) viewArgs.push("--repo", slug);
2483
2738
  const before = await gh(viewArgs, cwd, { reject: false });
2484
2739
  if (before.exitCode !== 0 || !before.stdout.trim()) {
@@ -2486,16 +2741,29 @@ async function mergePr(cwd, selector, opts) {
2486
2741
  }
2487
2742
  let url = "";
2488
2743
  let isDraft = false;
2744
+ let prNumber = null;
2489
2745
  try {
2490
2746
  const parsed = JSON.parse(before.stdout);
2491
2747
  url = String(parsed.url ?? "");
2492
2748
  isDraft = Boolean(parsed.isDraft);
2749
+ prNumber = typeof parsed.number === "number" && Number.isFinite(parsed.number) ? parsed.number : null;
2493
2750
  if (String(parsed.state ?? "").toUpperCase() === "MERGED") {
2494
2751
  return { url, state: "MERGED" };
2495
2752
  }
2496
2753
  } catch {
2497
2754
  throw new Error("Could not parse pull request details");
2498
2755
  }
2756
+ const stack = await getPrStack(cwd);
2757
+ const stackLayer = stack && prNumber != null ? stack.layers.find((l) => l.prNumber === prNumber) : null;
2758
+ if (stack && stackLayer && prNumber != null) {
2759
+ const throughIndex = stack.layers.findIndex((l) => l.prNumber === prNumber);
2760
+ const gate = stackMergeReadiness(stack.layers, throughIndex);
2761
+ if (!gate.readyToMerge) {
2762
+ throw new Error(gate.blockedReason || "Stack is not ready to merge");
2763
+ }
2764
+ await mergePrStack(cwd, { through: prNumber, method: opts?.method ?? "squash" });
2765
+ return { url, state: "MERGED" };
2766
+ }
2499
2767
  if (isDraft) {
2500
2768
  const readyArgs = ["pr", "ready", selector];
2501
2769
  if (slug) readyArgs.push("--repo", slug);
@@ -2517,10 +2785,10 @@ async function mergePr(cwd, selector, opts) {
2517
2785
  const after = await gh(viewArgs, cwd, { reject: false });
2518
2786
  if (after.exitCode === 0 && after.stdout.trim()) {
2519
2787
  try {
2520
- const parsed = JSON.parse(after.stdout);
2788
+ const parsed = after.stdout ? JSON.parse(after.stdout) : null;
2521
2789
  return {
2522
- url: String(parsed.url ?? url),
2523
- state: String(parsed.state ?? "MERGED")
2790
+ url: String(parsed?.url ?? url),
2791
+ state: String(parsed?.state ?? "MERGED")
2524
2792
  };
2525
2793
  } catch {
2526
2794
  }
@@ -2668,7 +2936,9 @@ var init_worktree = __esm({
2668
2936
  init_gh_errors();
2669
2937
  init_run();
2670
2938
  init_pr_gates();
2939
+ init_stack();
2671
2940
  init_teams();
2941
+ init_workspace_scratch();
2672
2942
  init_worktree_labels();
2673
2943
  }
2674
2944
  });
@@ -3346,6 +3616,7 @@ var init_coordinator_prompt = __esm({
3346
3616
  "Discover:",
3347
3617
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3348
3618
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3619
+ "- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
3349
3620
  "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3350
3621
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3351
3622
  "Workspaces:",
@@ -4331,7 +4602,9 @@ var init_injected_mcp = __esm({
4331
4602
  SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS = [
4332
4603
  "mcp__sideboard__present_artifact",
4333
4604
  "mcp__sideboard__present_schema",
4334
- "mcp__sideboard__present_files"
4605
+ "mcp__sideboard__present_files",
4606
+ "mcp__sideboard__ask_user",
4607
+ "mcp__sideboard__present_plan"
4335
4608
  ];
4336
4609
  brightsyMcpCommandCache = null;
4337
4610
  }
@@ -4370,7 +4643,7 @@ var PLAN_MODE_INSTRUCTION;
4370
4643
  var init_types = __esm({
4371
4644
  "src/agents/types.ts"() {
4372
4645
  "use strict";
4373
- 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.";
4646
+ 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.";
4374
4647
  }
4375
4648
  });
4376
4649
 
@@ -5442,6 +5715,7 @@ var init_opencode = __esm({
5442
5715
  }
5443
5716
  },
5444
5717
  async resolveSessionId(worktreePath, cached) {
5718
+ const cachedId = cached?.trim() || null;
5445
5719
  const listed = await run(
5446
5720
  "opencode",
5447
5721
  ["session", "list", "--format", "json"],
@@ -5453,15 +5727,21 @@ var init_opencode = __esm({
5453
5727
  if (Array.isArray(sessions) && sessions.length > 0) {
5454
5728
  const norm = (p) => p.replace(/\/+$/, "");
5455
5729
  const wt = norm(worktreePath);
5456
- const match = sessions.find(
5730
+ const forWorktree = sessions.filter(
5457
5731
  (s) => s.directory && norm(s.directory) === wt || s.path && norm(s.path) === wt
5458
5732
  );
5459
- if (match?.id) return match.id;
5733
+ if (cachedId && forWorktree.some((s) => s.id === cachedId)) {
5734
+ return cachedId;
5735
+ }
5736
+ if (cachedId && sessions.some((s) => s.id === cachedId)) {
5737
+ return cachedId;
5738
+ }
5739
+ return null;
5460
5740
  }
5461
5741
  } catch {
5462
5742
  }
5463
5743
  }
5464
- return cached;
5744
+ return cachedId;
5465
5745
  },
5466
5746
  async buildAttach(thread) {
5467
5747
  const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
@@ -6078,6 +6358,116 @@ var init_workspaces = __esm({
6078
6358
  }
6079
6359
  });
6080
6360
 
6361
+ // src/plan/plan-present.ts
6362
+ function asRecord2(v) {
6363
+ return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
6364
+ }
6365
+ function isPresentPlanToolName(name) {
6366
+ if (!name) return false;
6367
+ return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
6368
+ }
6369
+ function extractPresentedPlan(parts) {
6370
+ if (!parts?.length) return null;
6371
+ for (let i = parts.length - 1; i >= 0; i--) {
6372
+ const p = parts[i];
6373
+ if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
6374
+ const input = asRecord2(p.input) ?? {};
6375
+ const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
6376
+ if (!content.trim()) continue;
6377
+ const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
6378
+ const path = typeof input.path === "string" && input.path.trim() ? input.path.trim() : PLAN_FILE_REL;
6379
+ return { title, content: content.trim(), path, source: "present_plan" };
6380
+ }
6381
+ return null;
6382
+ }
6383
+ function resolvePlanMarkdown(opts) {
6384
+ const fromTool = extractPresentedPlan(opts.parts);
6385
+ if (fromTool) return fromTool;
6386
+ const file = opts.fileContent?.trim();
6387
+ if (file) {
6388
+ return {
6389
+ title: "Plan",
6390
+ content: file,
6391
+ path: PLAN_FILE_REL,
6392
+ source: "exit_plan"
6393
+ };
6394
+ }
6395
+ const text = opts.text?.trim();
6396
+ if (text && text.length >= 80) {
6397
+ return {
6398
+ title: "Plan",
6399
+ content: text,
6400
+ path: PLAN_FILE_REL,
6401
+ source: "text"
6402
+ };
6403
+ }
6404
+ return null;
6405
+ }
6406
+ var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
6407
+ var init_plan_present = __esm({
6408
+ "src/plan/plan-present.ts"() {
6409
+ "use strict";
6410
+ init_workspace_scratch();
6411
+ PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
6412
+ PLAN_FILE_NAME = "plan.md";
6413
+ LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
6414
+ }
6415
+ });
6416
+
6417
+ // src/plan/plan-file.ts
6418
+ var plan_file_exports = {};
6419
+ __export(plan_file_exports, {
6420
+ LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
6421
+ PLAN_FILE_NAME: () => PLAN_FILE_NAME,
6422
+ PLAN_FILE_REL: () => PLAN_FILE_REL,
6423
+ extractPresentedPlan: () => extractPresentedPlan,
6424
+ isPresentPlanToolName: () => isPresentPlanToolName,
6425
+ planFileAbs: () => planFileAbs,
6426
+ readPlanFile: () => readPlanFile,
6427
+ resolvePlanMarkdown: () => resolvePlanMarkdown,
6428
+ writePlanFile: () => writePlanFile
6429
+ });
6430
+ function ensureAttachmentsGitignore2(worktreePath) {
6431
+ const gitignoreAbs = (0, import_node_path24.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
6432
+ if ((0, import_node_fs26.existsSync)(gitignoreAbs)) return;
6433
+ (0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(gitignoreAbs), { recursive: true });
6434
+ (0, import_node_fs26.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
6435
+ }
6436
+ function planFileAbs(worktreePath) {
6437
+ return (0, import_node_path24.join)(worktreePath, PLAN_FILE_REL);
6438
+ }
6439
+ function readTextIfPresent2(abs) {
6440
+ if (!(0, import_node_fs26.existsSync)(abs)) return null;
6441
+ try {
6442
+ const content = (0, import_node_fs26.readFileSync)(abs, "utf8");
6443
+ return content.trim() ? content : null;
6444
+ } catch {
6445
+ return null;
6446
+ }
6447
+ }
6448
+ function readPlanFile(worktreePath) {
6449
+ 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));
6450
+ }
6451
+ function writePlanFile(worktreePath, content) {
6452
+ ensureAttachmentsGitignore2(worktreePath);
6453
+ const abs = planFileAbs(worktreePath);
6454
+ (0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(abs), { recursive: true });
6455
+ const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
6456
+ (0, import_node_fs26.writeFileSync)(abs, body, "utf8");
6457
+ return PLAN_FILE_REL;
6458
+ }
6459
+ var import_node_fs26, import_node_path24;
6460
+ var init_plan_file = __esm({
6461
+ "src/plan/plan-file.ts"() {
6462
+ "use strict";
6463
+ import_node_fs26 = require("fs");
6464
+ import_node_path24 = require("path");
6465
+ init_workspace_scratch();
6466
+ init_plan_present();
6467
+ init_plan_present();
6468
+ }
6469
+ });
6470
+
6081
6471
  // src/agents/cursor-recover.ts
6082
6472
  var cursor_recover_exports = {};
6083
6473
  __export(cursor_recover_exports, {
@@ -6086,10 +6476,10 @@ __export(cursor_recover_exports, {
6086
6476
  function recoverFinishedCursorRun(opts) {
6087
6477
  const agentId = opts.agentId.trim();
6088
6478
  if (!agentId) return null;
6089
- const runsPath = (0, import_node_path24.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6090
- if (!(0, import_node_fs25.existsSync)(runsPath)) return null;
6479
+ const runsPath = (0, import_node_path25.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6480
+ if (!(0, import_node_fs27.existsSync)(runsPath)) return null;
6091
6481
  try {
6092
- const lines = (0, import_node_fs25.readFileSync)(runsPath, "utf8").split("\n");
6482
+ const lines = (0, import_node_fs27.readFileSync)(runsPath, "utf8").split("\n");
6093
6483
  let best = null;
6094
6484
  for (const line of lines) {
6095
6485
  const trimmed = line.trim();
@@ -6115,12 +6505,12 @@ function recoverFinishedCursorRun(opts) {
6115
6505
  return null;
6116
6506
  }
6117
6507
  }
6118
- var import_node_fs25, import_node_path24;
6508
+ var import_node_fs27, import_node_path25;
6119
6509
  var init_cursor_recover = __esm({
6120
6510
  "src/agents/cursor-recover.ts"() {
6121
6511
  "use strict";
6122
- import_node_fs25 = require("fs");
6123
- import_node_path24 = require("path");
6512
+ import_node_fs27 = require("fs");
6513
+ import_node_path25 = require("path");
6124
6514
  init_paths();
6125
6515
  }
6126
6516
  });
@@ -6129,11 +6519,11 @@ var init_cursor_recover = __esm({
6129
6519
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
6130
6520
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6131
6521
  var import_zod = require("zod");
6132
- var import_node_path25 = require("path");
6522
+ var import_node_path26 = require("path");
6133
6523
 
6134
6524
  // src/orchestrator/orchestrator.ts
6135
6525
  var import_node_events = require("events");
6136
- var import_node_fs26 = require("fs");
6526
+ var import_node_fs28 = require("fs");
6137
6527
  init_error_detail();
6138
6528
 
6139
6529
  // src/agents/spawn.ts
@@ -6153,18 +6543,18 @@ function asRecord(input) {
6153
6543
  }
6154
6544
  return void 0;
6155
6545
  }
6156
- function str(v) {
6546
+ function str2(v) {
6157
6547
  return typeof v === "string" && v.trim() ? v : void 0;
6158
6548
  }
6159
6549
  function toolDetail(name, input) {
6160
6550
  if (!input) return void 0;
6161
- const command = str(input.command) ?? str(input.cmd);
6551
+ const command = str2(input.command) ?? str2(input.cmd);
6162
6552
  if (command) return command;
6163
- const path = str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
6553
+ const path = str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
6164
6554
  if (path) return path;
6165
- const pattern = str(input.pattern) ?? str(input.glob) ?? str(input.glob_pattern);
6555
+ const pattern = str2(input.pattern) ?? str2(input.glob) ?? str2(input.glob_pattern);
6166
6556
  if (pattern) return pattern;
6167
- const query = str(input.query) ?? str(input.prompt);
6557
+ const query = str2(input.query) ?? str2(input.prompt);
6168
6558
  if (query) return query.length > 80 ? `${query.slice(0, 77)}\u2026` : query;
6169
6559
  try {
6170
6560
  const raw = JSON.stringify(input);
@@ -6177,23 +6567,26 @@ function toolDescription(name, input) {
6177
6567
  const n = name.replace(/^mcp__/, "").replace(/__/g, " \xB7 ");
6178
6568
  if (/^get_record_types$|recordTypes/i.test(name)) return "List record types";
6179
6569
  if (/connectedAgentRequest/i.test(name)) {
6180
- return str(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
6570
+ return str2(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
6181
6571
  }
6182
6572
  if (/present_artifact$/i.test(name)) {
6183
- return str(input?.title) ? `Present ${str(input?.title)}` : "Present artifact";
6573
+ return str2(input?.title) ? `Present ${str2(input?.title)}` : "Present artifact";
6574
+ }
6575
+ if (/present_plan$/i.test(name)) {
6576
+ return str2(input?.title) ? `Plan ${str2(input?.title)}` : "Present plan";
6184
6577
  }
6185
6578
  if (/present_schema$/i.test(name)) {
6186
- return str(input?.title) ? `Schema ${str(input?.title)}` : "Present schema";
6579
+ return str2(input?.title) ? `Schema ${str2(input?.title)}` : "Present schema";
6187
6580
  }
6188
6581
  if (/present_files$/i.test(name)) {
6189
- return str(input?.title) ? `Files ${str(input?.title)}` : "Present files";
6582
+ return str2(input?.title) ? `Files ${str2(input?.title)}` : "Present files";
6190
6583
  }
6191
6584
  if (/^(create|update)_artifact$/i.test(name.replace(/^mcp__[^_]+__/, ""))) {
6192
- return str(input?.title) ? `Artifact ${str(input?.title)}` : "Artifact";
6585
+ return str2(input?.title) ? `Artifact ${str2(input?.title)}` : "Artifact";
6193
6586
  }
6194
6587
  if (!input) return n;
6195
- if (/bash|shell|terminal/i.test(name) && str(input.command)) {
6196
- const cmd = str(input.command);
6588
+ if (/bash|shell|terminal/i.test(name) && str2(input.command)) {
6589
+ const cmd = str2(input.command);
6197
6590
  if (/git\s+fetch/i.test(cmd)) return "Fetch latest from origin and check status";
6198
6591
  if (/git\s+status/i.test(cmd)) return "Check git status";
6199
6592
  if (/git\s+log/i.test(cmd)) return "Inspect recent commits";
@@ -6202,11 +6595,11 @@ function toolDescription(name, input) {
6202
6595
  return "Run shell command";
6203
6596
  }
6204
6597
  if (/edit|write|apply/i.test(name)) {
6205
- const path = str(input.file_path) ?? str(input.path);
6598
+ const path = str2(input.file_path) ?? str2(input.path);
6206
6599
  return path ? `Edit ${path.split("/").pop()}` : "Edit file";
6207
6600
  }
6208
6601
  if (/read/i.test(name)) {
6209
- const path = str(input.file_path) ?? str(input.path);
6602
+ const path = str2(input.file_path) ?? str2(input.path);
6210
6603
  return path ? `Read ${path.split("/").pop()}` : "Read file";
6211
6604
  }
6212
6605
  if (/grep/i.test(name)) return "Search files";
@@ -6215,7 +6608,7 @@ function toolDescription(name, input) {
6215
6608
  }
6216
6609
  function toolFilePath(input) {
6217
6610
  if (!input) return void 0;
6218
- return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
6611
+ return str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
6219
6612
  }
6220
6613
  function countLines(text) {
6221
6614
  if (!text) return 0;
@@ -6223,8 +6616,8 @@ function countLines(text) {
6223
6616
  }
6224
6617
  function diffFromInput(input) {
6225
6618
  if (!input) return {};
6226
- const oldS = str(input.old_string) ?? str(input.oldString);
6227
- const newS = str(input.new_string) ?? str(input.newString) ?? str(input.content);
6619
+ const oldS = str2(input.old_string) ?? str2(input.oldString);
6620
+ const newS = str2(input.new_string) ?? str2(input.newString) ?? str2(input.content);
6228
6621
  if (oldS != null || newS != null) {
6229
6622
  return {
6230
6623
  additions: countLines(newS),
@@ -6486,6 +6879,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
6486
6879
  // src/orchestrator/orchestrator.ts
6487
6880
  init_agents();
6488
6881
  init_worktree();
6882
+ init_stack();
6489
6883
 
6490
6884
  // src/hook/conductor.ts
6491
6885
  var import_node_fs13 = require("fs");
@@ -7121,7 +7515,7 @@ async function requireAgent(agent, opts) {
7121
7515
  init_worktree();
7122
7516
  init_thread_store();
7123
7517
  init_workspaces();
7124
- async function createThread(input, onSetupLine) {
7518
+ async function createThread(input, _onSetupLine) {
7125
7519
  await requireAgent(input.agent);
7126
7520
  const repoPath = await resolveRepoRoot(input.repoPath);
7127
7521
  if (!(0, import_node_fs18.existsSync)(repoPath)) {
@@ -7131,10 +7525,10 @@ async function createThread(input, onSetupLine) {
7131
7525
  let sourceIsFork = false;
7132
7526
  let prUrl = null;
7133
7527
  if (input.sourceType === "pr") {
7134
- const num = Number(input.sourceRef.replace(/^#/, ""));
7135
- if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
7136
- const pr = await getPr(repoPath, num);
7137
- if (!pr) throw new Error(`PR #${num} not found`);
7528
+ const num2 = Number(input.sourceRef.replace(/^#/, ""));
7529
+ if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
7530
+ const pr = await getPr(repoPath, num2);
7531
+ if (!pr) throw new Error(`PR #${num2} not found`);
7138
7532
  sourceIsFork = pr.isCrossRepository;
7139
7533
  prUrl = pr.url;
7140
7534
  const localFetchBranch = `sideboard-pr-${pr.number}`;
@@ -7179,22 +7573,6 @@ async function createThread(input, onSetupLine) {
7179
7573
  });
7180
7574
  writeThread(thread);
7181
7575
  await ensureWorkspace(repoPath);
7182
- try {
7183
- let setup = await runSetupScript(repoPath, worktreePath, onSetupLine);
7184
- if (!setup.ran) {
7185
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onSetupLine);
7186
- }
7187
- if (setup.ran && setup.exitCode !== 0 && setup.exitCode !== null) {
7188
- updateThread(thread.id, {
7189
- lastError: `Setup exited ${setup.exitCode} (thread is still usable)`
7190
- });
7191
- }
7192
- } catch (err) {
7193
- const message = err instanceof Error ? err.message : String(err);
7194
- updateThread(thread.id, {
7195
- lastError: `Setup failed: ${message}`
7196
- });
7197
- }
7198
7576
  return readThread(thread.id) ?? thread;
7199
7577
  }
7200
7578
  async function listLinearIssues(agent, repoPath) {
@@ -7526,7 +7904,9 @@ function worktreeBindingFrom(from) {
7526
7904
  sourceIsFork: from.sourceIsFork,
7527
7905
  parentThreadId: from.parentThreadId,
7528
7906
  prUrl: from.prUrl,
7529
- prTitle: from.prTitle
7907
+ prTitle: from.prTitle,
7908
+ stackId: from.stackId,
7909
+ stackLayer: from.stackLayer
7530
7910
  };
7531
7911
  }
7532
7912
  function threadsSharingWorktree(worktreePath) {
@@ -7724,15 +8104,13 @@ File: src/client/frontends/desktop/core/UserData.ts
7724
8104
  `;
7725
8105
 
7726
8106
  // src/review/request-review.ts
8107
+ init_workspace_scratch();
7727
8108
  var REPO_REVIEW_PATH = ".sideboard/review.md";
7728
8109
  var REPO_REVIEW_NAME = "review.md";
7729
- var REVIEW_REQUEST_PATH = ".sideboard/attachments/Review request.md";
8110
+ var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
8111
+ var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
7730
8112
  var REVIEW_REQUEST_NAME = "Review request.md";
7731
8113
  var REVIEW_REQUEST_PREFILL = "Review.";
7732
- var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
7733
- *
7734
- !.gitignore
7735
- `;
7736
8114
  var LEGACY_REVIEW_TEMPLATE_MARKERS = [
7737
8115
  "You are acting as a reviewer for a proposed code change made by another engineer.",
7738
8116
  "HOW MANY FINDINGS TO RETURN:"
@@ -7755,10 +8133,10 @@ function readTextIfPresent(abs) {
7755
8133
  }
7756
8134
  }
7757
8135
  function ensureAttachmentsGitignore(worktreePath) {
7758
- const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ".sideboard", "attachments", ".gitignore");
8136
+ const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
7759
8137
  if ((0, import_node_fs19.existsSync)(gitignoreAbs)) return;
7760
8138
  (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(gitignoreAbs), { recursive: true });
7761
- (0, import_node_fs19.writeFileSync)(gitignoreAbs, ATTACHMENTS_GITIGNORE, "utf8");
8139
+ (0, import_node_fs19.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
7762
8140
  }
7763
8141
  function resolveReviewGuidelines(worktreePath) {
7764
8142
  const repoAbs = (0, import_node_path18.join)(worktreePath, REPO_REVIEW_PATH);
@@ -7781,6 +8159,16 @@ function resolveReviewGuidelines(worktreePath) {
7781
8159
  source: "local"
7782
8160
  };
7783
8161
  }
8162
+ const legacyAbs = (0, import_node_path18.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
8163
+ const legacyContent = readTextIfPresent(legacyAbs);
8164
+ if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
8165
+ return {
8166
+ path: LEGACY_REVIEW_REQUEST_PATH,
8167
+ name: REVIEW_REQUEST_NAME,
8168
+ content: legacyContent,
8169
+ source: "local"
8170
+ };
8171
+ }
7784
8172
  ensureAttachmentsGitignore(worktreePath);
7785
8173
  (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(localAbs), { recursive: true });
7786
8174
  (0, import_node_fs19.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
@@ -8260,16 +8648,305 @@ async function importConductorWorkspaceAsync(workspaceId) {
8260
8648
  return importConductorWorkspace(workspaceId);
8261
8649
  }
8262
8650
 
8651
+ // src/threads/stack-layers.ts
8652
+ var import_node_fs21 = require("fs");
8653
+ init_run();
8654
+ init_stack();
8655
+ init_worktree();
8656
+ init_thread_store();
8657
+ init_workspaces();
8658
+ function stackIdFrom(stack) {
8659
+ if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
8660
+ const key = stack.layers.map((l) => l.branchName).join("|");
8661
+ if (!key) return null;
8662
+ return `gh-stack-local-${hashShort(key)}`;
8663
+ }
8664
+ function hashShort(s) {
8665
+ let h = 0;
8666
+ for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
8667
+ return Math.abs(h).toString(36);
8668
+ }
8669
+ function requireThread3(idOrRef) {
8670
+ const thread = findThreadByRef(idOrRef) ?? readThread(idOrRef);
8671
+ if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
8672
+ return thread;
8673
+ }
8674
+ function sanitizeSlugPart(name) {
8675
+ return name.trim().replace(/^refs\/heads\//, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "layer";
8676
+ }
8677
+ function layerSlug(stackId, layer) {
8678
+ const stackPart = stackId.replace(/^gh-stack-/, "s");
8679
+ return sanitizeSlugPart(`${stackPart}-L${layer.position}-${layer.branchName}`);
8680
+ }
8681
+ function findThreadForStackLayer(repoPath, stackId, layer) {
8682
+ const threads = listThreads({ includeArchived: false }).filter(
8683
+ (t) => t.repoPath === repoPath && t.stackId === stackId
8684
+ );
8685
+ const byLayer = threads.find((t) => t.stackLayer === layer.position);
8686
+ if (byLayer) return byLayer;
8687
+ return threads.find((t) => t.branchName === layer.branchName) ?? listThreads({ includeArchived: false }).find(
8688
+ (t) => t.repoPath === repoPath && t.branchName === layer.branchName
8689
+ ) ?? null;
8690
+ }
8691
+ async function openStackLayer(input, _onSetupLine) {
8692
+ await requireAgent(input.agent);
8693
+ const repoPath = await resolveRepoRoot(input.repoPath);
8694
+ const stackId = stackIdFrom(input.stack);
8695
+ if (!stackId) throw new Error("Cannot open stack layer without a stack id");
8696
+ const existing = findThreadForStackLayer(repoPath, stackId, input.layer);
8697
+ if (existing) {
8698
+ const patch = {};
8699
+ if (existing.stackId !== stackId) patch.stackId = stackId;
8700
+ if (existing.stackLayer !== input.layer.position) {
8701
+ patch.stackLayer = input.layer.position;
8702
+ }
8703
+ if (input.layer.prUrl && existing.prUrl !== input.layer.prUrl) {
8704
+ patch.prUrl = input.layer.prUrl;
8705
+ }
8706
+ if (input.layer.title && existing.prTitle !== input.layer.title) {
8707
+ patch.prTitle = input.layer.title;
8708
+ }
8709
+ if (Object.keys(patch).length > 0) updateThread(existing.id, patch);
8710
+ return {
8711
+ thread: readThread(existing.id) ?? existing,
8712
+ createdWorktree: false
8713
+ };
8714
+ }
8715
+ let worktreePath = null;
8716
+ let branchName = input.layer.branchName;
8717
+ let createdWorktree = false;
8718
+ const trees = await listWorktrees(repoPath);
8719
+ const checkedOut = trees.find((w) => w.branch === branchName);
8720
+ if (checkedOut?.path && (0, import_node_fs21.existsSync)(checkedOut.path)) {
8721
+ if (input.reuseExistingWorktree !== false) {
8722
+ worktreePath = checkedOut.path;
8723
+ } else {
8724
+ throw new Error(
8725
+ `Branch ${branchName} is already checked out at ${checkedOut.path}`
8726
+ );
8727
+ }
8728
+ }
8729
+ if (!worktreePath) {
8730
+ const slug = layerSlug(stackId, input.layer);
8731
+ const created = await createExistingBranchWorktree({
8732
+ repoPath,
8733
+ branchName,
8734
+ slug
8735
+ });
8736
+ worktreePath = created.worktreePath;
8737
+ branchName = created.branchName;
8738
+ copyConfiguredFiles(repoPath, worktreePath);
8739
+ createdWorktree = true;
8740
+ }
8741
+ const title = input.layer.title?.trim() || (input.layer.prNumber != null ? `PR #${input.layer.prNumber}` : input.layer.branchName);
8742
+ const thread = createEmptyThread({
8743
+ title,
8744
+ userSetTitle: Boolean(input.layer.title?.trim()),
8745
+ sourceType: input.layer.prNumber != null ? "pr" : "branch",
8746
+ sourceRef: input.layer.prNumber != null ? String(input.layer.prNumber) : input.layer.branchName,
8747
+ branchName,
8748
+ worktreePath,
8749
+ repoPath,
8750
+ agent: input.agent,
8751
+ autonomy: input.autonomy ?? "default",
8752
+ model: input.model ?? null,
8753
+ effort: input.effort ?? "high",
8754
+ fast: Boolean(input.fast),
8755
+ planMode: Boolean(input.planMode),
8756
+ parentThreadId: input.parentThreadId ?? null,
8757
+ status: "idle",
8758
+ prUrl: input.layer.prUrl,
8759
+ prTitle: input.layer.title ?? null,
8760
+ stackId,
8761
+ stackLayer: input.layer.position
8762
+ });
8763
+ writeThread(thread);
8764
+ await ensureWorkspace(repoPath);
8765
+ return { thread: readThread(thread.id) ?? thread, createdWorktree };
8766
+ }
8767
+ async function openPrStackLayers(input, onSetupLine) {
8768
+ const from = requireThread3(input.threadRef);
8769
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
8770
+ throw new Error("Thread has no worktree");
8771
+ }
8772
+ const stack = await getPrStack(from.worktreePath);
8773
+ if (!stack) throw new Error("Current branch is not part of a GitHub PR stack");
8774
+ const layers = input.layer != null ? stack.layers.filter((l) => l.position === input.layer) : stack.layers;
8775
+ if (!layers.length) {
8776
+ throw new Error(
8777
+ input.layer != null ? `No stack layer at position ${input.layer}` : "Stack has no layers"
8778
+ );
8779
+ }
8780
+ const threads = [];
8781
+ const createdThreadIds = [];
8782
+ for (const layer of layers) {
8783
+ const { thread, createdWorktree } = await openStackLayer(
8784
+ {
8785
+ repoPath: from.repoPath,
8786
+ stack,
8787
+ layer,
8788
+ agent: from.agent,
8789
+ autonomy: from.autonomy,
8790
+ model: from.model,
8791
+ effort: from.effort,
8792
+ fast: from.fast,
8793
+ planMode: from.planMode,
8794
+ parentThreadId: from.id
8795
+ },
8796
+ onSetupLine
8797
+ );
8798
+ threads.push(thread);
8799
+ if (createdWorktree) createdThreadIds.push(thread.id);
8800
+ }
8801
+ const stackId = stackIdFrom(stack);
8802
+ const current = stack.layers[stack.currentIndex];
8803
+ if (stackId) {
8804
+ updateThread(from.id, {
8805
+ stackId,
8806
+ stackLayer: current?.position ?? from.stackLayer
8807
+ });
8808
+ }
8809
+ return { stack, threads, createdThreadIds };
8810
+ }
8811
+ async function addStackLayerFromThread(input, onSetupLine) {
8812
+ const from = requireThread3(input.threadRef);
8813
+ if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
8814
+ throw new Error("Thread has no worktree");
8815
+ }
8816
+ const status = await detectGhStack(from.worktreePath);
8817
+ if (!status.available) throw new Error(status.reason);
8818
+ await addPrStackLayer(from.worktreePath, input.branchName);
8819
+ const stack = await getPrStack(from.worktreePath);
8820
+ if (!stack) throw new Error("Stack not found after adding layer");
8821
+ const layer = stack.layers.find((l) => l.branchName === input.branchName.trim()) ?? stack.layers[stack.layers.length - 1];
8822
+ if (!layer) throw new Error("New stack layer not found");
8823
+ if (input.title?.trim()) {
8824
+ layer.title = input.title.trim();
8825
+ }
8826
+ const thread = await openStackLayer(
8827
+ {
8828
+ repoPath: from.repoPath,
8829
+ stack,
8830
+ layer,
8831
+ agent: from.agent,
8832
+ autonomy: from.autonomy,
8833
+ model: from.model,
8834
+ effort: from.effort,
8835
+ fast: from.fast,
8836
+ planMode: from.planMode,
8837
+ parentThreadId: from.id
8838
+ },
8839
+ onSetupLine
8840
+ );
8841
+ return { stack, thread: thread.thread, createdWorktree: thread.createdWorktree };
8842
+ }
8843
+ async function initStackFromThread(input, onSetupLine) {
8844
+ const from = requireThread3(input.threadRef);
8845
+ if (!from.worktreePath?.trim() || !from.branchName?.trim() || !from.repoPath?.trim()) {
8846
+ throw new Error("Thread has no worktree/branch");
8847
+ }
8848
+ const status = await detectGhStack(from.worktreePath);
8849
+ if (!status.available) throw new Error(status.reason);
8850
+ const branches = [
8851
+ from.branchName,
8852
+ ...(input.additionalBranches ?? []).map((b) => b.trim()).filter(Boolean)
8853
+ ];
8854
+ await initPrStack(from.worktreePath, branches, {
8855
+ base: input.base ?? await resolveDefaultBranch(from.repoPath)
8856
+ });
8857
+ return openPrStackLayers({ threadRef: from.id }, onSetupLine);
8858
+ }
8859
+ async function createPrStack(input, onSetupLine) {
8860
+ await requireAgent(input.agent);
8861
+ const repoPath = await resolveRepoRoot(input.repoPath);
8862
+ if (!(0, import_node_fs21.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
8863
+ if (!input.branches.length) throw new Error("At least one branch name required");
8864
+ const status = await detectGhStack(repoPath);
8865
+ if (!status.available) throw new Error(status.reason);
8866
+ const team = allocateTeamSlug(repoPath);
8867
+ const base = input.base ?? await resolveDefaultBranch(repoPath);
8868
+ const bootstrap = await createThreadWorktree({
8869
+ repoPath,
8870
+ sourceRef: base,
8871
+ slug: `${team.slug}-stack-init`
8872
+ });
8873
+ copyConfiguredFiles(repoPath, bootstrap.worktreePath);
8874
+ try {
8875
+ await initPrStack(bootstrap.worktreePath, input.branches, { base });
8876
+ const bottom = input.branches[0];
8877
+ const co = await git(["checkout", bottom], bootstrap.worktreePath, {
8878
+ reject: false
8879
+ });
8880
+ if (co.exitCode !== 0) {
8881
+ throw new Error(
8882
+ co.stderr.trim() || co.stdout.trim() || `Could not check out ${bottom} after stack init`
8883
+ );
8884
+ }
8885
+ } catch (err) {
8886
+ try {
8887
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
8888
+ deleteBranch: bootstrap.branchName
8889
+ });
8890
+ } catch {
8891
+ }
8892
+ throw err;
8893
+ }
8894
+ const stack = await getPrStack(bootstrap.worktreePath);
8895
+ if (!stack) {
8896
+ try {
8897
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
8898
+ deleteBranch: bootstrap.branchName
8899
+ });
8900
+ } catch {
8901
+ }
8902
+ throw new Error("Stack init succeeded but gh stack view returned no stack");
8903
+ }
8904
+ const threads = [];
8905
+ const createdThreadIds = [];
8906
+ for (const layer of stack.layers) {
8907
+ const title = layer.position === 1 && input.title?.trim() ? input.title.trim() : layer.title;
8908
+ const opened = await openStackLayer(
8909
+ {
8910
+ repoPath,
8911
+ stack,
8912
+ layer: title ? { ...layer, title } : layer,
8913
+ agent: input.agent,
8914
+ autonomy: input.autonomy,
8915
+ model: input.model,
8916
+ effort: input.effort,
8917
+ fast: input.fast,
8918
+ planMode: input.planMode,
8919
+ reuseExistingWorktree: true
8920
+ },
8921
+ onSetupLine
8922
+ );
8923
+ threads.push(opened.thread);
8924
+ if (opened.createdWorktree || opened.thread.worktreePath === bootstrap.worktreePath) {
8925
+ createdThreadIds.push(opened.thread.id);
8926
+ }
8927
+ }
8928
+ const claimed = new Set(threads.map((t) => t.worktreePath));
8929
+ if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs21.existsSync)(bootstrap.worktreePath)) {
8930
+ try {
8931
+ await removeWorktree(repoPath, bootstrap.worktreePath, {
8932
+ deleteBranch: bootstrap.branchName
8933
+ });
8934
+ } catch {
8935
+ }
8936
+ }
8937
+ return { stack, threads, createdThreadIds };
8938
+ }
8939
+
8263
8940
  // src/land/land.ts
8264
8941
  init_worktree();
8265
8942
 
8266
8943
  // src/diff/diff.ts
8267
- var import_node_fs21 = require("fs");
8944
+ var import_node_fs22 = require("fs");
8268
8945
  var import_node_path20 = require("path");
8269
8946
  init_run();
8270
8947
  init_worktree();
8271
8948
  async function inspectGitWorktree(worktreePath) {
8272
- if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) return "missing_worktree";
8949
+ if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) return "missing_worktree";
8273
8950
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
8274
8951
  reject: false
8275
8952
  });
@@ -8277,7 +8954,7 @@ async function inspectGitWorktree(worktreePath) {
8277
8954
  return "ok";
8278
8955
  }
8279
8956
  async function initializeGitRepository(worktreePath) {
8280
- if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) {
8957
+ if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) {
8281
8958
  throw new Error("Worktree not found");
8282
8959
  }
8283
8960
  const status = await inspectGitWorktree(worktreePath);
@@ -8573,33 +9250,33 @@ async function getDiff(worktreePath, repoPath, opts) {
8573
9250
  let combinedDiff = "";
8574
9251
  let labelBase = base;
8575
9252
  if (scope === "staged") {
8576
- const [ns, num, diff] = await Promise.all([
9253
+ const [ns, num2, diff] = await Promise.all([
8577
9254
  git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
8578
9255
  git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
8579
9256
  git(["diff", "--cached"], worktreePath, { reject: false })
8580
9257
  ]);
8581
9258
  nameStatus = ns.stdout;
8582
- numstat = num.stdout;
9259
+ numstat = num2.stdout;
8583
9260
  combinedDiff = diff.stdout;
8584
9261
  labelBase = "staged";
8585
9262
  } else if (scope === "unstaged") {
8586
- const [ns, num, diff] = await Promise.all([
9263
+ const [ns, num2, diff] = await Promise.all([
8587
9264
  git(["diff", "--name-status"], worktreePath, { reject: false }),
8588
9265
  git(["diff", "--numstat"], worktreePath, { reject: false }),
8589
9266
  git(["diff"], worktreePath, { reject: false })
8590
9267
  ]);
8591
9268
  nameStatus = ns.stdout;
8592
- numstat = num.stdout;
9269
+ numstat = num2.stdout;
8593
9270
  combinedDiff = diff.stdout;
8594
9271
  labelBase = "unstaged";
8595
9272
  } else if (scope === "uncommitted") {
8596
- const [ns, num, diff] = await Promise.all([
9273
+ const [ns, num2, diff] = await Promise.all([
8597
9274
  git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
8598
9275
  git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
8599
9276
  git(["diff", "HEAD"], worktreePath, { reject: false })
8600
9277
  ]);
8601
9278
  nameStatus = ns.stdout;
8602
- numstat = num.stdout;
9279
+ numstat = num2.stdout;
8603
9280
  combinedDiff = diff.stdout;
8604
9281
  labelBase = "HEAD";
8605
9282
  } else if (scope === "last_turn") {
@@ -8618,34 +9295,34 @@ async function getDiff(worktreePath, repoPath, opts) {
8618
9295
  scopeStats
8619
9296
  };
8620
9297
  }
8621
- const [ns, num, diff] = await Promise.all([
9298
+ const [ns, num2, diff] = await Promise.all([
8622
9299
  git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
8623
9300
  git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
8624
9301
  git(["diff", lastTurnBase], worktreePath, { reject: false })
8625
9302
  ]);
8626
9303
  nameStatus = ns.stdout;
8627
- numstat = num.stdout;
9304
+ numstat = num2.stdout;
8628
9305
  combinedDiff = diff.stdout;
8629
9306
  labelBase = "last turn";
8630
9307
  } else if (scope === "commits" && commitSha) {
8631
9308
  const range = `${commitSha}^!`;
8632
- const [ns, num, diff] = await Promise.all([
9309
+ const [ns, num2, diff] = await Promise.all([
8633
9310
  git(["diff", "--name-status", range], worktreePath, { reject: false }),
8634
9311
  git(["diff", "--numstat", range], worktreePath, { reject: false }),
8635
9312
  git(["diff", range], worktreePath, { reject: false })
8636
9313
  ]);
8637
9314
  nameStatus = ns.stdout;
8638
- numstat = num.stdout;
9315
+ numstat = num2.stdout;
8639
9316
  combinedDiff = diff.stdout;
8640
9317
  labelBase = commitSha.slice(0, 7);
8641
9318
  } else {
8642
- const [ns, num, diff] = await Promise.all([
9319
+ const [ns, num2, diff] = await Promise.all([
8643
9320
  git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
8644
9321
  git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
8645
9322
  git(["diff", mergeBase], worktreePath, { reject: false })
8646
9323
  ]);
8647
9324
  nameStatus = ns.stdout;
8648
- numstat = num.stdout;
9325
+ numstat = num2.stdout;
8649
9326
  combinedDiff = diff.stdout;
8650
9327
  labelBase = base;
8651
9328
  }
@@ -8715,7 +9392,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
8715
9392
  assertSafeRelativePath(relativePath);
8716
9393
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
8717
9394
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8718
- const st = (0, import_node_fs21.statSync)(abs);
9395
+ const st = (0, import_node_fs22.statSync)(abs);
8719
9396
  if (!st.isFile()) {
8720
9397
  throw new Error(`Not a file: ${relativePath}`);
8721
9398
  }
@@ -8724,7 +9401,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
8724
9401
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
8725
9402
  );
8726
9403
  }
8727
- const buf = (0, import_node_fs21.readFileSync)(abs);
9404
+ const buf = (0, import_node_fs22.readFileSync)(abs);
8728
9405
  return {
8729
9406
  path: relativePath,
8730
9407
  contentBase64: buf.toString("base64"),
@@ -8735,11 +9412,11 @@ function readWorktreeFile(worktreePath, relativePath, opts) {
8735
9412
  assertSafeRelativePath(relativePath);
8736
9413
  const maxBytes = opts?.maxBytes ?? 2e5;
8737
9414
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8738
- const st = (0, import_node_fs21.statSync)(abs);
9415
+ const st = (0, import_node_fs22.statSync)(abs);
8739
9416
  if (!st.isFile()) {
8740
9417
  throw new Error(`Not a file: ${relativePath}`);
8741
9418
  }
8742
- const buf = (0, import_node_fs21.readFileSync)(abs);
9419
+ const buf = (0, import_node_fs22.readFileSync)(abs);
8743
9420
  if (isImageRelativePath(relativePath)) {
8744
9421
  const maxImageBytes = Math.max(maxBytes, 15e6);
8745
9422
  const truncated2 = buf.length > maxImageBytes;
@@ -8783,8 +9460,8 @@ function assertSafeRelativePath(relativePath) {
8783
9460
  function writeWorktreeFile(worktreePath, relativePath, content) {
8784
9461
  assertSafeRelativePath(relativePath);
8785
9462
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8786
- (0, import_node_fs21.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
8787
- (0, import_node_fs21.writeFileSync)(abs, content, "utf8");
9463
+ (0, import_node_fs22.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
9464
+ (0, import_node_fs22.writeFileSync)(abs, content, "utf8");
8788
9465
  return { path: relativePath };
8789
9466
  }
8790
9467
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -8963,7 +9640,7 @@ async function confirmLand(thread, opts) {
8963
9640
  }
8964
9641
 
8965
9642
  // src/skills/discover.ts
8966
- var import_node_fs22 = require("fs");
9643
+ var import_node_fs23 = require("fs");
8967
9644
  var import_node_os9 = require("os");
8968
9645
  var import_node_path21 = require("path");
8969
9646
  function toCommand(name) {
@@ -8997,7 +9674,7 @@ function parseFrontmatter(content) {
8997
9674
  }
8998
9675
  function readSkill(skillMd, source) {
8999
9676
  try {
9000
- const content = (0, import_node_fs22.readFileSync)(skillMd, "utf8");
9677
+ const content = (0, import_node_fs23.readFileSync)(skillMd, "utf8");
9001
9678
  const { name: fmName, description } = parseFrontmatter(content);
9002
9679
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
9003
9680
  const name = fmName || dirName;
@@ -9016,19 +9693,19 @@ function readSkill(skillMd, source) {
9016
9693
  }
9017
9694
  }
9018
9695
  function scanSkillsDir(dir, source, out) {
9019
- if (!(0, import_node_fs22.existsSync)(dir)) return;
9696
+ if (!(0, import_node_fs23.existsSync)(dir)) return;
9020
9697
  let entries;
9021
9698
  try {
9022
- entries = (0, import_node_fs22.readdirSync)(dir);
9699
+ entries = (0, import_node_fs23.readdirSync)(dir);
9023
9700
  } catch {
9024
9701
  return;
9025
9702
  }
9026
9703
  for (const entry of entries) {
9027
9704
  if (entry.startsWith(".")) continue;
9028
9705
  const skillMd = (0, import_node_path21.join)(dir, entry, "SKILL.md");
9029
- if (!(0, import_node_fs22.existsSync)(skillMd)) continue;
9706
+ if (!(0, import_node_fs23.existsSync)(skillMd)) continue;
9030
9707
  try {
9031
- if (!(0, import_node_fs22.statSync)(skillMd).isFile()) continue;
9708
+ if (!(0, import_node_fs23.statSync)(skillMd).isFile()) continue;
9032
9709
  } catch {
9033
9710
  continue;
9034
9711
  }
@@ -9037,12 +9714,12 @@ function scanSkillsDir(dir, source, out) {
9037
9714
  }
9038
9715
  }
9039
9716
  function scanClaudePluginSkills(pluginsRoot, out) {
9040
- if (!(0, import_node_fs22.existsSync)(pluginsRoot)) return;
9717
+ if (!(0, import_node_fs23.existsSync)(pluginsRoot)) return;
9041
9718
  const walk = (dir, depth, lookingForSkillsDir) => {
9042
9719
  if (depth > 7) return;
9043
9720
  let entries;
9044
9721
  try {
9045
- entries = (0, import_node_fs22.readdirSync)(dir);
9722
+ entries = (0, import_node_fs23.readdirSync)(dir);
9046
9723
  } catch {
9047
9724
  return;
9048
9725
  }
@@ -9054,7 +9731,7 @@ function scanClaudePluginSkills(pluginsRoot, out) {
9054
9731
  if (entry === "node_modules" || entry === ".git") continue;
9055
9732
  const full = (0, import_node_path21.join)(dir, entry);
9056
9733
  try {
9057
- if (!(0, import_node_fs22.statSync)(full).isDirectory()) continue;
9734
+ if (!(0, import_node_fs23.statSync)(full).isDirectory()) continue;
9058
9735
  } catch {
9059
9736
  continue;
9060
9737
  }
@@ -9094,7 +9771,7 @@ function discoverSkills(worktreePath) {
9094
9771
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
9095
9772
  }
9096
9773
  function readSkillBody(skillPath, maxChars = 12e3) {
9097
- const raw = (0, import_node_fs22.readFileSync)(skillPath, "utf8");
9774
+ const raw = (0, import_node_fs23.readFileSync)(skillPath, "utf8");
9098
9775
  if (raw.startsWith("---")) {
9099
9776
  const end = raw.indexOf("\n---", 3);
9100
9777
  if (end >= 0) {
@@ -9187,9 +9864,10 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
9187
9864
  }
9188
9865
 
9189
9866
  // src/composer/stage-files.ts
9190
- var import_node_fs23 = require("fs");
9867
+ var import_node_fs24 = require("fs");
9191
9868
  var import_node_path22 = require("path");
9192
9869
  var import_node_crypto5 = require("crypto");
9870
+ init_workspace_scratch();
9193
9871
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
9194
9872
  "png",
9195
9873
  "jpg",
@@ -9210,11 +9888,6 @@ var IMAGE_MIME_BY_EXT = {
9210
9888
  bmp: "image/bmp",
9211
9889
  ico: "image/x-icon"
9212
9890
  };
9213
- var ATTACHMENTS_DIR = ".sideboard/attachments";
9214
- var ATTACHMENTS_GITIGNORE2 = `# Sideboard review / composer attachments (local only)
9215
- *
9216
- !.gitignore
9217
- `;
9218
9891
  var MAX_INLINE_BYTES = 4e5;
9219
9892
  var MAX_PREVIEW_BYTES = 5e6;
9220
9893
  function fileExtension(filePath) {
@@ -9229,21 +9902,21 @@ function imageMimeType(filePath) {
9229
9902
  }
9230
9903
  function ensureAttachmentsDir(worktreePath) {
9231
9904
  const dir = (0, import_node_path22.join)(worktreePath, ATTACHMENTS_DIR);
9232
- (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
9905
+ (0, import_node_fs24.mkdirSync)(dir, { recursive: true });
9233
9906
  const gi = (0, import_node_path22.join)(dir, ".gitignore");
9234
- if (!(0, import_node_fs23.existsSync)(gi)) {
9235
- (0, import_node_fs23.writeFileSync)(gi, ATTACHMENTS_GITIGNORE2, "utf8");
9907
+ if (!(0, import_node_fs24.existsSync)(gi)) {
9908
+ (0, import_node_fs24.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
9236
9909
  }
9237
9910
  return dir;
9238
9911
  }
9239
9912
  function uniqueAttachmentName(dir, originalName) {
9240
9913
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
9241
- if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
9914
+ if (!(0, import_node_fs24.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
9242
9915
  const ext = (0, import_node_path22.extname)(safe);
9243
9916
  const stem = ext ? safe.slice(0, -ext.length) : safe;
9244
9917
  for (let i = 1; i < 1e4; i++) {
9245
9918
  const candidate = `${stem}-${i}${ext}`;
9246
- if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
9919
+ if (!(0, import_node_fs24.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
9247
9920
  }
9248
9921
  return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
9249
9922
  }
@@ -9301,13 +9974,13 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
9301
9974
  for (const abs of absolutePaths) {
9302
9975
  const originalName = (0, import_node_path22.basename)(abs);
9303
9976
  try {
9304
- const st = (0, import_node_fs23.statSync)(abs);
9977
+ const st = (0, import_node_fs24.statSync)(abs);
9305
9978
  if (!st.isFile()) continue;
9306
9979
  const name = uniqueAttachmentName(dir, originalName);
9307
9980
  const destAbs = (0, import_node_path22.join)(dir, name);
9308
- (0, import_node_fs23.copyFileSync)(abs, destAbs);
9981
+ (0, import_node_fs24.copyFileSync)(abs, destAbs);
9309
9982
  const rel = `${ATTACHMENTS_DIR}/${name}`;
9310
- const buf = (0, import_node_fs23.readFileSync)(destAbs);
9983
+ const buf = (0, import_node_fs24.readFileSync)(destAbs);
9311
9984
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
9312
9985
  } catch (err) {
9313
9986
  out.push({
@@ -9330,7 +10003,7 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
9330
10003
  const buf = Buffer.from(item.dataBase64, "base64");
9331
10004
  const name = uniqueAttachmentName(dir, originalName);
9332
10005
  const destAbs = (0, import_node_path22.join)(dir, name);
9333
- (0, import_node_fs23.writeFileSync)(destAbs, buf);
10006
+ (0, import_node_fs24.writeFileSync)(destAbs, buf);
9334
10007
  const rel = `${ATTACHMENTS_DIR}/${name}`;
9335
10008
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
9336
10009
  } catch (err) {
@@ -9359,9 +10032,9 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
9359
10032
  const name = (0, import_node_path22.basename)(rel);
9360
10033
  try {
9361
10034
  const abs = (0, import_node_path22.join)(worktreePath, rel);
9362
- const st = (0, import_node_fs23.statSync)(abs);
10035
+ const st = (0, import_node_fs24.statSync)(abs);
9363
10036
  if (!st.isFile()) continue;
9364
- const buf = (0, import_node_fs23.readFileSync)(abs);
10037
+ const buf = (0, import_node_fs24.readFileSync)(abs);
9365
10038
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
9366
10039
  } catch (err) {
9367
10040
  out.push({
@@ -9376,7 +10049,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
9376
10049
  }
9377
10050
 
9378
10051
  // src/agents/instructions.ts
9379
- var import_node_fs24 = require("fs");
10052
+ var import_node_fs25 = require("fs");
9380
10053
  var import_node_path23 = require("path");
9381
10054
  init_worktree_labels();
9382
10055
  function normPath2(p) {
@@ -9517,10 +10190,10 @@ function loadAgentInstructions(worktreePath, agent) {
9517
10190
  for (const rel of candidates) {
9518
10191
  if (seen.has(rel)) continue;
9519
10192
  const abs = (0, import_node_path23.join)(worktreePath, rel);
9520
- if (!(0, import_node_fs24.existsSync)(abs)) continue;
10193
+ if (!(0, import_node_fs25.existsSync)(abs)) continue;
9521
10194
  try {
9522
- if (!(0, import_node_fs24.statSync)(abs).isFile()) continue;
9523
- let content = (0, import_node_fs24.readFileSync)(abs, "utf8");
10195
+ if (!(0, import_node_fs25.statSync)(abs).isFile()) continue;
10196
+ let content = (0, import_node_fs25.readFileSync)(abs, "utf8");
9524
10197
  if (!content.trim()) continue;
9525
10198
  if (content.length > MAX_CHARS_PER_FILE) {
9526
10199
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -9553,6 +10226,7 @@ function formatAgentInstructions(files) {
9553
10226
 
9554
10227
  // src/orchestrator/orchestrator.ts
9555
10228
  init_types();
10229
+ init_plan_file();
9556
10230
  init_settings();
9557
10231
 
9558
10232
  // src/threads/sync-branch.ts
@@ -9672,7 +10346,7 @@ var Orchestrator = class {
9672
10346
  }
9673
10347
  continue;
9674
10348
  }
9675
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
10349
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
9676
10350
  setStatus(thread.id, "broken", "Worktree missing on disk");
9677
10351
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
9678
10352
  continue;
@@ -9817,26 +10491,43 @@ var Orchestrator = class {
9817
10491
  return findThreadByRef(idOrRef) ?? readThread(idOrRef);
9818
10492
  }
9819
10493
  async createThread(input) {
9820
- let thread = await createThread(input, (line) => {
9821
- this.emit({
9822
- type: "turn_output",
9823
- threadId: "pending",
9824
- event: { type: "stdout", data: line }
9825
- });
9826
- });
10494
+ const thread = await createThread(input);
9827
10495
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
10496
+ void this.finishCreateThread(thread.id, input.prompt?.trim() || void 0);
10497
+ return thread;
10498
+ }
10499
+ async finishCreateThread(threadId, prompt) {
10500
+ await this.runSetupAfterCreate(threadId);
9828
10501
  const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
9829
10502
  if (autoRunAfterSetupEnabled2()) {
9830
10503
  try {
9831
- await this.startDev(thread.id);
10504
+ await this.startDev(threadId);
9832
10505
  } catch {
9833
10506
  }
9834
10507
  }
9835
- const prompt = input.prompt?.trim();
9836
10508
  if (prompt) {
9837
- thread = await this.send(thread.id, prompt);
10509
+ try {
10510
+ await this.send(threadId, prompt);
10511
+ } catch (err) {
10512
+ const message = err instanceof Error ? err.message : String(err);
10513
+ updateThread(threadId, {
10514
+ lastError: `First prompt failed: ${message}`
10515
+ });
10516
+ }
10517
+ }
10518
+ }
10519
+ /** Run workspace setup after a new worktree is created (no-op if none configured). */
10520
+ async runSetupAfterCreate(threadId) {
10521
+ try {
10522
+ await this.runSetup(threadId);
10523
+ } catch (err) {
10524
+ const message = err instanceof Error ? err.message : String(err);
10525
+ if (/no setup script/i.test(message)) return;
10526
+ if (/already running/i.test(message)) return;
10527
+ updateThread(threadId, {
10528
+ lastError: `Setup failed: ${message}`
10529
+ });
9838
10530
  }
9839
- return thread;
9840
10531
  }
9841
10532
  listWorkspaces() {
9842
10533
  const fromThreads = listThreads({ includeArchived: false }).map((t) => t.repoPath);
@@ -10169,8 +10860,73 @@ var Orchestrator = class {
10169
10860
  }
10170
10861
  }
10171
10862
  }
10172
- const lastStderr = summarizeTurnStderr(stderrTail);
10173
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10863
+ let lastStderr = summarizeTurnStderr(stderrTail);
10864
+ let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10865
+ 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") {
10866
+ updateThread(threadId, { sessionId: null });
10867
+ pushTurnStderr(
10868
+ stderrTail,
10869
+ "Agent session missing \u2014 starting a fresh session"
10870
+ );
10871
+ this.emit({
10872
+ type: "turn_output",
10873
+ threadId,
10874
+ event: {
10875
+ type: "stderr",
10876
+ data: "Agent session missing \u2014 starting a fresh session"
10877
+ }
10878
+ });
10879
+ const retryThread = this.requireThread(threadId);
10880
+ const prior = retryThread.messages.slice(0, -1);
10881
+ const retrySeed = buildSessionSeed(prior);
10882
+ const retryInstructions = retryThread.agent === "claude" ? null : formatAgentInstructions(
10883
+ loadAgentInstructions(retryThread.worktreePath, retryThread.agent)
10884
+ );
10885
+ const retryPrefix = [
10886
+ coordinatorDirective,
10887
+ worktreeDirective,
10888
+ artifactDirective,
10889
+ renameBranchDirective,
10890
+ retryInstructions,
10891
+ retrySeed
10892
+ ].filter(Boolean).join("\n\n---\n\n");
10893
+ const retryHandle = await spawnAgentTurn(
10894
+ retryThread,
10895
+ { cachedPrefix: retryPrefix, prompt: agentPrompt },
10896
+ (event) => {
10897
+ this.emit({ type: "turn_output", threadId, event });
10898
+ if (event.type === "session_id") {
10899
+ updateThread(threadId, { sessionId: event.data });
10900
+ }
10901
+ if (event.type === "stderr" && typeof event.data === "string") {
10902
+ pushTurnStderr(stderrTail, event.data);
10903
+ }
10904
+ }
10905
+ );
10906
+ this.activeTurns.set(threadId, retryHandle);
10907
+ if (typeof retryHandle.pid === "number" && retryHandle.pid > 0) {
10908
+ updateThread(threadId, { agentPid: retryHandle.pid });
10909
+ }
10910
+ this.processes.set(`${threadId}:agent`, {
10911
+ kind: "agent",
10912
+ pid: retryHandle.pid,
10913
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
10914
+ kill: retryHandle.kill
10915
+ });
10916
+ if (this.stoppedTurns.has(threadId)) {
10917
+ retryHandle.kill();
10918
+ }
10919
+ const retryResult = await retryHandle.done;
10920
+ if (retryResult.sessionId) {
10921
+ updateThread(threadId, { sessionId: retryResult.sessionId });
10922
+ }
10923
+ assistantText = retryResult.assistantText.trim();
10924
+ parts = retryResult.parts;
10925
+ usage = retryResult.usage ?? void 0;
10926
+ exitCode = retryResult.exitCode;
10927
+ lastStderr = summarizeTurnStderr(stderrTail);
10928
+ detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10929
+ }
10174
10930
  let chatText = assistantText;
10175
10931
  if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
10176
10932
  chatText = humanizeAgentFailDetail(detail);
@@ -10186,6 +10942,19 @@ var Orchestrator = class {
10186
10942
  });
10187
10943
  }
10188
10944
  const afterTurn = this.requireThread(threadId);
10945
+ if (afterTurn.planMode && afterTurn.worktreePath?.trim()) {
10946
+ const presented = extractPresentedPlan(parts);
10947
+ const exited = parts.some(
10948
+ (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
10949
+ );
10950
+ if (presented?.content) {
10951
+ writePlanFile(afterTurn.worktreePath, presented.content);
10952
+ } else if (exited || chatText && chatText.trim().length >= 400) {
10953
+ if (!readPlanFile(afterTurn.worktreePath) && chatText?.trim()) {
10954
+ writePlanFile(afterTurn.worktreePath, chatText.trim());
10955
+ }
10956
+ }
10957
+ }
10189
10958
  if (afterTurn.planMode && afterTurn.agent === "claude" && parts.some(
10190
10959
  (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
10191
10960
  )) {
@@ -10667,6 +11436,82 @@ var Orchestrator = class {
10667
11436
  }
10668
11437
  return meta;
10669
11438
  }
11439
+ async getPrStack(threadRef) {
11440
+ const thread = this.requireThread(threadRef);
11441
+ if (!thread.worktreePath?.trim()) return null;
11442
+ const stack = await getPrStack(thread.worktreePath);
11443
+ if (!stack) return null;
11444
+ const current = stack.currentIndex >= 0 ? stack.layers[stack.currentIndex] : null;
11445
+ const patch = {};
11446
+ if (stack.stackNumber != null) {
11447
+ const id = `gh-stack-${stack.stackNumber}`;
11448
+ if (thread.stackId !== id) patch.stackId = id;
11449
+ }
11450
+ if (current?.position != null && thread.stackLayer !== current.position) {
11451
+ patch.stackLayer = current.position;
11452
+ }
11453
+ if (current?.prUrl && current.prUrl !== thread.prUrl) patch.prUrl = current.prUrl;
11454
+ if (current?.title && current.title !== thread.prTitle) patch.prTitle = current.title;
11455
+ if (current?.branchName && current.branchName !== thread.branchName) {
11456
+ patch.branchName = current.branchName;
11457
+ }
11458
+ if (Object.keys(patch).length > 0) updateThread(thread.id, patch);
11459
+ return stack;
11460
+ }
11461
+ /** Open worktrees for all (or one) stack layers discovered from a thread. */
11462
+ async openPrStackLayers(threadRef, opts) {
11463
+ const result = await openPrStackLayers({ threadRef, layer: opts?.layer });
11464
+ for (const t of result.threads) {
11465
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
11466
+ }
11467
+ for (const id of result.createdThreadIds) {
11468
+ await this.runSetupAfterCreate(id);
11469
+ }
11470
+ return { stack: result.stack, threads: result.threads };
11471
+ }
11472
+ /** Add a branch on top of the thread's stack and open its worktree. */
11473
+ async addStackLayer(threadRef, branchName, opts) {
11474
+ const result = await addStackLayerFromThread({
11475
+ threadRef,
11476
+ branchName,
11477
+ title: opts?.title
11478
+ });
11479
+ this.emit({
11480
+ type: "status_changed",
11481
+ threadId: result.thread.id,
11482
+ status: result.thread.status
11483
+ });
11484
+ if (result.createdWorktree) {
11485
+ await this.runSetupAfterCreate(result.thread.id);
11486
+ }
11487
+ return { stack: result.stack, thread: result.thread };
11488
+ }
11489
+ /** Initialize a stack from the current thread branch (optional extra layers). */
11490
+ async initStackFromThread(threadRef, opts) {
11491
+ const result = await initStackFromThread({
11492
+ threadRef,
11493
+ additionalBranches: opts?.additionalBranches,
11494
+ base: opts?.base
11495
+ });
11496
+ for (const t of result.threads) {
11497
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
11498
+ }
11499
+ for (const id of result.createdThreadIds) {
11500
+ await this.runSetupAfterCreate(id);
11501
+ }
11502
+ return { stack: result.stack, threads: result.threads };
11503
+ }
11504
+ /** Create a new multi-layer stack with one worktree per layer. */
11505
+ async createPrStack(input) {
11506
+ const result = await createPrStack(input);
11507
+ for (const t of result.threads) {
11508
+ this.emit({ type: "status_changed", threadId: t.id, status: t.status });
11509
+ }
11510
+ for (const id of result.createdThreadIds) {
11511
+ await this.runSetupAfterCreate(id);
11512
+ }
11513
+ return { stack: result.stack, threads: result.threads };
11514
+ }
10670
11515
  async getPrDetails(threadRef) {
10671
11516
  const { thread, selector, cwd } = await this.withPrSelector(threadRef);
10672
11517
  if (!selector) return null;
@@ -10729,14 +11574,9 @@ var Orchestrator = class {
10729
11574
  return forkChatTab(input);
10730
11575
  }
10731
11576
  async forkThreadWorktree(input) {
10732
- const thread = await forkThreadWorktree(input, (line) => {
10733
- this.emit({
10734
- type: "turn_output",
10735
- threadId: "pending",
10736
- event: { type: "stdout", data: line }
10737
- });
10738
- });
11577
+ const thread = await forkThreadWorktree(input);
10739
11578
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
11579
+ await this.runSetupAfterCreate(thread.id);
10740
11580
  return thread;
10741
11581
  }
10742
11582
  renameThread(threadRef, title) {
@@ -10750,7 +11590,7 @@ var Orchestrator = class {
10750
11590
  }
10751
11591
  /**
10752
11592
  * Stage OS / worktree files into composer attachments (copies external files
10753
- * into `.sideboard/attachments/` so agents can Read images and binaries).
11593
+ * into `.context/attachments/` so agents can Read images and binaries).
10754
11594
  */
10755
11595
  attachComposerFiles(threadRef, opts) {
10756
11596
  const thread = this.requireThread(threadRef);
@@ -10824,7 +11664,7 @@ var Orchestrator = class {
10824
11664
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
10825
11665
  return setStatus(thread.id, "idle");
10826
11666
  }
10827
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
11667
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
10828
11668
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
10829
11669
  const { execa: execa7 } = await import("execa");
10830
11670
  const slug = thread.worktreePath.split("/").pop();
@@ -11043,7 +11883,7 @@ async function startMcpServer() {
11043
11883
  async () => {
11044
11884
  const threads = orch.getThreads(true);
11045
11885
  const lines = threads.map((t) => {
11046
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path25.basename)(t.repoPath) || t.repoPath;
11886
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path26.basename)(t.repoPath) || t.repoPath;
11047
11887
  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}` : ""}`;
11048
11888
  });
11049
11889
  return {
@@ -11105,6 +11945,59 @@ async function startMcpServer() {
11105
11945
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
11106
11946
  }
11107
11947
  );
11948
+ server.tool(
11949
+ "ask_user",
11950
+ "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.",
11951
+ {
11952
+ questions: import_zod.z.array(
11953
+ import_zod.z.object({
11954
+ question: import_zod.z.string().describe("Full question text ending with ?"),
11955
+ header: import_zod.z.string().max(24).optional().describe("Short label shown above the question"),
11956
+ multiSelect: import_zod.z.boolean().optional().describe("Allow selecting multiple options"),
11957
+ options: import_zod.z.array(
11958
+ import_zod.z.object({
11959
+ label: import_zod.z.string(),
11960
+ description: import_zod.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
11961
+ })
11962
+ ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
11963
+ })
11964
+ ).min(1).max(4).describe("1\u20134 questions")
11965
+ },
11966
+ async ({ questions }) => {
11967
+ const payload = {
11968
+ ok: true,
11969
+ questions,
11970
+ message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
11971
+ };
11972
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
11973
+ }
11974
+ );
11975
+ server.tool(
11976
+ "present_plan",
11977
+ "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.",
11978
+ {
11979
+ title: import_zod.z.string().optional().describe("Short plan title (defaults to Plan)"),
11980
+ content: import_zod.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
11981
+ thread_id: import_zod.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
11982
+ },
11983
+ async ({ title, content, thread_id }) => {
11984
+ const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
11985
+ let root = process.cwd();
11986
+ if (thread_id?.trim()) {
11987
+ const t = orch.getThread(thread_id.trim());
11988
+ if (t?.worktreePath?.trim()) root = t.worktreePath;
11989
+ }
11990
+ const path = writePlanFile2(root, content);
11991
+ const payload = {
11992
+ ok: true,
11993
+ path,
11994
+ title: title?.trim() || "Plan",
11995
+ content,
11996
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
11997
+ };
11998
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
11999
+ }
12000
+ );
11108
12001
  server.tool(
11109
12002
  "present_schema",
11110
12003
  "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.",
@@ -11683,6 +12576,126 @@ async function startMcpServer() {
11683
12576
  };
11684
12577
  }
11685
12578
  );
12579
+ server.tool(
12580
+ "get_pr_stack",
12581
+ "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.",
12582
+ { ref: import_zod.z.string() },
12583
+ async ({ ref }) => {
12584
+ const stack = await orch.getPrStack(ref);
12585
+ return {
12586
+ content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
12587
+ };
12588
+ }
12589
+ );
12590
+ server.tool(
12591
+ "open_pr_stack_layers",
12592
+ "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
12593
+ {
12594
+ ref: import_zod.z.string(),
12595
+ layer: import_zod.z.number().int().positive().optional()
12596
+ },
12597
+ async ({ ref, layer }) => {
12598
+ const result = await orch.openPrStackLayers(ref, { layer });
12599
+ return {
12600
+ content: [
12601
+ {
12602
+ type: "text",
12603
+ text: JSON.stringify(
12604
+ {
12605
+ stackNumber: result.stack.stackNumber,
12606
+ trunk: result.stack.trunk,
12607
+ threads: result.threads.map((t) => ({
12608
+ id: t.id,
12609
+ title: t.title,
12610
+ branchName: t.branchName,
12611
+ stackLayer: t.stackLayer,
12612
+ worktreePath: t.worktreePath,
12613
+ prUrl: t.prUrl,
12614
+ link: `sideboard://thread/${t.id}`
12615
+ }))
12616
+ },
12617
+ null,
12618
+ 2
12619
+ )
12620
+ }
12621
+ ]
12622
+ };
12623
+ }
12624
+ );
12625
+ server.tool(
12626
+ "add_stack_layer",
12627
+ "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
12628
+ {
12629
+ ref: import_zod.z.string(),
12630
+ branchName: import_zod.z.string(),
12631
+ title: import_zod.z.string().optional()
12632
+ },
12633
+ async ({ ref, branchName, title }) => {
12634
+ const result = await orch.addStackLayer(ref, branchName, { title });
12635
+ return {
12636
+ content: [
12637
+ {
12638
+ type: "text",
12639
+ text: JSON.stringify(
12640
+ {
12641
+ id: result.thread.id,
12642
+ title: result.thread.title,
12643
+ branchName: result.thread.branchName,
12644
+ stackLayer: result.thread.stackLayer,
12645
+ worktreePath: result.thread.worktreePath,
12646
+ link: `sideboard://thread/${result.thread.id}`
12647
+ },
12648
+ null,
12649
+ 2
12650
+ )
12651
+ }
12652
+ ]
12653
+ };
12654
+ }
12655
+ );
12656
+ server.tool(
12657
+ "create_pr_stack",
12658
+ "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
12659
+ {
12660
+ repoPath: import_zod.z.string(),
12661
+ branches: import_zod.z.array(import_zod.z.string()).min(1),
12662
+ agent: import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
12663
+ base: import_zod.z.string().optional(),
12664
+ title: import_zod.z.string().optional()
12665
+ },
12666
+ async (args) => {
12667
+ const result = await orch.createPrStack({
12668
+ repoPath: args.repoPath,
12669
+ branches: args.branches,
12670
+ agent: args.agent,
12671
+ base: args.base,
12672
+ title: args.title
12673
+ });
12674
+ return {
12675
+ content: [
12676
+ {
12677
+ type: "text",
12678
+ text: JSON.stringify(
12679
+ {
12680
+ stackNumber: result.stack.stackNumber,
12681
+ trunk: result.stack.trunk,
12682
+ threads: result.threads.map((t) => ({
12683
+ id: t.id,
12684
+ title: t.title,
12685
+ branchName: t.branchName,
12686
+ stackLayer: t.stackLayer,
12687
+ worktreePath: t.worktreePath,
12688
+ link: `sideboard://thread/${t.id}`
12689
+ }))
12690
+ },
12691
+ null,
12692
+ 2
12693
+ )
12694
+ }
12695
+ ]
12696
+ };
12697
+ }
12698
+ );
11686
12699
  server.tool(
11687
12700
  "list_issues",
11688
12701
  "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.",