@sideboard-ai/core 0.1.107 → 0.1.109
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/cursor-runner.js +2 -2
- package/dist/{agents-T4RHL5UX.js → agents-XB332VUJ.js} +5 -5
- package/dist/{agents-4XQANGFO.js → agents-YIMHDI46.js} +7 -7
- package/dist/{app-settings-2WH6DVOW.js → app-settings-EBCVMFF6.js} +1 -1
- package/dist/{app-settings-6AXBWM3K.js → app-settings-IYSSVZHH.js} +2 -2
- package/dist/{chunk-7ZTQHC2Q.js → chunk-4LKD3PGO.js} +1 -1
- package/dist/{chunk-TLPJHLLM.js → chunk-4TR3HZFT.js} +1 -1
- package/dist/{chunk-SMWVSSE5.js → chunk-756EO4GK.js} +3 -3
- package/dist/{chunk-XOYH3OMI.js → chunk-AY2EQI2P.js} +16 -13
- package/dist/{chunk-PZVA2VDF.js → chunk-EW7COXYE.js} +1 -1
- package/dist/{chunk-VTTTV2LM.js → chunk-FBV6NK62.js} +2 -2
- package/dist/{chunk-GYFNFU62.js → chunk-I2OJ2YSP.js} +8 -16
- package/dist/{chunk-6JJTMI7G.js → chunk-JVIW55KT.js} +1 -1
- package/dist/{chunk-6HRR4T4P.js → chunk-KKHDPV45.js} +15 -14
- package/dist/{chunk-XEFHG6VG.js → chunk-LTPX5N6K.js} +1310 -1265
- package/dist/{chunk-EX4NZN3O.js → chunk-NE4TOOKS.js} +1 -1
- package/dist/{chunk-LRO7YVLJ.js → chunk-OQDIYVDD.js} +2 -2
- package/dist/{chunk-3SIFRHJ3.js → chunk-Q6B3NRCT.js} +1457 -1431
- package/dist/{chunk-THXSFETX.js → chunk-TROJ3PVH.js} +3 -3
- package/dist/{chunk-5XUKY2JY.js → chunk-VRTKGKUW.js} +8 -16
- package/dist/{chunk-HUKCGRAT.js → chunk-WD2TARQS.js} +7 -2
- package/dist/{coordinator-prompt-4TUQUEHY.js → coordinator-prompt-JKOU6BWV.js} +4 -4
- package/dist/{coordinator-prompt-YQWRXCPX.js → coordinator-prompt-V66BYTUV.js} +3 -3
- package/dist/{global-workspace-V6TMECDU.js → global-workspace-NCTBE7G7.js} +5 -5
- package/dist/{global-workspace-H37LFFXI.js → global-workspace-WBQWQQQY.js} +4 -4
- package/dist/index.cjs +1697 -1661
- package/dist/index.d.cts +65 -24
- package/dist/index.d.ts +65 -24
- package/dist/index.js +107 -85
- package/dist/mcp/run-stdio.cjs +1660 -1619
- package/dist/mcp/run-stdio.js +82 -79
- package/dist/{orchestrator-DEHSKERK.js → orchestrator-6W7EKSPO.js} +9 -9
- package/dist/{orchestrator-2P7DX44Q.js → orchestrator-JVGVPKLI.js} +7 -7
- package/dist/{workspaces-JU3P6FHB.js → workspaces-I3554R3F.js} +5 -5
- package/dist/{workspaces-UPEVANU4.js → workspaces-SUBEDSGB.js} +6 -6
- package/dist/{worktree-NR5YJG2E.js → worktree-2UWO7VEK.js} +3 -3
- package/dist/{worktree-6MFKE465.js → worktree-7H6HB6GW.js} +2 -2
- package/package.json +1 -1
package/dist/mcp/run-stdio.cjs
CHANGED
|
@@ -61,7 +61,7 @@ function isElectronLikeCommand(command) {
|
|
|
61
61
|
const name = command.trim();
|
|
62
62
|
if (!name) return false;
|
|
63
63
|
if (process.versions.electron && name === process.execPath) return true;
|
|
64
|
-
return /(?:^|[/\\])Electron(?:\.exe)?$/i.test(name) || /
|
|
64
|
+
return /(?:^|[/\\])Electron(?:\.exe)?$/i.test(name) || /(?:^|[/\\])Sideboard(?:\.exe)?$/i.test(name) || /(?:Sideboard|Electron)\.app[/\\]Contents[/\\]MacOS[/\\]/i.test(name);
|
|
65
65
|
}
|
|
66
66
|
function unwrapStrippedElectronLaunch(command, args) {
|
|
67
67
|
if (!isStrippedElectronLaunch(command, args) || !args || args.length < 4) {
|
|
@@ -1456,6 +1456,8 @@ function summarizeTurnStderr(tail, maxChars = 500) {
|
|
|
1456
1456
|
if (tail.length === 0) return "";
|
|
1457
1457
|
const cursorStartup = [...tail].reverse().find((line) => /cursor startup failed:/i.test(line));
|
|
1458
1458
|
if (cursorStartup) return clipStderr(cursorStartup, maxChars);
|
|
1459
|
+
const cursorRunFailed = [...tail].reverse().find((line) => /^cursor run failed\b/i.test(line.trim()));
|
|
1460
|
+
if (cursorRunFailed) return clipStderr(cursorRunFailed, maxChars);
|
|
1459
1461
|
if (tail.some(looksLikeNestedElectronCrash)) return NESTED_ELECTRON_SUMMARY;
|
|
1460
1462
|
if (tail.some(looksLikeHomebrewLibuvCrash)) return HOMEBREW_LIBUV_SUMMARY;
|
|
1461
1463
|
if (tail.some(
|
|
@@ -1486,7 +1488,7 @@ function looksLikeRetryableRunnerCrash(text4) {
|
|
|
1486
1488
|
const lower = text4.trim().toLowerCase();
|
|
1487
1489
|
if (/cannot find (?:package|module)|err_module_not_found/.test(lower)) return false;
|
|
1488
1490
|
if (!lower) return true;
|
|
1489
|
-
return /uv_run|spineventloopinternal|libuv|homebrew node \+ shared libuv|hascustomhostobject|electroninitializeicuandstartnode|nested chromium|truncated crash dump|sig(?:segv|abrt|ill)|segmentation fault|illegal instruction|fatal error/.test(
|
|
1491
|
+
return /uv_run|spineventloopinternal|libuv|homebrew node \+ shared libuv|hascustomhostobject|electroninitializeicuandstartnode|nested chromium|truncated crash dump|connection stalled|sig(?:segv|abrt|ill)|segmentation fault|illegal instruction|fatal error/.test(
|
|
1490
1492
|
lower
|
|
1491
1493
|
);
|
|
1492
1494
|
}
|
|
@@ -1544,6 +1546,9 @@ function humanizeAgentFailDetail(detail) {
|
|
|
1544
1546
|
if (/corrupt local agent checkpoint|missing root blob|truncated crash dump/.test(lower)) {
|
|
1545
1547
|
return `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
|
|
1546
1548
|
}
|
|
1549
|
+
if (/connection stalled/.test(lower)) {
|
|
1550
|
+
return /retry the turn/i.test(raw) ? raw : `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
|
|
1551
|
+
}
|
|
1547
1552
|
return raw;
|
|
1548
1553
|
}
|
|
1549
1554
|
function turnFailChatText(opts) {
|
|
@@ -5637,19 +5642,11 @@ function coordinatorTurnReminder(opts) {
|
|
|
5637
5642
|
const goal = opts.goal?.trim();
|
|
5638
5643
|
return [
|
|
5639
5644
|
"Sideboard Orchestration (mandatory):",
|
|
5640
|
-
"- You oversee
|
|
5641
|
-
|
|
5642
|
-
"- Registered workspaces / child threads are the fleet you manage via Sideboard MCP.",
|
|
5643
|
-
`- YOUR orchestration thread id is ${opts.parentId}`,
|
|
5644
|
-
`- Always pass parentThreadId="${opts.parentId}" on create_thread (never invent or reuse another uuid).`,
|
|
5645
|
-
`- Or omit parentThreadId \u2014 Sideboard MCP already binds children to ${opts.parentId}.`,
|
|
5645
|
+
"- You oversee worktree agents from a synthetic empty cwd (not a git repo). Follow AGENTS.md / CLAUDE.md.",
|
|
5646
|
+
`- YOUR orchestration thread id is ${opts.parentId} \u2014 pass parentThreadId="${opts.parentId}" on create_thread, or omit it.`,
|
|
5646
5647
|
goal ? `- Goal / title: ${goal}` : null,
|
|
5647
5648
|
accountDefaultsPlaybookLine(),
|
|
5648
|
-
|
|
5649
|
-
"- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If stillRunning, the child is working (progress is tools/thinking) \u2014 wait_for_turn again; do not ping it. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
|
|
5650
|
-
"- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
|
|
5651
|
-
"- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
|
|
5652
|
-
"- If they will wait on Slack, leave the Mac, or rely on overnight schedules, call set_caffeinate enabled=true (or they can enable Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled). When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
|
|
5649
|
+
"- Status: list_threads. Link chats as `[Title](sideboard://thread/<id>)`. Merge only if the user asked."
|
|
5653
5650
|
].filter(Boolean).join("\n");
|
|
5654
5651
|
}
|
|
5655
5652
|
function ensureGlobalCoordinatorCwd(opts) {
|
|
@@ -5778,14 +5775,14 @@ var init_coordinator_prompt = __esm({
|
|
|
5778
5775
|
"- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
|
|
5779
5776
|
"Inspect / review / PRs:",
|
|
5780
5777
|
"- get_diff \u2014 compact diff summary",
|
|
5781
|
-
'- request_review \u2014 open a Review chat tab on a worktree thread (attaches .
|
|
5778
|
+
'- request_review \u2014 open a Review chat tab on a worktree thread (attaches .claude/skills/review/SKILL.md when present, else seeds that skill / local guidelines; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id',
|
|
5782
5779
|
"- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing.",
|
|
5783
5780
|
'- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
|
|
5784
5781
|
'- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
|
|
5785
5782
|
"Process guides:",
|
|
5786
5783
|
"- Recurring multi-item / fan-out: if the child worktree has `.claude/skills/graph-engineering/SKILL.md`, tell the worker to follow it (`/graph-engineering`). Judge first; state on disk; grow the rulebook; do not patch three threads.",
|
|
5787
|
-
"- Recurring shapes: have the worktree agent write `.claude/skills/<kebab-name>/SKILL.md` (commit it) so later threads and native Claude Code / attach see it. Do not use `.sideboard/skills
|
|
5788
|
-
"- One-offs: no guide. Same miss across threads: edit the skill or `.
|
|
5784
|
+
"- Recurring shapes: have the worktree agent write `.claude/skills/<kebab-name>/SKILL.md` (commit it) so later threads and native Claude Code / attach see it. Merge-review sentences go in `.claude/skills/review/SKILL.md` (create it \u2014 that is allowed). Do not use `.sideboard/skills/` (that folder only). Codex/OpenCode: one line in AGENTS.md pointing at that file.",
|
|
5785
|
+
"- One-offs: no guide. Same miss across threads: edit the skill or `.claude/skills/review/SKILL.md`, then rerun the batch \u2014 do not patch three threads.",
|
|
5789
5786
|
"Human-only (do not attempt): ready-for-review land (confirm_land), purge_thread.",
|
|
5790
5787
|
"Thread links in replies: when mentioning a chat/thread for the user, include a markdown link `[Title](sideboard://thread/<id>)` using the full id (or the link field from create_thread / list_threads). Sideboard renders these as clickable opens.",
|
|
5791
5788
|
"Bash / Read / etc: allowed for (1) inspecting target worktrees / registered repo paths from MCP, and (2) greenfield setup under ~/sideboard/repos (git clone, gh repo create, git init+remote). Never git init/clone *inside* this synthetic home cwd \u2014 emptiness here is expected, not a bug."
|
|
@@ -6260,6 +6257,10 @@ function applyTurnUsage(current, incoming, scope = "request") {
|
|
|
6260
6257
|
const merged = mergeUsage(current, incoming);
|
|
6261
6258
|
return { ...merged, lastRequestTokens: requestOccupancy(incoming) };
|
|
6262
6259
|
}
|
|
6260
|
+
function contextTokens(u) {
|
|
6261
|
+
if (u.lastRequestTokens != null && u.lastRequestTokens > 0) return u.lastRequestTokens;
|
|
6262
|
+
return requestOccupancy(u);
|
|
6263
|
+
}
|
|
6263
6264
|
var init_usage = __esm({
|
|
6264
6265
|
"src/agents/usage.ts"() {
|
|
6265
6266
|
"use strict";
|
|
@@ -7200,14 +7201,7 @@ var init_injected_mcp = __esm({
|
|
|
7200
7201
|
"mcp__sideboard__present_schema",
|
|
7201
7202
|
"mcp__sideboard__present_files",
|
|
7202
7203
|
"mcp__sideboard__ask_user",
|
|
7203
|
-
"mcp__sideboard__present_plan"
|
|
7204
|
-
"mcp__sideboard__list_teams",
|
|
7205
|
-
"mcp__sideboard__slack_list_channels",
|
|
7206
|
-
"mcp__sideboard__slack_list_users",
|
|
7207
|
-
"mcp__sideboard__slack_search",
|
|
7208
|
-
"mcp__sideboard__slack_read",
|
|
7209
|
-
"mcp__sideboard__slack_post",
|
|
7210
|
-
"mcp__sideboard__slack_replies"
|
|
7204
|
+
"mcp__sideboard__present_plan"
|
|
7211
7205
|
];
|
|
7212
7206
|
brightsyMcpCommandCache = null;
|
|
7213
7207
|
BRIGHTSY_WORD = /(?<![a-z0-9_-])brightsy(?![a-z0-9_-])/i;
|
|
@@ -9491,6 +9485,14 @@ var init_cursor_stream_coalesce = __esm({
|
|
|
9491
9485
|
});
|
|
9492
9486
|
|
|
9493
9487
|
// src/agents/spawn.ts
|
|
9488
|
+
function applyPromptCacheTtlEnv(agent, env) {
|
|
9489
|
+
if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
|
|
9490
|
+
env.ENABLE_PROMPT_CACHING_1H ??= "1";
|
|
9491
|
+
}
|
|
9492
|
+
if (agent === "opencode" && env.OPENCODE_ANTHROPIC_FORCE_PROMPT_CACHING_5M !== "1") {
|
|
9493
|
+
env.OPENCODE_ANTHROPIC_PROMPT_CACHING_1H ??= "1";
|
|
9494
|
+
}
|
|
9495
|
+
}
|
|
9494
9496
|
async function spawnAgentTurn(thread, input, onEvent) {
|
|
9495
9497
|
ensureAgentPath();
|
|
9496
9498
|
if (!thread.worktreePath?.trim()) {
|
|
@@ -9516,6 +9518,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
9516
9518
|
);
|
|
9517
9519
|
}
|
|
9518
9520
|
const env = childEnvWithAppSettings(cmd.env);
|
|
9521
|
+
applyPromptCacheTtlEnv(thread.agent, env);
|
|
9519
9522
|
try {
|
|
9520
9523
|
if (isOrchestratorThread(thread)) {
|
|
9521
9524
|
mergeAgentGitAuthEnv(env, await resolveAgentGitAuthEnv(env));
|
|
@@ -9761,1701 +9764,1743 @@ var init_pr_merge_archive = __esm({
|
|
|
9761
9764
|
}
|
|
9762
9765
|
});
|
|
9763
9766
|
|
|
9764
|
-
// src/
|
|
9765
|
-
function
|
|
9766
|
-
const
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
return (0, import_node_fs26.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
9773
|
-
}
|
|
9774
|
-
function resolveFilesToCopy(repoPath) {
|
|
9775
|
-
const fromInclude = readWorktreeInclude(repoPath);
|
|
9776
|
-
if (fromInclude.length) return fromInclude;
|
|
9777
|
-
const settings = loadRepoSettings(repoPath);
|
|
9778
|
-
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
9779
|
-
if (settings?.fileIncludeGlobs?.length) {
|
|
9780
|
-
const matched = [];
|
|
9781
|
-
try {
|
|
9782
|
-
for (const entry of (0, import_node_fs26.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
9783
|
-
if (!entry.isFile()) continue;
|
|
9784
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
9785
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path25.basename)(glob), entry.name)) {
|
|
9786
|
-
matched.push(entry.name);
|
|
9787
|
-
break;
|
|
9788
|
-
}
|
|
9789
|
-
}
|
|
9790
|
-
}
|
|
9791
|
-
} catch {
|
|
9792
|
-
}
|
|
9793
|
-
if (matched.length) return [...new Set(matched)];
|
|
9794
|
-
}
|
|
9795
|
-
const defaults = [];
|
|
9796
|
-
try {
|
|
9797
|
-
for (const entry of (0, import_node_fs26.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
9798
|
-
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
9799
|
-
defaults.push(entry.name);
|
|
9800
|
-
}
|
|
9801
|
-
}
|
|
9802
|
-
} catch {
|
|
9803
|
-
}
|
|
9804
|
-
if (defaults.length) return defaults;
|
|
9805
|
-
return [".env.local", ".env"];
|
|
9806
|
-
}
|
|
9807
|
-
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
9808
|
-
const patterns = resolveFilesToCopy(repoPath);
|
|
9809
|
-
const copied = [];
|
|
9810
|
-
for (const rel of patterns) {
|
|
9811
|
-
const src = (0, import_node_path25.join)(repoPath, rel);
|
|
9812
|
-
if (!(0, import_node_fs26.existsSync)(src)) continue;
|
|
9813
|
-
const dest = (0, import_node_path25.join)(worktreePath, rel);
|
|
9814
|
-
(0, import_node_fs26.mkdirSync)((0, import_node_path25.dirname)(dest), { recursive: true });
|
|
9815
|
-
(0, import_node_fs26.copyFileSync)(src, dest);
|
|
9816
|
-
copied.push(rel);
|
|
9767
|
+
// src/composer/summarize.ts
|
|
9768
|
+
async function summarizeConversation(transcript, opts) {
|
|
9769
|
+
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
9770
|
+
|
|
9771
|
+
[\u2026truncated\u2026]` : transcript;
|
|
9772
|
+
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
9773
|
+
if (claudeSummary) {
|
|
9774
|
+
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
9817
9775
|
}
|
|
9818
|
-
return
|
|
9776
|
+
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
9819
9777
|
}
|
|
9820
|
-
async function
|
|
9821
|
-
|
|
9822
|
-
const
|
|
9778
|
+
async function tryClaudeSummary(transcript, opts) {
|
|
9779
|
+
ensureAgentPath();
|
|
9780
|
+
const prompt = [
|
|
9781
|
+
"Summarize this coding-agent conversation for continuity in a fresh session.",
|
|
9782
|
+
"Use concise markdown bullets covering:",
|
|
9783
|
+
"- Goal / request",
|
|
9784
|
+
"- Key decisions and constraints",
|
|
9785
|
+
"- Files / areas touched",
|
|
9786
|
+
"- Current state and open tasks",
|
|
9787
|
+
"- Anything the next turn must remember",
|
|
9788
|
+
"Do not invent details. No preamble.",
|
|
9789
|
+
"",
|
|
9790
|
+
"Transcript:",
|
|
9791
|
+
transcript
|
|
9792
|
+
].join("\n");
|
|
9823
9793
|
try {
|
|
9824
|
-
const
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9794
|
+
const { stdout, exitCode } = await run(
|
|
9795
|
+
"claude",
|
|
9796
|
+
[
|
|
9797
|
+
"-p",
|
|
9798
|
+
prompt,
|
|
9799
|
+
"--output-format",
|
|
9800
|
+
"text",
|
|
9801
|
+
"--permission-mode",
|
|
9802
|
+
"bypassPermissions",
|
|
9803
|
+
"--model",
|
|
9804
|
+
"haiku"
|
|
9805
|
+
],
|
|
9806
|
+
{ cwd: opts?.cwd, reject: false }
|
|
9807
|
+
);
|
|
9808
|
+
if (exitCode !== 0) return null;
|
|
9809
|
+
const text4 = stdout.trim();
|
|
9810
|
+
if (text4.length < 40) return null;
|
|
9811
|
+
return text4;
|
|
9841
9812
|
} catch {
|
|
9813
|
+
return null;
|
|
9842
9814
|
}
|
|
9843
|
-
cachedLoginEnv = { ...process.env };
|
|
9844
|
-
return { ...cachedLoginEnv };
|
|
9845
9815
|
}
|
|
9846
|
-
function
|
|
9847
|
-
const
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
const
|
|
9851
|
-
|
|
9852
|
-
const
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
9870
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
9816
|
+
function extractiveSummary(transcript) {
|
|
9817
|
+
const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
9818
|
+
const users = [];
|
|
9819
|
+
const tools = [];
|
|
9820
|
+
const agents = [];
|
|
9821
|
+
let mode = "none";
|
|
9822
|
+
for (const line of lines) {
|
|
9823
|
+
if (line.startsWith("### User")) {
|
|
9824
|
+
mode = "user";
|
|
9825
|
+
continue;
|
|
9826
|
+
}
|
|
9827
|
+
if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
|
|
9828
|
+
mode = "agent";
|
|
9829
|
+
continue;
|
|
9830
|
+
}
|
|
9831
|
+
if (line.startsWith("- ") && line.includes(":")) {
|
|
9832
|
+
tools.push(line.replace(/^- /, ""));
|
|
9833
|
+
continue;
|
|
9834
|
+
}
|
|
9835
|
+
if (mode === "user" && users.length < 8) {
|
|
9836
|
+
users.push(line.slice(0, 240));
|
|
9837
|
+
} else if (mode === "agent" && agents.length < 6) {
|
|
9838
|
+
agents.push(line.slice(0, 240));
|
|
9871
9839
|
}
|
|
9872
9840
|
}
|
|
9873
|
-
|
|
9841
|
+
const uniqueTools = [...new Set(tools)].slice(0, 20);
|
|
9842
|
+
const parts = [
|
|
9843
|
+
"## Compacted context (extractive)",
|
|
9844
|
+
"",
|
|
9845
|
+
"### User goals",
|
|
9846
|
+
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
9847
|
+
"",
|
|
9848
|
+
"### Recent agent notes",
|
|
9849
|
+
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
9850
|
+
];
|
|
9851
|
+
if (uniqueTools.length) {
|
|
9852
|
+
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
9853
|
+
}
|
|
9854
|
+
parts.push(
|
|
9855
|
+
"",
|
|
9856
|
+
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
9857
|
+
);
|
|
9858
|
+
return parts.join("\n");
|
|
9874
9859
|
}
|
|
9875
|
-
function
|
|
9876
|
-
if (
|
|
9877
|
-
|
|
9878
|
-
|
|
9860
|
+
function clipSummary(text4) {
|
|
9861
|
+
if (text4.length <= MAX_SUMMARY_CHARS) return text4;
|
|
9862
|
+
return `${text4.slice(0, MAX_SUMMARY_CHARS)}
|
|
9863
|
+
|
|
9864
|
+
[\u2026summary truncated\u2026]`;
|
|
9879
9865
|
}
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
setTimeout(() => {
|
|
9889
|
-
try {
|
|
9890
|
-
process.kill(-pid, "SIGKILL");
|
|
9891
|
-
} catch {
|
|
9892
|
-
}
|
|
9893
|
-
}, 2500).unref?.();
|
|
9894
|
-
}
|
|
9895
|
-
} catch {
|
|
9896
|
-
try {
|
|
9897
|
-
child.kill("SIGTERM");
|
|
9898
|
-
} catch {
|
|
9899
|
-
}
|
|
9900
|
-
}
|
|
9866
|
+
var MAX_TRANSCRIPT_CHARS, MAX_SUMMARY_CHARS;
|
|
9867
|
+
var init_summarize = __esm({
|
|
9868
|
+
"src/composer/summarize.ts"() {
|
|
9869
|
+
"use strict";
|
|
9870
|
+
init_run();
|
|
9871
|
+
init_path();
|
|
9872
|
+
MAX_TRANSCRIPT_CHARS = 12e4;
|
|
9873
|
+
MAX_SUMMARY_CHARS = 6e3;
|
|
9901
9874
|
}
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
);
|
|
9875
|
+
});
|
|
9876
|
+
|
|
9877
|
+
// src/composer/context-compact.ts
|
|
9878
|
+
function estimateMessageChars(message) {
|
|
9879
|
+
let n = message.text.length + 16;
|
|
9880
|
+
for (const part of message.parts ?? []) {
|
|
9881
|
+
n += estimatePartChars(part);
|
|
9882
|
+
}
|
|
9883
|
+
return n;
|
|
9884
|
+
}
|
|
9885
|
+
function estimatePartChars(part) {
|
|
9886
|
+
switch (part.type) {
|
|
9887
|
+
case "text":
|
|
9888
|
+
case "thinking":
|
|
9889
|
+
return part.text.length;
|
|
9890
|
+
case "tool": {
|
|
9891
|
+
const input = part.input ? JSON.stringify(part.input) : "";
|
|
9892
|
+
return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
|
|
9920
9893
|
}
|
|
9894
|
+
default:
|
|
9895
|
+
return 0;
|
|
9921
9896
|
}
|
|
9922
9897
|
}
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
const env = buildWorkspaceScriptEnv(
|
|
9926
|
-
{
|
|
9927
|
-
worktreePath: opts.worktreePath,
|
|
9928
|
-
repoPath: opts.repoPath,
|
|
9929
|
-
workspaceName: opts.workspaceName,
|
|
9930
|
-
defaultBranch: opts.defaultBranch,
|
|
9931
|
-
ports: opts.ports
|
|
9932
|
-
},
|
|
9933
|
-
loginEnv
|
|
9934
|
-
);
|
|
9935
|
-
try {
|
|
9936
|
-
mergeAgentGitAuthEnv(
|
|
9937
|
-
env,
|
|
9938
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
9939
|
-
);
|
|
9940
|
-
} catch {
|
|
9941
|
-
}
|
|
9942
|
-
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
9943
|
-
const child = (0, import_execa4.execa)(shell, ["-lc", command], {
|
|
9944
|
-
cwd: opts.worktreePath,
|
|
9945
|
-
reject: false,
|
|
9946
|
-
env,
|
|
9947
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
9948
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
9949
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
9950
|
-
});
|
|
9951
|
-
pipeLines(child.stdout, opts.onLine);
|
|
9952
|
-
pipeLines(child.stderr, opts.onLine);
|
|
9953
|
-
const ports = opts.ports ?? [];
|
|
9954
|
-
return {
|
|
9955
|
-
pid: child.pid,
|
|
9956
|
-
kill: () => killScriptTree(child, ports),
|
|
9957
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
9958
|
-
child
|
|
9959
|
-
};
|
|
9960
|
-
}
|
|
9961
|
-
async function attachAbort(handle, signal) {
|
|
9962
|
-
if (signal) {
|
|
9963
|
-
const onAbort = () => handle.kill();
|
|
9964
|
-
if (signal.aborted) onAbort();
|
|
9965
|
-
else signal.addEventListener("abort", onAbort, { once: true });
|
|
9966
|
-
}
|
|
9967
|
-
return handle.done;
|
|
9898
|
+
function estimateThreadChars(messages) {
|
|
9899
|
+
return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
|
|
9968
9900
|
}
|
|
9969
|
-
|
|
9970
|
-
const
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
repoPath,
|
|
9975
|
-
defaultBranch: opts?.defaultBranch,
|
|
9976
|
-
onLine
|
|
9977
|
-
});
|
|
9978
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
9979
|
-
return {
|
|
9980
|
-
ran: true,
|
|
9981
|
-
exitCode,
|
|
9982
|
-
source: workspaceSettingsSourceLabel(worktreePath, repoPath),
|
|
9983
|
-
kill: handle.kill
|
|
9984
|
-
};
|
|
9901
|
+
function shouldCompactContext(messages, thresholds = {}) {
|
|
9902
|
+
const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
|
|
9903
|
+
const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
|
|
9904
|
+
if (messages.length < minMessages) return false;
|
|
9905
|
+
return estimateThreadChars(messages) >= maxChars;
|
|
9985
9906
|
}
|
|
9986
|
-
|
|
9987
|
-
const
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9907
|
+
function splitForCompaction(messages, thresholds = {}) {
|
|
9908
|
+
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
9909
|
+
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
9910
|
+
if (messages.length === 0) return { older: [], recent: [] };
|
|
9911
|
+
let recentChars = 0;
|
|
9912
|
+
let cut = messages.length;
|
|
9913
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
9914
|
+
const next = recentChars + estimateMessageChars(messages[i]);
|
|
9915
|
+
const kept = messages.length - i;
|
|
9916
|
+
if (kept >= keepCount && next > keepChars) break;
|
|
9917
|
+
recentChars = next;
|
|
9918
|
+
cut = i;
|
|
9919
|
+
}
|
|
9920
|
+
const minRecent = Math.min(keepCount, messages.length);
|
|
9921
|
+
cut = Math.min(cut, messages.length - minRecent);
|
|
9922
|
+
if (cut <= 0) return { older: [], recent: messages };
|
|
9997
9923
|
return {
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
source: found.source,
|
|
10001
|
-
kill: handle.kill
|
|
9924
|
+
older: messages.slice(0, cut),
|
|
9925
|
+
recent: messages.slice(cut)
|
|
10002
9926
|
};
|
|
10003
9927
|
}
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
9928
|
+
function formatToolPart(t, detail) {
|
|
9929
|
+
if (detail === "summary") {
|
|
9930
|
+
const label = t.description || t.detail || t.name;
|
|
9931
|
+
const path = t.filePath ? ` (${t.filePath})` : "";
|
|
9932
|
+
return `- ${t.name}: ${label}${path}`;
|
|
10008
9933
|
}
|
|
10009
|
-
|
|
10010
|
-
|
|
9934
|
+
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
9935
|
+
if (t.description) lines.push(`Description: ${t.description}`);
|
|
9936
|
+
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
9937
|
+
if (t.input && Object.keys(t.input).length > 0) {
|
|
9938
|
+
lines.push("Input:");
|
|
9939
|
+
lines.push("```json");
|
|
9940
|
+
lines.push(JSON.stringify(t.input, null, 2));
|
|
9941
|
+
lines.push("```");
|
|
9942
|
+
} else if (t.detail) {
|
|
9943
|
+
lines.push(`Detail: ${t.detail}`);
|
|
10011
9944
|
}
|
|
10012
|
-
|
|
9945
|
+
if (t.result != null && t.result !== "") {
|
|
9946
|
+
lines.push("Result:");
|
|
9947
|
+
lines.push("```");
|
|
9948
|
+
lines.push(t.result);
|
|
9949
|
+
lines.push("```");
|
|
9950
|
+
}
|
|
9951
|
+
return lines.join("\n");
|
|
10013
9952
|
}
|
|
10014
|
-
|
|
10015
|
-
const
|
|
10016
|
-
|
|
10017
|
-
const
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
9953
|
+
function formatMessagesAsTranscript(messages, opts) {
|
|
9954
|
+
const tools = opts?.tools ?? "full";
|
|
9955
|
+
const blocks = [];
|
|
9956
|
+
for (const m of messages) {
|
|
9957
|
+
if (m.role === "summary") {
|
|
9958
|
+
blocks.push(`## Prior summary
|
|
9959
|
+
${m.text}`);
|
|
9960
|
+
continue;
|
|
9961
|
+
}
|
|
9962
|
+
if (m.role === "user") {
|
|
9963
|
+
blocks.push(`### User
|
|
9964
|
+
${m.text}`);
|
|
9965
|
+
continue;
|
|
9966
|
+
}
|
|
9967
|
+
const bits = [];
|
|
9968
|
+
if (m.text?.trim()) {
|
|
9969
|
+
bits.push(`### Agent
|
|
9970
|
+
${m.text}`);
|
|
9971
|
+
} else {
|
|
9972
|
+
bits.push("### Agent");
|
|
9973
|
+
}
|
|
9974
|
+
const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
|
|
9975
|
+
for (const th of thinking) {
|
|
9976
|
+
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
9977
|
+
bits.push("Thinking:");
|
|
9978
|
+
bits.push(th.text);
|
|
9979
|
+
}
|
|
9980
|
+
const toolParts = (m.parts ?? []).filter(
|
|
9981
|
+
(p) => p.type === "tool"
|
|
9982
|
+
);
|
|
9983
|
+
if (tools !== "none" && toolParts.length > 0) {
|
|
9984
|
+
if (tools === "summary") bits.push("Tools:");
|
|
9985
|
+
for (const t of toolParts) {
|
|
9986
|
+
bits.push(formatToolPart(t, tools));
|
|
9987
|
+
}
|
|
9988
|
+
}
|
|
9989
|
+
blocks.push(bits.join("\n"));
|
|
9990
|
+
}
|
|
9991
|
+
return blocks.join("\n\n");
|
|
10024
9992
|
}
|
|
10025
|
-
function
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
9993
|
+
function buildSessionSeed(messages, opts) {
|
|
9994
|
+
if (messages.length === 0) return null;
|
|
9995
|
+
const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
|
|
9996
|
+
if (!body.trim()) return null;
|
|
9997
|
+
return [
|
|
9998
|
+
"Sideboard conversation context (restored after compaction or a new session):",
|
|
9999
|
+
"",
|
|
10000
|
+
body,
|
|
10001
|
+
"",
|
|
10002
|
+
"Continue from this context. Do not repeat the summary unless asked."
|
|
10003
|
+
].join("\n");
|
|
10032
10004
|
}
|
|
10033
|
-
function
|
|
10034
|
-
const
|
|
10035
|
-
if (
|
|
10036
|
-
|
|
10005
|
+
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
10006
|
+
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
10007
|
+
if (older.length === 0) return messages;
|
|
10008
|
+
const summary = {
|
|
10009
|
+
role: "summary",
|
|
10010
|
+
text: summaryText.trim(),
|
|
10011
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
10012
|
+
};
|
|
10013
|
+
return [summary, ...recent];
|
|
10037
10014
|
}
|
|
10038
|
-
function
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
return scripts.find((s) => s.name === name) ?? null;
|
|
10015
|
+
function lastRequestOccupancy(thread) {
|
|
10016
|
+
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
10017
|
+
const usage = thread.messages[i]?.usage;
|
|
10018
|
+
if (usage) return contextTokens(usage);
|
|
10043
10019
|
}
|
|
10044
|
-
return
|
|
10020
|
+
return 0;
|
|
10045
10021
|
}
|
|
10046
|
-
function
|
|
10047
|
-
return
|
|
10022
|
+
function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
|
|
10023
|
+
return lastRequestOccupancy(thread) >= occupancyTokens;
|
|
10048
10024
|
}
|
|
10049
|
-
async function
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
server.close((err) => err ? reject(err) : resolve(port));
|
|
10061
|
-
});
|
|
10062
|
-
server.on("error", reject);
|
|
10025
|
+
async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
|
|
10026
|
+
if (!shouldCompactContext(thread.messages, thresholds)) {
|
|
10027
|
+
return { didCompact: false, thread };
|
|
10028
|
+
}
|
|
10029
|
+
const { older } = splitForCompaction(thread.messages, thresholds);
|
|
10030
|
+
if (older.length === 0) {
|
|
10031
|
+
return { didCompact: false, thread };
|
|
10032
|
+
}
|
|
10033
|
+
const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
|
|
10034
|
+
const { summary, method } = await summarize(transcript, {
|
|
10035
|
+
cwd: thread.worktreePath
|
|
10063
10036
|
});
|
|
10037
|
+
const messages = applyCompaction(thread.messages, summary, thresholds);
|
|
10038
|
+
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
10039
|
+
const next = {
|
|
10040
|
+
...thread,
|
|
10041
|
+
messages,
|
|
10042
|
+
sessionId: resetSession ? null : thread.sessionId,
|
|
10043
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10044
|
+
};
|
|
10045
|
+
return {
|
|
10046
|
+
didCompact: true,
|
|
10047
|
+
thread: next,
|
|
10048
|
+
summary,
|
|
10049
|
+
method,
|
|
10050
|
+
olderCount: older.length
|
|
10051
|
+
};
|
|
10064
10052
|
}
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
for (let i = 1; i < size; i++) {
|
|
10069
|
-
const candidate = base + i;
|
|
10070
|
-
const free = await new Promise((resolve) => {
|
|
10071
|
-
const server = (0, import_node_net.createServer)();
|
|
10072
|
-
server.once("error", () => resolve(false));
|
|
10073
|
-
server.listen(candidate, "127.0.0.1", () => {
|
|
10074
|
-
server.close(() => resolve(true));
|
|
10075
|
-
});
|
|
10076
|
-
});
|
|
10077
|
-
if (free) {
|
|
10078
|
-
ports.push(candidate);
|
|
10079
|
-
} else {
|
|
10080
|
-
ports.push(await allocatePort());
|
|
10081
|
-
}
|
|
10082
|
-
}
|
|
10083
|
-
return ports;
|
|
10084
|
-
}
|
|
10085
|
-
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
10086
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
10087
|
-
if (!script) return null;
|
|
10088
|
-
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
10089
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
10090
|
-
worktreePath,
|
|
10091
|
-
repoPath,
|
|
10092
|
-
ports,
|
|
10093
|
-
defaultBranch: opts?.defaultBranch,
|
|
10094
|
-
onLine
|
|
10095
|
-
});
|
|
10096
|
-
return {
|
|
10097
|
-
pid: handle.pid,
|
|
10098
|
-
port: ports[0],
|
|
10099
|
-
ports,
|
|
10100
|
-
scriptName: script.name,
|
|
10101
|
-
kill: handle.kill,
|
|
10102
|
-
done: handle.done
|
|
10103
|
-
};
|
|
10104
|
-
}
|
|
10105
|
-
var import_node_fs26, import_node_net, import_node_path25, import_execa4, import_node_readline3, PORT_RANGE_SIZE, cachedLoginEnv;
|
|
10106
|
-
var init_conductor = __esm({
|
|
10107
|
-
"src/hook/conductor.ts"() {
|
|
10053
|
+
var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAGES, CONTEXT_MIN_MESSAGES, SESSION_RESET_OCCUPANCY_TOKENS;
|
|
10054
|
+
var init_context_compact = __esm({
|
|
10055
|
+
"src/composer/context-compact.ts"() {
|
|
10108
10056
|
"use strict";
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
init_convention_setup();
|
|
10117
|
-
init_cursor_worktrees();
|
|
10118
|
-
init_git_auth_mode();
|
|
10119
|
-
init_nested_electron_env();
|
|
10120
|
-
PORT_RANGE_SIZE = 10;
|
|
10121
|
-
cachedLoginEnv = null;
|
|
10057
|
+
init_usage();
|
|
10058
|
+
init_summarize();
|
|
10059
|
+
CONTEXT_COMPACT_CHARS = 4e5;
|
|
10060
|
+
CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
10061
|
+
CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
10062
|
+
CONTEXT_MIN_MESSAGES = 10;
|
|
10063
|
+
SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
|
|
10122
10064
|
}
|
|
10123
10065
|
});
|
|
10124
10066
|
|
|
10125
|
-
// src/
|
|
10126
|
-
function
|
|
10127
|
-
return
|
|
10128
|
-
}
|
|
10129
|
-
async function findOrphanWorktrees(repoPaths) {
|
|
10130
|
-
const threads = listThreads({ includeArchived: true });
|
|
10131
|
-
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
10132
|
-
const repos = new Set(
|
|
10133
|
-
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
10134
|
-
);
|
|
10135
|
-
const homeRoot = sideboardWorkspacesDir();
|
|
10136
|
-
if ((0, import_node_fs27.existsSync)(homeRoot)) {
|
|
10137
|
-
try {
|
|
10138
|
-
for (const entry of (0, import_node_fs27.readdirSync)(homeRoot, { withFileTypes: true })) {
|
|
10139
|
-
if (!entry.isDirectory()) continue;
|
|
10140
|
-
void entry;
|
|
10141
|
-
}
|
|
10142
|
-
} catch {
|
|
10143
|
-
}
|
|
10144
|
-
}
|
|
10145
|
-
const orphans = [];
|
|
10146
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10147
|
-
for (const repoPath of repos) {
|
|
10148
|
-
if (!repoPath || !(0, import_node_fs27.existsSync)(repoPath)) continue;
|
|
10149
|
-
try {
|
|
10150
|
-
const wts = await listWorktrees(repoPath);
|
|
10151
|
-
for (const wt of wts) {
|
|
10152
|
-
const path = wt.path.replace(/\/$/, "");
|
|
10153
|
-
if (!isSideboardWorktreePath(path)) continue;
|
|
10154
|
-
if (known.has(path)) continue;
|
|
10155
|
-
if (seen.has(path)) continue;
|
|
10156
|
-
seen.add(path);
|
|
10157
|
-
let mtimeMs = 0;
|
|
10158
|
-
try {
|
|
10159
|
-
mtimeMs = (0, import_node_fs27.statSync)(path).mtimeMs;
|
|
10160
|
-
} catch {
|
|
10161
|
-
mtimeMs = 0;
|
|
10162
|
-
}
|
|
10163
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
10164
|
-
}
|
|
10165
|
-
} catch {
|
|
10166
|
-
}
|
|
10167
|
-
try {
|
|
10168
|
-
const root = worktreesRoot(repoPath);
|
|
10169
|
-
if ((0, import_node_fs27.existsSync)(root)) {
|
|
10170
|
-
for (const entry of (0, import_node_fs27.readdirSync)(root, { withFileTypes: true })) {
|
|
10171
|
-
if (!entry.isDirectory()) continue;
|
|
10172
|
-
const path = (0, import_node_path26.join)(root, entry.name).replace(/\/$/, "");
|
|
10173
|
-
if (known.has(path) || seen.has(path)) continue;
|
|
10174
|
-
if (!(0, import_node_fs27.existsSync)((0, import_node_path26.join)(path, ".git"))) continue;
|
|
10175
|
-
seen.add(path);
|
|
10176
|
-
let mtimeMs = 0;
|
|
10177
|
-
try {
|
|
10178
|
-
mtimeMs = (0, import_node_fs27.statSync)(path).mtimeMs;
|
|
10179
|
-
} catch {
|
|
10180
|
-
mtimeMs = Date.now();
|
|
10181
|
-
}
|
|
10182
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
10183
|
-
}
|
|
10184
|
-
}
|
|
10185
|
-
} catch {
|
|
10186
|
-
}
|
|
10187
|
-
}
|
|
10188
|
-
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
10067
|
+
// src/threads/chat-tabs.ts
|
|
10068
|
+
function sameWorktreePath(a, b) {
|
|
10069
|
+
return normalizeWorktreePath(a) === normalizeWorktreePath(b);
|
|
10189
10070
|
}
|
|
10190
|
-
|
|
10191
|
-
const
|
|
10192
|
-
const
|
|
10193
|
-
const
|
|
10194
|
-
|
|
10195
|
-
const
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
const removed = [];
|
|
10199
|
-
const kept = [];
|
|
10200
|
-
for (const orphan of orphans) {
|
|
10201
|
-
if (keep.has(orphan.path)) {
|
|
10202
|
-
kept.push(orphan.path);
|
|
10203
|
-
continue;
|
|
10204
|
-
}
|
|
10205
|
-
if (opts?.dryRun) {
|
|
10206
|
-
removed.push(orphan.path);
|
|
10207
|
-
continue;
|
|
10208
|
-
}
|
|
10209
|
-
try {
|
|
10210
|
-
await removeWorktree(orphan.repoPath, orphan.path);
|
|
10211
|
-
removed.push(orphan.path);
|
|
10212
|
-
} catch {
|
|
10213
|
-
kept.push(orphan.path);
|
|
10071
|
+
function takenTeamSlugsForChatTab(worktreePath) {
|
|
10072
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
10073
|
+
const taken = /* @__PURE__ */ new Set();
|
|
10074
|
+
const dir = worktreeNameFromPath(key);
|
|
10075
|
+
if (dir) taken.add(dir.toLowerCase());
|
|
10076
|
+
for (const sibling of threadsSharingWorktree(key)) {
|
|
10077
|
+
for (const slug of takenSlugsFromThread(sibling)) {
|
|
10078
|
+
taken.add(slug);
|
|
10214
10079
|
}
|
|
10215
10080
|
}
|
|
10216
|
-
|
|
10217
|
-
updateAdvancedSettings({
|
|
10218
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10219
|
-
});
|
|
10220
|
-
}
|
|
10221
|
-
return { removed, kept, orphans };
|
|
10081
|
+
return [...taken];
|
|
10222
10082
|
}
|
|
10223
|
-
function
|
|
10224
|
-
const
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
const elapsed = Date.now() - Date.parse(last);
|
|
10228
|
-
return elapsed >= intervalHours * 36e5;
|
|
10083
|
+
function requireThread(idOrRef) {
|
|
10084
|
+
const thread = findThreadByRef(idOrRef) ?? null;
|
|
10085
|
+
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
10086
|
+
return thread;
|
|
10229
10087
|
}
|
|
10230
|
-
|
|
10231
|
-
var init_orphan_cleanup = __esm({
|
|
10232
|
-
"src/git/orphan-cleanup.ts"() {
|
|
10233
|
-
"use strict";
|
|
10234
|
-
import_node_fs27 = require("fs");
|
|
10235
|
-
import_node_path26 = require("path");
|
|
10236
|
-
init_worktree();
|
|
10237
|
-
init_thread_store();
|
|
10238
|
-
init_paths();
|
|
10239
|
-
init_app_settings();
|
|
10240
|
-
}
|
|
10241
|
-
});
|
|
10242
|
-
|
|
10243
|
-
// src/git/apply-into-main.ts
|
|
10244
|
-
async function applyThreadIntoMain(thread, opts) {
|
|
10245
|
-
const method = opts?.method ?? "merge";
|
|
10246
|
-
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
10247
|
-
const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
|
|
10248
|
-
cwd: thread.repoPath,
|
|
10249
|
-
reject: false
|
|
10250
|
-
});
|
|
10251
|
-
if (status.stdout.trim()) {
|
|
10252
|
-
throw new Error(
|
|
10253
|
-
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
10254
|
-
);
|
|
10255
|
-
}
|
|
10256
|
-
const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
|
|
10257
|
-
cwd: thread.repoPath,
|
|
10258
|
-
reject: false
|
|
10259
|
-
});
|
|
10260
|
-
if (checkout.exitCode !== 0) {
|
|
10261
|
-
throw new Error(
|
|
10262
|
-
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
10263
|
-
);
|
|
10264
|
-
}
|
|
10265
|
-
if (method === "cherry-pick") {
|
|
10266
|
-
const log = await (0, import_execa5.execa)(
|
|
10267
|
-
"git",
|
|
10268
|
-
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
10269
|
-
{ cwd: thread.repoPath, reject: false }
|
|
10270
|
-
);
|
|
10271
|
-
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
10272
|
-
if (!shas.length) {
|
|
10273
|
-
return {
|
|
10274
|
-
applied: false,
|
|
10275
|
-
method,
|
|
10276
|
-
targetBranch: target,
|
|
10277
|
-
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
10278
|
-
};
|
|
10279
|
-
}
|
|
10280
|
-
for (const sha of shas) {
|
|
10281
|
-
const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
|
|
10282
|
-
cwd: thread.repoPath,
|
|
10283
|
-
reject: false
|
|
10284
|
-
});
|
|
10285
|
-
if (cp.exitCode !== 0) {
|
|
10286
|
-
await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
|
|
10287
|
-
cwd: thread.repoPath,
|
|
10288
|
-
reject: false
|
|
10289
|
-
});
|
|
10290
|
-
throw new Error(
|
|
10291
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
10292
|
-
);
|
|
10293
|
-
}
|
|
10294
|
-
}
|
|
10295
|
-
return {
|
|
10296
|
-
applied: true,
|
|
10297
|
-
method,
|
|
10298
|
-
targetBranch: target,
|
|
10299
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
10300
|
-
};
|
|
10301
|
-
}
|
|
10302
|
-
const merge = await (0, import_execa5.execa)(
|
|
10303
|
-
"git",
|
|
10304
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
10305
|
-
{ cwd: thread.repoPath, reject: false }
|
|
10306
|
-
);
|
|
10307
|
-
if (merge.exitCode !== 0) {
|
|
10308
|
-
await (0, import_execa5.execa)("git", ["merge", "--abort"], {
|
|
10309
|
-
cwd: thread.repoPath,
|
|
10310
|
-
reject: false
|
|
10311
|
-
});
|
|
10312
|
-
throw new Error(
|
|
10313
|
-
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
10314
|
-
);
|
|
10315
|
-
}
|
|
10088
|
+
function worktreeBindingFrom(from) {
|
|
10316
10089
|
return {
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
10090
|
+
worktreePath: normalizeWorktreePath(from.worktreePath),
|
|
10091
|
+
repoPath: from.repoPath,
|
|
10092
|
+
branchName: from.branchName,
|
|
10093
|
+
sourceRef: from.sourceRef,
|
|
10094
|
+
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
10095
|
+
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
10096
|
+
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
10097
|
+
sourceIsFork: from.sourceIsFork,
|
|
10098
|
+
parentThreadId: from.parentThreadId,
|
|
10099
|
+
prUrl: from.prUrl,
|
|
10100
|
+
prTitle: from.prTitle,
|
|
10101
|
+
prState: from.prState,
|
|
10102
|
+
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
10103
|
+
stackId: from.stackId,
|
|
10104
|
+
stackLayer: from.stackLayer
|
|
10321
10105
|
};
|
|
10322
10106
|
}
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
}
|
|
10330
|
-
});
|
|
10331
|
-
|
|
10332
|
-
// src/store/workspaces.ts
|
|
10333
|
-
var workspaces_exports = {};
|
|
10334
|
-
__export(workspaces_exports, {
|
|
10335
|
-
addWorkspace: () => addWorkspace,
|
|
10336
|
-
ensureWorkspace: () => ensureWorkspace,
|
|
10337
|
-
listWorkspaces: () => listWorkspaces,
|
|
10338
|
-
removeWorkspace: () => removeWorkspace,
|
|
10339
|
-
syncWorkspacesFromThreads: () => syncWorkspacesFromThreads
|
|
10340
|
-
});
|
|
10341
|
-
function workspacesFile() {
|
|
10342
|
-
return (0, import_node_path27.join)(appDataDir(), "workspaces.json");
|
|
10343
|
-
}
|
|
10344
|
-
function removedWorkspacesFile() {
|
|
10345
|
-
return (0, import_node_path27.join)(appDataDir(), "removed-workspaces.json");
|
|
10346
|
-
}
|
|
10347
|
-
function readAll() {
|
|
10348
|
-
const path = workspacesFile();
|
|
10349
|
-
if (!(0, import_node_fs28.existsSync)(path)) return [];
|
|
10350
|
-
try {
|
|
10351
|
-
const raw = JSON.parse((0, import_node_fs28.readFileSync)(path, "utf8"));
|
|
10352
|
-
return Array.isArray(raw) ? raw : [];
|
|
10353
|
-
} catch {
|
|
10354
|
-
return [];
|
|
10355
|
-
}
|
|
10356
|
-
}
|
|
10357
|
-
function writeAll(list) {
|
|
10358
|
-
(0, import_node_fs28.mkdirSync)(appDataDir(), { recursive: true });
|
|
10359
|
-
(0, import_node_fs28.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
|
|
10360
|
-
}
|
|
10361
|
-
function readRemoved() {
|
|
10362
|
-
const path = removedWorkspacesFile();
|
|
10363
|
-
if (!(0, import_node_fs28.existsSync)(path)) return /* @__PURE__ */ new Set();
|
|
10364
|
-
try {
|
|
10365
|
-
const raw = JSON.parse((0, import_node_fs28.readFileSync)(path, "utf8"));
|
|
10366
|
-
return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
|
|
10367
|
-
} catch {
|
|
10368
|
-
return /* @__PURE__ */ new Set();
|
|
10369
|
-
}
|
|
10370
|
-
}
|
|
10371
|
-
function writeRemoved(paths) {
|
|
10372
|
-
(0, import_node_fs28.mkdirSync)(appDataDir(), { recursive: true });
|
|
10373
|
-
(0, import_node_fs28.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
|
|
10374
|
-
}
|
|
10375
|
-
function rememberRemoved(repoPath) {
|
|
10376
|
-
const next = readRemoved();
|
|
10377
|
-
next.add(repoPath);
|
|
10378
|
-
writeRemoved(next);
|
|
10379
|
-
}
|
|
10380
|
-
function forgetRemoved(repoPath) {
|
|
10381
|
-
const next = readRemoved();
|
|
10382
|
-
if (!next.delete(repoPath)) return;
|
|
10383
|
-
writeRemoved(next);
|
|
10384
|
-
}
|
|
10385
|
-
function listWorkspaces() {
|
|
10386
|
-
const all = readAll();
|
|
10387
|
-
const valid = all.filter(
|
|
10388
|
-
(w) => Boolean(w.path) && w.path !== "/" && w.path !== "." && !isGlobalRepoPath(w.path)
|
|
10389
|
-
);
|
|
10390
|
-
if (valid.length !== all.length) writeAll(valid);
|
|
10391
|
-
return valid.sort((a, b) => a.name.localeCompare(b.name));
|
|
10107
|
+
function threadsSharingWorktree(worktreePath) {
|
|
10108
|
+
if (!worktreePath) return [];
|
|
10109
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
10110
|
+
return listThreads({ includeArchived: true }).filter(
|
|
10111
|
+
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
10112
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
10392
10113
|
}
|
|
10393
|
-
|
|
10394
|
-
const
|
|
10395
|
-
|
|
10396
|
-
if (!(0, import_node_fs28.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
|
|
10397
|
-
forgetRemoved(root);
|
|
10398
|
-
await ensureGhPreferOrigin(root);
|
|
10399
|
-
const current = readAll();
|
|
10400
|
-
const existing = current.find((w) => w.path === root);
|
|
10401
|
-
if (existing) return existing;
|
|
10402
|
-
const next = {
|
|
10403
|
-
path: root,
|
|
10404
|
-
name: (0, import_node_path27.basename)(root),
|
|
10405
|
-
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10406
|
-
};
|
|
10407
|
-
writeAll([...current, next]);
|
|
10408
|
-
return next;
|
|
10114
|
+
function formatTranscriptMarkdown(title, messages) {
|
|
10115
|
+
const body = formatMessagesAsTranscript(messages, { tools: "full" });
|
|
10116
|
+
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
10409
10117
|
}
|
|
10410
|
-
function
|
|
10411
|
-
|
|
10412
|
-
|
|
10118
|
+
function forkMessageSlice(from, throughIndex) {
|
|
10119
|
+
const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
|
|
10120
|
+
return through >= 0 ? from.messages.slice(0, through + 1) : [];
|
|
10413
10121
|
}
|
|
10414
|
-
|
|
10415
|
-
|
|
10122
|
+
function buildForkTranscriptAttachment(baseTitle, messages) {
|
|
10123
|
+
const title = baseTitle || "Chat";
|
|
10124
|
+
return {
|
|
10125
|
+
id: (0, import_node_crypto4.randomUUID)(),
|
|
10126
|
+
name: `Transcript of ${title}.md`,
|
|
10127
|
+
kind: "transcript",
|
|
10128
|
+
content: formatTranscriptMarkdown(title, messages)
|
|
10129
|
+
};
|
|
10416
10130
|
}
|
|
10417
|
-
function
|
|
10418
|
-
const
|
|
10419
|
-
const
|
|
10420
|
-
const
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
}
|
|
10426
|
-
if (!(0, import_node_fs28.existsSync)(path)) continue;
|
|
10427
|
-
const ws = {
|
|
10428
|
-
path,
|
|
10429
|
-
name: (0, import_node_path27.basename)(path),
|
|
10430
|
-
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10431
|
-
};
|
|
10432
|
-
byPath.set(path, ws);
|
|
10433
|
-
dirty = true;
|
|
10131
|
+
function createChatTab(input) {
|
|
10132
|
+
const from = requireThread(input.fromThreadId);
|
|
10133
|
+
const binding = worktreeBindingFrom(from);
|
|
10134
|
+
const explicitTitle = input.title?.trim();
|
|
10135
|
+
const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
|
|
10136
|
+
const nextAgent = input.agent ?? from.agent;
|
|
10137
|
+
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
10138
|
+
assertOrchestratorCapableAgent(nextAgent);
|
|
10434
10139
|
}
|
|
10435
|
-
const
|
|
10436
|
-
|
|
10437
|
-
|
|
10140
|
+
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
10141
|
+
const thread = createEmptyThread({
|
|
10142
|
+
title,
|
|
10143
|
+
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
10144
|
+
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
10145
|
+
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
10146
|
+
userSetTitle: true,
|
|
10147
|
+
...binding,
|
|
10148
|
+
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
10149
|
+
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
10150
|
+
// the connected chat.
|
|
10151
|
+
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
10152
|
+
agent: nextAgent,
|
|
10153
|
+
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
10154
|
+
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
10155
|
+
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
10156
|
+
planMode: from.planMode,
|
|
10157
|
+
autonomy: input.autonomy ?? from.autonomy,
|
|
10158
|
+
attachments: input.attachments ?? [],
|
|
10159
|
+
status: "idle"
|
|
10160
|
+
});
|
|
10161
|
+
writeThread(thread);
|
|
10162
|
+
return thread;
|
|
10438
10163
|
}
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10164
|
+
function forkChatTab(input) {
|
|
10165
|
+
const from = requireThread(input.threadId);
|
|
10166
|
+
const slice = forkMessageSlice(from, input.throughIndex);
|
|
10167
|
+
const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
|
|
10168
|
+
const tab = createChatTab({
|
|
10169
|
+
fromThreadId: input.threadId,
|
|
10170
|
+
agent: input.agent ?? from.agent,
|
|
10171
|
+
model: input.model,
|
|
10172
|
+
title: input.title?.trim() || void 0,
|
|
10173
|
+
attachments: [attachment]
|
|
10174
|
+
});
|
|
10175
|
+
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
10176
|
+
const next = { ...tab, parentThreadId: from.id };
|
|
10177
|
+
writeThread(next);
|
|
10178
|
+
return next;
|
|
10179
|
+
}
|
|
10180
|
+
return tab;
|
|
10181
|
+
}
|
|
10182
|
+
var import_node_crypto4;
|
|
10183
|
+
var init_chat_tabs = __esm({
|
|
10184
|
+
"src/threads/chat-tabs.ts"() {
|
|
10442
10185
|
"use strict";
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10186
|
+
import_node_crypto4 = require("crypto");
|
|
10187
|
+
init_context_compact();
|
|
10188
|
+
init_teams();
|
|
10189
|
+
init_worktree_labels();
|
|
10446
10190
|
init_global_workspace();
|
|
10447
|
-
|
|
10191
|
+
init_orchestrator_capable();
|
|
10192
|
+
init_thread_store();
|
|
10193
|
+
init_worktree_labels();
|
|
10448
10194
|
}
|
|
10449
10195
|
});
|
|
10450
10196
|
|
|
10451
|
-
// src/
|
|
10452
|
-
|
|
10453
|
-
const
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
name = leaf || "repo";
|
|
10459
|
-
}
|
|
10460
|
-
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
10461
|
-
const dest = (0, import_node_path28.join)(sideboardReposDir(), name);
|
|
10462
|
-
if ((0, import_node_fs29.existsSync)(dest)) {
|
|
10463
|
-
const repoPath2 = await resolveRepoRoot(dest);
|
|
10464
|
-
const workspace2 = await ensureWorkspace(repoPath2);
|
|
10465
|
-
return { repoPath: repoPath2, workspace: workspace2 };
|
|
10466
|
-
}
|
|
10467
|
-
const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
|
|
10468
|
-
if (result.exitCode !== 0) {
|
|
10469
|
-
throw new Error(
|
|
10470
|
-
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
10471
|
-
);
|
|
10197
|
+
// src/review/review-request-template.ts
|
|
10198
|
+
function wrapReviewSkillMarkdown(body) {
|
|
10199
|
+
const trimmed = body.replace(/^\uFEFF/, "").trim();
|
|
10200
|
+
const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
|
|
10201
|
+
if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
|
|
10202
|
+
return source.endsWith("\n") ? source : `${source}
|
|
10203
|
+
`;
|
|
10472
10204
|
}
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
return { repoPath, workspace };
|
|
10205
|
+
return `${REVIEW_SKILL_FRONTMATTER}${source}
|
|
10206
|
+
`;
|
|
10476
10207
|
}
|
|
10477
|
-
var
|
|
10478
|
-
var
|
|
10479
|
-
"src/
|
|
10208
|
+
var REVIEW_REQUEST_TEMPLATE, REVIEW_SKILL_PATH, REVIEW_SKILL_NAME, REVIEW_SKILL_FRONTMATTER;
|
|
10209
|
+
var init_review_request_template = __esm({
|
|
10210
|
+
"src/review/review-request-template.ts"() {
|
|
10480
10211
|
"use strict";
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10212
|
+
REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
|
|
10213
|
+
|
|
10214
|
+
You are reviewing a proposed code change so a human can decide whether it is **ready to merge / land**. Findings matter, but the primary deliverable is a clear readiness recommendation \u2014 not a laundry list of style notes.
|
|
10215
|
+
|
|
10216
|
+
## Required outcome
|
|
10217
|
+
|
|
10218
|
+
Start your reply with a **Recommendation** section using exactly one of:
|
|
10219
|
+
|
|
10220
|
+
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
10221
|
+
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
10222
|
+
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
10223
|
+
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
10224
|
+
|
|
10225
|
+
In 1\u20133 sentences, say **why** \u2014 grounded in correctness, risk, test coverage, and scope \u2014 not vibes. If you request changes, name the blockers explicitly.
|
|
10226
|
+
|
|
10227
|
+
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
10228
|
+
|
|
10229
|
+
## Findings
|
|
10230
|
+
|
|
10231
|
+
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
10232
|
+
|
|
10233
|
+
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
10234
|
+
|
|
10235
|
+
Flag something as a bug / blocking finding only when:
|
|
10236
|
+
|
|
10237
|
+
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
10238
|
+
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
10239
|
+
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
10240
|
+
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
10241
|
+
5. The author would likely fix it if made aware.
|
|
10242
|
+
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
10243
|
+
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
10244
|
+
8. It is clearly not just an intentional change by the author.
|
|
10245
|
+
|
|
10246
|
+
When flagging an issue, include a short accompanying comment:
|
|
10247
|
+
|
|
10248
|
+
1. Clear about why it is a problem.
|
|
10249
|
+
2. Severity must match reality \u2014 do not inflate.
|
|
10250
|
+
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
10251
|
+
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
10252
|
+
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
10253
|
+
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
10254
|
+
7. Skimmable on first read.
|
|
10255
|
+
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
10256
|
+
|
|
10257
|
+
HOW MANY FINDINGS TO RETURN:
|
|
10258
|
+
|
|
10259
|
+
List every finding the author would fix if they knew about it. If nothing qualifies, say so and still give the Recommendation. Do not stop at the first finding.
|
|
10260
|
+
|
|
10261
|
+
GUIDELINES:
|
|
10262
|
+
|
|
10263
|
+
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
10264
|
+
- One comment per distinct issue (or a short multi-line range if needed).
|
|
10265
|
+
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
10266
|
+
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
10267
|
+
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
10268
|
+
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
10269
|
+
|
|
10270
|
+
The report appears in chat (and can become Sideboard diff comments). Avoid unnecessary location chatter in the body; keep line ranges as short as possible (prefer \u22645\u201310 lines).
|
|
10271
|
+
|
|
10272
|
+
## Getting the diff
|
|
10273
|
+
|
|
10274
|
+
Use Sideboard's diff for this thread's worktree. Prefer the \`get_diff\` MCP tool (pass this thread's ref) for a compact summary, then read specific files with Read/Glob as needed. In the Sideboard desktop app, the Changes panel shows the same worktree diff.
|
|
10275
|
+
|
|
10276
|
+
If the user asks you to address or read line comments they added in the Changes / file diff UI, those arrive as \`diff-comment\` attachments on the next turn \u2014 follow them precisely.
|
|
10277
|
+
|
|
10278
|
+
## Fallback: if you don't have access to the Sideboard diff tool
|
|
10279
|
+
|
|
10280
|
+
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
10281
|
+
|
|
10282
|
+
\`\`\`bash
|
|
10283
|
+
# Get the merge base between this branch and the target
|
|
10284
|
+
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
10285
|
+
|
|
10286
|
+
# Get the committed diff against the merge base
|
|
10287
|
+
git diff $MERGE_BASE HEAD
|
|
10288
|
+
|
|
10289
|
+
# Get any uncommitted changes (staged and unstaged)
|
|
10290
|
+
git diff HEAD
|
|
10291
|
+
\`\`\`
|
|
10292
|
+
|
|
10293
|
+
Review the combination of both outputs: the first shows all committed changes on this branch relative to the target, and the second shows any uncommitted work in progress.
|
|
10294
|
+
|
|
10295
|
+
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
10296
|
+
|
|
10297
|
+
## Output format
|
|
10298
|
+
|
|
10299
|
+
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
10300
|
+
|
|
10301
|
+
Only report ONE finding per unique issue.
|
|
10302
|
+
|
|
10303
|
+
<example>
|
|
10304
|
+
## Recommendation
|
|
10305
|
+
|
|
10306
|
+
**Request changes** \u2014 The empty-input crash on load will break first-run users; fix that before merge. The unused helper is a nit and can wait.
|
|
10307
|
+
|
|
10308
|
+
## Findings
|
|
10309
|
+
|
|
10310
|
+
### **#1 Empty input causes crash** (blocking)
|
|
10311
|
+
|
|
10312
|
+
If the input field is empty when the page loads, the app will crash.
|
|
10313
|
+
|
|
10314
|
+
File: src/client/frontends/desktop/ui/Input.tsx
|
|
10315
|
+
|
|
10316
|
+
### **#2 Dead code** (nit)
|
|
10317
|
+
|
|
10318
|
+
The getUserData function is now unused. It should be deleted.
|
|
10319
|
+
|
|
10320
|
+
File: src/client/frontends/desktop/core/UserData.ts
|
|
10321
|
+
</example>
|
|
10322
|
+
|
|
10323
|
+
<example>
|
|
10324
|
+
## Recommendation
|
|
10325
|
+
|
|
10326
|
+
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
10327
|
+
</example>
|
|
10328
|
+
|
|
10329
|
+
## Growing the rules
|
|
10330
|
+
|
|
10331
|
+
If a blocking issue is a missing or ambiguous repo rule that will recur, add one sentence to \`.claude/skills/review/SKILL.md\` (create the skill if it is missing \u2014 that is allowed and should be committed). Do not only patch this diff when the same miss will happen again. Do not write new skills under \`.sideboard/skills/\`. Do not use \`.sideboard/review.md\` for new notes.
|
|
10332
|
+
`;
|
|
10333
|
+
REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
|
|
10334
|
+
REVIEW_SKILL_NAME = "review";
|
|
10335
|
+
REVIEW_SKILL_FRONTMATTER = `---
|
|
10336
|
+
name: review
|
|
10337
|
+
description: >-
|
|
10338
|
+
Merge-readiness review for this repository. Use when reviewing a PR, worktree
|
|
10339
|
+
diff, or Sideboard Review (Approve / Approve with nits / Request changes).
|
|
10340
|
+
---
|
|
10341
|
+
|
|
10342
|
+
`;
|
|
10487
10343
|
}
|
|
10488
10344
|
});
|
|
10489
10345
|
|
|
10490
|
-
// src/
|
|
10491
|
-
function
|
|
10492
|
-
|
|
10346
|
+
// src/review/request-review.ts
|
|
10347
|
+
function shouldRefreshReviewRequestTemplate(content) {
|
|
10348
|
+
const trimmed = content.trim();
|
|
10349
|
+
if (!trimmed) return true;
|
|
10350
|
+
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
10351
|
+
return false;
|
|
10352
|
+
}
|
|
10353
|
+
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
10493
10354
|
}
|
|
10494
|
-
function
|
|
10355
|
+
function readTextIfPresent(abs) {
|
|
10356
|
+
if (!(0, import_node_fs26.existsSync)(abs)) return null;
|
|
10495
10357
|
try {
|
|
10496
|
-
const
|
|
10497
|
-
|
|
10498
|
-
return pid;
|
|
10358
|
+
const content = (0, import_node_fs26.readFileSync)(abs, "utf8");
|
|
10359
|
+
return content.trim() ? content : null;
|
|
10499
10360
|
} catch {
|
|
10500
10361
|
return null;
|
|
10501
10362
|
}
|
|
10502
10363
|
}
|
|
10503
|
-
function
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
return false;
|
|
10364
|
+
function readLocalGuidelines(worktreePath) {
|
|
10365
|
+
const localAbs = (0, import_node_path25.join)(worktreePath, REVIEW_REQUEST_PATH);
|
|
10366
|
+
const localContent = readTextIfPresent(localAbs);
|
|
10367
|
+
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
10368
|
+
return { path: REVIEW_REQUEST_PATH, content: localContent };
|
|
10509
10369
|
}
|
|
10370
|
+
const legacyAbs = (0, import_node_path25.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
10371
|
+
const legacyContent = readTextIfPresent(legacyAbs);
|
|
10372
|
+
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
10373
|
+
return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
|
|
10374
|
+
}
|
|
10375
|
+
return null;
|
|
10510
10376
|
}
|
|
10511
|
-
function
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
}
|
|
10518
|
-
function thisProcessShouldDrainAgentQueues() {
|
|
10519
|
-
if (isThisProcessDesktopHost()) return true;
|
|
10520
|
-
return !isDesktopHostAlive();
|
|
10377
|
+
function skillGuidelines(content, source) {
|
|
10378
|
+
return {
|
|
10379
|
+
path: REVIEW_SKILL_PATH,
|
|
10380
|
+
name: REVIEW_SKILL_NAME,
|
|
10381
|
+
content,
|
|
10382
|
+
source
|
|
10383
|
+
};
|
|
10521
10384
|
}
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
import_node_path29 = require("path");
|
|
10528
|
-
init_paths();
|
|
10385
|
+
function ensureReviewSkillFile(worktreePath) {
|
|
10386
|
+
const abs = (0, import_node_path25.join)(worktreePath, REVIEW_SKILL_PATH);
|
|
10387
|
+
const existing = readTextIfPresent(abs);
|
|
10388
|
+
if (existing) {
|
|
10389
|
+
return { path: REVIEW_SKILL_PATH, content: existing, wrote: false };
|
|
10529
10390
|
}
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
if (!status.installed || !status.authenticated) {
|
|
10549
|
-
throw new Error(status.reason ?? `${agent} is not available`);
|
|
10391
|
+
const fromRepo = readTextIfPresent((0, import_node_path25.join)(worktreePath, REPO_REVIEW_PATH));
|
|
10392
|
+
const fromLocal = readLocalGuidelines(worktreePath)?.content ?? null;
|
|
10393
|
+
const content = wrapReviewSkillMarkdown(fromRepo ?? fromLocal ?? REVIEW_REQUEST_TEMPLATE);
|
|
10394
|
+
(0, import_node_fs26.mkdirSync)((0, import_node_path25.dirname)(abs), { recursive: true });
|
|
10395
|
+
(0, import_node_fs26.writeFileSync)(abs, content, "utf8");
|
|
10396
|
+
return { path: REVIEW_SKILL_PATH, content, wrote: true };
|
|
10397
|
+
}
|
|
10398
|
+
function resolveReviewGuidelines(worktreePath) {
|
|
10399
|
+
const skillContent = readTextIfPresent((0, import_node_path25.join)(worktreePath, REVIEW_SKILL_PATH));
|
|
10400
|
+
if (skillContent) return skillGuidelines(skillContent, "skill");
|
|
10401
|
+
const local = readLocalGuidelines(worktreePath);
|
|
10402
|
+
if (local) {
|
|
10403
|
+
return {
|
|
10404
|
+
path: local.path,
|
|
10405
|
+
name: REVIEW_REQUEST_NAME,
|
|
10406
|
+
content: local.content,
|
|
10407
|
+
source: "local"
|
|
10408
|
+
};
|
|
10550
10409
|
}
|
|
10551
|
-
|
|
10410
|
+
const seeded = ensureReviewSkillFile(worktreePath);
|
|
10411
|
+
return skillGuidelines(seeded.content, seeded.wrote ? "stock" : "skill");
|
|
10412
|
+
}
|
|
10413
|
+
function buildReviewRequestAttachment(content, opts) {
|
|
10414
|
+
const path = opts?.path ?? REVIEW_SKILL_PATH;
|
|
10415
|
+
const name = opts?.name ?? (path === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
|
|
10416
|
+
return {
|
|
10417
|
+
id: (0, import_node_crypto5.randomUUID)(),
|
|
10418
|
+
name,
|
|
10419
|
+
kind: "file",
|
|
10420
|
+
path,
|
|
10421
|
+
content
|
|
10422
|
+
};
|
|
10423
|
+
}
|
|
10424
|
+
async function requestReview(threadRef, send) {
|
|
10425
|
+
const from = findThreadByRef(threadRef);
|
|
10426
|
+
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
10427
|
+
if (isOrchestratorThread(from)) {
|
|
10552
10428
|
throw new Error(
|
|
10553
|
-
|
|
10429
|
+
"request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
10554
10430
|
);
|
|
10555
10431
|
}
|
|
10556
|
-
|
|
10432
|
+
if (from.status === "archived") {
|
|
10433
|
+
throw new Error(`Thread is archived: ${from.id}`);
|
|
10434
|
+
}
|
|
10435
|
+
const guidelines = resolveReviewGuidelines(from.worktreePath);
|
|
10436
|
+
const tab = createChatTab({
|
|
10437
|
+
fromThreadId: from.id,
|
|
10438
|
+
title: "Review",
|
|
10439
|
+
attachments: [
|
|
10440
|
+
buildReviewRequestAttachment(guidelines.content, {
|
|
10441
|
+
path: guidelines.path,
|
|
10442
|
+
name: guidelines.name
|
|
10443
|
+
})
|
|
10444
|
+
]
|
|
10445
|
+
});
|
|
10446
|
+
const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
|
|
10447
|
+
return { tab: started, from };
|
|
10557
10448
|
}
|
|
10558
|
-
var
|
|
10559
|
-
var
|
|
10560
|
-
"src/
|
|
10449
|
+
var import_node_crypto5, import_node_fs26, import_node_path25, REPO_REVIEW_PATH, REPO_REVIEW_NAME, REVIEW_REQUEST_PATH, LEGACY_REVIEW_REQUEST_PATH, REVIEW_REQUEST_NAME, REVIEW_REQUEST_PREFILL, LEGACY_REVIEW_TEMPLATE_MARKERS;
|
|
10450
|
+
var init_request_review = __esm({
|
|
10451
|
+
"src/review/request-review.ts"() {
|
|
10561
10452
|
"use strict";
|
|
10562
|
-
|
|
10563
|
-
|
|
10453
|
+
import_node_crypto5 = require("crypto");
|
|
10454
|
+
import_node_fs26 = require("fs");
|
|
10455
|
+
import_node_path25 = require("path");
|
|
10456
|
+
init_global_workspace();
|
|
10457
|
+
init_chat_tabs();
|
|
10458
|
+
init_thread_store();
|
|
10459
|
+
init_review_request_template();
|
|
10460
|
+
init_workspace_scratch();
|
|
10461
|
+
REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
10462
|
+
REPO_REVIEW_NAME = "review.md";
|
|
10463
|
+
REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
10464
|
+
LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
10465
|
+
REVIEW_REQUEST_NAME = "Review request.md";
|
|
10466
|
+
REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
10467
|
+
LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
10468
|
+
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
10469
|
+
"HOW MANY FINDINGS TO RETURN:"
|
|
10470
|
+
];
|
|
10564
10471
|
}
|
|
10565
10472
|
});
|
|
10566
10473
|
|
|
10567
|
-
// src/
|
|
10568
|
-
|
|
10569
|
-
const
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10474
|
+
// src/hook/conductor.ts
|
|
10475
|
+
function matchSimpleGlob(pattern, name) {
|
|
10476
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
10477
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
10478
|
+
}
|
|
10479
|
+
function readWorktreeInclude(repoPath) {
|
|
10480
|
+
const path = (0, import_node_path26.join)(repoPath, ".worktreeinclude");
|
|
10481
|
+
if (!(0, import_node_fs27.existsSync)(path)) return [];
|
|
10482
|
+
return (0, import_node_fs27.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
10483
|
+
}
|
|
10484
|
+
function resolveFilesToCopy(repoPath) {
|
|
10485
|
+
const fromInclude = readWorktreeInclude(repoPath);
|
|
10486
|
+
if (fromInclude.length) return fromInclude;
|
|
10487
|
+
const settings = loadRepoSettings(repoPath);
|
|
10488
|
+
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
10489
|
+
if (settings?.fileIncludeGlobs?.length) {
|
|
10490
|
+
const matched = [];
|
|
10491
|
+
try {
|
|
10492
|
+
for (const entry of (0, import_node_fs27.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
10493
|
+
if (!entry.isFile()) continue;
|
|
10494
|
+
for (const glob of settings.fileIncludeGlobs) {
|
|
10495
|
+
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path26.basename)(glob), entry.name)) {
|
|
10496
|
+
matched.push(entry.name);
|
|
10497
|
+
break;
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
}
|
|
10501
|
+
} catch {
|
|
10502
|
+
}
|
|
10503
|
+
if (matched.length) return [...new Set(matched)];
|
|
10579
10504
|
}
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
if (!
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10505
|
+
const defaults = [];
|
|
10506
|
+
try {
|
|
10507
|
+
for (const entry of (0, import_node_fs27.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
10508
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
10509
|
+
defaults.push(entry.name);
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
} catch {
|
|
10513
|
+
}
|
|
10514
|
+
if (defaults.length) return defaults;
|
|
10515
|
+
return [".env.local", ".env"];
|
|
10516
|
+
}
|
|
10517
|
+
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
10518
|
+
const patterns = resolveFilesToCopy(repoPath);
|
|
10519
|
+
const copied = [];
|
|
10520
|
+
for (const rel of patterns) {
|
|
10521
|
+
const src = (0, import_node_path26.join)(repoPath, rel);
|
|
10522
|
+
if (!(0, import_node_fs27.existsSync)(src)) continue;
|
|
10523
|
+
const dest = (0, import_node_path26.join)(worktreePath, rel);
|
|
10524
|
+
(0, import_node_fs27.mkdirSync)((0, import_node_path26.dirname)(dest), { recursive: true });
|
|
10525
|
+
(0, import_node_fs27.copyFileSync)(src, dest);
|
|
10526
|
+
copied.push(rel);
|
|
10527
|
+
}
|
|
10528
|
+
return copied;
|
|
10529
|
+
}
|
|
10530
|
+
async function captureLoginEnv() {
|
|
10531
|
+
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
10532
|
+
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
10533
|
+
try {
|
|
10534
|
+
const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
|
|
10535
|
+
reject: false,
|
|
10536
|
+
timeout: 5e3
|
|
10537
|
+
});
|
|
10538
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
10539
|
+
const env = { ...process.env };
|
|
10540
|
+
for (const entry of result.stdout.split("\0")) {
|
|
10541
|
+
if (!entry) continue;
|
|
10542
|
+
const eq = entry.indexOf("=");
|
|
10543
|
+
if (eq <= 0) continue;
|
|
10544
|
+
const key = entry.slice(0, eq);
|
|
10545
|
+
const value = entry.slice(eq + 1);
|
|
10546
|
+
if (key) env[key] = value;
|
|
10604
10547
|
}
|
|
10548
|
+
cachedLoginEnv = env;
|
|
10549
|
+
return { ...env };
|
|
10550
|
+
}
|
|
10551
|
+
} catch {
|
|
10552
|
+
}
|
|
10553
|
+
cachedLoginEnv = { ...process.env };
|
|
10554
|
+
return { ...cachedLoginEnv };
|
|
10555
|
+
}
|
|
10556
|
+
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
10557
|
+
const env = stripNestedElectronEnv({
|
|
10558
|
+
...baseEnv ?? process.env
|
|
10559
|
+
});
|
|
10560
|
+
const name = opts.workspaceName ?? (0, import_node_path26.basename)(opts.worktreePath);
|
|
10561
|
+
const ports = opts.ports ?? [];
|
|
10562
|
+
const primary = ports[0];
|
|
10563
|
+
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
10564
|
+
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
10565
|
+
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
10566
|
+
env.SIDEBOARD_IS_LOCAL = "1";
|
|
10567
|
+
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
10568
|
+
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
10569
|
+
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
10570
|
+
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
10571
|
+
env.CONDUCTOR_IS_LOCAL = "1";
|
|
10572
|
+
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
10573
|
+
if (primary != null) {
|
|
10574
|
+
env.SIDEBOARD_PORT = String(primary);
|
|
10575
|
+
env.CONDUCTOR_PORT = String(primary);
|
|
10576
|
+
env.PORT = String(primary);
|
|
10577
|
+
for (let i = 1; i < ports.length; i++) {
|
|
10578
|
+
const p = ports[i];
|
|
10579
|
+
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
10580
|
+
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
10605
10581
|
}
|
|
10606
|
-
} else if (input.sourceType === "adopt") {
|
|
10607
|
-
throw new Error("Use adoptThread() for adopt sources");
|
|
10608
10582
|
}
|
|
10609
|
-
|
|
10610
|
-
const { branchName, worktreePath } = await createThreadWorktree({
|
|
10611
|
-
repoPath,
|
|
10612
|
-
sourceRef,
|
|
10613
|
-
slug: team.slug
|
|
10614
|
-
});
|
|
10615
|
-
copyConfiguredFiles(repoPath, worktreePath);
|
|
10616
|
-
const explicitTitle = input.title?.trim();
|
|
10617
|
-
const thread = createEmptyThread({
|
|
10618
|
-
title: explicitTitle || team.name,
|
|
10619
|
-
userSetTitle: Boolean(explicitTitle),
|
|
10620
|
-
sourceType: input.sourceType,
|
|
10621
|
-
sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
|
|
10622
|
-
branchName,
|
|
10623
|
-
worktreePath,
|
|
10624
|
-
repoPath,
|
|
10625
|
-
agent: resolved.agent,
|
|
10626
|
-
autonomy: input.autonomy ?? "default",
|
|
10627
|
-
model: resolved.model,
|
|
10628
|
-
effort: resolved.effort,
|
|
10629
|
-
fast: resolved.fast,
|
|
10630
|
-
planMode: Boolean(input.planMode),
|
|
10631
|
-
attachments: input.attachments ?? [],
|
|
10632
|
-
sourceIsFork,
|
|
10633
|
-
parentThreadId: input.parentThreadId ?? null,
|
|
10634
|
-
status: "idle",
|
|
10635
|
-
prUrl,
|
|
10636
|
-
prTitle
|
|
10637
|
-
});
|
|
10638
|
-
writeThread(thread);
|
|
10639
|
-
await ensureWorkspace(repoPath);
|
|
10640
|
-
return readThread(thread.id) ?? thread;
|
|
10583
|
+
return env;
|
|
10641
10584
|
}
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
if (!adapter.listLinearIssues) {
|
|
10647
|
-
throw new Error(`${agent} does not support Linear issue listing`);
|
|
10648
|
-
}
|
|
10649
|
-
return adapter.listLinearIssues(repoPath);
|
|
10585
|
+
function pipeLines(stream, onLine) {
|
|
10586
|
+
if (!stream || !onLine) return;
|
|
10587
|
+
const rl = (0, import_node_readline3.createInterface)({ input: stream });
|
|
10588
|
+
rl.on("line", onLine);
|
|
10650
10589
|
}
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10590
|
+
function killScriptTree(child, ports = []) {
|
|
10591
|
+
const pid = child.pid;
|
|
10592
|
+
if (pid) {
|
|
10593
|
+
try {
|
|
10594
|
+
if (process.platform === "win32") {
|
|
10595
|
+
void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
10596
|
+
} else {
|
|
10597
|
+
process.kill(-pid, "SIGTERM");
|
|
10598
|
+
setTimeout(() => {
|
|
10599
|
+
try {
|
|
10600
|
+
process.kill(-pid, "SIGKILL");
|
|
10601
|
+
} catch {
|
|
10602
|
+
}
|
|
10603
|
+
}, 2500).unref?.();
|
|
10604
|
+
}
|
|
10605
|
+
} catch {
|
|
10606
|
+
try {
|
|
10607
|
+
child.kill("SIGTERM");
|
|
10608
|
+
} catch {
|
|
10609
|
+
}
|
|
10610
|
+
}
|
|
10662
10611
|
}
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10612
|
+
for (const port of ports) {
|
|
10613
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
10614
|
+
if (process.platform === "win32") {
|
|
10615
|
+
void (0, import_execa4.execa)(
|
|
10616
|
+
"powershell",
|
|
10617
|
+
[
|
|
10618
|
+
"-NoProfile",
|
|
10619
|
+
"-Command",
|
|
10620
|
+
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
10621
|
+
],
|
|
10622
|
+
{ reject: false }
|
|
10623
|
+
);
|
|
10624
|
+
} else {
|
|
10625
|
+
void (0, import_execa4.execa)(
|
|
10626
|
+
"zsh",
|
|
10627
|
+
["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
|
|
10628
|
+
{ reject: false }
|
|
10629
|
+
);
|
|
10630
|
+
}
|
|
10673
10631
|
}
|
|
10674
|
-
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
10675
10632
|
}
|
|
10676
|
-
async function
|
|
10677
|
-
|
|
10678
|
-
const
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
"Transcript:",
|
|
10689
|
-
transcript
|
|
10690
|
-
].join("\n");
|
|
10633
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
10634
|
+
const loginEnv = await captureLoginEnv();
|
|
10635
|
+
const env = buildWorkspaceScriptEnv(
|
|
10636
|
+
{
|
|
10637
|
+
worktreePath: opts.worktreePath,
|
|
10638
|
+
repoPath: opts.repoPath,
|
|
10639
|
+
workspaceName: opts.workspaceName,
|
|
10640
|
+
defaultBranch: opts.defaultBranch,
|
|
10641
|
+
ports: opts.ports
|
|
10642
|
+
},
|
|
10643
|
+
loginEnv
|
|
10644
|
+
);
|
|
10691
10645
|
try {
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
"-p",
|
|
10696
|
-
prompt,
|
|
10697
|
-
"--output-format",
|
|
10698
|
-
"text",
|
|
10699
|
-
"--permission-mode",
|
|
10700
|
-
"bypassPermissions",
|
|
10701
|
-
"--model",
|
|
10702
|
-
"haiku"
|
|
10703
|
-
],
|
|
10704
|
-
{ cwd: opts?.cwd, reject: false }
|
|
10646
|
+
mergeAgentGitAuthEnv(
|
|
10647
|
+
env,
|
|
10648
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
10705
10649
|
);
|
|
10706
|
-
if (exitCode !== 0) return null;
|
|
10707
|
-
const text4 = stdout.trim();
|
|
10708
|
-
if (text4.length < 40) return null;
|
|
10709
|
-
return text4;
|
|
10710
10650
|
} catch {
|
|
10711
|
-
return null;
|
|
10712
10651
|
}
|
|
10652
|
+
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
10653
|
+
const child = (0, import_execa4.execa)(shell, ["-lc", command], {
|
|
10654
|
+
cwd: opts.worktreePath,
|
|
10655
|
+
reject: false,
|
|
10656
|
+
env,
|
|
10657
|
+
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
10658
|
+
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
10659
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
10660
|
+
});
|
|
10661
|
+
pipeLines(child.stdout, opts.onLine);
|
|
10662
|
+
pipeLines(child.stderr, opts.onLine);
|
|
10663
|
+
const ports = opts.ports ?? [];
|
|
10664
|
+
return {
|
|
10665
|
+
pid: child.pid,
|
|
10666
|
+
kill: () => killScriptTree(child, ports),
|
|
10667
|
+
done: child.then((r) => r.exitCode ?? null),
|
|
10668
|
+
child
|
|
10669
|
+
};
|
|
10713
10670
|
}
|
|
10714
|
-
function
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
let mode = "none";
|
|
10720
|
-
for (const line of lines) {
|
|
10721
|
-
if (line.startsWith("### User")) {
|
|
10722
|
-
mode = "user";
|
|
10723
|
-
continue;
|
|
10724
|
-
}
|
|
10725
|
-
if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
|
|
10726
|
-
mode = "agent";
|
|
10727
|
-
continue;
|
|
10728
|
-
}
|
|
10729
|
-
if (line.startsWith("- ") && line.includes(":")) {
|
|
10730
|
-
tools.push(line.replace(/^- /, ""));
|
|
10731
|
-
continue;
|
|
10732
|
-
}
|
|
10733
|
-
if (mode === "user" && users.length < 8) {
|
|
10734
|
-
users.push(line.slice(0, 240));
|
|
10735
|
-
} else if (mode === "agent" && agents.length < 6) {
|
|
10736
|
-
agents.push(line.slice(0, 240));
|
|
10737
|
-
}
|
|
10738
|
-
}
|
|
10739
|
-
const uniqueTools = [...new Set(tools)].slice(0, 20);
|
|
10740
|
-
const parts = [
|
|
10741
|
-
"## Compacted context (extractive)",
|
|
10742
|
-
"",
|
|
10743
|
-
"### User goals",
|
|
10744
|
-
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
10745
|
-
"",
|
|
10746
|
-
"### Recent agent notes",
|
|
10747
|
-
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
10748
|
-
];
|
|
10749
|
-
if (uniqueTools.length) {
|
|
10750
|
-
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
10671
|
+
async function attachAbort(handle, signal) {
|
|
10672
|
+
if (signal) {
|
|
10673
|
+
const onAbort = () => handle.kill();
|
|
10674
|
+
if (signal.aborted) onAbort();
|
|
10675
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
10751
10676
|
}
|
|
10752
|
-
|
|
10753
|
-
"",
|
|
10754
|
-
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
10755
|
-
);
|
|
10756
|
-
return parts.join("\n");
|
|
10677
|
+
return handle.done;
|
|
10757
10678
|
}
|
|
10758
|
-
function
|
|
10759
|
-
|
|
10760
|
-
return
|
|
10761
|
-
|
|
10762
|
-
|
|
10679
|
+
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
10680
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
10681
|
+
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
10682
|
+
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
10683
|
+
worktreePath,
|
|
10684
|
+
repoPath,
|
|
10685
|
+
defaultBranch: opts?.defaultBranch,
|
|
10686
|
+
onLine
|
|
10687
|
+
});
|
|
10688
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
10689
|
+
return {
|
|
10690
|
+
ran: true,
|
|
10691
|
+
exitCode,
|
|
10692
|
+
source: workspaceSettingsSourceLabel(worktreePath, repoPath),
|
|
10693
|
+
kill: handle.kill
|
|
10694
|
+
};
|
|
10695
|
+
}
|
|
10696
|
+
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
10697
|
+
const found = findConventionSetup(worktreePath, repoPath);
|
|
10698
|
+
if (!found) return { ran: false, exitCode: null, source: null };
|
|
10699
|
+
onLine?.(`[setup] ${found.source}`);
|
|
10700
|
+
const handle = await spawnWorkspaceScript(found.command, {
|
|
10701
|
+
worktreePath,
|
|
10702
|
+
repoPath,
|
|
10703
|
+
defaultBranch: opts?.defaultBranch,
|
|
10704
|
+
onLine
|
|
10705
|
+
});
|
|
10706
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
10707
|
+
return {
|
|
10708
|
+
ran: true,
|
|
10709
|
+
exitCode,
|
|
10710
|
+
source: found.source,
|
|
10711
|
+
kill: handle.kill
|
|
10712
|
+
};
|
|
10763
10713
|
}
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
MAX_TRANSCRIPT_CHARS = 12e4;
|
|
10771
|
-
MAX_SUMMARY_CHARS = 6e3;
|
|
10714
|
+
async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
|
|
10715
|
+
const reviewSkill = ensureReviewSkillFile(worktreePath);
|
|
10716
|
+
if (reviewSkill.wrote) {
|
|
10717
|
+
onLine?.(
|
|
10718
|
+
`[setup] wrote ${REVIEW_SKILL_PATH} \u2014 commit it so later worktrees inherit Review guidelines`
|
|
10719
|
+
);
|
|
10772
10720
|
}
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
function estimateMessageChars(message) {
|
|
10777
|
-
let n = message.text.length + 16;
|
|
10778
|
-
for (const part of message.parts ?? []) {
|
|
10779
|
-
n += estimatePartChars(part);
|
|
10721
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
10722
|
+
if (!setup.ran) {
|
|
10723
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
10780
10724
|
}
|
|
10781
|
-
|
|
10725
|
+
if (!setup.ran) {
|
|
10726
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
10727
|
+
}
|
|
10728
|
+
return setup;
|
|
10782
10729
|
}
|
|
10783
|
-
function
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10730
|
+
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
10731
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
10732
|
+
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
10733
|
+
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
10734
|
+
worktreePath,
|
|
10735
|
+
repoPath,
|
|
10736
|
+
onLine
|
|
10737
|
+
});
|
|
10738
|
+
const exitCode = await handle.done;
|
|
10739
|
+
return { ran: true, exitCode };
|
|
10740
|
+
}
|
|
10741
|
+
function listRunScripts(worktreePath, repoPath) {
|
|
10742
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
10743
|
+
if (!settings?.runScripts.length) return [];
|
|
10744
|
+
return settings.runScripts.filter((s) => {
|
|
10745
|
+
if (!s.availableIn?.length) return true;
|
|
10746
|
+
return s.availableIn.includes("local");
|
|
10747
|
+
});
|
|
10748
|
+
}
|
|
10749
|
+
function getDefaultRunScript(worktreePath, repoPath) {
|
|
10750
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
10751
|
+
if (!scripts.length) return null;
|
|
10752
|
+
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
10753
|
+
}
|
|
10754
|
+
function getRunScript(worktreePath, repoPath, name) {
|
|
10755
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
10756
|
+
if (!scripts.length) return null;
|
|
10757
|
+
if (name) {
|
|
10758
|
+
return scripts.find((s) => s.name === name) ?? null;
|
|
10794
10759
|
}
|
|
10760
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
10795
10761
|
}
|
|
10796
|
-
function
|
|
10797
|
-
return
|
|
10762
|
+
function getRunMode(worktreePath, repoPath) {
|
|
10763
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
10798
10764
|
}
|
|
10799
|
-
function
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10765
|
+
async function allocatePort() {
|
|
10766
|
+
return new Promise((resolve, reject) => {
|
|
10767
|
+
const server = (0, import_node_net.createServer)();
|
|
10768
|
+
server.listen(0, "127.0.0.1", () => {
|
|
10769
|
+
const addr = server.address();
|
|
10770
|
+
if (!addr || typeof addr === "string") {
|
|
10771
|
+
server.close();
|
|
10772
|
+
reject(new Error("Failed to allocate port"));
|
|
10773
|
+
return;
|
|
10774
|
+
}
|
|
10775
|
+
const port = addr.port;
|
|
10776
|
+
server.close((err) => err ? reject(err) : resolve(port));
|
|
10777
|
+
});
|
|
10778
|
+
server.on("error", reject);
|
|
10779
|
+
});
|
|
10804
10780
|
}
|
|
10805
|
-
function
|
|
10806
|
-
const
|
|
10807
|
-
const
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10781
|
+
async function allocatePortRange(size = PORT_RANGE_SIZE) {
|
|
10782
|
+
const base = await allocatePort();
|
|
10783
|
+
const ports = [base];
|
|
10784
|
+
for (let i = 1; i < size; i++) {
|
|
10785
|
+
const candidate = base + i;
|
|
10786
|
+
const free = await new Promise((resolve) => {
|
|
10787
|
+
const server = (0, import_node_net.createServer)();
|
|
10788
|
+
server.once("error", () => resolve(false));
|
|
10789
|
+
server.listen(candidate, "127.0.0.1", () => {
|
|
10790
|
+
server.close(() => resolve(true));
|
|
10791
|
+
});
|
|
10792
|
+
});
|
|
10793
|
+
if (free) {
|
|
10794
|
+
ports.push(candidate);
|
|
10795
|
+
} else {
|
|
10796
|
+
ports.push(await allocatePort());
|
|
10797
|
+
}
|
|
10817
10798
|
}
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10799
|
+
return ports;
|
|
10800
|
+
}
|
|
10801
|
+
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
10802
|
+
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
10803
|
+
if (!script) return null;
|
|
10804
|
+
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
10805
|
+
const handle = await spawnWorkspaceScript(script.command, {
|
|
10806
|
+
worktreePath,
|
|
10807
|
+
repoPath,
|
|
10808
|
+
ports,
|
|
10809
|
+
defaultBranch: opts?.defaultBranch,
|
|
10810
|
+
onLine
|
|
10811
|
+
});
|
|
10821
10812
|
return {
|
|
10822
|
-
|
|
10823
|
-
|
|
10813
|
+
pid: handle.pid,
|
|
10814
|
+
port: ports[0],
|
|
10815
|
+
ports,
|
|
10816
|
+
scriptName: script.name,
|
|
10817
|
+
kill: handle.kill,
|
|
10818
|
+
done: handle.done
|
|
10824
10819
|
};
|
|
10825
10820
|
}
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10821
|
+
var import_node_fs27, import_node_net, import_node_path26, import_execa4, import_node_readline3, PORT_RANGE_SIZE, cachedLoginEnv;
|
|
10822
|
+
var init_conductor = __esm({
|
|
10823
|
+
"src/hook/conductor.ts"() {
|
|
10824
|
+
"use strict";
|
|
10825
|
+
import_node_fs27 = require("fs");
|
|
10826
|
+
import_node_net = require("net");
|
|
10827
|
+
import_node_path26 = require("path");
|
|
10828
|
+
import_execa4 = require("execa");
|
|
10829
|
+
import_node_readline3 = require("readline");
|
|
10830
|
+
init_settings();
|
|
10831
|
+
init_nested_electron_env();
|
|
10832
|
+
init_convention_setup();
|
|
10833
|
+
init_cursor_worktrees();
|
|
10834
|
+
init_git_auth_mode();
|
|
10835
|
+
init_request_review();
|
|
10836
|
+
init_nested_electron_env();
|
|
10837
|
+
PORT_RANGE_SIZE = 10;
|
|
10838
|
+
cachedLoginEnv = null;
|
|
10831
10839
|
}
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10840
|
+
});
|
|
10841
|
+
|
|
10842
|
+
// src/git/orphan-cleanup.ts
|
|
10843
|
+
function isSideboardWorktreePath(path) {
|
|
10844
|
+
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
10845
|
+
}
|
|
10846
|
+
async function findOrphanWorktrees(repoPaths) {
|
|
10847
|
+
const threads = listThreads({ includeArchived: true });
|
|
10848
|
+
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
10849
|
+
const repos = new Set(
|
|
10850
|
+
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
10851
|
+
);
|
|
10852
|
+
const homeRoot = sideboardWorkspacesDir();
|
|
10853
|
+
if ((0, import_node_fs28.existsSync)(homeRoot)) {
|
|
10854
|
+
try {
|
|
10855
|
+
for (const entry of (0, import_node_fs28.readdirSync)(homeRoot, { withFileTypes: true })) {
|
|
10856
|
+
if (!entry.isDirectory()) continue;
|
|
10857
|
+
void entry;
|
|
10858
|
+
}
|
|
10859
|
+
} catch {
|
|
10860
|
+
}
|
|
10842
10861
|
}
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10862
|
+
const orphans = [];
|
|
10863
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10864
|
+
for (const repoPath of repos) {
|
|
10865
|
+
if (!repoPath || !(0, import_node_fs28.existsSync)(repoPath)) continue;
|
|
10866
|
+
try {
|
|
10867
|
+
const wts = await listWorktrees(repoPath);
|
|
10868
|
+
for (const wt of wts) {
|
|
10869
|
+
const path = wt.path.replace(/\/$/, "");
|
|
10870
|
+
if (!isSideboardWorktreePath(path)) continue;
|
|
10871
|
+
if (known.has(path)) continue;
|
|
10872
|
+
if (seen.has(path)) continue;
|
|
10873
|
+
seen.add(path);
|
|
10874
|
+
let mtimeMs = 0;
|
|
10875
|
+
try {
|
|
10876
|
+
mtimeMs = (0, import_node_fs28.statSync)(path).mtimeMs;
|
|
10877
|
+
} catch {
|
|
10878
|
+
mtimeMs = 0;
|
|
10879
|
+
}
|
|
10880
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
10881
|
+
}
|
|
10882
|
+
} catch {
|
|
10883
|
+
}
|
|
10884
|
+
try {
|
|
10885
|
+
const root = worktreesRoot(repoPath);
|
|
10886
|
+
if ((0, import_node_fs28.existsSync)(root)) {
|
|
10887
|
+
for (const entry of (0, import_node_fs28.readdirSync)(root, { withFileTypes: true })) {
|
|
10888
|
+
if (!entry.isDirectory()) continue;
|
|
10889
|
+
const path = (0, import_node_path27.join)(root, entry.name).replace(/\/$/, "");
|
|
10890
|
+
if (known.has(path) || seen.has(path)) continue;
|
|
10891
|
+
if (!(0, import_node_fs28.existsSync)((0, import_node_path27.join)(path, ".git"))) continue;
|
|
10892
|
+
seen.add(path);
|
|
10893
|
+
let mtimeMs = 0;
|
|
10894
|
+
try {
|
|
10895
|
+
mtimeMs = (0, import_node_fs28.statSync)(path).mtimeMs;
|
|
10896
|
+
} catch {
|
|
10897
|
+
mtimeMs = Date.now();
|
|
10898
|
+
}
|
|
10899
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
} catch {
|
|
10903
|
+
}
|
|
10848
10904
|
}
|
|
10849
|
-
return
|
|
10905
|
+
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
10850
10906
|
}
|
|
10851
|
-
function
|
|
10852
|
-
const
|
|
10853
|
-
const
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10907
|
+
async function cleanupOrphanWorktrees(opts) {
|
|
10908
|
+
const settings = loadAppSettings();
|
|
10909
|
+
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
10910
|
+
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
10911
|
+
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
10912
|
+
const budget = Math.max(0, maxCount - activeCount);
|
|
10913
|
+
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
10914
|
+
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
10915
|
+
const removed = [];
|
|
10916
|
+
const kept = [];
|
|
10917
|
+
for (const orphan of orphans) {
|
|
10918
|
+
if (keep.has(orphan.path)) {
|
|
10919
|
+
kept.push(orphan.path);
|
|
10858
10920
|
continue;
|
|
10859
10921
|
}
|
|
10860
|
-
if (
|
|
10861
|
-
|
|
10862
|
-
${m.text}`);
|
|
10922
|
+
if (opts?.dryRun) {
|
|
10923
|
+
removed.push(orphan.path);
|
|
10863
10924
|
continue;
|
|
10864
10925
|
}
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
bits.push("### Agent");
|
|
10871
|
-
}
|
|
10872
|
-
const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
|
|
10873
|
-
for (const th of thinking) {
|
|
10874
|
-
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
10875
|
-
bits.push("Thinking:");
|
|
10876
|
-
bits.push(th.text);
|
|
10877
|
-
}
|
|
10878
|
-
const toolParts = (m.parts ?? []).filter(
|
|
10879
|
-
(p) => p.type === "tool"
|
|
10880
|
-
);
|
|
10881
|
-
if (tools !== "none" && toolParts.length > 0) {
|
|
10882
|
-
if (tools === "summary") bits.push("Tools:");
|
|
10883
|
-
for (const t of toolParts) {
|
|
10884
|
-
bits.push(formatToolPart(t, tools));
|
|
10885
|
-
}
|
|
10926
|
+
try {
|
|
10927
|
+
await removeWorktree(orphan.repoPath, orphan.path);
|
|
10928
|
+
removed.push(orphan.path);
|
|
10929
|
+
} catch {
|
|
10930
|
+
kept.push(orphan.path);
|
|
10886
10931
|
}
|
|
10887
|
-
blocks.push(bits.join("\n"));
|
|
10888
10932
|
}
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
return [
|
|
10896
|
-
"Sideboard conversation context (restored after compaction or a new session):",
|
|
10897
|
-
"",
|
|
10898
|
-
body,
|
|
10899
|
-
"",
|
|
10900
|
-
"Continue from this context. Do not repeat the summary unless asked."
|
|
10901
|
-
].join("\n");
|
|
10933
|
+
if (!opts?.dryRun) {
|
|
10934
|
+
updateAdvancedSettings({
|
|
10935
|
+
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10936
|
+
});
|
|
10937
|
+
}
|
|
10938
|
+
return { removed, kept, orphans };
|
|
10902
10939
|
}
|
|
10903
|
-
function
|
|
10904
|
-
const
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
10910
|
-
};
|
|
10911
|
-
return [summary, ...recent];
|
|
10940
|
+
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
10941
|
+
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
10942
|
+
const last = settings.advanced.worktreeLastCleanupAt;
|
|
10943
|
+
if (!last) return true;
|
|
10944
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
10945
|
+
return elapsed >= intervalHours * 36e5;
|
|
10912
10946
|
}
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10947
|
+
var import_node_fs28, import_node_path27;
|
|
10948
|
+
var init_orphan_cleanup = __esm({
|
|
10949
|
+
"src/git/orphan-cleanup.ts"() {
|
|
10950
|
+
"use strict";
|
|
10951
|
+
import_node_fs28 = require("fs");
|
|
10952
|
+
import_node_path27 = require("path");
|
|
10953
|
+
init_worktree();
|
|
10954
|
+
init_thread_store();
|
|
10955
|
+
init_paths();
|
|
10956
|
+
init_app_settings();
|
|
10916
10957
|
}
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10958
|
+
});
|
|
10959
|
+
|
|
10960
|
+
// src/git/apply-into-main.ts
|
|
10961
|
+
async function applyThreadIntoMain(thread, opts) {
|
|
10962
|
+
const method = opts?.method ?? "merge";
|
|
10963
|
+
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
10964
|
+
const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
|
|
10965
|
+
cwd: thread.repoPath,
|
|
10966
|
+
reject: false
|
|
10967
|
+
});
|
|
10968
|
+
if (status.stdout.trim()) {
|
|
10969
|
+
throw new Error(
|
|
10970
|
+
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
10971
|
+
);
|
|
10920
10972
|
}
|
|
10921
|
-
const
|
|
10922
|
-
|
|
10923
|
-
|
|
10973
|
+
const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
|
|
10974
|
+
cwd: thread.repoPath,
|
|
10975
|
+
reject: false
|
|
10924
10976
|
});
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10977
|
+
if (checkout.exitCode !== 0) {
|
|
10978
|
+
throw new Error(
|
|
10979
|
+
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
10980
|
+
);
|
|
10981
|
+
}
|
|
10982
|
+
if (method === "cherry-pick") {
|
|
10983
|
+
const log = await (0, import_execa5.execa)(
|
|
10984
|
+
"git",
|
|
10985
|
+
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
10986
|
+
{ cwd: thread.repoPath, reject: false }
|
|
10987
|
+
);
|
|
10988
|
+
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
10989
|
+
if (!shas.length) {
|
|
10990
|
+
return {
|
|
10991
|
+
applied: false,
|
|
10992
|
+
method,
|
|
10993
|
+
targetBranch: target,
|
|
10994
|
+
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
10995
|
+
};
|
|
10996
|
+
}
|
|
10997
|
+
for (const sha of shas) {
|
|
10998
|
+
const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
|
|
10999
|
+
cwd: thread.repoPath,
|
|
11000
|
+
reject: false
|
|
11001
|
+
});
|
|
11002
|
+
if (cp.exitCode !== 0) {
|
|
11003
|
+
await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
|
|
11004
|
+
cwd: thread.repoPath,
|
|
11005
|
+
reject: false
|
|
11006
|
+
});
|
|
11007
|
+
throw new Error(
|
|
11008
|
+
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
11009
|
+
);
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
return {
|
|
11013
|
+
applied: true,
|
|
11014
|
+
method,
|
|
11015
|
+
targetBranch: target,
|
|
11016
|
+
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
11017
|
+
};
|
|
11018
|
+
}
|
|
11019
|
+
const merge = await (0, import_execa5.execa)(
|
|
11020
|
+
"git",
|
|
11021
|
+
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
11022
|
+
{ cwd: thread.repoPath, reject: false }
|
|
11023
|
+
);
|
|
11024
|
+
if (merge.exitCode !== 0) {
|
|
11025
|
+
await (0, import_execa5.execa)("git", ["merge", "--abort"], {
|
|
11026
|
+
cwd: thread.repoPath,
|
|
11027
|
+
reject: false
|
|
11028
|
+
});
|
|
11029
|
+
throw new Error(
|
|
11030
|
+
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
11031
|
+
);
|
|
11032
|
+
}
|
|
10932
11033
|
return {
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
olderCount: older.length
|
|
11034
|
+
applied: true,
|
|
11035
|
+
method: "merge",
|
|
11036
|
+
targetBranch: target,
|
|
11037
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
10938
11038
|
};
|
|
10939
11039
|
}
|
|
10940
|
-
var
|
|
10941
|
-
var
|
|
10942
|
-
"src/
|
|
11040
|
+
var import_execa5;
|
|
11041
|
+
var init_apply_into_main = __esm({
|
|
11042
|
+
"src/git/apply-into-main.ts"() {
|
|
10943
11043
|
"use strict";
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
10947
|
-
CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
10948
|
-
CONTEXT_MIN_MESSAGES = 10;
|
|
11044
|
+
import_execa5 = require("execa");
|
|
11045
|
+
init_worktree();
|
|
10949
11046
|
}
|
|
10950
11047
|
});
|
|
10951
11048
|
|
|
10952
|
-
// src/
|
|
10953
|
-
|
|
10954
|
-
|
|
11049
|
+
// src/store/workspaces.ts
|
|
11050
|
+
var workspaces_exports = {};
|
|
11051
|
+
__export(workspaces_exports, {
|
|
11052
|
+
addWorkspace: () => addWorkspace,
|
|
11053
|
+
ensureWorkspace: () => ensureWorkspace,
|
|
11054
|
+
listWorkspaces: () => listWorkspaces,
|
|
11055
|
+
removeWorkspace: () => removeWorkspace,
|
|
11056
|
+
syncWorkspacesFromThreads: () => syncWorkspacesFromThreads
|
|
11057
|
+
});
|
|
11058
|
+
function workspacesFile() {
|
|
11059
|
+
return (0, import_node_path28.join)(appDataDir(), "workspaces.json");
|
|
10955
11060
|
}
|
|
10956
|
-
function
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
11061
|
+
function removedWorkspacesFile() {
|
|
11062
|
+
return (0, import_node_path28.join)(appDataDir(), "removed-workspaces.json");
|
|
11063
|
+
}
|
|
11064
|
+
function readAll() {
|
|
11065
|
+
const path = workspacesFile();
|
|
11066
|
+
if (!(0, import_node_fs29.existsSync)(path)) return [];
|
|
11067
|
+
try {
|
|
11068
|
+
const raw = JSON.parse((0, import_node_fs29.readFileSync)(path, "utf8"));
|
|
11069
|
+
return Array.isArray(raw) ? raw : [];
|
|
11070
|
+
} catch {
|
|
11071
|
+
return [];
|
|
10965
11072
|
}
|
|
10966
|
-
return [...taken];
|
|
10967
11073
|
}
|
|
10968
|
-
function
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
return thread;
|
|
11074
|
+
function writeAll(list) {
|
|
11075
|
+
(0, import_node_fs29.mkdirSync)(appDataDir(), { recursive: true });
|
|
11076
|
+
(0, import_node_fs29.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
|
|
10972
11077
|
}
|
|
10973
|
-
function
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
sourceIsFork: from.sourceIsFork,
|
|
10983
|
-
parentThreadId: from.parentThreadId,
|
|
10984
|
-
prUrl: from.prUrl,
|
|
10985
|
-
prTitle: from.prTitle,
|
|
10986
|
-
prState: from.prState,
|
|
10987
|
-
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
10988
|
-
stackId: from.stackId,
|
|
10989
|
-
stackLayer: from.stackLayer
|
|
10990
|
-
};
|
|
11078
|
+
function readRemoved() {
|
|
11079
|
+
const path = removedWorkspacesFile();
|
|
11080
|
+
if (!(0, import_node_fs29.existsSync)(path)) return /* @__PURE__ */ new Set();
|
|
11081
|
+
try {
|
|
11082
|
+
const raw = JSON.parse((0, import_node_fs29.readFileSync)(path, "utf8"));
|
|
11083
|
+
return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
|
|
11084
|
+
} catch {
|
|
11085
|
+
return /* @__PURE__ */ new Set();
|
|
11086
|
+
}
|
|
10991
11087
|
}
|
|
10992
|
-
function
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
return listThreads({ includeArchived: true }).filter(
|
|
10996
|
-
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
10997
|
-
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
11088
|
+
function writeRemoved(paths) {
|
|
11089
|
+
(0, import_node_fs29.mkdirSync)(appDataDir(), { recursive: true });
|
|
11090
|
+
(0, import_node_fs29.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
|
|
10998
11091
|
}
|
|
10999
|
-
function
|
|
11000
|
-
const
|
|
11001
|
-
|
|
11092
|
+
function rememberRemoved(repoPath) {
|
|
11093
|
+
const next = readRemoved();
|
|
11094
|
+
next.add(repoPath);
|
|
11095
|
+
writeRemoved(next);
|
|
11002
11096
|
}
|
|
11003
|
-
function
|
|
11004
|
-
const
|
|
11005
|
-
|
|
11097
|
+
function forgetRemoved(repoPath) {
|
|
11098
|
+
const next = readRemoved();
|
|
11099
|
+
if (!next.delete(repoPath)) return;
|
|
11100
|
+
writeRemoved(next);
|
|
11006
11101
|
}
|
|
11007
|
-
function
|
|
11008
|
-
const
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11102
|
+
function listWorkspaces() {
|
|
11103
|
+
const all = readAll();
|
|
11104
|
+
const valid = all.filter(
|
|
11105
|
+
(w) => Boolean(w.path) && w.path !== "/" && w.path !== "." && !isGlobalRepoPath(w.path)
|
|
11106
|
+
);
|
|
11107
|
+
if (valid.length !== all.length) writeAll(valid);
|
|
11108
|
+
return valid.sort((a, b) => a.name.localeCompare(b.name));
|
|
11109
|
+
}
|
|
11110
|
+
async function addWorkspace(repoPath) {
|
|
11111
|
+
const root = await resolveRepoRoot(repoPath);
|
|
11112
|
+
if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
|
|
11113
|
+
if (!(0, import_node_fs29.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
|
|
11114
|
+
forgetRemoved(root);
|
|
11115
|
+
await ensureGhPreferOrigin(root);
|
|
11116
|
+
const current = readAll();
|
|
11117
|
+
const existing = current.find((w) => w.path === root);
|
|
11118
|
+
if (existing) return existing;
|
|
11119
|
+
const next = {
|
|
11120
|
+
path: root,
|
|
11121
|
+
name: (0, import_node_path28.basename)(root),
|
|
11122
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11014
11123
|
};
|
|
11124
|
+
writeAll([...current, next]);
|
|
11125
|
+
return next;
|
|
11015
11126
|
}
|
|
11016
|
-
function
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
const explicitTitle = input.title?.trim();
|
|
11020
|
-
const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
|
|
11021
|
-
const nextAgent = input.agent ?? from.agent;
|
|
11022
|
-
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
11023
|
-
assertOrchestratorCapableAgent(nextAgent);
|
|
11024
|
-
}
|
|
11025
|
-
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
11026
|
-
const thread = createEmptyThread({
|
|
11027
|
-
title,
|
|
11028
|
-
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
11029
|
-
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
11030
|
-
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
11031
|
-
userSetTitle: true,
|
|
11032
|
-
...binding,
|
|
11033
|
-
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
11034
|
-
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
11035
|
-
// the connected chat.
|
|
11036
|
-
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
11037
|
-
agent: nextAgent,
|
|
11038
|
-
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
11039
|
-
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
11040
|
-
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
11041
|
-
planMode: from.planMode,
|
|
11042
|
-
autonomy: input.autonomy ?? from.autonomy,
|
|
11043
|
-
attachments: input.attachments ?? [],
|
|
11044
|
-
status: "idle"
|
|
11045
|
-
});
|
|
11046
|
-
writeThread(thread);
|
|
11047
|
-
return thread;
|
|
11127
|
+
function removeWorkspace(repoPath) {
|
|
11128
|
+
writeAll(readAll().filter((w) => w.path !== repoPath));
|
|
11129
|
+
rememberRemoved(repoPath);
|
|
11048
11130
|
}
|
|
11049
|
-
function
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
const
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11131
|
+
async function ensureWorkspace(repoPath) {
|
|
11132
|
+
return addWorkspace(repoPath);
|
|
11133
|
+
}
|
|
11134
|
+
function syncWorkspacesFromThreads(repoPaths) {
|
|
11135
|
+
const current = readAll();
|
|
11136
|
+
const removed = readRemoved();
|
|
11137
|
+
const byPath = new Map(current.map((w) => [w.path, w]));
|
|
11138
|
+
let dirty = false;
|
|
11139
|
+
for (const path of repoPaths) {
|
|
11140
|
+
if (!path || path === "/" || isGlobalRepoPath(path) || byPath.has(path) || removed.has(path)) {
|
|
11141
|
+
continue;
|
|
11142
|
+
}
|
|
11143
|
+
if (!(0, import_node_fs29.existsSync)(path)) continue;
|
|
11144
|
+
const ws = {
|
|
11145
|
+
path,
|
|
11146
|
+
name: (0, import_node_path28.basename)(path),
|
|
11147
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11148
|
+
};
|
|
11149
|
+
byPath.set(path, ws);
|
|
11150
|
+
dirty = true;
|
|
11064
11151
|
}
|
|
11065
|
-
|
|
11152
|
+
const next = [...byPath.values()];
|
|
11153
|
+
if (dirty) writeAll(next);
|
|
11154
|
+
return next.sort((a, b) => a.name.localeCompare(b.name));
|
|
11066
11155
|
}
|
|
11067
|
-
var
|
|
11068
|
-
var
|
|
11069
|
-
"src/
|
|
11156
|
+
var import_node_fs29, import_node_path28;
|
|
11157
|
+
var init_workspaces2 = __esm({
|
|
11158
|
+
"src/store/workspaces.ts"() {
|
|
11070
11159
|
"use strict";
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
init_worktree_labels();
|
|
11160
|
+
import_node_fs29 = require("fs");
|
|
11161
|
+
import_node_path28 = require("path");
|
|
11162
|
+
init_paths();
|
|
11075
11163
|
init_global_workspace();
|
|
11076
|
-
|
|
11077
|
-
init_thread_store();
|
|
11078
|
-
init_worktree_labels();
|
|
11164
|
+
init_worktree();
|
|
11079
11165
|
}
|
|
11080
11166
|
});
|
|
11081
11167
|
|
|
11082
|
-
// src/
|
|
11083
|
-
function
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
if (p.type === type && p.text.trim()) return p.text.trim();
|
|
11091
|
-
}
|
|
11092
|
-
return "";
|
|
11093
|
-
}
|
|
11094
|
-
function summarizeTurnLive(parts) {
|
|
11095
|
-
const tools = parts.filter(
|
|
11096
|
-
(p) => p.type === "tool"
|
|
11097
|
-
);
|
|
11098
|
-
const lastTool = tools[tools.length - 1];
|
|
11099
|
-
const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
|
|
11100
|
-
const running = lastTool?.status === "running";
|
|
11101
|
-
const thinking = lastText(parts, "thinking");
|
|
11102
|
-
const text4 = lastText(parts, "text");
|
|
11103
|
-
const excerptRaw = thinking || text4;
|
|
11104
|
-
const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
|
|
11105
|
-
let summary = "Working\u2026";
|
|
11106
|
-
if (lastToolLabel) {
|
|
11107
|
-
const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
|
|
11108
|
-
summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
|
|
11109
|
-
} else if (thinking) {
|
|
11110
|
-
summary = "Thinking\u2026";
|
|
11111
|
-
} else if (text4) {
|
|
11112
|
-
summary = "Writing reply\u2026";
|
|
11113
|
-
}
|
|
11114
|
-
return {
|
|
11115
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11116
|
-
summary,
|
|
11117
|
-
lastTool: lastToolLabel,
|
|
11118
|
-
toolCount: tools.length,
|
|
11119
|
-
excerpt
|
|
11120
|
-
};
|
|
11121
|
-
}
|
|
11122
|
-
function flush(threadId) {
|
|
11123
|
-
const buf = buffers.get(threadId);
|
|
11124
|
-
if (!buf) return;
|
|
11125
|
-
if (buf.timer) {
|
|
11126
|
-
clearTimeout(buf.timer);
|
|
11127
|
-
buf.timer = null;
|
|
11168
|
+
// src/git/clone-repo.ts
|
|
11169
|
+
async function cloneRepoIntoSideboard(opts) {
|
|
11170
|
+
const url = opts.url.trim();
|
|
11171
|
+
if (!url) throw new Error("Clone URL is required");
|
|
11172
|
+
let name = opts.name?.trim();
|
|
11173
|
+
if (!name) {
|
|
11174
|
+
const leaf = (0, import_node_path29.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
11175
|
+
name = leaf || "repo";
|
|
11128
11176
|
}
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
buf.timer = setTimeout(() => {
|
|
11136
|
-
const live = buffers.get(threadId);
|
|
11137
|
-
if (live) live.timer = null;
|
|
11138
|
-
flush(threadId);
|
|
11139
|
-
}, FLUSH_MS);
|
|
11140
|
-
}
|
|
11141
|
-
function noteTurnLiveEvent(threadId, event) {
|
|
11142
|
-
if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
|
|
11143
|
-
return;
|
|
11177
|
+
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
11178
|
+
const dest = (0, import_node_path29.join)(sideboardReposDir(), name);
|
|
11179
|
+
if ((0, import_node_fs30.existsSync)(dest)) {
|
|
11180
|
+
const repoPath2 = await resolveRepoRoot(dest);
|
|
11181
|
+
const workspace2 = await ensureWorkspace(repoPath2);
|
|
11182
|
+
return { repoPath: repoPath2, workspace: workspace2 };
|
|
11144
11183
|
}
|
|
11145
|
-
const
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
const cur = buffers.get(threadId);
|
|
11151
|
-
if (cur?.timer) clearTimeout(cur.timer);
|
|
11152
|
-
buffers.delete(threadId);
|
|
11153
|
-
return;
|
|
11184
|
+
const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
|
|
11185
|
+
if (result.exitCode !== 0) {
|
|
11186
|
+
throw new Error(
|
|
11187
|
+
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
11188
|
+
);
|
|
11154
11189
|
}
|
|
11155
|
-
const
|
|
11156
|
-
|
|
11157
|
-
|
|
11190
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
11191
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
11192
|
+
return { repoPath, workspace };
|
|
11158
11193
|
}
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
}
|
|
11194
|
+
var import_node_fs30, import_node_path29, import_execa6;
|
|
11195
|
+
var init_clone_repo = __esm({
|
|
11196
|
+
"src/git/clone-repo.ts"() {
|
|
11197
|
+
"use strict";
|
|
11198
|
+
import_node_fs30 = require("fs");
|
|
11199
|
+
import_node_path29 = require("path");
|
|
11200
|
+
import_execa6 = require("execa");
|
|
11201
|
+
init_paths();
|
|
11202
|
+
init_workspaces2();
|
|
11203
|
+
init_worktree();
|
|
11170
11204
|
}
|
|
11205
|
+
});
|
|
11206
|
+
|
|
11207
|
+
// src/store/desktop-host.ts
|
|
11208
|
+
function desktopHostPidPath() {
|
|
11209
|
+
return (0, import_node_path30.join)(appDataDir(), "desktop-host.pid");
|
|
11171
11210
|
}
|
|
11172
|
-
function
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
if (!raw || typeof raw.summary !== "string") return null;
|
|
11178
|
-
return raw;
|
|
11211
|
+
function readDesktopHostPid() {
|
|
11212
|
+
try {
|
|
11213
|
+
const pid = Number.parseInt((0, import_node_fs31.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
|
|
11214
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
11215
|
+
return pid;
|
|
11179
11216
|
} catch {
|
|
11180
11217
|
return null;
|
|
11181
11218
|
}
|
|
11182
11219
|
}
|
|
11183
|
-
function
|
|
11184
|
-
const buf = buffers.get(threadId);
|
|
11185
|
-
if (buf?.timer) clearTimeout(buf.timer);
|
|
11186
|
-
buffers.delete(threadId);
|
|
11187
|
-
const path = threadLivePath(threadId);
|
|
11188
|
-
if (!(0, import_node_fs32.existsSync)(path)) return;
|
|
11220
|
+
function pidAlive(pid) {
|
|
11189
11221
|
try {
|
|
11190
|
-
(
|
|
11222
|
+
process.kill(pid, 0);
|
|
11223
|
+
return true;
|
|
11191
11224
|
} catch {
|
|
11225
|
+
return false;
|
|
11192
11226
|
}
|
|
11193
11227
|
}
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11228
|
+
function isDesktopHostAlive() {
|
|
11229
|
+
const pid = readDesktopHostPid();
|
|
11230
|
+
return pid != null && pidAlive(pid);
|
|
11231
|
+
}
|
|
11232
|
+
function isThisProcessDesktopHost() {
|
|
11233
|
+
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
11234
|
+
}
|
|
11235
|
+
function thisProcessShouldDrainAgentQueues() {
|
|
11236
|
+
if (isThisProcessDesktopHost()) return true;
|
|
11237
|
+
return !isDesktopHostAlive();
|
|
11238
|
+
}
|
|
11239
|
+
var import_node_fs31, import_node_path30;
|
|
11240
|
+
var init_desktop_host = __esm({
|
|
11241
|
+
"src/store/desktop-host.ts"() {
|
|
11197
11242
|
"use strict";
|
|
11198
|
-
|
|
11199
|
-
|
|
11243
|
+
import_node_fs31 = require("fs");
|
|
11244
|
+
import_node_path30 = require("path");
|
|
11200
11245
|
init_paths();
|
|
11201
|
-
buffers = /* @__PURE__ */ new Map();
|
|
11202
|
-
FLUSH_MS = 800;
|
|
11203
|
-
MAX_PARTS = 80;
|
|
11204
11246
|
}
|
|
11205
11247
|
});
|
|
11206
11248
|
|
|
11207
|
-
// src/
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11249
|
+
// src/detect/detect.ts
|
|
11250
|
+
async function requireAgent(agent, opts) {
|
|
11251
|
+
ensureAgentPath();
|
|
11252
|
+
enrichPathWithNpmGlobalBin();
|
|
11253
|
+
const status = await Promise.race([
|
|
11254
|
+
getAdapter(agent).detect(),
|
|
11255
|
+
new Promise((_, reject) => {
|
|
11256
|
+
setTimeout(() => {
|
|
11257
|
+
reject(
|
|
11258
|
+
new Error(
|
|
11259
|
+
`${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
|
|
11260
|
+
)
|
|
11261
|
+
);
|
|
11262
|
+
}, REQUIRE_AGENT_TIMEOUT_MS);
|
|
11263
|
+
})
|
|
11264
|
+
]);
|
|
11265
|
+
if (!status.installed || !status.authenticated) {
|
|
11266
|
+
throw new Error(status.reason ?? `${agent} is not available`);
|
|
11267
|
+
}
|
|
11268
|
+
if (opts?.requireLinear && !status.linearMcp) {
|
|
11269
|
+
throw new Error(
|
|
11270
|
+
`${agent} has no Linear MCP server configured \u2014 ticket sources require it`
|
|
11271
|
+
);
|
|
11272
|
+
}
|
|
11273
|
+
return status;
|
|
11274
|
+
}
|
|
11275
|
+
var REQUIRE_AGENT_TIMEOUT_MS;
|
|
11276
|
+
var init_detect = __esm({
|
|
11277
|
+
"src/detect/detect.ts"() {
|
|
11211
11278
|
"use strict";
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
## Required outcome
|
|
11217
|
-
|
|
11218
|
-
Start your reply with a **Recommendation** section using exactly one of:
|
|
11219
|
-
|
|
11220
|
-
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
11221
|
-
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
11222
|
-
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
11223
|
-
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
11224
|
-
|
|
11225
|
-
In 1\u20133 sentences, say **why** \u2014 grounded in correctness, risk, test coverage, and scope \u2014 not vibes. If you request changes, name the blockers explicitly.
|
|
11226
|
-
|
|
11227
|
-
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
11228
|
-
|
|
11229
|
-
## Findings
|
|
11230
|
-
|
|
11231
|
-
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
11232
|
-
|
|
11233
|
-
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
11234
|
-
|
|
11235
|
-
Flag something as a bug / blocking finding only when:
|
|
11236
|
-
|
|
11237
|
-
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
11238
|
-
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
11239
|
-
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
11240
|
-
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
11241
|
-
5. The author would likely fix it if made aware.
|
|
11242
|
-
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
11243
|
-
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
11244
|
-
8. It is clearly not just an intentional change by the author.
|
|
11245
|
-
|
|
11246
|
-
When flagging an issue, include a short accompanying comment:
|
|
11247
|
-
|
|
11248
|
-
1. Clear about why it is a problem.
|
|
11249
|
-
2. Severity must match reality \u2014 do not inflate.
|
|
11250
|
-
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
11251
|
-
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
11252
|
-
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
11253
|
-
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
11254
|
-
7. Skimmable on first read.
|
|
11255
|
-
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
11256
|
-
|
|
11257
|
-
HOW MANY FINDINGS TO RETURN:
|
|
11258
|
-
|
|
11259
|
-
List every finding the author would fix if they knew about it. If nothing qualifies, say so and still give the Recommendation. Do not stop at the first finding.
|
|
11260
|
-
|
|
11261
|
-
GUIDELINES:
|
|
11262
|
-
|
|
11263
|
-
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
11264
|
-
- One comment per distinct issue (or a short multi-line range if needed).
|
|
11265
|
-
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
11266
|
-
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
11267
|
-
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
11268
|
-
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
11269
|
-
|
|
11270
|
-
The report appears in chat (and can become Sideboard diff comments). Avoid unnecessary location chatter in the body; keep line ranges as short as possible (prefer \u22645\u201310 lines).
|
|
11271
|
-
|
|
11272
|
-
## Getting the diff
|
|
11273
|
-
|
|
11274
|
-
Use Sideboard's diff for this thread's worktree. Prefer the \`get_diff\` MCP tool (pass this thread's ref) for a compact summary, then read specific files with Read/Glob as needed. In the Sideboard desktop app, the Changes panel shows the same worktree diff.
|
|
11275
|
-
|
|
11276
|
-
If the user asks you to address or read line comments they added in the Changes / file diff UI, those arrive as \`diff-comment\` attachments on the next turn \u2014 follow them precisely.
|
|
11277
|
-
|
|
11278
|
-
## Fallback: if you don't have access to the Sideboard diff tool
|
|
11279
|
-
|
|
11280
|
-
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
11281
|
-
|
|
11282
|
-
\`\`\`bash
|
|
11283
|
-
# Get the merge base between this branch and the target
|
|
11284
|
-
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
11285
|
-
|
|
11286
|
-
# Get the committed diff against the merge base
|
|
11287
|
-
git diff $MERGE_BASE HEAD
|
|
11288
|
-
|
|
11289
|
-
# Get any uncommitted changes (staged and unstaged)
|
|
11290
|
-
git diff HEAD
|
|
11291
|
-
\`\`\`
|
|
11292
|
-
|
|
11293
|
-
Review the combination of both outputs: the first shows all committed changes on this branch relative to the target, and the second shows any uncommitted work in progress.
|
|
11294
|
-
|
|
11295
|
-
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
11296
|
-
|
|
11297
|
-
## Output format
|
|
11298
|
-
|
|
11299
|
-
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
11300
|
-
|
|
11301
|
-
Only report ONE finding per unique issue.
|
|
11302
|
-
|
|
11303
|
-
<example>
|
|
11304
|
-
## Recommendation
|
|
11305
|
-
|
|
11306
|
-
**Request changes** \u2014 The empty-input crash on load will break first-run users; fix that before merge. The unused helper is a nit and can wait.
|
|
11307
|
-
|
|
11308
|
-
## Findings
|
|
11309
|
-
|
|
11310
|
-
### **#1 Empty input causes crash** (blocking)
|
|
11311
|
-
|
|
11312
|
-
If the input field is empty when the page loads, the app will crash.
|
|
11313
|
-
|
|
11314
|
-
File: src/client/frontends/desktop/ui/Input.tsx
|
|
11315
|
-
|
|
11316
|
-
### **#2 Dead code** (nit)
|
|
11317
|
-
|
|
11318
|
-
The getUserData function is now unused. It should be deleted.
|
|
11319
|
-
|
|
11320
|
-
File: src/client/frontends/desktop/core/UserData.ts
|
|
11321
|
-
</example>
|
|
11322
|
-
|
|
11323
|
-
<example>
|
|
11324
|
-
## Recommendation
|
|
11325
|
-
|
|
11326
|
-
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
11327
|
-
</example>
|
|
11328
|
-
|
|
11329
|
-
## Growing the rules
|
|
11279
|
+
init_agents();
|
|
11280
|
+
REQUIRE_AGENT_TIMEOUT_MS = 8e3;
|
|
11281
|
+
}
|
|
11282
|
+
});
|
|
11330
11283
|
|
|
11331
|
-
|
|
11332
|
-
|
|
11284
|
+
// src/threads/create.ts
|
|
11285
|
+
async function createThread(input, _onSetupLine) {
|
|
11286
|
+
const resolved = resolveNewThreadOptions({
|
|
11287
|
+
agent: input.agent,
|
|
11288
|
+
model: input.model,
|
|
11289
|
+
effort: input.effort,
|
|
11290
|
+
fast: input.fast
|
|
11291
|
+
});
|
|
11292
|
+
await requireAgent(resolved.agent);
|
|
11293
|
+
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
11294
|
+
if (!(0, import_node_fs32.existsSync)(repoPath)) {
|
|
11295
|
+
throw new Error(`Repo not found: ${repoPath}`);
|
|
11296
|
+
}
|
|
11297
|
+
let sourceRef = input.sourceRef;
|
|
11298
|
+
let sourceIsFork = false;
|
|
11299
|
+
let prUrl = null;
|
|
11300
|
+
let prTitle = null;
|
|
11301
|
+
if (input.sourceType === "pr") {
|
|
11302
|
+
const num2 = Number(input.sourceRef.replace(/^#/, ""));
|
|
11303
|
+
if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
|
|
11304
|
+
const pr = await getPr(repoPath, num2);
|
|
11305
|
+
if (!pr) throw new Error(`PR #${num2} not found`);
|
|
11306
|
+
sourceIsFork = pr.isCrossRepository;
|
|
11307
|
+
prUrl = pr.url;
|
|
11308
|
+
const localFetchBranch = `sideboard-pr-${pr.number}`;
|
|
11309
|
+
await fetchPrHead(repoPath, pr.number, localFetchBranch);
|
|
11310
|
+
sourceRef = localFetchBranch;
|
|
11311
|
+
} else if (input.sourceType === "ticket") {
|
|
11312
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
11313
|
+
} else if (input.sourceType === "branch") {
|
|
11314
|
+
if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
|
|
11315
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
11316
|
+
} else {
|
|
11317
|
+
const existing = await getPrForHeadBranch(repoPath, sourceRef);
|
|
11318
|
+
if (existing?.url) {
|
|
11319
|
+
prUrl = existing.url;
|
|
11320
|
+
prTitle = existing.title;
|
|
11321
|
+
}
|
|
11322
|
+
}
|
|
11323
|
+
} else if (input.sourceType === "adopt") {
|
|
11324
|
+
throw new Error("Use adoptThread() for adopt sources");
|
|
11325
|
+
}
|
|
11326
|
+
const team = allocateTeamSlug(repoPath);
|
|
11327
|
+
const { branchName, worktreePath } = await createThreadWorktree({
|
|
11328
|
+
repoPath,
|
|
11329
|
+
sourceRef,
|
|
11330
|
+
slug: team.slug
|
|
11331
|
+
});
|
|
11332
|
+
copyConfiguredFiles(repoPath, worktreePath);
|
|
11333
|
+
const explicitTitle = input.title?.trim();
|
|
11334
|
+
const thread = createEmptyThread({
|
|
11335
|
+
title: explicitTitle || team.name,
|
|
11336
|
+
userSetTitle: Boolean(explicitTitle),
|
|
11337
|
+
sourceType: input.sourceType,
|
|
11338
|
+
sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
|
|
11339
|
+
branchName,
|
|
11340
|
+
worktreePath,
|
|
11341
|
+
repoPath,
|
|
11342
|
+
agent: resolved.agent,
|
|
11343
|
+
autonomy: input.autonomy ?? "default",
|
|
11344
|
+
model: resolved.model,
|
|
11345
|
+
effort: resolved.effort,
|
|
11346
|
+
fast: resolved.fast,
|
|
11347
|
+
planMode: Boolean(input.planMode),
|
|
11348
|
+
attachments: input.attachments ?? [],
|
|
11349
|
+
sourceIsFork,
|
|
11350
|
+
parentThreadId: input.parentThreadId ?? null,
|
|
11351
|
+
status: "idle",
|
|
11352
|
+
prUrl,
|
|
11353
|
+
prTitle
|
|
11354
|
+
});
|
|
11355
|
+
writeThread(thread);
|
|
11356
|
+
await ensureWorkspace(repoPath);
|
|
11357
|
+
return readThread(thread.id) ?? thread;
|
|
11358
|
+
}
|
|
11359
|
+
async function listLinearIssues(agent, repoPath) {
|
|
11360
|
+
const { getAdapter: getAdapter2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
11361
|
+
await requireAgent(agent, { requireLinear: true });
|
|
11362
|
+
const adapter = getAdapter2(agent);
|
|
11363
|
+
if (!adapter.listLinearIssues) {
|
|
11364
|
+
throw new Error(`${agent} does not support Linear issue listing`);
|
|
11365
|
+
}
|
|
11366
|
+
return adapter.listLinearIssues(repoPath);
|
|
11367
|
+
}
|
|
11368
|
+
var import_node_fs32;
|
|
11369
|
+
var init_create = __esm({
|
|
11370
|
+
"src/threads/create.ts"() {
|
|
11371
|
+
"use strict";
|
|
11372
|
+
import_node_fs32 = require("fs");
|
|
11373
|
+
init_detect();
|
|
11374
|
+
init_worktree();
|
|
11375
|
+
init_conductor();
|
|
11376
|
+
init_app_settings();
|
|
11377
|
+
init_thread_store();
|
|
11378
|
+
init_workspaces2();
|
|
11333
11379
|
}
|
|
11334
11380
|
});
|
|
11335
11381
|
|
|
11336
|
-
// src/
|
|
11337
|
-
function
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
11341
|
-
return false;
|
|
11342
|
-
}
|
|
11343
|
-
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
11382
|
+
// src/store/turn-live.ts
|
|
11383
|
+
function trimParts(parts) {
|
|
11384
|
+
if (parts.length <= MAX_PARTS) return parts;
|
|
11385
|
+
return parts.slice(-Math.floor(MAX_PARTS / 2));
|
|
11344
11386
|
}
|
|
11345
|
-
function
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
return content.trim() ? content : null;
|
|
11350
|
-
} catch {
|
|
11351
|
-
return null;
|
|
11387
|
+
function lastText(parts, type) {
|
|
11388
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
11389
|
+
const p = parts[i];
|
|
11390
|
+
if (p.type === type && p.text.trim()) return p.text.trim();
|
|
11352
11391
|
}
|
|
11392
|
+
return "";
|
|
11353
11393
|
}
|
|
11354
|
-
function
|
|
11355
|
-
const
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11394
|
+
function summarizeTurnLive(parts) {
|
|
11395
|
+
const tools = parts.filter(
|
|
11396
|
+
(p) => p.type === "tool"
|
|
11397
|
+
);
|
|
11398
|
+
const lastTool = tools[tools.length - 1];
|
|
11399
|
+
const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
|
|
11400
|
+
const running = lastTool?.status === "running";
|
|
11401
|
+
const thinking = lastText(parts, "thinking");
|
|
11402
|
+
const text4 = lastText(parts, "text");
|
|
11403
|
+
const excerptRaw = thinking || text4;
|
|
11404
|
+
const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
|
|
11405
|
+
let summary = "Working\u2026";
|
|
11406
|
+
if (lastToolLabel) {
|
|
11407
|
+
const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
|
|
11408
|
+
summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
|
|
11409
|
+
} else if (thinking) {
|
|
11410
|
+
summary = "Thinking\u2026";
|
|
11411
|
+
} else if (text4) {
|
|
11412
|
+
summary = "Writing reply\u2026";
|
|
11413
|
+
}
|
|
11414
|
+
return {
|
|
11415
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11416
|
+
summary,
|
|
11417
|
+
lastTool: lastToolLabel,
|
|
11418
|
+
toolCount: tools.length,
|
|
11419
|
+
excerpt
|
|
11420
|
+
};
|
|
11359
11421
|
}
|
|
11360
|
-
function
|
|
11361
|
-
const
|
|
11362
|
-
|
|
11363
|
-
if (
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
name: REPO_REVIEW_NAME,
|
|
11367
|
-
content: repoContent,
|
|
11368
|
-
source: "repo"
|
|
11369
|
-
};
|
|
11422
|
+
function flush(threadId) {
|
|
11423
|
+
const buf = buffers.get(threadId);
|
|
11424
|
+
if (!buf) return;
|
|
11425
|
+
if (buf.timer) {
|
|
11426
|
+
clearTimeout(buf.timer);
|
|
11427
|
+
buf.timer = null;
|
|
11370
11428
|
}
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11429
|
+
writeTurnLive(threadId, summarizeTurnLive(buf.parts));
|
|
11430
|
+
}
|
|
11431
|
+
function scheduleFlush(threadId) {
|
|
11432
|
+
const buf = buffers.get(threadId);
|
|
11433
|
+
if (!buf) return;
|
|
11434
|
+
if (buf.timer) return;
|
|
11435
|
+
buf.timer = setTimeout(() => {
|
|
11436
|
+
const live = buffers.get(threadId);
|
|
11437
|
+
if (live) live.timer = null;
|
|
11438
|
+
flush(threadId);
|
|
11439
|
+
}, FLUSH_MS);
|
|
11440
|
+
}
|
|
11441
|
+
function noteTurnLiveEvent(threadId, event) {
|
|
11442
|
+
if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
|
|
11443
|
+
return;
|
|
11380
11444
|
}
|
|
11381
|
-
const
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
|
|
11445
|
+
const prev = buffers.get(threadId) ?? { parts: [], timer: null };
|
|
11446
|
+
prev.parts = trimParts(applyAgentEvent(prev.parts, event));
|
|
11447
|
+
buffers.set(threadId, prev);
|
|
11448
|
+
if (event.type === "exit") {
|
|
11449
|
+
flush(threadId);
|
|
11450
|
+
const cur = buffers.get(threadId);
|
|
11451
|
+
if (cur?.timer) clearTimeout(cur.timer);
|
|
11452
|
+
buffers.delete(threadId);
|
|
11453
|
+
return;
|
|
11390
11454
|
}
|
|
11391
|
-
|
|
11392
|
-
(
|
|
11393
|
-
|
|
11394
|
-
return {
|
|
11395
|
-
path: REVIEW_REQUEST_PATH,
|
|
11396
|
-
name: REVIEW_REQUEST_NAME,
|
|
11397
|
-
content: REVIEW_REQUEST_TEMPLATE,
|
|
11398
|
-
source: "stock"
|
|
11399
|
-
};
|
|
11455
|
+
const immediate = event.type === "tool_use" || event.type === "tool_result";
|
|
11456
|
+
if (immediate) flush(threadId);
|
|
11457
|
+
else scheduleFlush(threadId);
|
|
11400
11458
|
}
|
|
11401
|
-
function
|
|
11402
|
-
const path =
|
|
11403
|
-
const
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11459
|
+
function writeTurnLive(threadId, progress) {
|
|
11460
|
+
const path = threadLivePath(threadId);
|
|
11461
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
11462
|
+
try {
|
|
11463
|
+
(0, import_node_fs33.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
|
|
11464
|
+
(0, import_node_fs33.renameSync)(tmp, path);
|
|
11465
|
+
} catch {
|
|
11466
|
+
try {
|
|
11467
|
+
(0, import_node_fs33.unlinkSync)(tmp);
|
|
11468
|
+
} catch {
|
|
11469
|
+
}
|
|
11470
|
+
}
|
|
11411
11471
|
}
|
|
11412
|
-
|
|
11413
|
-
const
|
|
11414
|
-
if (!
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
11472
|
+
function readTurnLive(threadId) {
|
|
11473
|
+
const path = threadLivePath(threadId);
|
|
11474
|
+
if (!(0, import_node_fs33.existsSync)(path)) return null;
|
|
11475
|
+
try {
|
|
11476
|
+
const raw = JSON.parse((0, import_node_fs33.readFileSync)(path, "utf8"));
|
|
11477
|
+
if (!raw || typeof raw.summary !== "string") return null;
|
|
11478
|
+
return raw;
|
|
11479
|
+
} catch {
|
|
11480
|
+
return null;
|
|
11419
11481
|
}
|
|
11420
|
-
|
|
11421
|
-
|
|
11482
|
+
}
|
|
11483
|
+
function clearTurnLive(threadId) {
|
|
11484
|
+
const buf = buffers.get(threadId);
|
|
11485
|
+
if (buf?.timer) clearTimeout(buf.timer);
|
|
11486
|
+
buffers.delete(threadId);
|
|
11487
|
+
const path = threadLivePath(threadId);
|
|
11488
|
+
if (!(0, import_node_fs33.existsSync)(path)) return;
|
|
11489
|
+
try {
|
|
11490
|
+
(0, import_node_fs33.unlinkSync)(path);
|
|
11491
|
+
} catch {
|
|
11422
11492
|
}
|
|
11423
|
-
const guidelines = resolveReviewGuidelines(from.worktreePath);
|
|
11424
|
-
const tab = createChatTab({
|
|
11425
|
-
fromThreadId: from.id,
|
|
11426
|
-
title: "Review",
|
|
11427
|
-
attachments: [
|
|
11428
|
-
buildReviewRequestAttachment(guidelines.content, {
|
|
11429
|
-
path: guidelines.path,
|
|
11430
|
-
name: guidelines.name
|
|
11431
|
-
})
|
|
11432
|
-
]
|
|
11433
|
-
});
|
|
11434
|
-
const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
|
|
11435
|
-
return { tab: started, from };
|
|
11436
11493
|
}
|
|
11437
|
-
var
|
|
11438
|
-
var
|
|
11439
|
-
"src/
|
|
11494
|
+
var import_node_fs33, buffers, FLUSH_MS, MAX_PARTS;
|
|
11495
|
+
var init_turn_live = __esm({
|
|
11496
|
+
"src/store/turn-live.ts"() {
|
|
11440
11497
|
"use strict";
|
|
11441
|
-
import_node_crypto5 = require("crypto");
|
|
11442
11498
|
import_node_fs33 = require("fs");
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
init_workspace_scratch();
|
|
11449
|
-
REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
11450
|
-
REPO_REVIEW_NAME = "review.md";
|
|
11451
|
-
REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
11452
|
-
LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
11453
|
-
REVIEW_REQUEST_NAME = "Review request.md";
|
|
11454
|
-
REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
11455
|
-
LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
11456
|
-
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
11457
|
-
"HOW MANY FINDINGS TO RETURN:"
|
|
11458
|
-
];
|
|
11499
|
+
init_message_parts();
|
|
11500
|
+
init_paths();
|
|
11501
|
+
buffers = /* @__PURE__ */ new Map();
|
|
11502
|
+
FLUSH_MS = 800;
|
|
11503
|
+
MAX_PARTS = 80;
|
|
11459
11504
|
}
|
|
11460
11505
|
});
|
|
11461
11506
|
|
|
@@ -13595,8 +13640,9 @@ function formatProcessGuideDirective() {
|
|
|
13595
13640
|
"Process guides (recurring work only):",
|
|
13596
13641
|
"- If `.claude/skills/graph-engineering/SKILL.md` exists, follow it (`/graph-engineering`) for migrations, ports, batch fixes, and other fan-out. Judge first; state on disk; grow the rulebook; do not patch around it.",
|
|
13597
13642
|
"- If this same shape of work will happen again, write `.claude/skills/<kebab-name>/SKILL.md` in this worktree (Claude Code project skill). Sideboard `/name`, Claude Code, and `attach` all load that path. Do not leave the method only in chat.",
|
|
13598
|
-
"-
|
|
13599
|
-
"-
|
|
13643
|
+
"- Merge-readiness notes: create or edit `.claude/skills/review/SKILL.md` and commit it. That file is allowed. Do not use `.sideboard/review.md` for new notes (legacy; setup copies it into the skill).",
|
|
13644
|
+
"- Do not write new skills under `.sideboard/skills/` (that folder only \u2014 other `.sideboard/` files such as settings.toml are fine). Point Codex/OpenCode at the Claude skill from `AGENTS.md`. Optional: symlink `.cursor/skills/<name>` to the Claude skill.",
|
|
13645
|
+
"- Skip a guide for a one-off. If a matching skill exists, follow it. Same miss twice \u2192 edit the skill, do not patch around it."
|
|
13600
13646
|
].join("\n");
|
|
13601
13647
|
}
|
|
13602
13648
|
function formatArtifactDirective() {
|
|
@@ -13623,15 +13669,7 @@ function formatArtifactDirective() {
|
|
|
13623
13669
|
].join("\n");
|
|
13624
13670
|
}
|
|
13625
13671
|
function formatUiReminder() {
|
|
13626
|
-
return
|
|
13627
|
-
"Sideboard side column (important):",
|
|
13628
|
-
"There is no claude.ai Artifact tool here \u2014 that is normal.",
|
|
13629
|
-
"HTML/SVG/markdown: ```html fence or MCP present_artifact.",
|
|
13630
|
-
"CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
|
|
13631
|
-
"Files column: MCP present_files (brightsy account storage or memory).",
|
|
13632
|
-
"Do not say artifacts/CMS UI are unavailable.",
|
|
13633
|
-
"ask_user: only when blocked on a real multiple-choice (approach fork, which API). Never for hellos, check-ins, or \u201Cwhat next?\u201D menus \u2014 reply in chat."
|
|
13634
|
-
].join(" ");
|
|
13672
|
+
return "Sideboard UI: html fence or present_artifact / present_schema / present_files; ask_user only for a real multiple-choice (not hellos or \u201Cwhat next?\u201D) \u2014 reply in chat. Do not say artifacts/CMS UI are unavailable.";
|
|
13635
13673
|
}
|
|
13636
13674
|
var import_node_fs39, import_node_path35;
|
|
13637
13675
|
var init_instructions = __esm({
|
|
@@ -13713,7 +13751,7 @@ __export(plan_file_exports, {
|
|
|
13713
13751
|
resolvePlanMarkdown: () => resolvePlanMarkdown,
|
|
13714
13752
|
writePlanFile: () => writePlanFile
|
|
13715
13753
|
});
|
|
13716
|
-
function
|
|
13754
|
+
function ensureAttachmentsGitignore(worktreePath) {
|
|
13717
13755
|
const gitignoreAbs = (0, import_node_path36.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
|
|
13718
13756
|
if ((0, import_node_fs40.existsSync)(gitignoreAbs)) return;
|
|
13719
13757
|
(0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(gitignoreAbs), { recursive: true });
|
|
@@ -13735,7 +13773,7 @@ function readPlanFile(worktreePath) {
|
|
|
13735
13773
|
return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path36.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path36.join)(worktreePath, LEGACY_PLAN_FILE_REL));
|
|
13736
13774
|
}
|
|
13737
13775
|
function writePlanFile(worktreePath, content) {
|
|
13738
|
-
|
|
13776
|
+
ensureAttachmentsGitignore(worktreePath);
|
|
13739
13777
|
const abs = planFileAbs(worktreePath);
|
|
13740
13778
|
(0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(abs), { recursive: true });
|
|
13741
13779
|
const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
|
|
@@ -15022,7 +15060,7 @@ var init_orchestrator = __esm({
|
|
|
15022
15060
|
if (compact.didCompact) {
|
|
15023
15061
|
thread = updateThread(threadId, {
|
|
15024
15062
|
messages: compact.thread.messages,
|
|
15025
|
-
sessionId:
|
|
15063
|
+
sessionId: compact.thread.sessionId
|
|
15026
15064
|
});
|
|
15027
15065
|
this.emit({
|
|
15028
15066
|
type: "context_compacted",
|
|
@@ -17601,74 +17639,77 @@ async function startMcpServer() {
|
|
|
17601
17639
|
name: "sideboard",
|
|
17602
17640
|
version: "0.1.0"
|
|
17603
17641
|
});
|
|
17604
|
-
|
|
17605
|
-
|
|
17606
|
-
|
|
17607
|
-
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
workspaces.
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
|
|
17625
|
-
|
|
17626
|
-
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
const
|
|
17642
|
+
const worktreeProfile = sideboardMcpProfile() === "worktree";
|
|
17643
|
+
if (!worktreeProfile) {
|
|
17644
|
+
server.tool(
|
|
17645
|
+
"list_workspaces",
|
|
17646
|
+
"List registered Sideboard workspaces (repos). Each line is name, path, and github:owner/repo when resolvable \u2014 use path as repoPath for list_branches/list_prs/list_issues/create_thread.",
|
|
17647
|
+
{},
|
|
17648
|
+
async () => {
|
|
17649
|
+
const workspaces = orch.listWorkspaces();
|
|
17650
|
+
const lines = await Promise.all(
|
|
17651
|
+
workspaces.map(async (w) => {
|
|
17652
|
+
const slug = await resolveGithubRepoSlug(w.path).catch(() => null);
|
|
17653
|
+
return slug ? `${w.name} ${w.path} github:${slug}` : `${w.name} ${w.path}`;
|
|
17654
|
+
})
|
|
17655
|
+
);
|
|
17656
|
+
return {
|
|
17657
|
+
content: [
|
|
17658
|
+
{ type: "text", text: lines.join("\n") || "(no workspaces)" }
|
|
17659
|
+
]
|
|
17660
|
+
};
|
|
17661
|
+
}
|
|
17662
|
+
);
|
|
17663
|
+
server.tool(
|
|
17664
|
+
"list_threads",
|
|
17665
|
+
"List Sideboard threads across all workspaces (one summary line each \u2014 token-frugal). Each line ends with sideboard://thread/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
|
|
17666
|
+
{},
|
|
17667
|
+
async () => {
|
|
17668
|
+
const threads = orch.getThreads(true);
|
|
17669
|
+
const lines = threads.map((t) => {
|
|
17670
|
+
const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path40.basename)(t.repoPath) || t.repoPath;
|
|
17671
|
+
const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
|
|
17672
|
+
const progress = live?.summary ? ` ${live.summary}` : "";
|
|
17673
|
+
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}` : ""}${progress}`;
|
|
17674
|
+
});
|
|
17675
|
+
return {
|
|
17676
|
+
content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
|
|
17677
|
+
};
|
|
17678
|
+
}
|
|
17679
|
+
);
|
|
17680
|
+
server.tool(
|
|
17681
|
+
"get_thread",
|
|
17682
|
+
"Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
|
|
17683
|
+
{ ref: import_zod4.z.string() },
|
|
17684
|
+
async ({ ref }) => {
|
|
17685
|
+
const t = orch.getThread(ref);
|
|
17686
|
+
if (!t) {
|
|
17687
|
+
return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
|
|
17688
|
+
}
|
|
17631
17689
|
const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
|
|
17632
|
-
const
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17690
|
+
const summary = {
|
|
17691
|
+
id: t.id,
|
|
17692
|
+
title: t.title,
|
|
17693
|
+
status: t.status,
|
|
17694
|
+
agent: t.agent,
|
|
17695
|
+
sourceType: t.sourceType,
|
|
17696
|
+
sourceRef: t.sourceRef,
|
|
17697
|
+
branchName: t.branchName,
|
|
17698
|
+
worktreePath: t.worktreePath,
|
|
17699
|
+
sessionId: t.sessionId,
|
|
17700
|
+
queueLength: t.queue.length,
|
|
17701
|
+
messageCount: t.messages.length,
|
|
17702
|
+
devPort: t.devPort,
|
|
17703
|
+
prUrl: t.prUrl,
|
|
17704
|
+
lastError: t.lastError ?? null,
|
|
17705
|
+
stillRunning: t.status === "running" || t.status === "queued",
|
|
17706
|
+
progress: live?.summary ?? null,
|
|
17707
|
+
lastActivityAt: live?.updatedAt ?? null
|
|
17708
|
+
};
|
|
17709
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
17648
17710
|
}
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
id: t.id,
|
|
17652
|
-
title: t.title,
|
|
17653
|
-
status: t.status,
|
|
17654
|
-
agent: t.agent,
|
|
17655
|
-
sourceType: t.sourceType,
|
|
17656
|
-
sourceRef: t.sourceRef,
|
|
17657
|
-
branchName: t.branchName,
|
|
17658
|
-
worktreePath: t.worktreePath,
|
|
17659
|
-
sessionId: t.sessionId,
|
|
17660
|
-
queueLength: t.queue.length,
|
|
17661
|
-
messageCount: t.messages.length,
|
|
17662
|
-
devPort: t.devPort,
|
|
17663
|
-
prUrl: t.prUrl,
|
|
17664
|
-
lastError: t.lastError ?? null,
|
|
17665
|
-
stillRunning: t.status === "running" || t.status === "queued",
|
|
17666
|
-
progress: live?.summary ?? null,
|
|
17667
|
-
lastActivityAt: live?.updatedAt ?? null
|
|
17668
|
-
};
|
|
17669
|
-
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
17670
|
-
}
|
|
17671
|
-
);
|
|
17711
|
+
);
|
|
17712
|
+
}
|
|
17672
17713
|
server.tool(
|
|
17673
17714
|
"present_artifact",
|
|
17674
17715
|
"Show an HTML, SVG, markdown, or React document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Prefer type=html for interactive pages. For type=react, pass a single component module that `export default`s a component (JSX/TSX ok) \u2014 Sideboard bootstraps React/ReactDOM/Babel and renders it; only `react`/`react-dom` imports are available, no other npm packages.",
|
|
@@ -17797,9 +17838,9 @@ async function startMcpServer() {
|
|
|
17797
17838
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
17798
17839
|
}
|
|
17799
17840
|
);
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17841
|
+
if (!worktreeProfile) {
|
|
17842
|
+
registerSlackTools(server);
|
|
17843
|
+
registerLinearTools(server);
|
|
17803
17844
|
registerScheduleTools(server);
|
|
17804
17845
|
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await Promise.resolve().then(() => (init_caffeinate_hold(), caffeinate_hold_exports));
|
|
17805
17846
|
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
|
|
@@ -18153,7 +18194,7 @@ async function startMcpServer() {
|
|
|
18153
18194
|
);
|
|
18154
18195
|
server.tool(
|
|
18155
18196
|
"request_review",
|
|
18156
|
-
'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .
|
|
18197
|
+
'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .claude/skills/review/SKILL.md when present (else seeds that skill from .sideboard/review.md or the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
|
|
18157
18198
|
{ ref: import_zod4.z.string().describe("Worktree thread id/ref to review") },
|
|
18158
18199
|
async ({ ref }) => {
|
|
18159
18200
|
try {
|