@sideboard-ai/core 0.1.51 → 0.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agents-HIEJL3UV.js → agents-ON6RKKND.js} +1 -1
- package/dist/{agents-5ROTZNCX.js → agents-YKSS6VBO.js} +1 -1
- package/dist/{chunk-7EUWSBWR.js → chunk-6QTZVJ7A.js} +5 -3
- package/dist/chunk-B3SJXYIJ.js +24 -0
- package/dist/{chunk-5ZPSH7VI.js → chunk-D3METLRW.js} +5 -3
- package/dist/{chunk-ZH5QZ4CR.js → chunk-DZFH2KLT.js} +280 -7
- package/dist/chunk-FKOIHGKV.js +21 -0
- package/dist/{chunk-E4VVEKAM.js → chunk-GNML24AW.js} +2 -2
- package/dist/{chunk-EOYDCKQC.js → chunk-HLEX5AQ6.js} +4 -0
- package/dist/{chunk-K3WMKGFY.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-F4Q3IM6V.js → chunk-UEAHMGHW.js} +2 -1
- package/dist/{chunk-XRSAGVRW.js → chunk-XOU6HNQJ.js} +245 -7
- package/dist/{chunk-O5DOO7DP.js → chunk-XX5BB7NV.js} +3 -3
- package/dist/{chunk-QN7XNQAT.js → chunk-YDXQ72MD.js} +2 -2
- package/dist/{chunk-YFJ4FG2P.js → chunk-YOWIYAVA.js} +3 -3
- package/dist/{coordinator-prompt-7HHJRO7B.js → coordinator-prompt-6FXVTSFN.js} +4 -3
- package/dist/{coordinator-prompt-WD7FAMA2.js → coordinator-prompt-S6JZD5EF.js} +4 -3
- package/dist/{global-workspace-OJEPGDXA.js → global-workspace-EV4G2WMQ.js} +5 -4
- package/dist/{global-workspace-ECYN2MKL.js → global-workspace-MSX2K27Y.js} +5 -4
- package/dist/index.cjs +1288 -140
- package/dist/index.d.cts +388 -15
- package/dist/index.d.ts +388 -15
- package/dist/index.js +801 -84
- package/dist/mcp/run-stdio.cjs +1059 -132
- package/dist/mcp/run-stdio.js +628 -73
- package/dist/plan-file-6O7G4VPQ.js +23 -0
- package/dist/plan-file-PHVKUAEE.js +25 -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-MUU7RGVV.js → workspaces-3RQQZQRO.js} +6 -5
- package/dist/{workspaces-ZWOOFZUV.js → workspaces-AYTBR6KQ.js} +6 -5
- package/dist/{worktree-DVNDMWZ7.js → worktree-5KEQWSAF.js} +5 -2
- package/dist/{worktree-GDV56MX4.js → worktree-RWGL7FUV.js} +5 -2
- package/package.json +1 -1
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 ?? [],
|
|
@@ -1688,6 +1692,206 @@ var init_pr_gates = __esm({
|
|
|
1688
1692
|
}
|
|
1689
1693
|
});
|
|
1690
1694
|
|
|
1695
|
+
// src/git/stack.ts
|
|
1696
|
+
async function detectGhStack(cwd) {
|
|
1697
|
+
const now = Date.now();
|
|
1698
|
+
if (cachedStatus && now - cachedStatus.at < STATUS_TTL_MS) {
|
|
1699
|
+
return cachedStatus.status;
|
|
1700
|
+
}
|
|
1701
|
+
const probe = await gh(["stack", "view", "--help"], cwd, { reject: false });
|
|
1702
|
+
if (probe.exitCode === 0) {
|
|
1703
|
+
const status2 = { available: true };
|
|
1704
|
+
cachedStatus = { at: now, status: status2 };
|
|
1705
|
+
return status2;
|
|
1706
|
+
}
|
|
1707
|
+
const err = `${probe.stderr}
|
|
1708
|
+
${probe.stdout}`;
|
|
1709
|
+
const reason = /official extension|extension install|github\/gh-stack/i.test(err) ? "Install with: gh extension install github/gh-stack" : err.trim() || "gh stack is not available";
|
|
1710
|
+
const status = { available: false, reason };
|
|
1711
|
+
cachedStatus = { at: now, status };
|
|
1712
|
+
return status;
|
|
1713
|
+
}
|
|
1714
|
+
function str(v) {
|
|
1715
|
+
return typeof v === "string" ? v : v == null ? "" : String(v);
|
|
1716
|
+
}
|
|
1717
|
+
function num(v) {
|
|
1718
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1719
|
+
if (typeof v === "string" && v.trim() && Number.isFinite(Number(v))) {
|
|
1720
|
+
return Number(v);
|
|
1721
|
+
}
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
function parseGhStackViewJson(raw) {
|
|
1725
|
+
let data;
|
|
1726
|
+
try {
|
|
1727
|
+
data = JSON.parse(raw);
|
|
1728
|
+
} catch {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
if (!Array.isArray(data.branches) || data.branches.length === 0) return null;
|
|
1732
|
+
const trunk = str(data.trunk) || "main";
|
|
1733
|
+
const currentBranch2 = str(data.currentBranch);
|
|
1734
|
+
const stackNumber = num(data.stackNumber) ?? num(data.number);
|
|
1735
|
+
const layers = [];
|
|
1736
|
+
for (let i = 0; i < data.branches.length; i++) {
|
|
1737
|
+
const b = data.branches[i];
|
|
1738
|
+
if (!b || typeof b !== "object") continue;
|
|
1739
|
+
const name = str(b.name);
|
|
1740
|
+
if (!name) continue;
|
|
1741
|
+
const pr = b.pr && typeof b.pr === "object" ? b.pr : null;
|
|
1742
|
+
layers.push({
|
|
1743
|
+
position: i + 1,
|
|
1744
|
+
branchName: name,
|
|
1745
|
+
headSha: str(b.head) || void 0,
|
|
1746
|
+
baseSha: str(b.base) || void 0,
|
|
1747
|
+
isCurrent: Boolean(b.isCurrent) || name === currentBranch2,
|
|
1748
|
+
isMerged: Boolean(b.isMerged),
|
|
1749
|
+
isQueued: Boolean(b.isQueued),
|
|
1750
|
+
needsRebase: Boolean(b.needsRebase),
|
|
1751
|
+
prNumber: pr ? num(pr.number) : null,
|
|
1752
|
+
prUrl: pr && str(pr.url) ? str(pr.url) : null,
|
|
1753
|
+
prState: pr && str(pr.state) ? str(pr.state).toUpperCase() : null,
|
|
1754
|
+
title: pr && str(pr.title) ? str(pr.title) : void 0
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
if (!layers.length) return null;
|
|
1758
|
+
let currentIndex = layers.findIndex((l) => l.isCurrent);
|
|
1759
|
+
if (currentIndex < 0 && currentBranch2) {
|
|
1760
|
+
currentIndex = layers.findIndex((l) => l.branchName === currentBranch2);
|
|
1761
|
+
}
|
|
1762
|
+
const { readyToMerge, blockedReason } = stackMergeReadiness(layers, currentIndex);
|
|
1763
|
+
return {
|
|
1764
|
+
stackNumber,
|
|
1765
|
+
trunk,
|
|
1766
|
+
currentBranch: currentBranch2 || layers[currentIndex]?.branchName || layers[0].branchName,
|
|
1767
|
+
layers,
|
|
1768
|
+
currentIndex,
|
|
1769
|
+
readyToMerge,
|
|
1770
|
+
blockedReason
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
function stackMergeReadiness(layers, throughIndex) {
|
|
1774
|
+
if (throughIndex < 0 || throughIndex >= layers.length) {
|
|
1775
|
+
return { readyToMerge: false, blockedReason: "Not on a stack layer" };
|
|
1776
|
+
}
|
|
1777
|
+
for (let i = 0; i <= throughIndex; i++) {
|
|
1778
|
+
const layer = layers[i];
|
|
1779
|
+
if (layer.isMerged) continue;
|
|
1780
|
+
if (!layer.prNumber) {
|
|
1781
|
+
return {
|
|
1782
|
+
readyToMerge: false,
|
|
1783
|
+
blockedReason: `Layer ${layer.branchName} has no pull request yet`
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
if (layer.needsRebase) {
|
|
1787
|
+
return {
|
|
1788
|
+
readyToMerge: false,
|
|
1789
|
+
blockedReason: `PR #${layer.prNumber} needs rebase`
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
const state = (layer.prState ?? "").toUpperCase();
|
|
1793
|
+
if (state && state !== "OPEN" && state !== "QUEUED") {
|
|
1794
|
+
return {
|
|
1795
|
+
readyToMerge: false,
|
|
1796
|
+
blockedReason: `PR #${layer.prNumber} is ${state}`
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return { readyToMerge: true, blockedReason: null };
|
|
1801
|
+
}
|
|
1802
|
+
async function getPrStack(cwd) {
|
|
1803
|
+
const status = await detectGhStack(cwd);
|
|
1804
|
+
if (!status.available) return null;
|
|
1805
|
+
const result = await gh(["stack", "view", "--json"], cwd, { reject: false });
|
|
1806
|
+
if (result.exitCode === 2) return null;
|
|
1807
|
+
if (result.exitCode !== 0) {
|
|
1808
|
+
if (/not in a stack|no stack/i.test(`${result.stderr}
|
|
1809
|
+
${result.stdout}`)) {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
if (result.exitCode === 9) return null;
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
const json = result.stdout.trim();
|
|
1816
|
+
if (!json) return null;
|
|
1817
|
+
return parseGhStackViewJson(json);
|
|
1818
|
+
}
|
|
1819
|
+
async function mergePrStack(cwd, opts) {
|
|
1820
|
+
const status = await detectGhStack(cwd);
|
|
1821
|
+
if (!status.available) {
|
|
1822
|
+
throw new Error(status.reason);
|
|
1823
|
+
}
|
|
1824
|
+
const method = opts.method ?? "squash";
|
|
1825
|
+
const methodFlag = method === "rebase" ? "--rebase" : method === "merge" ? "--merge" : "--squash";
|
|
1826
|
+
const args = [
|
|
1827
|
+
"stack",
|
|
1828
|
+
"merge",
|
|
1829
|
+
String(opts.through),
|
|
1830
|
+
"--yes",
|
|
1831
|
+
methodFlag
|
|
1832
|
+
];
|
|
1833
|
+
const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
|
|
1834
|
+
if (exitCode !== 0) {
|
|
1835
|
+
throw new Error(stderr.trim() || stdout.trim() || "gh stack merge failed");
|
|
1836
|
+
}
|
|
1837
|
+
return { stdout };
|
|
1838
|
+
}
|
|
1839
|
+
async function initPrStack(cwd, branches, opts) {
|
|
1840
|
+
if (!branches.length) throw new Error("initPrStack requires at least one branch name");
|
|
1841
|
+
const status = await detectGhStack(cwd);
|
|
1842
|
+
if (!status.available) throw new Error(status.reason);
|
|
1843
|
+
const args = ["stack", "init"];
|
|
1844
|
+
if (opts?.base) args.push("--base", opts.base);
|
|
1845
|
+
args.push(...branches);
|
|
1846
|
+
const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
|
|
1847
|
+
if (exitCode !== 0) {
|
|
1848
|
+
throw new Error(stderr.trim() || stdout.trim() || "gh stack init failed");
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
async function addPrStackLayer(cwd, branchName) {
|
|
1852
|
+
if (!branchName.trim()) throw new Error("branch name required");
|
|
1853
|
+
const status = await detectGhStack(cwd);
|
|
1854
|
+
if (!status.available) throw new Error(status.reason);
|
|
1855
|
+
const { exitCode, stderr, stdout } = await gh(
|
|
1856
|
+
["stack", "add", branchName.trim()],
|
|
1857
|
+
cwd,
|
|
1858
|
+
{ reject: false }
|
|
1859
|
+
);
|
|
1860
|
+
if (exitCode !== 0) {
|
|
1861
|
+
throw new Error(stderr.trim() || stdout.trim() || "gh stack add failed");
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
var cachedStatus, STATUS_TTL_MS;
|
|
1865
|
+
var init_stack = __esm({
|
|
1866
|
+
"src/git/stack.ts"() {
|
|
1867
|
+
"use strict";
|
|
1868
|
+
init_run();
|
|
1869
|
+
cachedStatus = null;
|
|
1870
|
+
STATUS_TTL_MS = 6e4;
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
// src/paths/workspace-scratch.ts
|
|
1875
|
+
function attachmentsGitignoreBody() {
|
|
1876
|
+
return ATTACHMENTS_GITIGNORE;
|
|
1877
|
+
}
|
|
1878
|
+
function isWorkspaceScratchPath(relativePath) {
|
|
1879
|
+
const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
1880
|
+
return p === ATTACHMENTS_DIR || p.startsWith(`${ATTACHMENTS_DIR}/`) || p === LEGACY_ATTACHMENTS_DIR || p.startsWith(`${LEGACY_ATTACHMENTS_DIR}/`) || p === ".context" || p.startsWith(".context/");
|
|
1881
|
+
}
|
|
1882
|
+
var ATTACHMENTS_DIR, LEGACY_ATTACHMENTS_DIR, ATTACHMENTS_GITIGNORE;
|
|
1883
|
+
var init_workspace_scratch = __esm({
|
|
1884
|
+
"src/paths/workspace-scratch.ts"() {
|
|
1885
|
+
"use strict";
|
|
1886
|
+
ATTACHMENTS_DIR = ".context/attachments";
|
|
1887
|
+
LEGACY_ATTACHMENTS_DIR = ".sideboard/attachments";
|
|
1888
|
+
ATTACHMENTS_GITIGNORE = `# Sideboard / workspace attachments (local only)
|
|
1889
|
+
*
|
|
1890
|
+
!.gitignore
|
|
1891
|
+
`;
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1691
1895
|
// src/git/worktree.ts
|
|
1692
1896
|
var worktree_exports = {};
|
|
1693
1897
|
__export(worktree_exports, {
|
|
@@ -1697,6 +1901,7 @@ __export(worktree_exports, {
|
|
|
1697
1901
|
branchDisplayLabel: () => branchDisplayLabel,
|
|
1698
1902
|
collectTakenTeamSlugs: () => collectTakenTeamSlugs,
|
|
1699
1903
|
commitAll: () => commitAll,
|
|
1904
|
+
createExistingBranchWorktree: () => createExistingBranchWorktree,
|
|
1700
1905
|
createOrUpdatePr: () => createOrUpdatePr,
|
|
1701
1906
|
createThreadWorktree: () => createThreadWorktree,
|
|
1702
1907
|
currentBranch: () => currentBranch,
|
|
@@ -2410,6 +2615,52 @@ ${add.stdout}`;
|
|
|
2410
2615
|
await ensureGhPreferOrigin(worktreePath);
|
|
2411
2616
|
return { branchName, worktreePath };
|
|
2412
2617
|
}
|
|
2618
|
+
async function createExistingBranchWorktree(opts) {
|
|
2619
|
+
const branchName = opts.branchName.trim();
|
|
2620
|
+
if (!branchName) throw new Error("branch name required");
|
|
2621
|
+
const worktreePath = (0, import_node_path5.join)(worktreesRoot(opts.repoPath), opts.slug);
|
|
2622
|
+
if ((0, import_node_fs4.existsSync)(worktreePath)) {
|
|
2623
|
+
throw new Error(`Worktree already exists at ${worktreePath}`);
|
|
2624
|
+
}
|
|
2625
|
+
await ensureGhPreferOrigin(opts.repoPath);
|
|
2626
|
+
await git(["fetch", "origin", "--prune"], opts.repoPath, { reject: false });
|
|
2627
|
+
if (!branchName.startsWith("origin/") && !branchName.startsWith("refs/")) {
|
|
2628
|
+
await git(["fetch", "origin", branchName], opts.repoPath, { reject: false });
|
|
2629
|
+
}
|
|
2630
|
+
const existing = await listWorktrees(opts.repoPath);
|
|
2631
|
+
const already = existing.find((w) => w.branch === branchName);
|
|
2632
|
+
if (already?.path) {
|
|
2633
|
+
throw new Error(
|
|
2634
|
+
`Branch ${branchName} is already checked out at ${already.path}`
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
|
|
2638
|
+
const add = await git(
|
|
2639
|
+
["worktree", "add", worktreePath, startPoint],
|
|
2640
|
+
opts.repoPath,
|
|
2641
|
+
{ reject: false }
|
|
2642
|
+
);
|
|
2643
|
+
if (add.exitCode !== 0) {
|
|
2644
|
+
const retry = await git(
|
|
2645
|
+
["worktree", "add", worktreePath, branchName],
|
|
2646
|
+
opts.repoPath,
|
|
2647
|
+
{ reject: false }
|
|
2648
|
+
);
|
|
2649
|
+
if (retry.exitCode !== 0) {
|
|
2650
|
+
throw new Error(
|
|
2651
|
+
`Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
|
|
2656
|
+
reject: false
|
|
2657
|
+
});
|
|
2658
|
+
if (head.stdout.trim() === "HEAD" || head.stdout.trim() !== branchName) {
|
|
2659
|
+
await git(["checkout", "-B", branchName], worktreePath, { reject: false });
|
|
2660
|
+
}
|
|
2661
|
+
await ensureGhPreferOrigin(worktreePath);
|
|
2662
|
+
return { branchName, worktreePath };
|
|
2663
|
+
}
|
|
2413
2664
|
async function removeWorktree(repoPath, worktreePath, opts) {
|
|
2414
2665
|
await git(["worktree", "remove", "--force", worktreePath], repoPath, {
|
|
2415
2666
|
reject: false
|
|
@@ -2449,8 +2700,7 @@ async function isDirty(worktreePath) {
|
|
|
2449
2700
|
return false;
|
|
2450
2701
|
}
|
|
2451
2702
|
function isSideboardScratchPath(relativePath) {
|
|
2452
|
-
|
|
2453
|
-
return p === ".sideboard/attachments" || p.startsWith(".sideboard/attachments/");
|
|
2703
|
+
return isWorkspaceScratchPath(relativePath);
|
|
2454
2704
|
}
|
|
2455
2705
|
function porcelainStatusPath(line) {
|
|
2456
2706
|
const rest = line.length >= 3 ? line.slice(3) : "";
|
|
@@ -2478,7 +2728,7 @@ async function pushBranch(worktreePath, branchName) {
|
|
|
2478
2728
|
}
|
|
2479
2729
|
async function mergePr(cwd, selector, opts) {
|
|
2480
2730
|
const slug = await resolveGithubRepoSlug(cwd);
|
|
2481
|
-
const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft"];
|
|
2731
|
+
const viewArgs = ["pr", "view", selector, "--json", "url,state,isDraft,number"];
|
|
2482
2732
|
if (slug) viewArgs.push("--repo", slug);
|
|
2483
2733
|
const before = await gh(viewArgs, cwd, { reject: false });
|
|
2484
2734
|
if (before.exitCode !== 0 || !before.stdout.trim()) {
|
|
@@ -2486,16 +2736,29 @@ async function mergePr(cwd, selector, opts) {
|
|
|
2486
2736
|
}
|
|
2487
2737
|
let url = "";
|
|
2488
2738
|
let isDraft = false;
|
|
2739
|
+
let prNumber = null;
|
|
2489
2740
|
try {
|
|
2490
2741
|
const parsed = JSON.parse(before.stdout);
|
|
2491
2742
|
url = String(parsed.url ?? "");
|
|
2492
2743
|
isDraft = Boolean(parsed.isDraft);
|
|
2744
|
+
prNumber = typeof parsed.number === "number" && Number.isFinite(parsed.number) ? parsed.number : null;
|
|
2493
2745
|
if (String(parsed.state ?? "").toUpperCase() === "MERGED") {
|
|
2494
2746
|
return { url, state: "MERGED" };
|
|
2495
2747
|
}
|
|
2496
2748
|
} catch {
|
|
2497
2749
|
throw new Error("Could not parse pull request details");
|
|
2498
2750
|
}
|
|
2751
|
+
const stack = await getPrStack(cwd);
|
|
2752
|
+
const stackLayer = stack && prNumber != null ? stack.layers.find((l) => l.prNumber === prNumber) : null;
|
|
2753
|
+
if (stack && stackLayer && prNumber != null) {
|
|
2754
|
+
const throughIndex = stack.layers.findIndex((l) => l.prNumber === prNumber);
|
|
2755
|
+
const gate = stackMergeReadiness(stack.layers, throughIndex);
|
|
2756
|
+
if (!gate.readyToMerge) {
|
|
2757
|
+
throw new Error(gate.blockedReason || "Stack is not ready to merge");
|
|
2758
|
+
}
|
|
2759
|
+
await mergePrStack(cwd, { through: prNumber, method: opts?.method ?? "squash" });
|
|
2760
|
+
return { url, state: "MERGED" };
|
|
2761
|
+
}
|
|
2499
2762
|
if (isDraft) {
|
|
2500
2763
|
const readyArgs = ["pr", "ready", selector];
|
|
2501
2764
|
if (slug) readyArgs.push("--repo", slug);
|
|
@@ -2517,10 +2780,10 @@ async function mergePr(cwd, selector, opts) {
|
|
|
2517
2780
|
const after = await gh(viewArgs, cwd, { reject: false });
|
|
2518
2781
|
if (after.exitCode === 0 && after.stdout.trim()) {
|
|
2519
2782
|
try {
|
|
2520
|
-
const parsed = JSON.parse(after.stdout);
|
|
2783
|
+
const parsed = after.stdout ? JSON.parse(after.stdout) : null;
|
|
2521
2784
|
return {
|
|
2522
|
-
url: String(parsed
|
|
2523
|
-
state: String(parsed
|
|
2785
|
+
url: String(parsed?.url ?? url),
|
|
2786
|
+
state: String(parsed?.state ?? "MERGED")
|
|
2524
2787
|
};
|
|
2525
2788
|
} catch {
|
|
2526
2789
|
}
|
|
@@ -2668,7 +2931,9 @@ var init_worktree = __esm({
|
|
|
2668
2931
|
init_gh_errors();
|
|
2669
2932
|
init_run();
|
|
2670
2933
|
init_pr_gates();
|
|
2934
|
+
init_stack();
|
|
2671
2935
|
init_teams();
|
|
2936
|
+
init_workspace_scratch();
|
|
2672
2937
|
init_worktree_labels();
|
|
2673
2938
|
}
|
|
2674
2939
|
});
|
|
@@ -3346,6 +3611,7 @@ var init_coordinator_prompt = __esm({
|
|
|
3346
3611
|
"Discover:",
|
|
3347
3612
|
"- list_workspaces \u2014 registered repos (path + github slug when known)",
|
|
3348
3613
|
"- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
|
|
3614
|
+
"- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
|
|
3349
3615
|
"- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
|
|
3350
3616
|
"- list_threads / get_thread \u2014 fleet status (what is going on)",
|
|
3351
3617
|
"Workspaces:",
|
|
@@ -4331,7 +4597,9 @@ var init_injected_mcp = __esm({
|
|
|
4331
4597
|
SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS = [
|
|
4332
4598
|
"mcp__sideboard__present_artifact",
|
|
4333
4599
|
"mcp__sideboard__present_schema",
|
|
4334
|
-
"mcp__sideboard__present_files"
|
|
4600
|
+
"mcp__sideboard__present_files",
|
|
4601
|
+
"mcp__sideboard__ask_user",
|
|
4602
|
+
"mcp__sideboard__present_plan"
|
|
4335
4603
|
];
|
|
4336
4604
|
brightsyMcpCommandCache = null;
|
|
4337
4605
|
}
|
|
@@ -4370,7 +4638,7 @@ var PLAN_MODE_INSTRUCTION;
|
|
|
4370
4638
|
var init_types = __esm({
|
|
4371
4639
|
"src/agents/types.ts"() {
|
|
4372
4640
|
"use strict";
|
|
4373
|
-
PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or
|
|
4641
|
+
PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or Approves / Hands off the plan). Analyze the codebase, search and read files as needed, and produce or refine a clear implementation plan. Do not modify, create, or delete any project files except via Sideboard MCP present_plan (writes .context/attachments/plan.md). When you need a clarifying decision (approach forks, auth choice, scope): (1) first write a short chat message that explains the decision and what each option means (tradeoffs, when to pick it) \u2014 do not leave the user staring at bare labels; (2) then call Sideboard MCP ask_user with the same options, including a description on every option. Sideboard shows questions in the composer and mirrors them in chat. After ask_user, wait for the user's next message with their answers before finalizing the plan. When the plan is ready for approval: (1) call present_plan with the full markdown plan (title + content) so Sideboard saves .context/attachments/plan.md and shows it in chat for Approve / Hand off / Copy; (2) Claude should also call ExitPlanMode after present_plan. Do not skip present_plan \u2014 the plan must be a markdown file, not only chat prose.";
|
|
4374
4642
|
}
|
|
4375
4643
|
});
|
|
4376
4644
|
|
|
@@ -6078,6 +6346,116 @@ var init_workspaces = __esm({
|
|
|
6078
6346
|
}
|
|
6079
6347
|
});
|
|
6080
6348
|
|
|
6349
|
+
// src/plan/plan-present.ts
|
|
6350
|
+
function asRecord2(v) {
|
|
6351
|
+
return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
6352
|
+
}
|
|
6353
|
+
function isPresentPlanToolName(name) {
|
|
6354
|
+
if (!name) return false;
|
|
6355
|
+
return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
|
|
6356
|
+
}
|
|
6357
|
+
function extractPresentedPlan(parts) {
|
|
6358
|
+
if (!parts?.length) return null;
|
|
6359
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
6360
|
+
const p = parts[i];
|
|
6361
|
+
if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
|
|
6362
|
+
const input = asRecord2(p.input) ?? {};
|
|
6363
|
+
const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
|
|
6364
|
+
if (!content.trim()) continue;
|
|
6365
|
+
const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
|
|
6366
|
+
const path = typeof input.path === "string" && input.path.trim() ? input.path.trim() : PLAN_FILE_REL;
|
|
6367
|
+
return { title, content: content.trim(), path, source: "present_plan" };
|
|
6368
|
+
}
|
|
6369
|
+
return null;
|
|
6370
|
+
}
|
|
6371
|
+
function resolvePlanMarkdown(opts) {
|
|
6372
|
+
const fromTool = extractPresentedPlan(opts.parts);
|
|
6373
|
+
if (fromTool) return fromTool;
|
|
6374
|
+
const file = opts.fileContent?.trim();
|
|
6375
|
+
if (file) {
|
|
6376
|
+
return {
|
|
6377
|
+
title: "Plan",
|
|
6378
|
+
content: file,
|
|
6379
|
+
path: PLAN_FILE_REL,
|
|
6380
|
+
source: "exit_plan"
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6383
|
+
const text = opts.text?.trim();
|
|
6384
|
+
if (text && text.length >= 80) {
|
|
6385
|
+
return {
|
|
6386
|
+
title: "Plan",
|
|
6387
|
+
content: text,
|
|
6388
|
+
path: PLAN_FILE_REL,
|
|
6389
|
+
source: "text"
|
|
6390
|
+
};
|
|
6391
|
+
}
|
|
6392
|
+
return null;
|
|
6393
|
+
}
|
|
6394
|
+
var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
|
|
6395
|
+
var init_plan_present = __esm({
|
|
6396
|
+
"src/plan/plan-present.ts"() {
|
|
6397
|
+
"use strict";
|
|
6398
|
+
init_workspace_scratch();
|
|
6399
|
+
PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
|
|
6400
|
+
PLAN_FILE_NAME = "plan.md";
|
|
6401
|
+
LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
|
|
6402
|
+
}
|
|
6403
|
+
});
|
|
6404
|
+
|
|
6405
|
+
// src/plan/plan-file.ts
|
|
6406
|
+
var plan_file_exports = {};
|
|
6407
|
+
__export(plan_file_exports, {
|
|
6408
|
+
LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
|
|
6409
|
+
PLAN_FILE_NAME: () => PLAN_FILE_NAME,
|
|
6410
|
+
PLAN_FILE_REL: () => PLAN_FILE_REL,
|
|
6411
|
+
extractPresentedPlan: () => extractPresentedPlan,
|
|
6412
|
+
isPresentPlanToolName: () => isPresentPlanToolName,
|
|
6413
|
+
planFileAbs: () => planFileAbs,
|
|
6414
|
+
readPlanFile: () => readPlanFile,
|
|
6415
|
+
resolvePlanMarkdown: () => resolvePlanMarkdown,
|
|
6416
|
+
writePlanFile: () => writePlanFile
|
|
6417
|
+
});
|
|
6418
|
+
function ensureAttachmentsGitignore2(worktreePath) {
|
|
6419
|
+
const gitignoreAbs = (0, import_node_path24.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
|
|
6420
|
+
if ((0, import_node_fs26.existsSync)(gitignoreAbs)) return;
|
|
6421
|
+
(0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(gitignoreAbs), { recursive: true });
|
|
6422
|
+
(0, import_node_fs26.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
|
|
6423
|
+
}
|
|
6424
|
+
function planFileAbs(worktreePath) {
|
|
6425
|
+
return (0, import_node_path24.join)(worktreePath, PLAN_FILE_REL);
|
|
6426
|
+
}
|
|
6427
|
+
function readTextIfPresent2(abs) {
|
|
6428
|
+
if (!(0, import_node_fs26.existsSync)(abs)) return null;
|
|
6429
|
+
try {
|
|
6430
|
+
const content = (0, import_node_fs26.readFileSync)(abs, "utf8");
|
|
6431
|
+
return content.trim() ? content : null;
|
|
6432
|
+
} catch {
|
|
6433
|
+
return null;
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
function readPlanFile(worktreePath) {
|
|
6437
|
+
return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path24.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path24.join)(worktreePath, LEGACY_PLAN_FILE_REL));
|
|
6438
|
+
}
|
|
6439
|
+
function writePlanFile(worktreePath, content) {
|
|
6440
|
+
ensureAttachmentsGitignore2(worktreePath);
|
|
6441
|
+
const abs = planFileAbs(worktreePath);
|
|
6442
|
+
(0, import_node_fs26.mkdirSync)((0, import_node_path24.dirname)(abs), { recursive: true });
|
|
6443
|
+
const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
|
|
6444
|
+
(0, import_node_fs26.writeFileSync)(abs, body, "utf8");
|
|
6445
|
+
return PLAN_FILE_REL;
|
|
6446
|
+
}
|
|
6447
|
+
var import_node_fs26, import_node_path24;
|
|
6448
|
+
var init_plan_file = __esm({
|
|
6449
|
+
"src/plan/plan-file.ts"() {
|
|
6450
|
+
"use strict";
|
|
6451
|
+
import_node_fs26 = require("fs");
|
|
6452
|
+
import_node_path24 = require("path");
|
|
6453
|
+
init_workspace_scratch();
|
|
6454
|
+
init_plan_present();
|
|
6455
|
+
init_plan_present();
|
|
6456
|
+
}
|
|
6457
|
+
});
|
|
6458
|
+
|
|
6081
6459
|
// src/agents/cursor-recover.ts
|
|
6082
6460
|
var cursor_recover_exports = {};
|
|
6083
6461
|
__export(cursor_recover_exports, {
|
|
@@ -6086,10 +6464,10 @@ __export(cursor_recover_exports, {
|
|
|
6086
6464
|
function recoverFinishedCursorRun(opts) {
|
|
6087
6465
|
const agentId = opts.agentId.trim();
|
|
6088
6466
|
if (!agentId) return null;
|
|
6089
|
-
const runsPath = (0,
|
|
6090
|
-
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;
|
|
6091
6469
|
try {
|
|
6092
|
-
const lines = (0,
|
|
6470
|
+
const lines = (0, import_node_fs27.readFileSync)(runsPath, "utf8").split("\n");
|
|
6093
6471
|
let best = null;
|
|
6094
6472
|
for (const line of lines) {
|
|
6095
6473
|
const trimmed = line.trim();
|
|
@@ -6115,12 +6493,12 @@ function recoverFinishedCursorRun(opts) {
|
|
|
6115
6493
|
return null;
|
|
6116
6494
|
}
|
|
6117
6495
|
}
|
|
6118
|
-
var
|
|
6496
|
+
var import_node_fs27, import_node_path25;
|
|
6119
6497
|
var init_cursor_recover = __esm({
|
|
6120
6498
|
"src/agents/cursor-recover.ts"() {
|
|
6121
6499
|
"use strict";
|
|
6122
|
-
|
|
6123
|
-
|
|
6500
|
+
import_node_fs27 = require("fs");
|
|
6501
|
+
import_node_path25 = require("path");
|
|
6124
6502
|
init_paths();
|
|
6125
6503
|
}
|
|
6126
6504
|
});
|
|
@@ -6129,11 +6507,11 @@ var init_cursor_recover = __esm({
|
|
|
6129
6507
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6130
6508
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6131
6509
|
var import_zod = require("zod");
|
|
6132
|
-
var
|
|
6510
|
+
var import_node_path26 = require("path");
|
|
6133
6511
|
|
|
6134
6512
|
// src/orchestrator/orchestrator.ts
|
|
6135
6513
|
var import_node_events = require("events");
|
|
6136
|
-
var
|
|
6514
|
+
var import_node_fs28 = require("fs");
|
|
6137
6515
|
init_error_detail();
|
|
6138
6516
|
|
|
6139
6517
|
// src/agents/spawn.ts
|
|
@@ -6153,18 +6531,18 @@ function asRecord(input) {
|
|
|
6153
6531
|
}
|
|
6154
6532
|
return void 0;
|
|
6155
6533
|
}
|
|
6156
|
-
function
|
|
6534
|
+
function str2(v) {
|
|
6157
6535
|
return typeof v === "string" && v.trim() ? v : void 0;
|
|
6158
6536
|
}
|
|
6159
6537
|
function toolDetail(name, input) {
|
|
6160
6538
|
if (!input) return void 0;
|
|
6161
|
-
const command =
|
|
6539
|
+
const command = str2(input.command) ?? str2(input.cmd);
|
|
6162
6540
|
if (command) return command;
|
|
6163
|
-
const path =
|
|
6541
|
+
const path = str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
|
|
6164
6542
|
if (path) return path;
|
|
6165
|
-
const pattern =
|
|
6543
|
+
const pattern = str2(input.pattern) ?? str2(input.glob) ?? str2(input.glob_pattern);
|
|
6166
6544
|
if (pattern) return pattern;
|
|
6167
|
-
const query =
|
|
6545
|
+
const query = str2(input.query) ?? str2(input.prompt);
|
|
6168
6546
|
if (query) return query.length > 80 ? `${query.slice(0, 77)}\u2026` : query;
|
|
6169
6547
|
try {
|
|
6170
6548
|
const raw = JSON.stringify(input);
|
|
@@ -6177,23 +6555,26 @@ function toolDescription(name, input) {
|
|
|
6177
6555
|
const n = name.replace(/^mcp__/, "").replace(/__/g, " \xB7 ");
|
|
6178
6556
|
if (/^get_record_types$|recordTypes/i.test(name)) return "List record types";
|
|
6179
6557
|
if (/connectedAgentRequest/i.test(name)) {
|
|
6180
|
-
return
|
|
6558
|
+
return str2(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
|
|
6181
6559
|
}
|
|
6182
6560
|
if (/present_artifact$/i.test(name)) {
|
|
6183
|
-
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";
|
|
6184
6565
|
}
|
|
6185
6566
|
if (/present_schema$/i.test(name)) {
|
|
6186
|
-
return
|
|
6567
|
+
return str2(input?.title) ? `Schema ${str2(input?.title)}` : "Present schema";
|
|
6187
6568
|
}
|
|
6188
6569
|
if (/present_files$/i.test(name)) {
|
|
6189
|
-
return
|
|
6570
|
+
return str2(input?.title) ? `Files ${str2(input?.title)}` : "Present files";
|
|
6190
6571
|
}
|
|
6191
6572
|
if (/^(create|update)_artifact$/i.test(name.replace(/^mcp__[^_]+__/, ""))) {
|
|
6192
|
-
return
|
|
6573
|
+
return str2(input?.title) ? `Artifact ${str2(input?.title)}` : "Artifact";
|
|
6193
6574
|
}
|
|
6194
6575
|
if (!input) return n;
|
|
6195
|
-
if (/bash|shell|terminal/i.test(name) &&
|
|
6196
|
-
const cmd =
|
|
6576
|
+
if (/bash|shell|terminal/i.test(name) && str2(input.command)) {
|
|
6577
|
+
const cmd = str2(input.command);
|
|
6197
6578
|
if (/git\s+fetch/i.test(cmd)) return "Fetch latest from origin and check status";
|
|
6198
6579
|
if (/git\s+status/i.test(cmd)) return "Check git status";
|
|
6199
6580
|
if (/git\s+log/i.test(cmd)) return "Inspect recent commits";
|
|
@@ -6202,11 +6583,11 @@ function toolDescription(name, input) {
|
|
|
6202
6583
|
return "Run shell command";
|
|
6203
6584
|
}
|
|
6204
6585
|
if (/edit|write|apply/i.test(name)) {
|
|
6205
|
-
const path =
|
|
6586
|
+
const path = str2(input.file_path) ?? str2(input.path);
|
|
6206
6587
|
return path ? `Edit ${path.split("/").pop()}` : "Edit file";
|
|
6207
6588
|
}
|
|
6208
6589
|
if (/read/i.test(name)) {
|
|
6209
|
-
const path =
|
|
6590
|
+
const path = str2(input.file_path) ?? str2(input.path);
|
|
6210
6591
|
return path ? `Read ${path.split("/").pop()}` : "Read file";
|
|
6211
6592
|
}
|
|
6212
6593
|
if (/grep/i.test(name)) return "Search files";
|
|
@@ -6215,7 +6596,7 @@ function toolDescription(name, input) {
|
|
|
6215
6596
|
}
|
|
6216
6597
|
function toolFilePath(input) {
|
|
6217
6598
|
if (!input) return void 0;
|
|
6218
|
-
return
|
|
6599
|
+
return str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
|
|
6219
6600
|
}
|
|
6220
6601
|
function countLines(text) {
|
|
6221
6602
|
if (!text) return 0;
|
|
@@ -6223,8 +6604,8 @@ function countLines(text) {
|
|
|
6223
6604
|
}
|
|
6224
6605
|
function diffFromInput(input) {
|
|
6225
6606
|
if (!input) return {};
|
|
6226
|
-
const oldS =
|
|
6227
|
-
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);
|
|
6228
6609
|
if (oldS != null || newS != null) {
|
|
6229
6610
|
return {
|
|
6230
6611
|
additions: countLines(newS),
|
|
@@ -6486,6 +6867,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
6486
6867
|
// src/orchestrator/orchestrator.ts
|
|
6487
6868
|
init_agents();
|
|
6488
6869
|
init_worktree();
|
|
6870
|
+
init_stack();
|
|
6489
6871
|
|
|
6490
6872
|
// src/hook/conductor.ts
|
|
6491
6873
|
var import_node_fs13 = require("fs");
|
|
@@ -7121,7 +7503,7 @@ async function requireAgent(agent, opts) {
|
|
|
7121
7503
|
init_worktree();
|
|
7122
7504
|
init_thread_store();
|
|
7123
7505
|
init_workspaces();
|
|
7124
|
-
async function createThread(input,
|
|
7506
|
+
async function createThread(input, _onSetupLine) {
|
|
7125
7507
|
await requireAgent(input.agent);
|
|
7126
7508
|
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
7127
7509
|
if (!(0, import_node_fs18.existsSync)(repoPath)) {
|
|
@@ -7131,10 +7513,10 @@ async function createThread(input, onSetupLine) {
|
|
|
7131
7513
|
let sourceIsFork = false;
|
|
7132
7514
|
let prUrl = null;
|
|
7133
7515
|
if (input.sourceType === "pr") {
|
|
7134
|
-
const
|
|
7135
|
-
if (!Number.isFinite(
|
|
7136
|
-
const pr = await getPr(repoPath,
|
|
7137
|
-
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`);
|
|
7138
7520
|
sourceIsFork = pr.isCrossRepository;
|
|
7139
7521
|
prUrl = pr.url;
|
|
7140
7522
|
const localFetchBranch = `sideboard-pr-${pr.number}`;
|
|
@@ -7179,22 +7561,6 @@ async function createThread(input, onSetupLine) {
|
|
|
7179
7561
|
});
|
|
7180
7562
|
writeThread(thread);
|
|
7181
7563
|
await ensureWorkspace(repoPath);
|
|
7182
|
-
try {
|
|
7183
|
-
let setup = await runSetupScript(repoPath, worktreePath, onSetupLine);
|
|
7184
|
-
if (!setup.ran) {
|
|
7185
|
-
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onSetupLine);
|
|
7186
|
-
}
|
|
7187
|
-
if (setup.ran && setup.exitCode !== 0 && setup.exitCode !== null) {
|
|
7188
|
-
updateThread(thread.id, {
|
|
7189
|
-
lastError: `Setup exited ${setup.exitCode} (thread is still usable)`
|
|
7190
|
-
});
|
|
7191
|
-
}
|
|
7192
|
-
} catch (err) {
|
|
7193
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7194
|
-
updateThread(thread.id, {
|
|
7195
|
-
lastError: `Setup failed: ${message}`
|
|
7196
|
-
});
|
|
7197
|
-
}
|
|
7198
7564
|
return readThread(thread.id) ?? thread;
|
|
7199
7565
|
}
|
|
7200
7566
|
async function listLinearIssues(agent, repoPath) {
|
|
@@ -7526,7 +7892,9 @@ function worktreeBindingFrom(from) {
|
|
|
7526
7892
|
sourceIsFork: from.sourceIsFork,
|
|
7527
7893
|
parentThreadId: from.parentThreadId,
|
|
7528
7894
|
prUrl: from.prUrl,
|
|
7529
|
-
prTitle: from.prTitle
|
|
7895
|
+
prTitle: from.prTitle,
|
|
7896
|
+
stackId: from.stackId,
|
|
7897
|
+
stackLayer: from.stackLayer
|
|
7530
7898
|
};
|
|
7531
7899
|
}
|
|
7532
7900
|
function threadsSharingWorktree(worktreePath) {
|
|
@@ -7724,15 +8092,13 @@ File: src/client/frontends/desktop/core/UserData.ts
|
|
|
7724
8092
|
`;
|
|
7725
8093
|
|
|
7726
8094
|
// src/review/request-review.ts
|
|
8095
|
+
init_workspace_scratch();
|
|
7727
8096
|
var REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
7728
8097
|
var REPO_REVIEW_NAME = "review.md";
|
|
7729
|
-
var REVIEW_REQUEST_PATH =
|
|
8098
|
+
var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
8099
|
+
var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
7730
8100
|
var REVIEW_REQUEST_NAME = "Review request.md";
|
|
7731
8101
|
var REVIEW_REQUEST_PREFILL = "Review.";
|
|
7732
|
-
var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
|
|
7733
|
-
*
|
|
7734
|
-
!.gitignore
|
|
7735
|
-
`;
|
|
7736
8102
|
var LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
7737
8103
|
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
7738
8104
|
"HOW MANY FINDINGS TO RETURN:"
|
|
@@ -7755,10 +8121,10 @@ function readTextIfPresent(abs) {
|
|
|
7755
8121
|
}
|
|
7756
8122
|
}
|
|
7757
8123
|
function ensureAttachmentsGitignore(worktreePath) {
|
|
7758
|
-
const gitignoreAbs = (0, import_node_path18.join)(worktreePath,
|
|
8124
|
+
const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
|
|
7759
8125
|
if ((0, import_node_fs19.existsSync)(gitignoreAbs)) return;
|
|
7760
8126
|
(0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(gitignoreAbs), { recursive: true });
|
|
7761
|
-
(0, import_node_fs19.writeFileSync)(gitignoreAbs,
|
|
8127
|
+
(0, import_node_fs19.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
|
|
7762
8128
|
}
|
|
7763
8129
|
function resolveReviewGuidelines(worktreePath) {
|
|
7764
8130
|
const repoAbs = (0, import_node_path18.join)(worktreePath, REPO_REVIEW_PATH);
|
|
@@ -7781,6 +8147,16 @@ function resolveReviewGuidelines(worktreePath) {
|
|
|
7781
8147
|
source: "local"
|
|
7782
8148
|
};
|
|
7783
8149
|
}
|
|
8150
|
+
const legacyAbs = (0, import_node_path18.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
8151
|
+
const legacyContent = readTextIfPresent(legacyAbs);
|
|
8152
|
+
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
8153
|
+
return {
|
|
8154
|
+
path: LEGACY_REVIEW_REQUEST_PATH,
|
|
8155
|
+
name: REVIEW_REQUEST_NAME,
|
|
8156
|
+
content: legacyContent,
|
|
8157
|
+
source: "local"
|
|
8158
|
+
};
|
|
8159
|
+
}
|
|
7784
8160
|
ensureAttachmentsGitignore(worktreePath);
|
|
7785
8161
|
(0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(localAbs), { recursive: true });
|
|
7786
8162
|
(0, import_node_fs19.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
|
|
@@ -8260,16 +8636,305 @@ async function importConductorWorkspaceAsync(workspaceId) {
|
|
|
8260
8636
|
return importConductorWorkspace(workspaceId);
|
|
8261
8637
|
}
|
|
8262
8638
|
|
|
8639
|
+
// src/threads/stack-layers.ts
|
|
8640
|
+
var import_node_fs21 = require("fs");
|
|
8641
|
+
init_run();
|
|
8642
|
+
init_stack();
|
|
8643
|
+
init_worktree();
|
|
8644
|
+
init_thread_store();
|
|
8645
|
+
init_workspaces();
|
|
8646
|
+
function stackIdFrom(stack) {
|
|
8647
|
+
if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
|
|
8648
|
+
const key = stack.layers.map((l) => l.branchName).join("|");
|
|
8649
|
+
if (!key) return null;
|
|
8650
|
+
return `gh-stack-local-${hashShort(key)}`;
|
|
8651
|
+
}
|
|
8652
|
+
function hashShort(s) {
|
|
8653
|
+
let h = 0;
|
|
8654
|
+
for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
|
|
8655
|
+
return Math.abs(h).toString(36);
|
|
8656
|
+
}
|
|
8657
|
+
function requireThread3(idOrRef) {
|
|
8658
|
+
const thread = findThreadByRef(idOrRef) ?? readThread(idOrRef);
|
|
8659
|
+
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
8660
|
+
return thread;
|
|
8661
|
+
}
|
|
8662
|
+
function sanitizeSlugPart(name) {
|
|
8663
|
+
return name.trim().replace(/^refs\/heads\//, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "layer";
|
|
8664
|
+
}
|
|
8665
|
+
function layerSlug(stackId, layer) {
|
|
8666
|
+
const stackPart = stackId.replace(/^gh-stack-/, "s");
|
|
8667
|
+
return sanitizeSlugPart(`${stackPart}-L${layer.position}-${layer.branchName}`);
|
|
8668
|
+
}
|
|
8669
|
+
function findThreadForStackLayer(repoPath, stackId, layer) {
|
|
8670
|
+
const threads = listThreads({ includeArchived: false }).filter(
|
|
8671
|
+
(t) => t.repoPath === repoPath && t.stackId === stackId
|
|
8672
|
+
);
|
|
8673
|
+
const byLayer = threads.find((t) => t.stackLayer === layer.position);
|
|
8674
|
+
if (byLayer) return byLayer;
|
|
8675
|
+
return threads.find((t) => t.branchName === layer.branchName) ?? listThreads({ includeArchived: false }).find(
|
|
8676
|
+
(t) => t.repoPath === repoPath && t.branchName === layer.branchName
|
|
8677
|
+
) ?? null;
|
|
8678
|
+
}
|
|
8679
|
+
async function openStackLayer(input, _onSetupLine) {
|
|
8680
|
+
await requireAgent(input.agent);
|
|
8681
|
+
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
8682
|
+
const stackId = stackIdFrom(input.stack);
|
|
8683
|
+
if (!stackId) throw new Error("Cannot open stack layer without a stack id");
|
|
8684
|
+
const existing = findThreadForStackLayer(repoPath, stackId, input.layer);
|
|
8685
|
+
if (existing) {
|
|
8686
|
+
const patch = {};
|
|
8687
|
+
if (existing.stackId !== stackId) patch.stackId = stackId;
|
|
8688
|
+
if (existing.stackLayer !== input.layer.position) {
|
|
8689
|
+
patch.stackLayer = input.layer.position;
|
|
8690
|
+
}
|
|
8691
|
+
if (input.layer.prUrl && existing.prUrl !== input.layer.prUrl) {
|
|
8692
|
+
patch.prUrl = input.layer.prUrl;
|
|
8693
|
+
}
|
|
8694
|
+
if (input.layer.title && existing.prTitle !== input.layer.title) {
|
|
8695
|
+
patch.prTitle = input.layer.title;
|
|
8696
|
+
}
|
|
8697
|
+
if (Object.keys(patch).length > 0) updateThread(existing.id, patch);
|
|
8698
|
+
return {
|
|
8699
|
+
thread: readThread(existing.id) ?? existing,
|
|
8700
|
+
createdWorktree: false
|
|
8701
|
+
};
|
|
8702
|
+
}
|
|
8703
|
+
let worktreePath = null;
|
|
8704
|
+
let branchName = input.layer.branchName;
|
|
8705
|
+
let createdWorktree = false;
|
|
8706
|
+
const trees = await listWorktrees(repoPath);
|
|
8707
|
+
const checkedOut = trees.find((w) => w.branch === branchName);
|
|
8708
|
+
if (checkedOut?.path && (0, import_node_fs21.existsSync)(checkedOut.path)) {
|
|
8709
|
+
if (input.reuseExistingWorktree !== false) {
|
|
8710
|
+
worktreePath = checkedOut.path;
|
|
8711
|
+
} else {
|
|
8712
|
+
throw new Error(
|
|
8713
|
+
`Branch ${branchName} is already checked out at ${checkedOut.path}`
|
|
8714
|
+
);
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
if (!worktreePath) {
|
|
8718
|
+
const slug = layerSlug(stackId, input.layer);
|
|
8719
|
+
const created = await createExistingBranchWorktree({
|
|
8720
|
+
repoPath,
|
|
8721
|
+
branchName,
|
|
8722
|
+
slug
|
|
8723
|
+
});
|
|
8724
|
+
worktreePath = created.worktreePath;
|
|
8725
|
+
branchName = created.branchName;
|
|
8726
|
+
copyConfiguredFiles(repoPath, worktreePath);
|
|
8727
|
+
createdWorktree = true;
|
|
8728
|
+
}
|
|
8729
|
+
const title = input.layer.title?.trim() || (input.layer.prNumber != null ? `PR #${input.layer.prNumber}` : input.layer.branchName);
|
|
8730
|
+
const thread = createEmptyThread({
|
|
8731
|
+
title,
|
|
8732
|
+
userSetTitle: Boolean(input.layer.title?.trim()),
|
|
8733
|
+
sourceType: input.layer.prNumber != null ? "pr" : "branch",
|
|
8734
|
+
sourceRef: input.layer.prNumber != null ? String(input.layer.prNumber) : input.layer.branchName,
|
|
8735
|
+
branchName,
|
|
8736
|
+
worktreePath,
|
|
8737
|
+
repoPath,
|
|
8738
|
+
agent: input.agent,
|
|
8739
|
+
autonomy: input.autonomy ?? "default",
|
|
8740
|
+
model: input.model ?? null,
|
|
8741
|
+
effort: input.effort ?? "high",
|
|
8742
|
+
fast: Boolean(input.fast),
|
|
8743
|
+
planMode: Boolean(input.planMode),
|
|
8744
|
+
parentThreadId: input.parentThreadId ?? null,
|
|
8745
|
+
status: "idle",
|
|
8746
|
+
prUrl: input.layer.prUrl,
|
|
8747
|
+
prTitle: input.layer.title ?? null,
|
|
8748
|
+
stackId,
|
|
8749
|
+
stackLayer: input.layer.position
|
|
8750
|
+
});
|
|
8751
|
+
writeThread(thread);
|
|
8752
|
+
await ensureWorkspace(repoPath);
|
|
8753
|
+
return { thread: readThread(thread.id) ?? thread, createdWorktree };
|
|
8754
|
+
}
|
|
8755
|
+
async function openPrStackLayers(input, onSetupLine) {
|
|
8756
|
+
const from = requireThread3(input.threadRef);
|
|
8757
|
+
if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
|
|
8758
|
+
throw new Error("Thread has no worktree");
|
|
8759
|
+
}
|
|
8760
|
+
const stack = await getPrStack(from.worktreePath);
|
|
8761
|
+
if (!stack) throw new Error("Current branch is not part of a GitHub PR stack");
|
|
8762
|
+
const layers = input.layer != null ? stack.layers.filter((l) => l.position === input.layer) : stack.layers;
|
|
8763
|
+
if (!layers.length) {
|
|
8764
|
+
throw new Error(
|
|
8765
|
+
input.layer != null ? `No stack layer at position ${input.layer}` : "Stack has no layers"
|
|
8766
|
+
);
|
|
8767
|
+
}
|
|
8768
|
+
const threads = [];
|
|
8769
|
+
const createdThreadIds = [];
|
|
8770
|
+
for (const layer of layers) {
|
|
8771
|
+
const { thread, createdWorktree } = await openStackLayer(
|
|
8772
|
+
{
|
|
8773
|
+
repoPath: from.repoPath,
|
|
8774
|
+
stack,
|
|
8775
|
+
layer,
|
|
8776
|
+
agent: from.agent,
|
|
8777
|
+
autonomy: from.autonomy,
|
|
8778
|
+
model: from.model,
|
|
8779
|
+
effort: from.effort,
|
|
8780
|
+
fast: from.fast,
|
|
8781
|
+
planMode: from.planMode,
|
|
8782
|
+
parentThreadId: from.id
|
|
8783
|
+
},
|
|
8784
|
+
onSetupLine
|
|
8785
|
+
);
|
|
8786
|
+
threads.push(thread);
|
|
8787
|
+
if (createdWorktree) createdThreadIds.push(thread.id);
|
|
8788
|
+
}
|
|
8789
|
+
const stackId = stackIdFrom(stack);
|
|
8790
|
+
const current = stack.layers[stack.currentIndex];
|
|
8791
|
+
if (stackId) {
|
|
8792
|
+
updateThread(from.id, {
|
|
8793
|
+
stackId,
|
|
8794
|
+
stackLayer: current?.position ?? from.stackLayer
|
|
8795
|
+
});
|
|
8796
|
+
}
|
|
8797
|
+
return { stack, threads, createdThreadIds };
|
|
8798
|
+
}
|
|
8799
|
+
async function addStackLayerFromThread(input, onSetupLine) {
|
|
8800
|
+
const from = requireThread3(input.threadRef);
|
|
8801
|
+
if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
|
|
8802
|
+
throw new Error("Thread has no worktree");
|
|
8803
|
+
}
|
|
8804
|
+
const status = await detectGhStack(from.worktreePath);
|
|
8805
|
+
if (!status.available) throw new Error(status.reason);
|
|
8806
|
+
await addPrStackLayer(from.worktreePath, input.branchName);
|
|
8807
|
+
const stack = await getPrStack(from.worktreePath);
|
|
8808
|
+
if (!stack) throw new Error("Stack not found after adding layer");
|
|
8809
|
+
const layer = stack.layers.find((l) => l.branchName === input.branchName.trim()) ?? stack.layers[stack.layers.length - 1];
|
|
8810
|
+
if (!layer) throw new Error("New stack layer not found");
|
|
8811
|
+
if (input.title?.trim()) {
|
|
8812
|
+
layer.title = input.title.trim();
|
|
8813
|
+
}
|
|
8814
|
+
const thread = await openStackLayer(
|
|
8815
|
+
{
|
|
8816
|
+
repoPath: from.repoPath,
|
|
8817
|
+
stack,
|
|
8818
|
+
layer,
|
|
8819
|
+
agent: from.agent,
|
|
8820
|
+
autonomy: from.autonomy,
|
|
8821
|
+
model: from.model,
|
|
8822
|
+
effort: from.effort,
|
|
8823
|
+
fast: from.fast,
|
|
8824
|
+
planMode: from.planMode,
|
|
8825
|
+
parentThreadId: from.id
|
|
8826
|
+
},
|
|
8827
|
+
onSetupLine
|
|
8828
|
+
);
|
|
8829
|
+
return { stack, thread: thread.thread, createdWorktree: thread.createdWorktree };
|
|
8830
|
+
}
|
|
8831
|
+
async function initStackFromThread(input, onSetupLine) {
|
|
8832
|
+
const from = requireThread3(input.threadRef);
|
|
8833
|
+
if (!from.worktreePath?.trim() || !from.branchName?.trim() || !from.repoPath?.trim()) {
|
|
8834
|
+
throw new Error("Thread has no worktree/branch");
|
|
8835
|
+
}
|
|
8836
|
+
const status = await detectGhStack(from.worktreePath);
|
|
8837
|
+
if (!status.available) throw new Error(status.reason);
|
|
8838
|
+
const branches = [
|
|
8839
|
+
from.branchName,
|
|
8840
|
+
...(input.additionalBranches ?? []).map((b) => b.trim()).filter(Boolean)
|
|
8841
|
+
];
|
|
8842
|
+
await initPrStack(from.worktreePath, branches, {
|
|
8843
|
+
base: input.base ?? await resolveDefaultBranch(from.repoPath)
|
|
8844
|
+
});
|
|
8845
|
+
return openPrStackLayers({ threadRef: from.id }, onSetupLine);
|
|
8846
|
+
}
|
|
8847
|
+
async function createPrStack(input, onSetupLine) {
|
|
8848
|
+
await requireAgent(input.agent);
|
|
8849
|
+
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
8850
|
+
if (!(0, import_node_fs21.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
|
|
8851
|
+
if (!input.branches.length) throw new Error("At least one branch name required");
|
|
8852
|
+
const status = await detectGhStack(repoPath);
|
|
8853
|
+
if (!status.available) throw new Error(status.reason);
|
|
8854
|
+
const team = allocateTeamSlug(repoPath);
|
|
8855
|
+
const base = input.base ?? await resolveDefaultBranch(repoPath);
|
|
8856
|
+
const bootstrap = await createThreadWorktree({
|
|
8857
|
+
repoPath,
|
|
8858
|
+
sourceRef: base,
|
|
8859
|
+
slug: `${team.slug}-stack-init`
|
|
8860
|
+
});
|
|
8861
|
+
copyConfiguredFiles(repoPath, bootstrap.worktreePath);
|
|
8862
|
+
try {
|
|
8863
|
+
await initPrStack(bootstrap.worktreePath, input.branches, { base });
|
|
8864
|
+
const bottom = input.branches[0];
|
|
8865
|
+
const co = await git(["checkout", bottom], bootstrap.worktreePath, {
|
|
8866
|
+
reject: false
|
|
8867
|
+
});
|
|
8868
|
+
if (co.exitCode !== 0) {
|
|
8869
|
+
throw new Error(
|
|
8870
|
+
co.stderr.trim() || co.stdout.trim() || `Could not check out ${bottom} after stack init`
|
|
8871
|
+
);
|
|
8872
|
+
}
|
|
8873
|
+
} catch (err) {
|
|
8874
|
+
try {
|
|
8875
|
+
await removeWorktree(repoPath, bootstrap.worktreePath, {
|
|
8876
|
+
deleteBranch: bootstrap.branchName
|
|
8877
|
+
});
|
|
8878
|
+
} catch {
|
|
8879
|
+
}
|
|
8880
|
+
throw err;
|
|
8881
|
+
}
|
|
8882
|
+
const stack = await getPrStack(bootstrap.worktreePath);
|
|
8883
|
+
if (!stack) {
|
|
8884
|
+
try {
|
|
8885
|
+
await removeWorktree(repoPath, bootstrap.worktreePath, {
|
|
8886
|
+
deleteBranch: bootstrap.branchName
|
|
8887
|
+
});
|
|
8888
|
+
} catch {
|
|
8889
|
+
}
|
|
8890
|
+
throw new Error("Stack init succeeded but gh stack view returned no stack");
|
|
8891
|
+
}
|
|
8892
|
+
const threads = [];
|
|
8893
|
+
const createdThreadIds = [];
|
|
8894
|
+
for (const layer of stack.layers) {
|
|
8895
|
+
const title = layer.position === 1 && input.title?.trim() ? input.title.trim() : layer.title;
|
|
8896
|
+
const opened = await openStackLayer(
|
|
8897
|
+
{
|
|
8898
|
+
repoPath,
|
|
8899
|
+
stack,
|
|
8900
|
+
layer: title ? { ...layer, title } : layer,
|
|
8901
|
+
agent: input.agent,
|
|
8902
|
+
autonomy: input.autonomy,
|
|
8903
|
+
model: input.model,
|
|
8904
|
+
effort: input.effort,
|
|
8905
|
+
fast: input.fast,
|
|
8906
|
+
planMode: input.planMode,
|
|
8907
|
+
reuseExistingWorktree: true
|
|
8908
|
+
},
|
|
8909
|
+
onSetupLine
|
|
8910
|
+
);
|
|
8911
|
+
threads.push(opened.thread);
|
|
8912
|
+
if (opened.createdWorktree || opened.thread.worktreePath === bootstrap.worktreePath) {
|
|
8913
|
+
createdThreadIds.push(opened.thread.id);
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
const claimed = new Set(threads.map((t) => t.worktreePath));
|
|
8917
|
+
if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs21.existsSync)(bootstrap.worktreePath)) {
|
|
8918
|
+
try {
|
|
8919
|
+
await removeWorktree(repoPath, bootstrap.worktreePath, {
|
|
8920
|
+
deleteBranch: bootstrap.branchName
|
|
8921
|
+
});
|
|
8922
|
+
} catch {
|
|
8923
|
+
}
|
|
8924
|
+
}
|
|
8925
|
+
return { stack, threads, createdThreadIds };
|
|
8926
|
+
}
|
|
8927
|
+
|
|
8263
8928
|
// src/land/land.ts
|
|
8264
8929
|
init_worktree();
|
|
8265
8930
|
|
|
8266
8931
|
// src/diff/diff.ts
|
|
8267
|
-
var
|
|
8932
|
+
var import_node_fs22 = require("fs");
|
|
8268
8933
|
var import_node_path20 = require("path");
|
|
8269
8934
|
init_run();
|
|
8270
8935
|
init_worktree();
|
|
8271
8936
|
async function inspectGitWorktree(worktreePath) {
|
|
8272
|
-
if (!worktreePath || !(0,
|
|
8937
|
+
if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) return "missing_worktree";
|
|
8273
8938
|
const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
|
|
8274
8939
|
reject: false
|
|
8275
8940
|
});
|
|
@@ -8277,7 +8942,7 @@ async function inspectGitWorktree(worktreePath) {
|
|
|
8277
8942
|
return "ok";
|
|
8278
8943
|
}
|
|
8279
8944
|
async function initializeGitRepository(worktreePath) {
|
|
8280
|
-
if (!worktreePath || !(0,
|
|
8945
|
+
if (!worktreePath || !(0, import_node_fs22.existsSync)(worktreePath)) {
|
|
8281
8946
|
throw new Error("Worktree not found");
|
|
8282
8947
|
}
|
|
8283
8948
|
const status = await inspectGitWorktree(worktreePath);
|
|
@@ -8573,33 +9238,33 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
8573
9238
|
let combinedDiff = "";
|
|
8574
9239
|
let labelBase = base;
|
|
8575
9240
|
if (scope === "staged") {
|
|
8576
|
-
const [ns,
|
|
9241
|
+
const [ns, num2, diff] = await Promise.all([
|
|
8577
9242
|
git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
|
|
8578
9243
|
git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
|
|
8579
9244
|
git(["diff", "--cached"], worktreePath, { reject: false })
|
|
8580
9245
|
]);
|
|
8581
9246
|
nameStatus = ns.stdout;
|
|
8582
|
-
numstat =
|
|
9247
|
+
numstat = num2.stdout;
|
|
8583
9248
|
combinedDiff = diff.stdout;
|
|
8584
9249
|
labelBase = "staged";
|
|
8585
9250
|
} else if (scope === "unstaged") {
|
|
8586
|
-
const [ns,
|
|
9251
|
+
const [ns, num2, diff] = await Promise.all([
|
|
8587
9252
|
git(["diff", "--name-status"], worktreePath, { reject: false }),
|
|
8588
9253
|
git(["diff", "--numstat"], worktreePath, { reject: false }),
|
|
8589
9254
|
git(["diff"], worktreePath, { reject: false })
|
|
8590
9255
|
]);
|
|
8591
9256
|
nameStatus = ns.stdout;
|
|
8592
|
-
numstat =
|
|
9257
|
+
numstat = num2.stdout;
|
|
8593
9258
|
combinedDiff = diff.stdout;
|
|
8594
9259
|
labelBase = "unstaged";
|
|
8595
9260
|
} else if (scope === "uncommitted") {
|
|
8596
|
-
const [ns,
|
|
9261
|
+
const [ns, num2, diff] = await Promise.all([
|
|
8597
9262
|
git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
|
|
8598
9263
|
git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
|
|
8599
9264
|
git(["diff", "HEAD"], worktreePath, { reject: false })
|
|
8600
9265
|
]);
|
|
8601
9266
|
nameStatus = ns.stdout;
|
|
8602
|
-
numstat =
|
|
9267
|
+
numstat = num2.stdout;
|
|
8603
9268
|
combinedDiff = diff.stdout;
|
|
8604
9269
|
labelBase = "HEAD";
|
|
8605
9270
|
} else if (scope === "last_turn") {
|
|
@@ -8618,34 +9283,34 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
8618
9283
|
scopeStats
|
|
8619
9284
|
};
|
|
8620
9285
|
}
|
|
8621
|
-
const [ns,
|
|
9286
|
+
const [ns, num2, diff] = await Promise.all([
|
|
8622
9287
|
git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
|
|
8623
9288
|
git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
|
|
8624
9289
|
git(["diff", lastTurnBase], worktreePath, { reject: false })
|
|
8625
9290
|
]);
|
|
8626
9291
|
nameStatus = ns.stdout;
|
|
8627
|
-
numstat =
|
|
9292
|
+
numstat = num2.stdout;
|
|
8628
9293
|
combinedDiff = diff.stdout;
|
|
8629
9294
|
labelBase = "last turn";
|
|
8630
9295
|
} else if (scope === "commits" && commitSha) {
|
|
8631
9296
|
const range = `${commitSha}^!`;
|
|
8632
|
-
const [ns,
|
|
9297
|
+
const [ns, num2, diff] = await Promise.all([
|
|
8633
9298
|
git(["diff", "--name-status", range], worktreePath, { reject: false }),
|
|
8634
9299
|
git(["diff", "--numstat", range], worktreePath, { reject: false }),
|
|
8635
9300
|
git(["diff", range], worktreePath, { reject: false })
|
|
8636
9301
|
]);
|
|
8637
9302
|
nameStatus = ns.stdout;
|
|
8638
|
-
numstat =
|
|
9303
|
+
numstat = num2.stdout;
|
|
8639
9304
|
combinedDiff = diff.stdout;
|
|
8640
9305
|
labelBase = commitSha.slice(0, 7);
|
|
8641
9306
|
} else {
|
|
8642
|
-
const [ns,
|
|
9307
|
+
const [ns, num2, diff] = await Promise.all([
|
|
8643
9308
|
git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
|
|
8644
9309
|
git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
|
|
8645
9310
|
git(["diff", mergeBase], worktreePath, { reject: false })
|
|
8646
9311
|
]);
|
|
8647
9312
|
nameStatus = ns.stdout;
|
|
8648
|
-
numstat =
|
|
9313
|
+
numstat = num2.stdout;
|
|
8649
9314
|
combinedDiff = diff.stdout;
|
|
8650
9315
|
labelBase = base;
|
|
8651
9316
|
}
|
|
@@ -8715,7 +9380,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
|
8715
9380
|
assertSafeRelativePath(relativePath);
|
|
8716
9381
|
const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
|
|
8717
9382
|
const abs = (0, import_node_path20.join)(worktreePath, relativePath);
|
|
8718
|
-
const st = (0,
|
|
9383
|
+
const st = (0, import_node_fs22.statSync)(abs);
|
|
8719
9384
|
if (!st.isFile()) {
|
|
8720
9385
|
throw new Error(`Not a file: ${relativePath}`);
|
|
8721
9386
|
}
|
|
@@ -8724,7 +9389,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
|
8724
9389
|
`File too large to upload (${st.size} bytes; max ${maxBytes})`
|
|
8725
9390
|
);
|
|
8726
9391
|
}
|
|
8727
|
-
const buf = (0,
|
|
9392
|
+
const buf = (0, import_node_fs22.readFileSync)(abs);
|
|
8728
9393
|
return {
|
|
8729
9394
|
path: relativePath,
|
|
8730
9395
|
contentBase64: buf.toString("base64"),
|
|
@@ -8735,11 +9400,11 @@ function readWorktreeFile(worktreePath, relativePath, opts) {
|
|
|
8735
9400
|
assertSafeRelativePath(relativePath);
|
|
8736
9401
|
const maxBytes = opts?.maxBytes ?? 2e5;
|
|
8737
9402
|
const abs = (0, import_node_path20.join)(worktreePath, relativePath);
|
|
8738
|
-
const st = (0,
|
|
9403
|
+
const st = (0, import_node_fs22.statSync)(abs);
|
|
8739
9404
|
if (!st.isFile()) {
|
|
8740
9405
|
throw new Error(`Not a file: ${relativePath}`);
|
|
8741
9406
|
}
|
|
8742
|
-
const buf = (0,
|
|
9407
|
+
const buf = (0, import_node_fs22.readFileSync)(abs);
|
|
8743
9408
|
if (isImageRelativePath(relativePath)) {
|
|
8744
9409
|
const maxImageBytes = Math.max(maxBytes, 15e6);
|
|
8745
9410
|
const truncated2 = buf.length > maxImageBytes;
|
|
@@ -8783,8 +9448,8 @@ function assertSafeRelativePath(relativePath) {
|
|
|
8783
9448
|
function writeWorktreeFile(worktreePath, relativePath, content) {
|
|
8784
9449
|
assertSafeRelativePath(relativePath);
|
|
8785
9450
|
const abs = (0, import_node_path20.join)(worktreePath, relativePath);
|
|
8786
|
-
(0,
|
|
8787
|
-
(0,
|
|
9451
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
|
|
9452
|
+
(0, import_node_fs22.writeFileSync)(abs, content, "utf8");
|
|
8788
9453
|
return { path: relativePath };
|
|
8789
9454
|
}
|
|
8790
9455
|
async function getDiffSummary(worktreePath, repoPath, opts) {
|
|
@@ -8963,7 +9628,7 @@ async function confirmLand(thread, opts) {
|
|
|
8963
9628
|
}
|
|
8964
9629
|
|
|
8965
9630
|
// src/skills/discover.ts
|
|
8966
|
-
var
|
|
9631
|
+
var import_node_fs23 = require("fs");
|
|
8967
9632
|
var import_node_os9 = require("os");
|
|
8968
9633
|
var import_node_path21 = require("path");
|
|
8969
9634
|
function toCommand(name) {
|
|
@@ -8997,7 +9662,7 @@ function parseFrontmatter(content) {
|
|
|
8997
9662
|
}
|
|
8998
9663
|
function readSkill(skillMd, source) {
|
|
8999
9664
|
try {
|
|
9000
|
-
const content = (0,
|
|
9665
|
+
const content = (0, import_node_fs23.readFileSync)(skillMd, "utf8");
|
|
9001
9666
|
const { name: fmName, description } = parseFrontmatter(content);
|
|
9002
9667
|
const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
|
|
9003
9668
|
const name = fmName || dirName;
|
|
@@ -9016,19 +9681,19 @@ function readSkill(skillMd, source) {
|
|
|
9016
9681
|
}
|
|
9017
9682
|
}
|
|
9018
9683
|
function scanSkillsDir(dir, source, out) {
|
|
9019
|
-
if (!(0,
|
|
9684
|
+
if (!(0, import_node_fs23.existsSync)(dir)) return;
|
|
9020
9685
|
let entries;
|
|
9021
9686
|
try {
|
|
9022
|
-
entries = (0,
|
|
9687
|
+
entries = (0, import_node_fs23.readdirSync)(dir);
|
|
9023
9688
|
} catch {
|
|
9024
9689
|
return;
|
|
9025
9690
|
}
|
|
9026
9691
|
for (const entry of entries) {
|
|
9027
9692
|
if (entry.startsWith(".")) continue;
|
|
9028
9693
|
const skillMd = (0, import_node_path21.join)(dir, entry, "SKILL.md");
|
|
9029
|
-
if (!(0,
|
|
9694
|
+
if (!(0, import_node_fs23.existsSync)(skillMd)) continue;
|
|
9030
9695
|
try {
|
|
9031
|
-
if (!(0,
|
|
9696
|
+
if (!(0, import_node_fs23.statSync)(skillMd).isFile()) continue;
|
|
9032
9697
|
} catch {
|
|
9033
9698
|
continue;
|
|
9034
9699
|
}
|
|
@@ -9037,12 +9702,12 @@ function scanSkillsDir(dir, source, out) {
|
|
|
9037
9702
|
}
|
|
9038
9703
|
}
|
|
9039
9704
|
function scanClaudePluginSkills(pluginsRoot, out) {
|
|
9040
|
-
if (!(0,
|
|
9705
|
+
if (!(0, import_node_fs23.existsSync)(pluginsRoot)) return;
|
|
9041
9706
|
const walk = (dir, depth, lookingForSkillsDir) => {
|
|
9042
9707
|
if (depth > 7) return;
|
|
9043
9708
|
let entries;
|
|
9044
9709
|
try {
|
|
9045
|
-
entries = (0,
|
|
9710
|
+
entries = (0, import_node_fs23.readdirSync)(dir);
|
|
9046
9711
|
} catch {
|
|
9047
9712
|
return;
|
|
9048
9713
|
}
|
|
@@ -9054,7 +9719,7 @@ function scanClaudePluginSkills(pluginsRoot, out) {
|
|
|
9054
9719
|
if (entry === "node_modules" || entry === ".git") continue;
|
|
9055
9720
|
const full = (0, import_node_path21.join)(dir, entry);
|
|
9056
9721
|
try {
|
|
9057
|
-
if (!(0,
|
|
9722
|
+
if (!(0, import_node_fs23.statSync)(full).isDirectory()) continue;
|
|
9058
9723
|
} catch {
|
|
9059
9724
|
continue;
|
|
9060
9725
|
}
|
|
@@ -9094,7 +9759,7 @@ function discoverSkills(worktreePath) {
|
|
|
9094
9759
|
return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
|
|
9095
9760
|
}
|
|
9096
9761
|
function readSkillBody(skillPath, maxChars = 12e3) {
|
|
9097
|
-
const raw = (0,
|
|
9762
|
+
const raw = (0, import_node_fs23.readFileSync)(skillPath, "utf8");
|
|
9098
9763
|
if (raw.startsWith("---")) {
|
|
9099
9764
|
const end = raw.indexOf("\n---", 3);
|
|
9100
9765
|
if (end >= 0) {
|
|
@@ -9187,9 +9852,10 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
|
|
|
9187
9852
|
}
|
|
9188
9853
|
|
|
9189
9854
|
// src/composer/stage-files.ts
|
|
9190
|
-
var
|
|
9855
|
+
var import_node_fs24 = require("fs");
|
|
9191
9856
|
var import_node_path22 = require("path");
|
|
9192
9857
|
var import_node_crypto5 = require("crypto");
|
|
9858
|
+
init_workspace_scratch();
|
|
9193
9859
|
var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
9194
9860
|
"png",
|
|
9195
9861
|
"jpg",
|
|
@@ -9210,11 +9876,6 @@ var IMAGE_MIME_BY_EXT = {
|
|
|
9210
9876
|
bmp: "image/bmp",
|
|
9211
9877
|
ico: "image/x-icon"
|
|
9212
9878
|
};
|
|
9213
|
-
var ATTACHMENTS_DIR = ".sideboard/attachments";
|
|
9214
|
-
var ATTACHMENTS_GITIGNORE2 = `# Sideboard review / composer attachments (local only)
|
|
9215
|
-
*
|
|
9216
|
-
!.gitignore
|
|
9217
|
-
`;
|
|
9218
9879
|
var MAX_INLINE_BYTES = 4e5;
|
|
9219
9880
|
var MAX_PREVIEW_BYTES = 5e6;
|
|
9220
9881
|
function fileExtension(filePath) {
|
|
@@ -9229,21 +9890,21 @@ function imageMimeType(filePath) {
|
|
|
9229
9890
|
}
|
|
9230
9891
|
function ensureAttachmentsDir(worktreePath) {
|
|
9231
9892
|
const dir = (0, import_node_path22.join)(worktreePath, ATTACHMENTS_DIR);
|
|
9232
|
-
(0,
|
|
9893
|
+
(0, import_node_fs24.mkdirSync)(dir, { recursive: true });
|
|
9233
9894
|
const gi = (0, import_node_path22.join)(dir, ".gitignore");
|
|
9234
|
-
if (!(0,
|
|
9235
|
-
(0,
|
|
9895
|
+
if (!(0, import_node_fs24.existsSync)(gi)) {
|
|
9896
|
+
(0, import_node_fs24.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
|
|
9236
9897
|
}
|
|
9237
9898
|
return dir;
|
|
9238
9899
|
}
|
|
9239
9900
|
function uniqueAttachmentName(dir, originalName) {
|
|
9240
9901
|
const safe = originalName.replace(/[/\\]/g, "_") || "file";
|
|
9241
|
-
if (!(0,
|
|
9902
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
|
|
9242
9903
|
const ext = (0, import_node_path22.extname)(safe);
|
|
9243
9904
|
const stem = ext ? safe.slice(0, -ext.length) : safe;
|
|
9244
9905
|
for (let i = 1; i < 1e4; i++) {
|
|
9245
9906
|
const candidate = `${stem}-${i}${ext}`;
|
|
9246
|
-
if (!(0,
|
|
9907
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
|
|
9247
9908
|
}
|
|
9248
9909
|
return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
|
|
9249
9910
|
}
|
|
@@ -9301,13 +9962,13 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
|
|
|
9301
9962
|
for (const abs of absolutePaths) {
|
|
9302
9963
|
const originalName = (0, import_node_path22.basename)(abs);
|
|
9303
9964
|
try {
|
|
9304
|
-
const st = (0,
|
|
9965
|
+
const st = (0, import_node_fs24.statSync)(abs);
|
|
9305
9966
|
if (!st.isFile()) continue;
|
|
9306
9967
|
const name = uniqueAttachmentName(dir, originalName);
|
|
9307
9968
|
const destAbs = (0, import_node_path22.join)(dir, name);
|
|
9308
|
-
(0,
|
|
9969
|
+
(0, import_node_fs24.copyFileSync)(abs, destAbs);
|
|
9309
9970
|
const rel = `${ATTACHMENTS_DIR}/${name}`;
|
|
9310
|
-
const buf = (0,
|
|
9971
|
+
const buf = (0, import_node_fs24.readFileSync)(destAbs);
|
|
9311
9972
|
out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
|
|
9312
9973
|
} catch (err) {
|
|
9313
9974
|
out.push({
|
|
@@ -9330,7 +9991,7 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
|
|
|
9330
9991
|
const buf = Buffer.from(item.dataBase64, "base64");
|
|
9331
9992
|
const name = uniqueAttachmentName(dir, originalName);
|
|
9332
9993
|
const destAbs = (0, import_node_path22.join)(dir, name);
|
|
9333
|
-
(0,
|
|
9994
|
+
(0, import_node_fs24.writeFileSync)(destAbs, buf);
|
|
9334
9995
|
const rel = `${ATTACHMENTS_DIR}/${name}`;
|
|
9335
9996
|
out.push(attachmentFromBuffer(name, buf, { path: rel }));
|
|
9336
9997
|
} catch (err) {
|
|
@@ -9359,9 +10020,9 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
|
|
|
9359
10020
|
const name = (0, import_node_path22.basename)(rel);
|
|
9360
10021
|
try {
|
|
9361
10022
|
const abs = (0, import_node_path22.join)(worktreePath, rel);
|
|
9362
|
-
const st = (0,
|
|
10023
|
+
const st = (0, import_node_fs24.statSync)(abs);
|
|
9363
10024
|
if (!st.isFile()) continue;
|
|
9364
|
-
const buf = (0,
|
|
10025
|
+
const buf = (0, import_node_fs24.readFileSync)(abs);
|
|
9365
10026
|
out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
|
|
9366
10027
|
} catch (err) {
|
|
9367
10028
|
out.push({
|
|
@@ -9376,7 +10037,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
|
|
|
9376
10037
|
}
|
|
9377
10038
|
|
|
9378
10039
|
// src/agents/instructions.ts
|
|
9379
|
-
var
|
|
10040
|
+
var import_node_fs25 = require("fs");
|
|
9380
10041
|
var import_node_path23 = require("path");
|
|
9381
10042
|
init_worktree_labels();
|
|
9382
10043
|
function normPath2(p) {
|
|
@@ -9517,10 +10178,10 @@ function loadAgentInstructions(worktreePath, agent) {
|
|
|
9517
10178
|
for (const rel of candidates) {
|
|
9518
10179
|
if (seen.has(rel)) continue;
|
|
9519
10180
|
const abs = (0, import_node_path23.join)(worktreePath, rel);
|
|
9520
|
-
if (!(0,
|
|
10181
|
+
if (!(0, import_node_fs25.existsSync)(abs)) continue;
|
|
9521
10182
|
try {
|
|
9522
|
-
if (!(0,
|
|
9523
|
-
let content = (0,
|
|
10183
|
+
if (!(0, import_node_fs25.statSync)(abs).isFile()) continue;
|
|
10184
|
+
let content = (0, import_node_fs25.readFileSync)(abs, "utf8");
|
|
9524
10185
|
if (!content.trim()) continue;
|
|
9525
10186
|
if (content.length > MAX_CHARS_PER_FILE) {
|
|
9526
10187
|
content = `${content.slice(0, MAX_CHARS_PER_FILE)}
|
|
@@ -9553,6 +10214,7 @@ function formatAgentInstructions(files) {
|
|
|
9553
10214
|
|
|
9554
10215
|
// src/orchestrator/orchestrator.ts
|
|
9555
10216
|
init_types();
|
|
10217
|
+
init_plan_file();
|
|
9556
10218
|
init_settings();
|
|
9557
10219
|
|
|
9558
10220
|
// src/threads/sync-branch.ts
|
|
@@ -9672,7 +10334,7 @@ var Orchestrator = class {
|
|
|
9672
10334
|
}
|
|
9673
10335
|
continue;
|
|
9674
10336
|
}
|
|
9675
|
-
if (!(0,
|
|
10337
|
+
if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
|
|
9676
10338
|
setStatus(thread.id, "broken", "Worktree missing on disk");
|
|
9677
10339
|
this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
|
|
9678
10340
|
continue;
|
|
@@ -9817,14 +10479,9 @@ var Orchestrator = class {
|
|
|
9817
10479
|
return findThreadByRef(idOrRef) ?? readThread(idOrRef);
|
|
9818
10480
|
}
|
|
9819
10481
|
async createThread(input) {
|
|
9820
|
-
let thread = await createThread(input
|
|
9821
|
-
this.emit({
|
|
9822
|
-
type: "turn_output",
|
|
9823
|
-
threadId: "pending",
|
|
9824
|
-
event: { type: "stdout", data: line }
|
|
9825
|
-
});
|
|
9826
|
-
});
|
|
10482
|
+
let thread = await createThread(input);
|
|
9827
10483
|
this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
|
|
10484
|
+
await this.runSetupAfterCreate(thread.id);
|
|
9828
10485
|
const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
|
|
9829
10486
|
if (autoRunAfterSetupEnabled2()) {
|
|
9830
10487
|
try {
|
|
@@ -9838,6 +10495,19 @@ var Orchestrator = class {
|
|
|
9838
10495
|
}
|
|
9839
10496
|
return thread;
|
|
9840
10497
|
}
|
|
10498
|
+
/** Run workspace setup after a new worktree is created (no-op if none configured). */
|
|
10499
|
+
async runSetupAfterCreate(threadId) {
|
|
10500
|
+
try {
|
|
10501
|
+
await this.runSetup(threadId);
|
|
10502
|
+
} catch (err) {
|
|
10503
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10504
|
+
if (/no setup script/i.test(message)) return;
|
|
10505
|
+
if (/already running/i.test(message)) return;
|
|
10506
|
+
updateThread(threadId, {
|
|
10507
|
+
lastError: `Setup failed: ${message}`
|
|
10508
|
+
});
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
9841
10511
|
listWorkspaces() {
|
|
9842
10512
|
const fromThreads = listThreads({ includeArchived: false }).map((t) => t.repoPath);
|
|
9843
10513
|
return syncWorkspacesFromThreads(fromThreads);
|
|
@@ -10186,6 +10856,19 @@ var Orchestrator = class {
|
|
|
10186
10856
|
});
|
|
10187
10857
|
}
|
|
10188
10858
|
const afterTurn = this.requireThread(threadId);
|
|
10859
|
+
if (afterTurn.planMode && afterTurn.worktreePath?.trim()) {
|
|
10860
|
+
const presented = extractPresentedPlan(parts);
|
|
10861
|
+
const exited = parts.some(
|
|
10862
|
+
(p) => p.type === "tool" && /exitplanmode/i.test(p.name)
|
|
10863
|
+
);
|
|
10864
|
+
if (presented?.content) {
|
|
10865
|
+
writePlanFile(afterTurn.worktreePath, presented.content);
|
|
10866
|
+
} else if (exited || chatText && chatText.trim().length >= 400) {
|
|
10867
|
+
if (!readPlanFile(afterTurn.worktreePath) && chatText?.trim()) {
|
|
10868
|
+
writePlanFile(afterTurn.worktreePath, chatText.trim());
|
|
10869
|
+
}
|
|
10870
|
+
}
|
|
10871
|
+
}
|
|
10189
10872
|
if (afterTurn.planMode && afterTurn.agent === "claude" && parts.some(
|
|
10190
10873
|
(p) => p.type === "tool" && /exitplanmode/i.test(p.name)
|
|
10191
10874
|
)) {
|
|
@@ -10667,6 +11350,82 @@ var Orchestrator = class {
|
|
|
10667
11350
|
}
|
|
10668
11351
|
return meta;
|
|
10669
11352
|
}
|
|
11353
|
+
async getPrStack(threadRef) {
|
|
11354
|
+
const thread = this.requireThread(threadRef);
|
|
11355
|
+
if (!thread.worktreePath?.trim()) return null;
|
|
11356
|
+
const stack = await getPrStack(thread.worktreePath);
|
|
11357
|
+
if (!stack) return null;
|
|
11358
|
+
const current = stack.currentIndex >= 0 ? stack.layers[stack.currentIndex] : null;
|
|
11359
|
+
const patch = {};
|
|
11360
|
+
if (stack.stackNumber != null) {
|
|
11361
|
+
const id = `gh-stack-${stack.stackNumber}`;
|
|
11362
|
+
if (thread.stackId !== id) patch.stackId = id;
|
|
11363
|
+
}
|
|
11364
|
+
if (current?.position != null && thread.stackLayer !== current.position) {
|
|
11365
|
+
patch.stackLayer = current.position;
|
|
11366
|
+
}
|
|
11367
|
+
if (current?.prUrl && current.prUrl !== thread.prUrl) patch.prUrl = current.prUrl;
|
|
11368
|
+
if (current?.title && current.title !== thread.prTitle) patch.prTitle = current.title;
|
|
11369
|
+
if (current?.branchName && current.branchName !== thread.branchName) {
|
|
11370
|
+
patch.branchName = current.branchName;
|
|
11371
|
+
}
|
|
11372
|
+
if (Object.keys(patch).length > 0) updateThread(thread.id, patch);
|
|
11373
|
+
return stack;
|
|
11374
|
+
}
|
|
11375
|
+
/** Open worktrees for all (or one) stack layers discovered from a thread. */
|
|
11376
|
+
async openPrStackLayers(threadRef, opts) {
|
|
11377
|
+
const result = await openPrStackLayers({ threadRef, layer: opts?.layer });
|
|
11378
|
+
for (const t of result.threads) {
|
|
11379
|
+
this.emit({ type: "status_changed", threadId: t.id, status: t.status });
|
|
11380
|
+
}
|
|
11381
|
+
for (const id of result.createdThreadIds) {
|
|
11382
|
+
await this.runSetupAfterCreate(id);
|
|
11383
|
+
}
|
|
11384
|
+
return { stack: result.stack, threads: result.threads };
|
|
11385
|
+
}
|
|
11386
|
+
/** Add a branch on top of the thread's stack and open its worktree. */
|
|
11387
|
+
async addStackLayer(threadRef, branchName, opts) {
|
|
11388
|
+
const result = await addStackLayerFromThread({
|
|
11389
|
+
threadRef,
|
|
11390
|
+
branchName,
|
|
11391
|
+
title: opts?.title
|
|
11392
|
+
});
|
|
11393
|
+
this.emit({
|
|
11394
|
+
type: "status_changed",
|
|
11395
|
+
threadId: result.thread.id,
|
|
11396
|
+
status: result.thread.status
|
|
11397
|
+
});
|
|
11398
|
+
if (result.createdWorktree) {
|
|
11399
|
+
await this.runSetupAfterCreate(result.thread.id);
|
|
11400
|
+
}
|
|
11401
|
+
return { stack: result.stack, thread: result.thread };
|
|
11402
|
+
}
|
|
11403
|
+
/** Initialize a stack from the current thread branch (optional extra layers). */
|
|
11404
|
+
async initStackFromThread(threadRef, opts) {
|
|
11405
|
+
const result = await initStackFromThread({
|
|
11406
|
+
threadRef,
|
|
11407
|
+
additionalBranches: opts?.additionalBranches,
|
|
11408
|
+
base: opts?.base
|
|
11409
|
+
});
|
|
11410
|
+
for (const t of result.threads) {
|
|
11411
|
+
this.emit({ type: "status_changed", threadId: t.id, status: t.status });
|
|
11412
|
+
}
|
|
11413
|
+
for (const id of result.createdThreadIds) {
|
|
11414
|
+
await this.runSetupAfterCreate(id);
|
|
11415
|
+
}
|
|
11416
|
+
return { stack: result.stack, threads: result.threads };
|
|
11417
|
+
}
|
|
11418
|
+
/** Create a new multi-layer stack with one worktree per layer. */
|
|
11419
|
+
async createPrStack(input) {
|
|
11420
|
+
const result = await createPrStack(input);
|
|
11421
|
+
for (const t of result.threads) {
|
|
11422
|
+
this.emit({ type: "status_changed", threadId: t.id, status: t.status });
|
|
11423
|
+
}
|
|
11424
|
+
for (const id of result.createdThreadIds) {
|
|
11425
|
+
await this.runSetupAfterCreate(id);
|
|
11426
|
+
}
|
|
11427
|
+
return { stack: result.stack, threads: result.threads };
|
|
11428
|
+
}
|
|
10670
11429
|
async getPrDetails(threadRef) {
|
|
10671
11430
|
const { thread, selector, cwd } = await this.withPrSelector(threadRef);
|
|
10672
11431
|
if (!selector) return null;
|
|
@@ -10729,14 +11488,9 @@ var Orchestrator = class {
|
|
|
10729
11488
|
return forkChatTab(input);
|
|
10730
11489
|
}
|
|
10731
11490
|
async forkThreadWorktree(input) {
|
|
10732
|
-
const thread = await forkThreadWorktree(input
|
|
10733
|
-
this.emit({
|
|
10734
|
-
type: "turn_output",
|
|
10735
|
-
threadId: "pending",
|
|
10736
|
-
event: { type: "stdout", data: line }
|
|
10737
|
-
});
|
|
10738
|
-
});
|
|
11491
|
+
const thread = await forkThreadWorktree(input);
|
|
10739
11492
|
this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
|
|
11493
|
+
await this.runSetupAfterCreate(thread.id);
|
|
10740
11494
|
return thread;
|
|
10741
11495
|
}
|
|
10742
11496
|
renameThread(threadRef, title) {
|
|
@@ -10750,7 +11504,7 @@ var Orchestrator = class {
|
|
|
10750
11504
|
}
|
|
10751
11505
|
/**
|
|
10752
11506
|
* Stage OS / worktree files into composer attachments (copies external files
|
|
10753
|
-
* into `.
|
|
11507
|
+
* into `.context/attachments/` so agents can Read images and binaries).
|
|
10754
11508
|
*/
|
|
10755
11509
|
attachComposerFiles(threadRef, opts) {
|
|
10756
11510
|
const thread = this.requireThread(threadRef);
|
|
@@ -10824,7 +11578,7 @@ var Orchestrator = class {
|
|
|
10824
11578
|
updateThread(thread.id, { worktreePath: globalAgentCwd2() });
|
|
10825
11579
|
return setStatus(thread.id, "idle");
|
|
10826
11580
|
}
|
|
10827
|
-
if (!(0,
|
|
11581
|
+
if (!(0, import_node_fs28.existsSync)(thread.worktreePath)) {
|
|
10828
11582
|
const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
|
|
10829
11583
|
const { execa: execa7 } = await import("execa");
|
|
10830
11584
|
const slug = thread.worktreePath.split("/").pop();
|
|
@@ -11043,7 +11797,7 @@ async function startMcpServer() {
|
|
|
11043
11797
|
async () => {
|
|
11044
11798
|
const threads = orch.getThreads(true);
|
|
11045
11799
|
const lines = threads.map((t) => {
|
|
11046
|
-
const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0,
|
|
11800
|
+
const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path26.basename)(t.repoPath) || t.repoPath;
|
|
11047
11801
|
return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
|
|
11048
11802
|
});
|
|
11049
11803
|
return {
|
|
@@ -11105,6 +11859,59 @@ async function startMcpServer() {
|
|
|
11105
11859
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
11106
11860
|
}
|
|
11107
11861
|
);
|
|
11862
|
+
server.tool(
|
|
11863
|
+
"ask_user",
|
|
11864
|
+
"Ask the user clarifying multiple-choice questions in Sideboard\u2019s composer (plan mode). Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. Use for approach forks and requirements \u2014 not for \u201Cis the plan ready?\u201D. After calling, stop and wait for their next message with answers.",
|
|
11865
|
+
{
|
|
11866
|
+
questions: import_zod.z.array(
|
|
11867
|
+
import_zod.z.object({
|
|
11868
|
+
question: import_zod.z.string().describe("Full question text ending with ?"),
|
|
11869
|
+
header: import_zod.z.string().max(24).optional().describe("Short label shown above the question"),
|
|
11870
|
+
multiSelect: import_zod.z.boolean().optional().describe("Allow selecting multiple options"),
|
|
11871
|
+
options: import_zod.z.array(
|
|
11872
|
+
import_zod.z.object({
|
|
11873
|
+
label: import_zod.z.string(),
|
|
11874
|
+
description: import_zod.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
|
|
11875
|
+
})
|
|
11876
|
+
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
11877
|
+
})
|
|
11878
|
+
).min(1).max(4).describe("1\u20134 questions")
|
|
11879
|
+
},
|
|
11880
|
+
async ({ questions }) => {
|
|
11881
|
+
const payload = {
|
|
11882
|
+
ok: true,
|
|
11883
|
+
questions,
|
|
11884
|
+
message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
|
|
11885
|
+
};
|
|
11886
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
11887
|
+
}
|
|
11888
|
+
);
|
|
11889
|
+
server.tool(
|
|
11890
|
+
"present_plan",
|
|
11891
|
+
"Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
|
|
11892
|
+
{
|
|
11893
|
+
title: import_zod.z.string().optional().describe("Short plan title (defaults to Plan)"),
|
|
11894
|
+
content: import_zod.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
|
|
11895
|
+
thread_id: import_zod.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
|
|
11896
|
+
},
|
|
11897
|
+
async ({ title, content, thread_id }) => {
|
|
11898
|
+
const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
|
|
11899
|
+
let root = process.cwd();
|
|
11900
|
+
if (thread_id?.trim()) {
|
|
11901
|
+
const t = orch.getThread(thread_id.trim());
|
|
11902
|
+
if (t?.worktreePath?.trim()) root = t.worktreePath;
|
|
11903
|
+
}
|
|
11904
|
+
const path = writePlanFile2(root, content);
|
|
11905
|
+
const payload = {
|
|
11906
|
+
ok: true,
|
|
11907
|
+
path,
|
|
11908
|
+
title: title?.trim() || "Plan",
|
|
11909
|
+
content,
|
|
11910
|
+
message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
|
|
11911
|
+
};
|
|
11912
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
11913
|
+
}
|
|
11914
|
+
);
|
|
11108
11915
|
server.tool(
|
|
11109
11916
|
"present_schema",
|
|
11110
11917
|
"Open Sideboard\u2019s schema-driven CMS side column (filterable table and/or form). Pass JSON Schema + schemaUi (Brightsy extensions supported). Use datasource=brightsy with resource_id (record type UUID) when logged into Brightsy; use datasource=inline with embedded resource/records for any other source.",
|
|
@@ -11683,6 +12490,126 @@ async function startMcpServer() {
|
|
|
11683
12490
|
};
|
|
11684
12491
|
}
|
|
11685
12492
|
);
|
|
12493
|
+
server.tool(
|
|
12494
|
+
"get_pr_stack",
|
|
12495
|
+
"Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before mergePr on stacked PRs.",
|
|
12496
|
+
{ ref: import_zod.z.string() },
|
|
12497
|
+
async ({ ref }) => {
|
|
12498
|
+
const stack = await orch.getPrStack(ref);
|
|
12499
|
+
return {
|
|
12500
|
+
content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
|
|
12501
|
+
};
|
|
12502
|
+
}
|
|
12503
|
+
);
|
|
12504
|
+
server.tool(
|
|
12505
|
+
"open_pr_stack_layers",
|
|
12506
|
+
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
12507
|
+
{
|
|
12508
|
+
ref: import_zod.z.string(),
|
|
12509
|
+
layer: import_zod.z.number().int().positive().optional()
|
|
12510
|
+
},
|
|
12511
|
+
async ({ ref, layer }) => {
|
|
12512
|
+
const result = await orch.openPrStackLayers(ref, { layer });
|
|
12513
|
+
return {
|
|
12514
|
+
content: [
|
|
12515
|
+
{
|
|
12516
|
+
type: "text",
|
|
12517
|
+
text: JSON.stringify(
|
|
12518
|
+
{
|
|
12519
|
+
stackNumber: result.stack.stackNumber,
|
|
12520
|
+
trunk: result.stack.trunk,
|
|
12521
|
+
threads: result.threads.map((t) => ({
|
|
12522
|
+
id: t.id,
|
|
12523
|
+
title: t.title,
|
|
12524
|
+
branchName: t.branchName,
|
|
12525
|
+
stackLayer: t.stackLayer,
|
|
12526
|
+
worktreePath: t.worktreePath,
|
|
12527
|
+
prUrl: t.prUrl,
|
|
12528
|
+
link: `sideboard://thread/${t.id}`
|
|
12529
|
+
}))
|
|
12530
|
+
},
|
|
12531
|
+
null,
|
|
12532
|
+
2
|
|
12533
|
+
)
|
|
12534
|
+
}
|
|
12535
|
+
]
|
|
12536
|
+
};
|
|
12537
|
+
}
|
|
12538
|
+
);
|
|
12539
|
+
server.tool(
|
|
12540
|
+
"add_stack_layer",
|
|
12541
|
+
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
12542
|
+
{
|
|
12543
|
+
ref: import_zod.z.string(),
|
|
12544
|
+
branchName: import_zod.z.string(),
|
|
12545
|
+
title: import_zod.z.string().optional()
|
|
12546
|
+
},
|
|
12547
|
+
async ({ ref, branchName, title }) => {
|
|
12548
|
+
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
12549
|
+
return {
|
|
12550
|
+
content: [
|
|
12551
|
+
{
|
|
12552
|
+
type: "text",
|
|
12553
|
+
text: JSON.stringify(
|
|
12554
|
+
{
|
|
12555
|
+
id: result.thread.id,
|
|
12556
|
+
title: result.thread.title,
|
|
12557
|
+
branchName: result.thread.branchName,
|
|
12558
|
+
stackLayer: result.thread.stackLayer,
|
|
12559
|
+
worktreePath: result.thread.worktreePath,
|
|
12560
|
+
link: `sideboard://thread/${result.thread.id}`
|
|
12561
|
+
},
|
|
12562
|
+
null,
|
|
12563
|
+
2
|
|
12564
|
+
)
|
|
12565
|
+
}
|
|
12566
|
+
]
|
|
12567
|
+
};
|
|
12568
|
+
}
|
|
12569
|
+
);
|
|
12570
|
+
server.tool(
|
|
12571
|
+
"create_pr_stack",
|
|
12572
|
+
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
12573
|
+
{
|
|
12574
|
+
repoPath: import_zod.z.string(),
|
|
12575
|
+
branches: import_zod.z.array(import_zod.z.string()).min(1),
|
|
12576
|
+
agent: import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
|
|
12577
|
+
base: import_zod.z.string().optional(),
|
|
12578
|
+
title: import_zod.z.string().optional()
|
|
12579
|
+
},
|
|
12580
|
+
async (args) => {
|
|
12581
|
+
const result = await orch.createPrStack({
|
|
12582
|
+
repoPath: args.repoPath,
|
|
12583
|
+
branches: args.branches,
|
|
12584
|
+
agent: args.agent,
|
|
12585
|
+
base: args.base,
|
|
12586
|
+
title: args.title
|
|
12587
|
+
});
|
|
12588
|
+
return {
|
|
12589
|
+
content: [
|
|
12590
|
+
{
|
|
12591
|
+
type: "text",
|
|
12592
|
+
text: JSON.stringify(
|
|
12593
|
+
{
|
|
12594
|
+
stackNumber: result.stack.stackNumber,
|
|
12595
|
+
trunk: result.stack.trunk,
|
|
12596
|
+
threads: result.threads.map((t) => ({
|
|
12597
|
+
id: t.id,
|
|
12598
|
+
title: t.title,
|
|
12599
|
+
branchName: t.branchName,
|
|
12600
|
+
stackLayer: t.stackLayer,
|
|
12601
|
+
worktreePath: t.worktreePath,
|
|
12602
|
+
link: `sideboard://thread/${t.id}`
|
|
12603
|
+
}))
|
|
12604
|
+
},
|
|
12605
|
+
null,
|
|
12606
|
+
2
|
|
12607
|
+
)
|
|
12608
|
+
}
|
|
12609
|
+
]
|
|
12610
|
+
};
|
|
12611
|
+
}
|
|
12612
|
+
);
|
|
11686
12613
|
server.tool(
|
|
11687
12614
|
"list_issues",
|
|
11688
12615
|
"List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Then create_thread with sourceType=ticket.",
|