@sideboard-ai/core 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{agents-LUB3Q773.js → agents-ON6RKKND.js} +3 -3
  2. package/dist/{agents-3P4N6KHC.js → agents-YKSS6VBO.js} +3 -3
  3. package/dist/{chunk-RQI4TOXK.js → chunk-6QTZVJ7A.js} +7 -5
  4. package/dist/{chunk-AJ6ROGD7.js → chunk-7LNGIP3X.js} +20 -2
  5. package/dist/chunk-B3SJXYIJ.js +24 -0
  6. package/dist/{chunk-ILQK4P5R.js → chunk-BXJ76RHF.js} +4 -4
  7. package/dist/{chunk-SS34TVSY.js → chunk-D3METLRW.js} +7 -5
  8. package/dist/{chunk-FV6FN6V5.js → chunk-DZFH2KLT.js} +340 -22
  9. package/dist/chunk-FKOIHGKV.js +21 -0
  10. package/dist/{chunk-3NAS4MWH.js → chunk-GNML24AW.js} +2 -2
  11. package/dist/{chunk-E35APBHV.js → chunk-H6GGDLYS.js} +1 -1
  12. package/dist/{chunk-EOYDCKQC.js → chunk-HLEX5AQ6.js} +4 -0
  13. package/dist/{chunk-ZQCQIWIP.js → chunk-LRLKJM3O.js} +2 -1
  14. package/dist/chunk-N5PM7HGQ.js +103 -0
  15. package/dist/chunk-QTUESPAW.js +101 -0
  16. package/dist/{chunk-O6W3P7V3.js → chunk-TSRXOSVD.js} +4 -0
  17. package/dist/{chunk-GZXBYHJC.js → chunk-UEAHMGHW.js} +2 -1
  18. package/dist/{chunk-ZVV5EEQN.js → chunk-XOU6HNQJ.js} +305 -22
  19. package/dist/{chunk-5WYEJ3F3.js → chunk-XX5BB7NV.js} +3 -3
  20. package/dist/{chunk-V4JRXYYU.js → chunk-YDXQ72MD.js} +2 -2
  21. package/dist/{chunk-DF5VQQKA.js → chunk-YOWIYAVA.js} +3 -3
  22. package/dist/{chunk-ISY4EGF6.js → chunk-ZNM4MAN4.js} +20 -2
  23. package/dist/{connected-teams-UBXHZGO4.js → connected-teams-2ANNZCP5.js} +2 -2
  24. package/dist/{connected-teams-GF52Q7LB.js → connected-teams-ONC4666A.js} +2 -2
  25. package/dist/{coordinator-prompt-VG4BZ5JL.js → coordinator-prompt-6FXVTSFN.js} +5 -4
  26. package/dist/{coordinator-prompt-SPGDT7J5.js → coordinator-prompt-S6JZD5EF.js} +5 -4
  27. package/dist/{global-workspace-OJF4ENH4.js → global-workspace-EV4G2WMQ.js} +6 -5
  28. package/dist/{global-workspace-KSR3E63K.js → global-workspace-MSX2K27Y.js} +6 -5
  29. package/dist/index.cjs +1369 -155
  30. package/dist/index.d.cts +397 -14
  31. package/dist/index.d.ts +397 -14
  32. package/dist/index.js +811 -90
  33. package/dist/mcp/run-stdio.cjs +1136 -147
  34. package/dist/mcp/run-stdio.js +635 -79
  35. package/dist/plan-file-6O7G4VPQ.js +23 -0
  36. package/dist/plan-file-PHVKUAEE.js +25 -0
  37. package/dist/{run-HMRSRG3U.js → run-H3IMNOYN.js} +3 -1
  38. package/dist/run-ZJKYHXGN.js +12 -0
  39. package/dist/{thread-store-XICUWFNM.js → thread-store-GHOADGL2.js} +1 -1
  40. package/dist/{thread-store-OV2X6PYO.js → thread-store-UJIGMI5J.js} +1 -1
  41. package/dist/{workspaces-OZE7LRDO.js → workspaces-3RQQZQRO.js} +7 -6
  42. package/dist/{workspaces-UJX7JIGH.js → workspaces-AYTBR6KQ.js} +7 -6
  43. package/dist/{worktree-XG5PCLZY.js → worktree-5KEQWSAF.js} +8 -3
  44. package/dist/{worktree-TEDAJ57S.js → worktree-RWGL7FUV.js} +8 -3
  45. package/package.json +1 -1
  46. package/dist/run-LF6E5IKL.js +0 -10
