@sideboard-ai/core 0.1.51 → 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 (37) hide show
  1. package/dist/{agents-HIEJL3UV.js → agents-ON6RKKND.js} +1 -1
  2. package/dist/{agents-5ROTZNCX.js → agents-YKSS6VBO.js} +1 -1
  3. package/dist/{chunk-7EUWSBWR.js → chunk-6QTZVJ7A.js} +5 -3
  4. package/dist/chunk-B3SJXYIJ.js +24 -0
  5. package/dist/{chunk-5ZPSH7VI.js → chunk-D3METLRW.js} +5 -3
  6. package/dist/{chunk-ZH5QZ4CR.js → chunk-DZFH2KLT.js} +280 -7
  7. package/dist/chunk-FKOIHGKV.js +21 -0
  8. package/dist/{chunk-E4VVEKAM.js → chunk-GNML24AW.js} +2 -2
  9. package/dist/{chunk-EOYDCKQC.js → chunk-HLEX5AQ6.js} +4 -0
  10. package/dist/{chunk-K3WMKGFY.js → chunk-LRLKJM3O.js} +2 -1
  11. package/dist/chunk-N5PM7HGQ.js +103 -0
  12. package/dist/chunk-QTUESPAW.js +101 -0
  13. package/dist/{chunk-O6W3P7V3.js → chunk-TSRXOSVD.js} +4 -0
  14. package/dist/{chunk-F4Q3IM6V.js → chunk-UEAHMGHW.js} +2 -1
  15. package/dist/{chunk-XRSAGVRW.js → chunk-XOU6HNQJ.js} +245 -7
  16. package/dist/{chunk-O5DOO7DP.js → chunk-XX5BB7NV.js} +3 -3
  17. package/dist/{chunk-QN7XNQAT.js → chunk-YDXQ72MD.js} +2 -2
  18. package/dist/{chunk-YFJ4FG2P.js → chunk-YOWIYAVA.js} +3 -3
  19. package/dist/{coordinator-prompt-7HHJRO7B.js → coordinator-prompt-6FXVTSFN.js} +4 -3
  20. package/dist/{coordinator-prompt-WD7FAMA2.js → coordinator-prompt-S6JZD5EF.js} +4 -3
  21. package/dist/{global-workspace-OJEPGDXA.js → global-workspace-EV4G2WMQ.js} +5 -4
  22. package/dist/{global-workspace-ECYN2MKL.js → global-workspace-MSX2K27Y.js} +5 -4
  23. package/dist/index.cjs +1288 -140
  24. package/dist/index.d.cts +388 -15
  25. package/dist/index.d.ts +388 -15
  26. package/dist/index.js +801 -84
  27. package/dist/mcp/run-stdio.cjs +1059 -132
  28. package/dist/mcp/run-stdio.js +628 -73
  29. package/dist/plan-file-6O7G4VPQ.js +23 -0
  30. package/dist/plan-file-PHVKUAEE.js +25 -0
  31. package/dist/{thread-store-XICUWFNM.js → thread-store-GHOADGL2.js} +1 -1
  32. package/dist/{thread-store-OV2X6PYO.js → thread-store-UJIGMI5J.js} +1 -1
  33. package/dist/{workspaces-MUU7RGVV.js → workspaces-3RQQZQRO.js} +6 -5
  34. package/dist/{workspaces-ZWOOFZUV.js → workspaces-AYTBR6KQ.js} +6 -5
  35. package/dist/{worktree-DVNDMWZ7.js → worktree-5KEQWSAF.js} +5 -2
  36. package/dist/{worktree-GDV56MX4.js → worktree-RWGL7FUV.js} +5 -2
  37. package/package.json +1 -1
@@ -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 ?? [],
@@ -1688,6 +1692,206 @@ var init_pr_gates = __esm({
1688
1692
  }
1689
1693
  });
1690
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
+
1691
1895
  // src/git/worktree.ts
1692
1896
  var worktree_exports = {};
