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