@@ -0,0 +1,101 @@
1
+ import {
2
+ ATTACHMENTS_DIR,
3
+ LEGACY_ATTACHMENTS_DIR,
4
+ attachmentsGitignoreBody
5
+ } from "./chunk-FKOIHGKV.js";
6
+
7
+ // src/plan/plan-file.ts
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
+ import { dirname, join } from "path";
10
+
11
+ // src/plan/plan-present.ts
12
+ var PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
13
+ var PLAN_FILE_NAME = "plan.md";
14
+ var LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
15
+ function asRecord(v) {
16
+ return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
17
+ }
18
+ function isPresentPlanToolName(name) {
19
+ if (!name) return false;
20
+ return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
21
+ }
22
+ function extractPresentedPlan(parts) {
23
+ if (!parts?.length) return null;
24
+ for (let i = parts.length - 1; i >= 0; i--) {
25
+ const p = parts[i];
26
+ if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
27
+ const input = asRecord(p.input) ?? {};
28
+ const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
29
+ if (!content.trim()) continue;
30
+ const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
31
+ const path = typeof input.path === "string" && input.path.trim() ? input.path.trim() : PLAN_FILE_REL;
32
+ return { title, content: content.trim(), path, source: "present_plan" };
33
+ }
34
+ return null;
35
+ }
36
+ function resolvePlanMarkdown(opts) {
37
+ const fromTool = extractPresentedPlan(opts.parts);
38
+ if (fromTool) return fromTool;
39
+ const file = opts.fileContent?.trim();
40
+ if (file) {
41
+ return {
42
+ title: "Plan",
43
+ content: file,
44
+ path: PLAN_FILE_REL,
45
+ source: "exit_plan"
46
+ };
47
+ }
48
+ const text = opts.text?.trim();
49
+ if (text && text.length >= 80) {
50
+ return {
51
+ title: "Plan",
52
+ content: text,
53
+ path: PLAN_FILE_REL,
54
+ source: "text"
55
+ };
56
+ }
57
+ return null;
58
+ }
59
+
60
+ // src/plan/plan-file.ts
61
+ function ensureAttachmentsGitignore(worktreePath) {
62
+ const gitignoreAbs = join(worktreePath, ATTACHMENTS_DIR, ".gitignore");
63
+ if (existsSync(gitignoreAbs)) return;
64
+ mkdirSync(dirname(gitignoreAbs), { recursive: true });
65
+ writeFileSync(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
66
+ }
67
+ function planFileAbs(worktreePath) {
68
+ return join(worktreePath, PLAN_FILE_REL);
69
+ }
70
+ function readTextIfPresent(abs) {
71
+ if (!existsSync(abs)) return null;
72
+ try {
73
+ const content = readFileSync(abs, "utf8");
74
+ return content.trim() ? content : null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ function readPlanFile(worktreePath) {
80
+ return readTextIfPresent(planFileAbs(worktreePath)) ?? readTextIfPresent(join(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent(join(worktreePath, LEGACY_PLAN_FILE_REL));
81
+ }
82
+ function writePlanFile(worktreePath, content) {
83
+ ensureAttachmentsGitignore(worktreePath);
84
+ const abs = planFileAbs(worktreePath);
85
+ mkdirSync(dirname(abs), { recursive: true });
86
+ const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
87
+ writeFileSync(abs, body, "utf8");
88
+ return PLAN_FILE_REL;
89
+ }
90
+
91
+ export {
92
+ PLAN_FILE_REL,
93
+ PLAN_FILE_NAME,
94
+ LEGACY_PLAN_FILE_REL,
95
+ isPresentPlanToolName,
96
+ extractPresentedPlan,
97
+ resolvePlanMarkdown,
98
+ planFileAbs,
99
+ readPlanFile,
100
+ writePlanFile
101
+ };
@@ -39,6 +39,8 @@ function normalizeThread(raw) {
39
39
  agentPid: raw.agentPid ?? null,
40
40
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
41
41
  prTitle: raw.prTitle ?? null,
42
+ stackId: raw.stackId ?? null,
43
+ stackLayer: raw.stackLayer ?? null,
42
44
  userSetTitle: Boolean(raw.userSetTitle),
43
45
  activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
44
46
  quotaResumeAt: raw.quotaResumeAt ?? null,
@@ -63,6 +65,8 @@ function createEmptyThread(partial) {
63
65
  activeRuns: partial.activeRuns ?? [],
64
66
  prUrl: partial.prUrl ?? null,
65
67
  prTitle: partial.prTitle ?? null,
68
+ stackId: partial.stackId ?? null,
69
+ stackLayer: partial.stackLayer ?? null,
66
70
  userSetTitle: partial.userSetTitle ?? false,
67
71
  messages: partial.messages ?? [],
68
72
  attachments: partial.attachments ?? [],
@@ -2,7 +2,7 @@
2
2
 
3
3
  import {
4
4
  resolveGithubRepoSlug
5
- } from "./chunk-ZVV5EEQN.js";
5
+ } from "./chunk-XOU6HNQJ.js";
6
6
  import {
7
7
  globalAgentCwd,
8
8
  sideboardReposDir
@@ -44,6 +44,7 @@ var COORDINATOR_TOOL_PLAYBOOK = [
44
44
  "Discover:",
45
45
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
46
46
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
47
+ "- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
47
48
  "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
48
49
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
49
50
  "Workspaces:",
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import {
4
+ isWorkspaceScratchPath
5
+ } from "./chunk-B3SJXYIJ.js";
3
6
  import {
4
7
  listThreads
5
- } from "./chunk-EOYDCKQC.js";
8
+ } from "./chunk-HLEX5AQ6.js";
6
9
  import {
7
10
  gh,
8
- git
9
- } from "./chunk-ISY4EGF6.js";
11
+ git,
12
+ resolveGhAuthToken
13
+ } from "./chunk-ZNM4MAN4.js";
10
14
  import {
11
15
  worktreesRoot
12
16
  } from "./chunk-7MV3RXSC.js";
@@ -964,6 +968,178 @@ function buildMergeGateChecks(gate, opts = {}) {
964
968
  return rows;
965
969
  }
966
970
 
971
+ // src/git/stack.ts
972
+ var cachedStatus = null;
973
+ var STATUS_TTL_MS = 6e4;
974
+ async function detectGhStack(cwd) {
975
+ const now = Date.now();
976
+ if (cachedStatus && now - cachedStatus.at < STATUS_TTL_MS) {
977
+ return cachedStatus.status;
978
+ }
979
+ const probe = await gh(["stack", "view", "--help"], cwd, { reject: false });
980
+ if (probe.exitCode === 0) {
981
+ const status2 = { available: true };
982
+ cachedStatus = { at: now, status: status2 };
983
+ return status2;
984
+ }
985
+ const err = `${probe.stderr}
986
+ ${probe.stdout}`;
987
+ 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";
988
+ const status = { available: false, reason };
989
+ cachedStatus = { at: now, status };
990
+ return status;
991
+ }
992
+ function str(v) {
993
+ return typeof v === "string" ? v : v == null ? "" : String(v);
994
+ }
995
+ function num(v) {
996
+ if (typeof v === "number" && Number.isFinite(v)) return v;
997
+ if (typeof v === "string" && v.trim() && Number.isFinite(Number(v))) {
998
+ return Number(v);
999
+ }
1000
+ return null;
1001
+ }
1002
+ function parseGhStackViewJson(raw) {
1003
+ let data;
1004
+ try {
1005
+ data = JSON.parse(raw);
1006
+ } catch {
1007
+ return null;
1008
+ }
1009
+ if (!Array.isArray(data.branches) || data.branches.length === 0) return null;
1010
+ const trunk = str(data.trunk) || "main";
1011
+ const currentBranch2 = str(data.currentBranch);
1012
+ const stackNumber = num(data.stackNumber) ?? num(data.number);
1013
+ const layers = [];
1014
+ for (let i = 0; i < data.branches.length; i++) {
1015
+ const b = data.branches[i];
1016
+ if (!b || typeof b !== "object") continue;
1017
+ const name = str(b.name);
1018
+ if (!name) continue;
1019
+ const pr = b.pr && typeof b.pr === "object" ? b.pr : null;
1020
+ layers.push({
1021
+ position: i + 1,
1022
+ branchName: name,
1023
+ headSha: str(b.head) || void 0,
1024
+ baseSha: str(b.base) || void 0,
1025
+ isCurrent: Boolean(b.isCurrent) || name === currentBranch2,
1026
+ isMerged: Boolean(b.isMerged),
1027
+ isQueued: Boolean(b.isQueued),
1028
+ needsRebase: Boolean(b.needsRebase),
1029
+ prNumber: pr ? num(pr.number) : null,
1030
+ prUrl: pr && str(pr.url) ? str(pr.url) : null,
1031
+ prState: pr && str(pr.state) ? str(pr.state).toUpperCase() : null,
1032
+ title: pr && str(pr.title) ? str(pr.title) : void 0
1033
+ });
1034
+ }
1035
+ if (!layers.length) return null;
1036
+ let currentIndex = layers.findIndex((l) => l.isCurrent);
1037
+ if (currentIndex < 0 && currentBranch2) {
1038
+ currentIndex = layers.findIndex((l) => l.branchName === currentBranch2);
1039
+ }
1040
+ const { readyToMerge, blockedReason } = stackMergeReadiness(layers, currentIndex);
1041
+ return {
1042
+ stackNumber,
1043
+ trunk,
1044
+ currentBranch: currentBranch2 || layers[currentIndex]?.branchName || layers[0].branchName,
1045
+ layers,
1046
+ currentIndex,
1047
+ readyToMerge,
1048
+ blockedReason
1049
+ };
1050
+ }
1051
+ function stackMergeReadiness(layers, throughIndex) {
1052
+ if (throughIndex < 0 || throughIndex >= layers.length) {
1053
+ return { readyToMerge: false, blockedReason: "Not on a stack layer" };
1054
+ }
1055
+ for (let i = 0; i <= throughIndex; i++) {
1056
+ const layer = layers[i];
1057
+ if (layer.isMerged) continue;
1058
+ if (!layer.prNumber) {
1059
+ return {
1060
+ readyToMerge: false,
1061
+ blockedReason: `Layer ${layer.branchName} has no pull request yet`
1062
+ };
1063
+ }
1064
+ if (layer.needsRebase) {
1065
+ return {
1066
+ readyToMerge: false,
1067
+ blockedReason: `PR #${layer.prNumber} needs rebase`
1068
+ };
1069
+ }
1070
+ const state = (layer.prState ?? "").toUpperCase();
1071
+ if (state && state !== "OPEN" && state !== "QUEUED") {
1072
+ return {
1073
+ readyToMerge: false,
1074
+ blockedReason: `PR #${layer.prNumber} is ${state}`
1075
+ };
1076
+ }
1077
+ }
1078
+ return { readyToMerge: true, blockedReason: null };
1079
+ }
1080
+ async function getPrStack(cwd) {
1081
+ const status = await detectGhStack(cwd);
1082
+ if (!status.available) return null;
1083
+ const result = await gh(["stack", "view", "--json"], cwd, { reject: false });
1084
+ if (result.exitCode === 2) return null;
1085
+ if (result.exitCode !== 0) {
1086
+ if (/not in a stack|no stack/i.test(`${result.stderr}
1087
+ ${result.stdout}`)) {
1088
+ return null;
1089
+ }
1090
+ if (result.exitCode === 9) return null;
1091
+ return null;
1092
+ }
1093
+ const json = result.stdout.trim();
1094
+ if (!json) return null;
1095
+ return parseGhStackViewJson(json);
1096
+ }
1097
+ async function mergePrStack(cwd, opts) {
1098
+ const status = await detectGhStack(cwd);
1099
+ if (!status.available) {
1100
+ throw new Error(status.reason);
1101
+ }
1102
+ const method = opts.method ?? "squash";
1103
+ const methodFlag = method === "rebase" ? "--rebase" : method === "merge" ? "--merge" : "--squash";
1104
+ const args = [
1105
+ "stack",
1106
+ "merge",
1107
+ String(opts.through),
1108
+ "--yes",
1109
+ methodFlag
1110
+ ];
1111
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
1112
+ if (exitCode !== 0) {
1113
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack merge failed");
1114
+ }
1115
+ return { stdout };
1116
+ }
1117
+ async function initPrStack(cwd, branches, opts) {
1118
+ if (!branches.length) throw new Error("initPrStack requires at least one branch name");
1119
+ const status = await detectGhStack(cwd);
1120
+ if (!status.available) throw new Error(status.reason);
1121
+ const args = ["stack", "init"];
1122
+ if (opts?.base) args.push("--base", opts.base);
1123
+ args.push(...branches);
1124
+ const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
1125
+ if (exitCode !== 0) {
1126
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack init failed");
1127
+ }
1128
+ }
1129
+ async function addPrStackLayer(cwd, branchName) {
1130
+ if (!branchName.trim()) throw new Error("branch name required");
1131
+ const status = await detectGhStack(cwd);
1132
+ if (!status.available) throw new Error(status.reason);
1133
+ const { exitCode, stderr, stdout } = await gh(
1134
+ ["stack", "add", branchName.trim()],
1135
+ cwd,
1136
+ { reject: false }
1137
+ );
1138
+ if (exitCode !== 0) {
1139
+ throw new Error(stderr.trim() || stdout.trim() || "gh stack add failed");
1140
+ }
1141
+ }
1142
+
967
1143
  // src/git/worktree.ts
968
1144
  async function lookupGithubGraphqlReset(cwd) {
969
1145
  const result = await gh(["api", "rate_limit"], cwd, { reject: false });
@@ -1471,18 +1647,31 @@ async function fetchPrHead(repoPath, number, localBranch) {
1471
1647
  const slug = await resolveGithubRepoSlug(repoPath);
1472
1648
  const refspec = `+pull/${number}/head:${localBranch}`;
1473
1649
  const errors = [];
1474
- const tryFetch = async (remote) => {
1475
- const result = await git(["fetch", remote, refspec], repoPath, {
1476
- reject: false
1477
- });
1650
+ const httpsRemote = slug ? `https://github.com/${slug}.git` : null;
1651
+ const tryFetch = async (remote, opts) => {
1652
+ const label = opts?.ghAuth ? `${remote} (gh auth)` : remote;
1653
+ const gitOpts = { reject: false };
1654
+ if (opts?.ghAuth) {
1655
+ const token = await resolveGhAuthToken(repoPath);
1656
+ if (!token) {
1657
+ errors.push(`${label}: gh auth token unavailable`);
1658
+ return false;
1659
+ }
1660
+ gitOpts.env = { GIT_TERMINAL_PROMPT: "0" };
1661
+ gitOpts.config = {
1662
+ "http.extraHeader": `AUTHORIZATION: bearer ${token}`
1663
+ };
1664
+ }
1665
+ const result = await git(["fetch", remote, refspec], repoPath, gitOpts);
1478
1666
  if (result.exitCode === 0) return true;
1479
1667
  const detail = (result.stderr || result.stdout).trim();
1480
- if (detail) errors.push(`${remote}: ${detail}`);
1668
+ if (detail) errors.push(`${label}: ${detail}`);
1481
1669
  return false;
1482
1670
  };
1483
1671
  let fetched = await tryFetch("origin");
1484
- if (!fetched && slug) {
1485
- fetched = await tryFetch(`https://github.com/${slug}.git`);
1672
+ if (!fetched && httpsRemote) {
1673
+ fetched = await tryFetch(httpsRemote);
1674
+ if (!fetched) fetched = await tryFetch(httpsRemote, { ghAuth: true });
1486
1675
  }
1487
1676
  const localOk = async () => {
1488
1677
  const verify = await git(["rev-parse", "--verify", localBranch], repoPath, {
@@ -1511,18 +1700,28 @@ async function fetchPrHead(repoPath, number, localBranch) {
1511
1700
  }
1512
1701
  }
1513
1702
  if (oid) {
1514
- const ensureOid = async (remote) => {
1515
- const got = await git(["fetch", remote, oid], repoPath, {
1516
- reject: false
1517
- });
1703
+ const ensureOid = async (remote, opts) => {
1704
+ const gitOpts = { reject: false };
1705
+ if (opts?.ghAuth) {
1706
+ const token = await resolveGhAuthToken(repoPath);
1707
+ if (!token) return false;
1708
+ gitOpts.env = { GIT_TERMINAL_PROMPT: "0" };
1709
+ gitOpts.config = {
1710
+ "http.extraHeader": `AUTHORIZATION: bearer ${token}`
1711
+ };
1712
+ }
1713
+ const got = await git(["fetch", remote, oid], repoPath, gitOpts);
1518
1714
  return got.exitCode === 0;
1519
1715
  };
1520
1716
  let haveObject = (await git(["cat-file", "-e", `${oid}^{commit}`], repoPath, {
1521
1717
  reject: false
1522
1718
  })).exitCode === 0;
1523
1719
  if (!haveObject) haveObject = await ensureOid("origin");
1524
- if (!haveObject && slug) {
1525
- haveObject = await ensureOid(`https://github.com/${slug}.git`);
1720
+ if (!haveObject && httpsRemote) {
1721
+ haveObject = await ensureOid(httpsRemote);
1722
+ if (!haveObject) {
1723
+ haveObject = await ensureOid(httpsRemote, { ghAuth: true });
1724
+ }
1526
1725
  }
1527
1726
  if (haveObject) {
1528
1727
  const branched = await git(["branch", "-f", localBranch, oid], repoPath, {
@@ -1617,6 +1816,52 @@ ${add.stdout}`;
1617
1816
  await ensureGhPreferOrigin(worktreePath);
1618
1817
  return { branchName, worktreePath };
1619
1818
  }
1819
+ async function createExistingBranchWorktree(opts) {
1820
+ const branchName = opts.branchName.trim();
1821
+ if (!branchName) throw new Error("branch name required");
1822
+ const worktreePath = join(worktreesRoot(opts.repoPath), opts.slug);
1823
+ if (existsSync(worktreePath)) {
1824
+ throw new Error(`Worktree already exists at ${worktreePath}`);
1825
+ }
1826
+ await ensureGhPreferOrigin(opts.repoPath);
1827
+ await git(["fetch", "origin", "--prune"], opts.repoPath, { reject: false });
1828
+ if (!branchName.startsWith("origin/") && !branchName.startsWith("refs/")) {
1829
+ await git(["fetch", "origin", branchName], opts.repoPath, { reject: false });
1830
+ }
1831
+ const existing = await listWorktrees(opts.repoPath);
1832
+ const already = existing.find((w) => w.branch === branchName);
1833
+ if (already?.path) {
1834
+ throw new Error(
1835
+ `Branch ${branchName} is already checked out at ${already.path}`
1836
+ );
1837
+ }
1838
+ const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
1839
+ const add = await git(
1840
+ ["worktree", "add", worktreePath, startPoint],
1841
+ opts.repoPath,
1842
+ { reject: false }
1843
+ );
1844
+ if (add.exitCode !== 0) {
1845
+ const retry = await git(
1846
+ ["worktree", "add", worktreePath, branchName],
1847
+ opts.repoPath,
1848
+ { reject: false }
1849
+ );
1850
+ if (retry.exitCode !== 0) {
1851
+ throw new Error(
1852
+ `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
1853
+ );
1854
+ }
1855
+ }
1856
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
1857
+ reject: false
1858
+ });
1859
+ if (head.stdout.trim() === "HEAD" || head.stdout.trim() !== branchName) {
1860
+ await git(["checkout", "-B", branchName], worktreePath, { reject: false });
1861
+ }
1862
+ await ensureGhPreferOrigin(worktreePath);
1863
+ return { branchName, worktreePath };
1864
+ }
1620
1865
  async function removeWorktree(repoPath, worktreePath, opts) {
1621
1866
  await git(["worktree", "remove", "--force", worktreePath], repoPath, {
1622
1867
  reject: false
@@ -1646,8 +1891,27 @@ async function listWorktrees(repoPath) {
1646
1891
  return entries;
1647
1892
  }
1648
1893
  async function isDirty(worktreePath) {
1649
- const { stdout } = await git(["status", "--porcelain"], worktreePath);
1650
- return stdout.trim().length > 0;
1894
+ const { stdout } = await git(["status", "--porcelain", "-uall"], worktreePath);
1895
+ for (const line of stdout.split("\n")) {
1896
+ if (!line.trim()) continue;
1897
+ const rel = porcelainStatusPath(line);
1898
+ if (!rel || isSideboardScratchPath(rel)) continue;
1899
+ return true;
1900
+ }
1901
+ return false;
1902
+ }
1903
+ function isSideboardScratchPath(relativePath) {
1904
+ return isWorkspaceScratchPath(relativePath);
1905
+ }
1906
+ function porcelainStatusPath(line) {
1907
+ const rest = line.length >= 3 ? line.slice(3) : "";
1908
+ if (!rest) return "";
1909
+ const arrow = rest.lastIndexOf(" -> ");
1910
+ const raw = arrow >= 0 ? rest.slice(arrow + 4) : rest;
1911
+ if (raw.startsWith('"') && raw.endsWith('"')) {
1912
+ return raw.slice(1, -1).replace(/\\([\\"])/g, "$1");
1913
+ }
1914
+ return raw;
1651
1915
  }
1652
1916
  async function currentBranch(worktreePath) {
1653
1917
  const { stdout } = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
@@ -1665,7 +1929,7 @@ async function pushBranch(worktreePath, branchName) {
1665
1929
  }
1666
1930
  async function mergePr(cwd, selector, opts) {
1667
1931
  const slug = await resolveGithubRepoSlug(cwd);
1668
- const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft"];
1932
+ const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft,number"];
1669
1933
  if (slug) viewArgs.push("--repo", slug);
1670
1934
  const before = await gh(viewArgs, cwd, { reject: false });
1671
1935
  if (before.exitCode !== 0 || !before.stdout.trim()) {
@@ -1673,16 +1937,29 @@ async function mergePr(cwd, selector, opts) {
1673
1937
  }
1674
1938
  let url = "";
1675
1939
  let isDraft = false;
1940
+ let prNumber = null;
1676
1941
  try {
1677
1942
  const parsed = JSON.parse(before.stdout);
1678
1943
  url = String(parsed.url ?? "");
1679
1944
  isDraft = Boolean(parsed.isDraft);
1945
+ prNumber = typeof parsed.number === "number" && Number.isFinite(parsed.number) ? parsed.number : null;
1680
1946
  if (String(parsed.state ?? "").toUpperCase() === "MERGED") {
1681
1947
  return { url, state: "MERGED" };
1682
1948
  }
1683
1949
  } catch {
1684
1950
  throw new Error("Could not parse pull request details");
1685
1951
  }
1952
+ const stack = await getPrStack(cwd);
1953
+ const stackLayer = stack && prNumber != null ? stack.layers.find((l) => l.prNumber === prNumber) : null;
1954
+ if (stack && stackLayer && prNumber != null) {
1955
+ const throughIndex = stack.layers.findIndex((l) => l.prNumber === prNumber);
1956
+ const gate = stackMergeReadiness(stack.layers, throughIndex);
1957
+ if (!gate.readyToMerge) {
1958
+ throw new Error(gate.blockedReason || "Stack is not ready to merge");
1959
+ }
1960
+ await mergePrStack(cwd, { through: prNumber, method: opts?.method ?? "squash" });
1961
+ return { url, state: "MERGED" };
1962
+ }
1686
1963
  if (isDraft) {
1687
1964
  const readyArgs = ["pr", "ready", selector];
1688
1965
  if (slug) readyArgs.push("--repo", slug);
@@ -1704,10 +1981,10 @@ async function mergePr(cwd, selector, opts) {
1704
1981
  const after = await gh(viewArgs, cwd, { reject: false });
1705
1982
  if (after.exitCode === 0 && after.stdout.trim()) {
1706
1983
  try {
1707
- const parsed = JSON.parse(after.stdout);
1984
+ const parsed = after.stdout ? JSON.parse(after.stdout) : null;
1708
1985
  return {
1709
- url: String(parsed.url ?? url),
1710
- state: String(parsed.state ?? "MERGED")
1986
+ url: String(parsed?.url ?? url),
1987
+ state: String(parsed?.state ?? "MERGED")
1711
1988
  };
1712
1989
  } catch {
1713
1990
  }
@@ -1857,6 +2134,10 @@ export {
1857
2134
  worktreeDisplayLabel,
1858
2135
  worktreeDisplayLabelForGroup,
1859
2136
  formatGhLandError,
2137
+ detectGhStack,
2138
+ getPrStack,
2139
+ initPrStack,
2140
+ addPrStackLayer,
1860
2141
  slugify,
1861
2142
  resolveRepoRoot,
1862
2143
  parseGithubSlugFromRemoteUrl,
@@ -1878,9 +2159,11 @@ export {
1878
2159
  fetchPrHead,
1879
2160
  resolveWorktreeStartPoint,
1880
2161
  createThreadWorktree,
2162
+ createExistingBranchWorktree,
1881
2163
  removeWorktree,
1882
2164
  listWorktrees,
1883
2165
  isDirty,
2166
+ isSideboardScratchPath,
1884
2167
  currentBranch,
1885
2168
  commitAll,
1886
2169
  pushBranch,
@@ -3,18 +3,18 @@ import {
3
3
  } from "./chunk-GI7E5733.js";
4
4
  import {
5
5
  ensureGlobalCoordinatorCwd
6
- } from "./chunk-ZQCQIWIP.js";
6
+ } from "./chunk-LRLKJM3O.js";
7
7
  import {
8
8
  allocateTeamName,
9
9
  takenSlugsFromThread,
10
10
  teamSlugFromName
11
- } from "./chunk-FV6FN6V5.js";
11
+ } from "./chunk-DZFH2KLT.js";
12
12
  import {
13
13
  createEmptyThread,
14
14
  listThreads,
15
15
  updateThread,
16
16
  writeThread
17
- } from "./chunk-O6W3P7V3.js";
17
+ } from "./chunk-TSRXOSVD.js";
18
18
  import {
19
19
  globalAgentCwd
20
20
  } from "./chunk-M37RITA6.js";
@@ -2,11 +2,11 @@
2
2
 
3
3
  import {
4
4
  isGlobalRepoPath
5
- } from "./chunk-DF5VQQKA.js";
5
+ } from "./chunk-YOWIYAVA.js";
6
6
  import {
7
7
  ensureGhPreferOrigin,
8
8
  resolveRepoRoot
9
- } from "./chunk-ZVV5EEQN.js";
9
+ } from "./chunk-XOU6HNQJ.js";
10
10
  import {
11
11
  appDataDir
12
12
  } from "./chunk-7MV3RXSC.js";
@@ -5,18 +5,18 @@ import {
5
5
  } from "./chunk-S2LL5HA4.js";
6
6
  import {
7
7
  ensureGlobalCoordinatorCwd
8
- } from "./chunk-GZXBYHJC.js";
8
+ } from "./chunk-UEAHMGHW.js";
9
9
  import {
10
10
  allocateTeamName,
11
11
  takenSlugsFromThread,
12
12
  teamSlugFromName
13
- } from "./chunk-ZVV5EEQN.js";
13
+ } from "./chunk-XOU6HNQJ.js";
14
14
  import {
15
15
  createEmptyThread,
16
16
  listThreads,
17
17
  updateThread,
18
18
  writeThread
19
- } from "./chunk-EOYDCKQC.js";
19
+ } from "./chunk-HLEX5AQ6.js";
20
20
  import {
21
21
  globalAgentCwd
22
22
  } from "./chunk-7MV3RXSC.js";
@@ -63,15 +63,33 @@ async function run(file, args, opts) {
63
63
  }
64
64
  }
65
65
  async function git(args, cwd, opts) {
66
- return run("git", args, { cwd, reject: opts?.reject });
66
+ const prefix = [];
67
+ if (opts?.config) {
68
+ for (const [key, value] of Object.entries(opts.config)) {
69
+ if (!key) continue;
70
+ prefix.push("-c", `${key}=${value}`);
71
+ }
72
+ }
73
+ return run("git", [...prefix, ...args], {
74
+ cwd,
75
+ reject: opts?.reject,
76
+ env: opts?.env
77
+ });
67
78
  }
68
79
  async function gh(args, cwd, opts) {
69
80
  return run("gh", args, { cwd, reject: opts?.reject });
70
81
  }
82
+ async function resolveGhAuthToken(cwd) {
83
+ const result = await gh(["auth", "token"], cwd, { reject: false });
84
+ if (result.exitCode !== 0) return null;
85
+ const token = result.stdout.trim();
86
+ return token || null;
87
+ }
71
88
 
72
89
  export {
73
90
  ensureAgentPath,
74
91
  run,
75
92
  git,
76
- gh
93
+ gh,
94
+ resolveGhAuthToken
77
95
  };
@@ -9,8 +9,8 @@ import {
9
9
  ensureConnectedBrightsyTeamTokens,
10
10
  getConnectedBrightsyTeamsRaw,
11
11
  listConnectedBrightsyTeams
12
- } from "./chunk-E35APBHV.js";
13
- import "./chunk-ISY4EGF6.js";
12
+ } from "./chunk-H6GGDLYS.js";
13
+ import "./chunk-ZNM4MAN4.js";
14
14
  import "./chunk-7MV3RXSC.js";
15
15
  export {
16
16
  applyConnectedTeamToCli,
@@ -7,9 +7,9 @@ import {
7
7
  ensureConnectedBrightsyTeamTokens,
8
8
  getConnectedBrightsyTeamsRaw,
9
9
  listConnectedBrightsyTeams
10
- } from "./chunk-ILQK4P5R.js";
10
+ } from "./chunk-BXJ76RHF.js";
11
11
  import "./chunk-M37RITA6.js";
12
- import "./chunk-AJ6ROGD7.js";
12
+ import "./chunk-7LNGIP3X.js";
13
13
  export {
14
14
  applyConnectedTeamToCli,
15
15
  brightsyMcpServerName,