1693
1897
  __export(worktree_exports, {
@@ -1697,6 +1901,7 @@ __export(worktree_exports, {
1697
1901
  branchDisplayLabel: () => branchDisplayLabel,
1698
1902
  collectTakenTeamSlugs: () => collectTakenTeamSlugs,
1699
1903
  commitAll: () => commitAll,
1904
+ createExistingBranchWorktree: () => createExistingBranchWorktree,
1700
1905
  createOrUpdatePr: () => createOrUpdatePr,
1701
1906
  createThreadWorktree: () => createThreadWorktree,
1702
1907
  currentBranch: () => currentBranch,
@@ -2410,6 +2615,52 @@ ${add.stdout}`;
2410
2615
  await ensureGhPreferOrigin(worktreePath);
2411
2616
  return { branchName, worktreePath };
2412
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
+ }
2413
2664
  async function removeWorktree(repoPath, worktreePath, opts) {
2414
2665
  await git(["worktree", "remove", "--force", worktreePath], repoPath, {
2415
2666
  reject: false
@@ -2449,8 +2700,7 @@ async function isDirty(worktreePath) {
2449
2700
  return false;
2450
2701
  }
2451
2702
  function isSideboardScratchPath(relativePath) {
2452
- const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
2453
- return p === ".sideboard/attachments" || p.startsWith(".sideboard/attachments/");
2703
+ return isWorkspaceScratchPath(relativePath);
2454
2704
  }
2455
2705
  function porcelainStatusPath(line) {
2456
2706
  const rest = line.length >= 3 ? line.slice(3) : "";
@@ -2478,7 +2728,7 @@ async function pushBranch(worktreePath, branchName) {
2478
2728
  }
2479
2729
  async function mergePr(cwd, selector, opts) {
2480
2730
  const slug = await resolveGithubRepoSlug(cwd);
2481
- const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft"];
2731
+ const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft,number"];
2482
2732
  if (slug) viewArgs.push("--repo", slug);
2483
2733
  const before = await gh(viewArgs, cwd, { reject: false });
2484
2734
  if (before.exitCode !== 0 || !before.stdout.trim()) {
@@ -2486,16 +2736,29 @@ async function mergePr(cwd, selector, opts) {
2486
2736
  }
2487
2737
  let url = "";
2488
2738
  let isDraft = false;
2739
+ let prNumber = null;
2489
2740
  try {
2490
2741
  const parsed = JSON.parse(before.stdout);
2491
2742
  url = String(parsed.url ?? "");
2492
2743
  isDraft = Boolean(parsed.isDraft);
2744
+ prNumber = typeof parsed.number === "number" && Number.isFinite(parsed.number) ? parsed.number : null;
2493
2745
  if (String(parsed.state ?? "").toUpperCase() === "MERGED") {
2494
2746
  return { url, state: "MERGED" };
2495
2747
  }
2496
2748
  } catch {
2497
2749
  throw new Error("Could not parse pull request details");
2498
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
+ }
2499
2762
  if (isDraft) {
2500
2763
  const readyArgs = ["pr", "ready", selector];
2501
2764
  if (slug) readyArgs.push("--repo", slug);
@@ -2517,10 +2780,10 @@ async function mergePr(cwd, selector, opts) {
2517
2780
  const after = await gh(viewArgs, cwd, { reject: false });
2518
2781
  if (after.exitCode === 0 && after.stdout.trim()) {
2519
2782
  try {
2520
- const parsed = JSON.parse(after.stdout);
2783
+ const parsed = after.stdout ? JSON.parse(after.stdout) : null;
2521
2784
  return {
2522
- url: String(parsed.url ?? url),
2523
- state: String(parsed.state ?? "MERGED")
2785
+ url: String(parsed?.url ?? url),
2786
+ state: String(parsed?.state ?? "MERGED")
2524
2787
  };
2525
2788
  } catch {
2526
2789
  }
@@ -2668,7 +2931,9 @@ var init_worktree = __esm({
2668
2931
  init_gh_errors();
2669
2932
  init_run();
2670
2933
  init_pr_gates();
2934
+ init_stack();
2671
2935
  init_teams();
2936
+ init_workspace_scratch();
2672
2937
  init_worktree_labels();
2673
2938
  }
2674
2939
  });
@@ -3346,6 +3611,7 @@ var init_coordinator_prompt = __esm({
3346
3611
  "Discover:",
3347
3612
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3348
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",
3349
3615
  "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3350
3616
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3351
3617
  "Workspaces:",
@@ -4331,7 +4597,9 @@ var init_injected_mcp = __esm({
4331
4597
  SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS = [
4332
4598
  "mcp__sideboard__present_artifact",
4333
4599
  "mcp__sideboard__present_schema",
4334
- "mcp__sideboard__present_files"
4600
+ "mcp__sideboard__present_files",
4601
+ "mcp__sideboard__ask_user",
4602
+ "mcp__sideboard__present_plan"
4335
4603
  ];
4336
4604
  brightsyMcpCommandCache = null;
4337
4605
  }
@@ -4370,7 +4638,7 @@ var PLAN_MODE_INSTRUCTION;
4370
4638
  var init_types = __esm({
4371
4639
  "src/agents/types.ts"() {
4372
4640
  "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.";
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.";
4374
4642
  }
4375
4643
  });
4376
4644
 
@@ -6078,6 +6346,116 @@ var init_workspaces = __esm({
6078
6346
  }
6079
6347
  });
6080
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
+
6081
6459
  // src/agents/cursor-recover.ts
6082
6460
  var cursor_recover_exports = {};
6083
6461
  __export(cursor_recover_exports, {
@@ -6086,10 +6464,10 @@ __export(cursor_recover_exports, {
6086
6464
  function recoverFinishedCursorRun(opts) {
6087
6465
  const agentId = opts.agentId.trim();
6088
6466
  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;
6467
+ const runsPath = (0, import_node_path25.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6468
+ if (!(0, import_node_fs27.existsSync)(runsPath)) return null;
6091
6469
  try {
6092
- const lines = (0, import_node_fs25.readFileSync)(runsPath, "utf8").split("\n");
6470
+ const lines = (0, import_node_fs27.readFileSync)(runsPath, "utf8").split("\n");
6093
6471
  let best = null;
6094
6472
  for (const line of lines) {
6095
6473
  const trimmed = line.trim();
@@ -6115,12 +6493,12 @@ function recoverFinishedCursorRun(opts) {
6115
6493
  return null;
6116
6494
  }
6117
6495
  }
6118
- var import_node_fs25, import_node_path24;
6496
+ var import_node_fs27, import_node_path25;
6119
6497
  var init_cursor_recover = __esm({
6120
6498
  "src/agents/cursor-recover.ts"() {
6121
6499
  "use strict";
6122
- import_node_fs25 = require("fs");
6123
- import_node_path24 = require("path");
6500
+ import_node_fs27 = require("fs");
6501
+ import_node_path25 = require("path");
6124
6502
  init_paths();
6125
6503
  }
6126
6504
  });
@@ -6129,11 +6507,11 @@ var init_cursor_recover = __esm({
6129
6507
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
6130
6508
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6131
6509
  var import_zod = require("zod");
6132
- var import_node_path25 = require("path");
6510
+ var import_node_path26 = require("path");
6133
6511
 
6134
6512
  // src/orchestrator/orchestrator.ts
6135
6513
  var import_node_events = require("events");
6136
- var import_node_fs26 = require("fs");
6514
+ var import_node_fs28 = require("fs");
6137
6515
  init_error_detail();
6138
6516
 
6139
6517
  // src/agents/spawn.ts
@@ -6153,18 +6531,18 @@ function asRecord(input) {
6153
6531
  }
6154
6532
  return void 0;
6155
6533
  }
6156
- function str(v) {
6534
+ function str2(v) {
6157
6535
  return typeof v === "string" && v.trim() ? v : void 0;
6158
6536
  }
6159
6537
  function toolDetail(name, input) {
6160
6538
  if (!input) return void 0;
6161
- const command = str(input.command) ?? str(input.cmd);
6539
+ const command = str2(input.command) ?? str2(input.cmd);
6162
6540
  if (command) return command;
6163
- 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);
6164
6542
  if (path) return path;
6165
- 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);
6166
6544
  if (pattern) return pattern;
6167
- const query = str(input.query) ?? str(input.prompt);
6545
+ const query = str2(input.query) ?? str2(input.prompt);
6168
6546
  if (query) return query.length > 80 ? `${query.slice(0, 77)}\u2026` : query;
6169
6547
  try {
6170
6548
  const raw = JSON.stringify(input);
@@ -6177,23 +6555,26 @@ function toolDescription(name, input) {
6177
6555
  const n = name.replace(/^mcp__/, "").replace(/__/g, " \xB7 ");
6178
6556
  if (/^get_record_types$|recordTypes/i.test(name)) return "List record types";
6179
6557
  if (/connectedAgentRequest/i.test(name)) {
6180
- return str(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
6558
+ return str2(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
6181
6559
  }
6182
6560
  if (/present_artifact$/i.test(name)) {
6183
- 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";
6184
6565
  }
6185
6566
  if (/present_schema$/i.test(name)) {
6186
- return str(input?.title) ? `Schema ${str(input?.title)}` : "Present schema";
6567
+ return str2(input?.title) ? `Schema ${str2(input?.title)}` : "Present schema";
6187
6568
  }
6188
6569
  if (/present_files$/i.test(name)) {
6189
- return str(input?.title) ? `Files ${str(input?.title)}` : "Present files";
6570
+ return str2(input?.title) ? `Files ${str2(input?.title)}` : "Present files";
6190
6571
  }
6191
6572
  if (/^(create|update)_artifact$/i.test(name.replace(/^mcp__[^_]+__/, ""))) {
6192
- return str(input?.title) ? `Artifact ${str(input?.title)}` : "Artifact";
6573
+ return str2(input?.title) ? `Artifact ${str2(input?.title)}` : "Artifact";
6193
6574
  }
6194
6575
  if (!input) return n;
6195
- if (/bash|shell|terminal/i.test(name) && str(input.command)) {
6196
- const cmd = str(input.command);
6576
+ if (/bash|shell|terminal/i.test(name) && str2(input.command)) {
6577
+ const cmd = str2(input.command);
6197
6578
  if (/git\s+fetch/i.test(cmd)) return "Fetch latest from origin and check status";
6198
6579
  if (/git\s+status/i.test(cmd)) return "Check git status";
6199
6580
  if (/git\s+log/i.test(cmd)) return "Inspect recent commits";
@@ -6202,11 +6583,11 @@ function toolDescription(name, input) {
6202
6583
  return "Run shell command";
6203
6584
  }
6204
6585
  if (/edit|write|apply/i.test(name)) {
6205
- const path = str(input.file_path) ?? str(input.path);
6586
+ const path = str2(input.file_path) ?? str2(input.path);
6206
6587
  return path ? `Edit ${path.split("/").pop()}` : "Edit file";
6207
6588
  }
6208
6589
  if (/read/i.test(name)) {
6209
- const path = str(input.file_path) ?? str(input.path);
6590
+ const path = str2(input.file_path) ?? str2(input.path);
6210
6591
  return path ? `Read ${path.split("/").pop()}` : "Read file";
6211
6592
  }
6212
6593
  if (/grep/i.test(name)) return "Search files";
@@ -6215,7 +6596,7 @@ function toolDescription(name, input) {
6215
6596
  }
6216
6597
  function toolFilePath(input) {
6217
6598
  if (!input) return void 0;
6218
- 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);
6219
6600
  }
6220
6601
  function countLines(text) {
6221
6602
  if (!text) return 0;
@@ -6223,8 +6604,8 @@ function countLines(text) {
6223
6604
  }
6224
6605
  function diffFromInput(input) {
6225
6606
  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);
6607
+ const oldS = str2(input.old_string) ?? str2(input.oldString);
6608
+ const newS = str2(input.new_string) ?? str2(input.newString) ?? str2(input.content);
6228
6609
  if (oldS != null || newS != null) {
6229
6610
  return {
6230
6611
  additions: countLines(newS),
@@ -6486,6 +6867,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
6486
6867
  // src/orchestrator/orchestrator.ts
6487
6868
  init_agents();
6488
6869
  init_worktree();
6870
+ init_stack();
6489
6871
 
6490
6872
  // src/hook/conductor.ts
6491
6873
  var import_node_fs13 = require("fs");
@@ -7121,7 +7503,7 @@ async function requireAgent(agent, opts) {
7121
7503
  init_worktree();
7122
7504
  init_thread_store();
7123
7505
  init_workspaces();
7124
- async function createThread(input, onSetupLine) {
7506
+ async function createThread(input, _onSetupLine) {
7125
7507
  await requireAgent(input.agent);
7126
7508
  const repoPath = await resolveRepoRoot(input.repoPath);
7127
7509
  if (!(0, import_node_fs18.existsSync)(repoPath)) {
@@ -7131,10 +7513,10 @@ async function createThread(input, onSetupLine) {
7131
7513
  let sourceIsFork = false;
7132
7514
  let prUrl = null;
7133
7515
  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`);
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`);
7138
7520
  sourceIsFork = pr.isCrossRepository;
7139
7521
  prUrl = pr.url;
7140
7522
  const localFetchBranch = `sideboard-pr-${pr.number}`;
@@ -7179,22 +7561,6 @@ async function createThread(input, onSetupLine) {
7179
7561
  });
7180
7562
  writeThread(thread);
7181
7563
  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
7564
  return readThread(thread.id) ?? thread;
7199
7565
  }
7200
7566
  async function listLinearIssues(agent, repoPath) {
@@ -7526,7 +7892,9 @@ function worktreeBindingFrom(from) {
7526
7892
  sourceIsFork: from.sourceIsFork,
7527
7893
  parentThreadId: from.parentThreadId,
7528
7894
  prUrl: from.prUrl,
7529
- prTitle: from.prTitle
7895
+ prTitle: from.prTitle,
7896
+ stackId: from.stackId,
7897
+ stackLayer: from.stackLayer
7530
7898
  };
7531
7899
  }
7532
7900
  function threadsSharingWorktree(worktreePath) {
@@ -7724,15 +8092,13 @@ File: src/client/frontends/desktop/core/UserData.ts
7724
8092
  `;
7725
8093
 
7726
8094
  // src/review/request-review.ts
8095
+ init_workspace_scratch();
7727
8096
  var REPO_REVIEW_PATH = ".sideboard/review.md";
7728
8097
  var REPO_REVIEW_NAME = "review.md";
7729
- 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`;
7730
8100
  var REVIEW_REQUEST_NAME = "Review request.md";
7731
8101
  var REVIEW_REQUEST_PREFILL = "Review.";
7732
- var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
7733
- *
7734
- !.gitignore
7735
- `;
7736
8102
  var LEGACY_REVIEW_TEMPLATE_MARKERS = [
7737
8103
  "You are acting as a reviewer for a proposed code change made by another engineer.",
7738
8104
  "HOW MANY FINDINGS TO RETURN:"
@@ -7755,10 +8121,10 @@ function readTextIfPresent(abs) {
7755
8121
  }
7756
8122
  }
7757
8123
  function ensureAttachmentsGitignore(worktreePath) {
7758
- const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ".sideboard", "attachments", ".gitignore");
8124
+ const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
7759
8125
  if ((0, import_node_fs19.existsSync)(gitignoreAbs)) return;
7760
8126
  (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(gitignoreAbs), { recursive: true });
7761
- (0, import_node_fs19.writeFileSync)(gitignoreAbs, ATTACHMENTS_GITIGNORE, "utf8");
8127
+ (0, import_node_fs19.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
7762
8128
  }
7763
8129
  function resolveReviewGuidelines(worktreePath) {
7764
8130
  const repoAbs = (0, import_node_path18.join)(worktreePath, REPO_REVIEW_PATH);
@@ -7781,6 +8147,16 @@ function resolveReviewGuidelines(worktreePath) {
7781
8147
  source: "local"
7782
8148
  };
7783
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
+ }
7784
8160
  ensureAttachmentsGitignore(worktreePath);
7785
8161
  (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(localAbs), { recursive: true });
7786
8162
  (0, import_node_fs19.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
@@ -8260,16 +8636,305 @@ async function importConductorWorkspaceAsync(workspaceId) {
8260
8636
  return importConductorWorkspace(workspaceId);
8261
8637
  }
8262
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
+
8263
8928
  // src/land/land.ts
8264
8929
  init_worktree();
8265
8930
 
8266
8931
  // src/diff/diff.ts
8267
- var import_node_fs21 = require("fs");
8932
+ var import_node_fs22 = require("fs");
8268
8933
  var import_node_path20 = require("path");
8269
8934
  init_run();
8270
8935
  init_worktree();
8271
8936
  async function inspectGitWorktree(worktreePath) {
8272
- if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) return "missing_worktree";
8937
+ if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) return "missing_worktree";
8273
8938
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
8274
8939
  reject: false
8275
8940
  });
@@ -8277,7 +8942,7 @@ async function inspectGitWorktree(worktreePath) {
8277
8942
  return "ok";
8278
8943
  }
8279
8944
  async function initializeGitRepository(worktreePath) {
8280
- if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) {
8945
+ if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) {
8281
8946
  throw new Error("Worktree not found");
8282
8947
  }
8283
8948
  const status = await inspectGitWorktree(worktreePath);
@@ -8573,33 +9238,33 @@ async function getDiff(worktreePath, repoPath, opts) {
8573
9238
  let combinedDiff = "";
8574
9239
  let labelBase = base;
8575
9240
  if (scope === "staged") {
8576
- const [ns, num, diff] = await Promise.all([
9241
+ const [ns, num2, diff] = await Promise.all([
8577
9242
  git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
8578
9243
  git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
8579
9244
  git(["diff", "--cached"], worktreePath, { reject: false })
8580
9245
  ]);
8581
9246
  nameStatus = ns.stdout;
8582
- numstat = num.stdout;
9247
+ numstat = num2.stdout;
8583
9248
  combinedDiff = diff.stdout;
8584
9249
  labelBase = "staged";
8585
9250
  } else if (scope === "unstaged") {
8586
- const [ns, num, diff] = await Promise.all([
9251
+ const [ns, num2, diff] = await Promise.all([
8587
9252
  git(["diff", "--name-status"], worktreePath, { reject: false }),
8588
9253
  git(["diff", "--numstat"], worktreePath, { reject: false }),
8589
9254
  git(["diff"], worktreePath, { reject: false })
8590
9255
  ]);
8591
9256
  nameStatus = ns.stdout;
8592
- numstat = num.stdout;
9257
+ numstat = num2.stdout;
8593
9258
  combinedDiff = diff.stdout;
8594
9259
  labelBase = "unstaged";
8595
9260
  } else if (scope === "uncommitted") {
8596
- const [ns, num, diff] = await Promise.all([
9261
+ const [ns, num2, diff] = await Promise.all([
8597
9262
  git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
8598
9263
  git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
8599
9264
  git(["diff", "HEAD"], worktreePath, { reject: false })
8600
9265
  ]);
8601
9266
  nameStatus = ns.stdout;
8602
- numstat = num.stdout;
9267
+ numstat = num2.stdout;
8603
9268
  combinedDiff = diff.stdout;
8604
9269
  labelBase = "HEAD";
8605
9270
  } else if (scope === "last_turn") {
@@ -8618,34 +9283,34 @@ async function getDiff(worktreePath, repoPath, opts) {
8618
9283
  scopeStats
8619
9284
  };
8620
9285
  }
8621
- const [ns, num, diff] = await Promise.all([
9286
+ const [ns, num2, diff] = await Promise.all([
8622
9287
  git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
8623
9288
  git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
8624
9289
  git(["diff", lastTurnBase], worktreePath, { reject: false })
8625
9290
  ]);
8626
9291
  nameStatus = ns.stdout;
8627
- numstat = num.stdout;
9292
+ numstat = num2.stdout;
8628
9293
  combinedDiff = diff.stdout;
8629
9294
  labelBase = "last turn";
8630
9295
  } else if (scope === "commits" && commitSha) {
8631
9296
  const range = `${commitSha}^!`;
8632
- const [ns, num, diff] = await Promise.all([
9297
+ const [ns, num2, diff] = await Promise.all([
8633
9298
  git(["diff", "--name-status", range], worktreePath, { reject: false }),
8634
9299
  git(["diff", "--numstat", range], worktreePath, { reject: false }),
8635
9300
  git(["diff", range], worktreePath, { reject: false })
8636
9301
  ]);
8637
9302
  nameStatus = ns.stdout;
8638
- numstat = num.stdout;
9303
+ numstat = num2.stdout;
8639
9304
  combinedDiff = diff.stdout;
8640
9305
  labelBase = commitSha.slice(0, 7);
8641
9306
  } else {
8642
- const [ns, num, diff] = await Promise.all([
9307
+ const [ns, num2, diff] = await Promise.all([
8643
9308
  git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
8644
9309
  git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
8645
9310
  git(["diff", mergeBase], worktreePath, { reject: false })
8646
9311
  ]);
8647
9312
  nameStatus = ns.stdout;
8648
- numstat = num.stdout;
9313
+ numstat = num2.stdout;
8649
9314
  combinedDiff = diff.stdout;
8650
9315
  labelBase = base;
8651
9316
  }
@@ -8715,7 +9380,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
8715
9380
  assertSafeRelativePath(relativePath);
8716
9381
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
8717
9382
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8718
- const st = (0, import_node_fs21.statSync)(abs);
9383
+ const st = (0, import_node_fs22.statSync)(abs);
8719
9384
  if (!st.isFile()) {
8720
9385
  throw new Error(`Not a file: ${relativePath}`);
8721
9386
  }
@@ -8724,7 +9389,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
8724
9389
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
8725
9390
  );
8726
9391
  }
8727
- const buf = (0, import_node_fs21.readFileSync)(abs);
9392
+ const buf = (0, import_node_fs22.readFileSync)(abs);
8728
9393
  return {
8729
9394
  path: relativePath,
8730
9395
  contentBase64: buf.toString("base64"),
@@ -8735,11 +9400,11 @@ function readWorktreeFile(worktreePath, relativePath, opts) {
8735
9400
  assertSafeRelativePath(relativePath);
8736
9401
  const maxBytes = opts?.maxBytes ?? 2e5;
8737
9402
  const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8738
- const st = (0, import_node_fs21.statSync)(abs);
9403
+ const st = (0, import_node_fs22.statSync)(abs);
8739
9404
  if (!st.isFile()) {
8740
9405
  throw new Error(`Not a file: ${relativePath}`);
8741
9406
  }
8742
- const buf = (0, import_node_fs21.readFileSync)(abs);
9407
+ const buf = (0, import_node_fs22.readFileSync)(abs);
8743
9408
  if (isImageRelativePath(relativePath)) {
8744
9409
  const maxImageBytes = Math.max(maxBytes, 15e6);
8745
9410
  const truncated2 = buf.length > maxImageBytes;
@@ -8783,8 +9448,8 @@ function assertSafeRelativePath(relativePath) {
8783
9448
  function writeWorktreeFile(worktreePath, relativePath, content) {
8784
9449
  assertSafeRelativePath(relativePath);
8785
9450
  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");
9451
+ (0, import_node_fs22.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
9452
+ (0, import_node_fs22.writeFileSync)(abs, content, "utf8");
8788
9453
  return { path: relativePath };
8789
9454
  }
8790
9455
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -8963,7 +9628,7 @@ async function confirmLand(thread, opts) {
8963
9628
  }
8964
9629
 
8965
9630
  // src/skills/discover.ts
8966
- var import_node_fs22 = require("fs");
9631
+ var import_node_fs23 = require("fs");
8967
9632
  var import_node_os9 = require("os");
8968
9633
  var import_node_path21 = require("path");
8969
9634
  function toCommand(name) {
@@ -8997,7 +9662,7 @@ function parseFrontmatter(content) {
8997
9662
  }
8998
9663
  function readSkill(skillMd, source) {
8999
9664
  try {
9000
- const content = (0, import_node_fs22.readFileSync)(skillMd, "utf8");
9665
+ const content = (0, import_node_fs23.readFileSync)(skillMd, "utf8");
9001
9666
  const { name: fmName, description } = parseFrontmatter(content);
9002
9667
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
9003
9668
  const name = fmName || dirName;
@@ -9016,19 +9681,19 @@ function readSkill(skillMd, source) {
9016
9681
  }
9017
9682
  }
9018
9683
  function scanSkillsDir(dir, source, out) {
9019
- if (!(0, import_node_fs22.existsSync)(dir)) return;
9684
+ if (!(0, import_node_fs23.existsSync)(dir)) return;
9020
9685
  let entries;
9021
9686
  try {
9022
- entries = (0, import_node_fs22.readdirSync)(dir);
9687
+ entries = (0, import_node_fs23.readdirSync)(dir);
9023
9688
  } catch {
9024
9689
  return;
9025
9690
  }
9026
9691
  for (const entry of entries) {
9027
9692
  if (entry.startsWith(".")) continue;
9028
9693
  const skillMd = (0, import_node_path21.join)(dir, entry, "SKILL.md");
9029
- if (!(0, import_node_fs22.existsSync)(skillMd)) continue;
9694
+ if (!(0, import_node_fs23.existsSync)(skillMd)) continue;
9030
9695
  try {
9031
- if (!(0, import_node_fs22.statSync)(skillMd).isFile()) continue;
9696
+ if (!(0, import_node_fs23.statSync)(skillMd).isFile()) continue;
9032
9697
  } catch {
9033
9698
  continue;
9034
9699
  }
@@ -9037,12 +9702,12 @@ function scanSkillsDir(dir, source, out) {
9037
9702
  }
9038
9703
  }
9039
9704
  function scanClaudePluginSkills(pluginsRoot, out) {
9040
- if (!(0, import_node_fs22.existsSync)(pluginsRoot)) return;
9705
+ if (!(0, import_node_fs23.existsSync)(pluginsRoot)) return;
9041
9706
  const walk = (dir, depth, lookingForSkillsDir) => {
9042
9707
  if (depth > 7) return;
9043
9708
  let entries;
9044
9709
  try {
9045
- entries = (0, import_node_fs22.readdirSync)(dir);
9710
+ entries = (0, import_node_fs23.readdirSync)(dir);
9046
9711
  } catch {
9047
9712
  return;
9048
9713
  }
@@ -9054,7 +9719,7 @@ function scanClaudePluginSkills(pluginsRoot, out) {
9054
9719
  if (entry === "node_modules" || entry === ".git") continue;
9055
9720
  const full = (0, import_node_path21.join)(dir, entry);
9056
9721
  try {
9057
- if (!(0, import_node_fs22.statSync)(full).isDirectory()) continue;
9722
+ if (!(0, import_node_fs23.statSync)(full).isDirectory()) continue;
9058
9723
  } catch {
9059
9724
  continue;
9060
9725
  }
@@ -9094,7 +9759,7 @@ function discoverSkills(worktreePath) {
9094
9759
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
9095
9760
  }
9096
9761
  function readSkillBody(skillPath, maxChars = 12e3) {
9097
- const raw = (0, import_node_fs22.readFileSync)(skillPath, "utf8");
9762
+ const raw = (0, import_node_fs23.readFileSync)(skillPath, "utf8");
9098
9763
  if (raw.startsWith("---")) {
9099
9764
  const end = raw.indexOf("\n---", 3);
9100
9765
  if (end >= 0) {
@@ -9187,9 +9852,10 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
9187
9852
  }
9188
9853
 
9189
9854
  // src/composer/stage-files.ts
9190
- var import_node_fs23 = require("fs");
9855
+ var import_node_fs24 = require("fs");
9191
9856
  var import_node_path22 = require("path");
9192
9857
  var import_node_crypto5 = require("crypto");
9858
+ init_workspace_scratch();
9193
9859
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
9194
9860
  "png",
9195
9861
  "jpg",
@@ -9210,11 +9876,6 @@ var IMAGE_MIME_BY_EXT = {
9210
9876
  bmp: "image/bmp",
9211
9877
  ico: "image/x-icon"
9212
9878
  };
9213
- var ATTACHMENTS_DIR = ".sideboard/attachments";
9214
- var ATTACHMENTS_GITIGNORE2 = `# Sideboard review / composer attachments (local only)
9215
- *
9216
- !.gitignore
9217
- `;
9218
9879
  var MAX_INLINE_BYTES = 4e5;
9219
9880
  var MAX_PREVIEW_BYTES = 5e6;
9220
9881
  function fileExtension(filePath) {
@@ -9229,21 +9890,21 @@ function imageMimeType(filePath) {
9229
9890
  }
9230
9891
  function ensureAttachmentsDir(worktreePath) {
9231
9892
  const dir = (0, import_node_path22.join)(worktreePath, ATTACHMENTS_DIR);
9232
- (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
9893
+ (0, import_node_fs24.mkdirSync)(dir, { recursive: true });
9233
9894
  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");
9895
+ if (!(0, import_node_fs24.existsSync)(gi)) {
9896
+ (0, import_node_fs24.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
9236
9897
  }
9237
9898
  return dir;
9238
9899
  }
9239
9900
  function uniqueAttachmentName(dir, originalName) {
9240
9901
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
9241
- 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;
9242
9903
  const ext = (0, import_node_path22.extname)(safe);
9243
9904
  const stem = ext ? safe.slice(0, -ext.length) : safe;
9244
9905
  for (let i = 1; i < 1e4; i++) {
9245
9906
  const candidate = `${stem}-${i}${ext}`;
9246
- 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;
9247
9908
  }
9248
9909
  return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
9249
9910
  }
@@ -9301,13 +9962,13 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
9301
9962
  for (const abs of absolutePaths) {
9302
9963
  const originalName = (0, import_node_path22.basename)(abs);
9303
9964
  try {
9304
- const st = (0, import_node_fs23.statSync)(abs);
9965
+ const st = (0, import_node_fs24.statSync)(abs);
9305
9966
  if (!st.isFile()) continue;
9306
9967
  const name = uniqueAttachmentName(dir, originalName);
9307
9968
  const destAbs = (0, import_node_path22.join)(dir, name);
9308
- (0, import_node_fs23.copyFileSync)(abs, destAbs);
9969
+ (0, import_node_fs24.copyFileSync)(abs, destAbs);
9309
9970
  const rel = `${ATTACHMENTS_DIR}/${name}`;
9310
- const buf = (0, import_node_fs23.readFileSync)(destAbs);
9971
+ const buf = (0, import_node_fs24.readFileSync)(destAbs);
9311
9972
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
9312
9973
  } catch (err) {
9313
9974
  out.push({
@@ -9330,7 +9991,7 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
9330
9991
  const buf = Buffer.from(item.dataBase64, "base64");
9331
9992
  const name = uniqueAttachmentName(dir, originalName);
9332
9993
  const destAbs = (0, import_node_path22.join)(dir, name);
9333
- (0, import_node_fs23.writeFileSync)(destAbs, buf);
9994
+ (0, import_node_fs24.writeFileSync)(destAbs, buf);
9334
9995
  const rel = `${ATTACHMENTS_DIR}/${name}`;
9335
9996
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
9336
9997
  } catch (err) {
@@ -9359,9 +10020,9 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
9359
10020
  const name = (0, import_node_path22.basename)(rel);
9360
10021
  try {
9361
10022
  const abs = (0, import_node_path22.join)(worktreePath, rel);
9362
- const st = (0, import_node_fs23.statSync)(abs);
10023
+ const st = (0, import_node_fs24.statSync)(abs);
9363
10024
  if (!st.isFile()) continue;
9364
- const buf = (0, import_node_fs23.readFileSync)(abs);
10025
+ const buf = (0, import_node_fs24.readFileSync)(abs);
9365
10026
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
9366
10027
  } catch (err) {
9367
10028
  out.push({
@@ -9376,7 +10037,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
9376
10037
  }
9377
10038
 
9378
10039
  // src/agents/instructions.ts
9379
- var import_node_fs24 = require("fs");
10040
+ var import_node_fs25 = require("fs");
9380
10041
  var import_node_path23 = require("path");
9381
10042
  init_worktree_labels();
9382
10043
  function normPath2(p) {
@@ -9517,10 +10178,10 @@ function loadAgentInstructions(worktreePath, agent) {
9517
10178
  for (const rel of candidates) {
9518
10179
  if (seen.has(rel)) continue;
9519
10180
  const abs = (0, import_node_path23.join)(worktreePath, rel);
9520
- if (!(0, import_node_fs24.existsSync)(abs)) continue;
10181
+ if (!(0, import_node_fs25.existsSync)(abs)) continue;
9521
10182
  try {
9522
- if (!(0, import_node_fs24.statSync)(abs).isFile()) continue;
9523
- 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");
9524
10185
  if (!content.trim()) continue;
9525
10186
  if (content.length > MAX_CHARS_PER_FILE) {
9526
10187
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -9553,6 +10214,7 @@ function formatAgentInstructions(files) {
9553
10214
 
9554
10215
  // src/orchestrator/orchestrator.ts
9555
10216
  init_types();
10217
+ init_plan_file();
9556
10218
  init_settings();
9557
10219
 
9558
10220
  // src/threads/sync-branch.ts
@@ -9672,7 +10334,7 @@ var Orchestrator = class {
9672
10334
  }
9673
10335
  continue;
9674
10336
  }
9675
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
10337
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
9676
10338
  setStatus(thread.id, "broken", "Worktree missing on disk");
9677
10339
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
9678
10340
  continue;
@@ -9817,14 +10479,9 @@ var Orchestrator = class {
9817
10479
  return findThreadByRef(idOrRef) ?? readThread(idOrRef);
9818
10480
  }
9819
10481
  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
- });
10482
+ let thread = await createThread(input);
9827
10483
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
10484
+ await this.runSetupAfterCreate(thread.id);
9828
10485
  const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
9829
10486
  if (autoRunAfterSetupEnabled2()) {
9830
10487
  try {
@@ -9838,6 +10495,19 @@ var Orchestrator = class {
9838
10495
  }
9839
10496
  return thread;
9840
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
+ }
9841
10511
  listWorkspaces() {
9842
10512
  const fromThreads = listThreads({ includeArchived: false }).map((t) => t.repoPath);
9843
10513
  return syncWorkspacesFromThreads(fromThreads);
@@ -10186,6 +10856,19 @@ var Orchestrator = class {
10186
10856
  });
10187
10857
  }
10188
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
+ }
10189
10872
  if (afterTurn.planMode && afterTurn.agent === "claude" && parts.some(
10190
10873
  (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
10191
10874
  )) {
@@ -10667,6 +11350,82 @@ var Orchestrator = class {
10667
11350
  }
10668
11351
  return meta;
10669
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
+ }
10670
11429
  async getPrDetails(threadRef) {
10671
11430
  const { thread, selector, cwd } = await this.withPrSelector(threadRef);
10672
11431
  if (!selector) return null;
@@ -10729,14 +11488,9 @@ var Orchestrator = class {
10729
11488
  return forkChatTab(input);
10730
11489
  }
10731
11490
  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
- });
11491
+ const thread = await forkThreadWorktree(input);
10739
11492
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
11493
+ await this.runSetupAfterCreate(thread.id);
10740
11494
  return thread;
10741
11495
  }
10742
11496
  renameThread(threadRef, title) {
@@ -10750,7 +11504,7 @@ var Orchestrator = class {
10750
11504
  }
10751
11505
  /**
10752
11506
  * Stage OS / worktree files into composer attachments (copies external files
10753
- * into `.sideboard/attachments/` so agents can Read images and binaries).
11507
+ * into `.context/attachments/` so agents can Read images and binaries).
10754
11508
  */
10755
11509
  attachComposerFiles(threadRef, opts) {
10756
11510
  const thread = this.requireThread(threadRef);
@@ -10824,7 +11578,7 @@ var Orchestrator = class {
10824
11578
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
10825
11579
  return setStatus(thread.id, "idle");
10826
11580
  }
10827
- if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
11581
+ if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
10828
11582
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
10829
11583
  const { execa: execa7 } = await import("execa");
10830
11584
  const slug = thread.worktreePath.split("/").pop();
@@ -11043,7 +11797,7 @@ async function startMcpServer() {
11043
11797
  async () => {
11044
11798
  const threads = orch.getThreads(true);
11045
11799
  const lines = threads.map((t) => {
11046
- 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;
11047
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}` : ""}`;
11048
11802
  });
11049
11803
  return {
@@ -11105,6 +11859,59 @@ async function startMcpServer() {
11105
11859
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
11106
11860
  }
11107
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
+ );
11108
11915
  server.tool(
11109
11916
  "present_schema",
11110
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.",
@@ -11683,6 +12490,126 @@ async function startMcpServer() {
11683
12490
  };
11684
12491
  }
11685
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
+ );
11686
12613
  server.tool(
11687
12614
  "list_issues",
11688
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.",