@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.
Files changed (38) hide show
  1. package/dist/agents/cursor-runner.js +2 -2
  2. package/dist/{agents-T4RHL5UX.js → agents-XB332VUJ.js} +5 -5
  3. package/dist/{agents-4XQANGFO.js → agents-YIMHDI46.js} +7 -7
  4. package/dist/{app-settings-2WH6DVOW.js → app-settings-EBCVMFF6.js} +1 -1
  5. package/dist/{app-settings-6AXBWM3K.js → app-settings-IYSSVZHH.js} +2 -2
  6. package/dist/{chunk-7ZTQHC2Q.js → chunk-4LKD3PGO.js} +1 -1
  7. package/dist/{chunk-TLPJHLLM.js → chunk-4TR3HZFT.js} +1 -1
  8. package/dist/{chunk-SMWVSSE5.js → chunk-756EO4GK.js} +3 -3
  9. package/dist/{chunk-XOYH3OMI.js → chunk-AY2EQI2P.js} +16 -13
  10. package/dist/{chunk-PZVA2VDF.js → chunk-EW7COXYE.js} +1 -1
  11. package/dist/{chunk-VTTTV2LM.js → chunk-FBV6NK62.js} +2 -2
  12. package/dist/{chunk-GYFNFU62.js → chunk-I2OJ2YSP.js} +8 -16
  13. package/dist/{chunk-6JJTMI7G.js → chunk-JVIW55KT.js} +1 -1
  14. package/dist/{chunk-6HRR4T4P.js → chunk-KKHDPV45.js} +15 -14
  15. package/dist/{chunk-XEFHG6VG.js → chunk-LTPX5N6K.js} +1310 -1265
  16. package/dist/{chunk-EX4NZN3O.js → chunk-NE4TOOKS.js} +1 -1
  17. package/dist/{chunk-LRO7YVLJ.js → chunk-OQDIYVDD.js} +2 -2
  18. package/dist/{chunk-3SIFRHJ3.js → chunk-Q6B3NRCT.js} +1457 -1431
  19. package/dist/{chunk-THXSFETX.js → chunk-TROJ3PVH.js} +3 -3
  20. package/dist/{chunk-5XUKY2JY.js → chunk-VRTKGKUW.js} +8 -16
  21. package/dist/{chunk-HUKCGRAT.js → chunk-WD2TARQS.js} +7 -2
  22. package/dist/{coordinator-prompt-4TUQUEHY.js → coordinator-prompt-JKOU6BWV.js} +4 -4
  23. package/dist/{coordinator-prompt-YQWRXCPX.js → coordinator-prompt-V66BYTUV.js} +3 -3
  24. package/dist/{global-workspace-V6TMECDU.js → global-workspace-NCTBE7G7.js} +5 -5
  25. package/dist/{global-workspace-H37LFFXI.js → global-workspace-WBQWQQQY.js} +4 -4
  26. package/dist/index.cjs +1697 -1661
  27. package/dist/index.d.cts +65 -24
  28. package/dist/index.d.ts +65 -24
  29. package/dist/index.js +107 -85
  30. package/dist/mcp/run-stdio.cjs +1660 -1619
  31. package/dist/mcp/run-stdio.js +82 -79
  32. package/dist/{orchestrator-DEHSKERK.js → orchestrator-6W7EKSPO.js} +9 -9
  33. package/dist/{orchestrator-2P7DX44Q.js → orchestrator-JVGVPKLI.js} +7 -7
  34. package/dist/{workspaces-JU3P6FHB.js → workspaces-I3554R3F.js} +5 -5
  35. package/dist/{workspaces-UPEVANU4.js → workspaces-SUBEDSGB.js} +6 -6
  36. package/dist/{worktree-NR5YJG2E.js → worktree-2UWO7VEK.js} +3 -3
  37. package/dist/{worktree-6MFKE465.js → worktree-7H6HB6GW.js} +2 -2
  38. package/package.json +1 -1
@@ -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) || /Sideboard\.app[/\\]/i.test(name) || /Electron\.app[/\\]/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 Sideboard worktree agents. You are not yourself checked out in a project worktree.",
5641
- "- This cwd is a synthetic empty home (not a git repo). Emptiness here is expected \u2014 it is not a problem to fix.",
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
- `- For "what's going on": call list_threads (and list_workspaces if needed). Summarize fleet status \u2014 do not ls/git-status this synthetic home.`,
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 .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id',
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` for new guides. Codex/OpenCode: one line in AGENTS.md pointing at that file.",
5788
- "- One-offs: no guide. Same miss across threads: edit the skill or `.sideboard/review.md`, then rerun the batch \u2014 do not patch three threads.",
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/hook/conductor.ts
9765
- function matchSimpleGlob(pattern, name) {
9766
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
9767
- return new RegExp(`^${escaped}$`).test(name);
9768
- }
9769
- function readWorktreeInclude(repoPath) {
9770
- const path = (0, import_node_path25.join)(repoPath, ".worktreeinclude");
9771
- if (!(0, import_node_fs26.existsSync)(path)) return [];
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 copied;
9776
+ return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
9819
9777
  }
9820
- async function captureLoginEnv() {
9821
- if (cachedLoginEnv) return { ...cachedLoginEnv };
9822
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
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 result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
9825
- reject: false,
9826
- timeout: 5e3
9827
- });
9828
- if (result.exitCode === 0 && result.stdout) {
9829
- const env = { ...process.env };
9830
- for (const entry of result.stdout.split("\0")) {
9831
- if (!entry) continue;
9832
- const eq = entry.indexOf("=");
9833
- if (eq <= 0) continue;
9834
- const key = entry.slice(0, eq);
9835
- const value = entry.slice(eq + 1);
9836
- if (key) env[key] = value;
9837
- }
9838
- cachedLoginEnv = env;
9839
- return { ...env };
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 buildWorkspaceScriptEnv(opts, baseEnv) {
9847
- const env = stripNestedElectronEnv({
9848
- ...baseEnv ?? process.env
9849
- });
9850
- const name = opts.workspaceName ?? (0, import_node_path25.basename)(opts.worktreePath);
9851
- const ports = opts.ports ?? [];
9852
- const primary = ports[0];
9853
- env.SIDEBOARD_WORKSPACE_NAME = name;
9854
- env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
9855
- env.SIDEBOARD_ROOT_PATH = opts.repoPath;
9856
- env.SIDEBOARD_IS_LOCAL = "1";
9857
- if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
9858
- env.CONDUCTOR_WORKSPACE_NAME = name;
9859
- env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
9860
- env.CONDUCTOR_ROOT_PATH = opts.repoPath;
9861
- env.CONDUCTOR_IS_LOCAL = "1";
9862
- if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
9863
- if (primary != null) {
9864
- env.SIDEBOARD_PORT = String(primary);
9865
- env.CONDUCTOR_PORT = String(primary);
9866
- env.PORT = String(primary);
9867
- for (let i = 1; i < ports.length; i++) {
9868
- const p = ports[i];
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
- return env;
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 pipeLines(stream, onLine) {
9876
- if (!stream || !onLine) return;
9877
- const rl = (0, import_node_readline3.createInterface)({ input: stream });
9878
- rl.on("line", onLine);
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
- function killScriptTree(child, ports = []) {
9881
- const pid = child.pid;
9882
- if (pid) {
9883
- try {
9884
- if (process.platform === "win32") {
9885
- void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
9886
- } else {
9887
- process.kill(-pid, "SIGTERM");
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
- for (const port of ports) {
9903
- if (!Number.isFinite(port) || port <= 0) continue;
9904
- if (process.platform === "win32") {
9905
- void (0, import_execa4.execa)(
9906
- "powershell",
9907
- [
9908
- "-NoProfile",
9909
- "-Command",
9910
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
9911
- ],
9912
- { reject: false }
9913
- );
9914
- } else {
9915
- void (0, import_execa4.execa)(
9916
- "zsh",
9917
- ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
9918
- { reject: false }
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
- async function spawnWorkspaceScript(command, opts) {
9924
- const loginEnv = await captureLoginEnv();
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
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
9970
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
9971
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
9972
- const handle = await spawnWorkspaceScript(settings.setup, {
9973
- worktreePath,
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
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
9987
- const found = findConventionSetup(worktreePath, repoPath);
9988
- if (!found) return { ran: false, exitCode: null, source: null };
9989
- onLine?.(`[setup] ${found.source}`);
9990
- const handle = await spawnWorkspaceScript(found.command, {
9991
- worktreePath,
9992
- repoPath,
9993
- defaultBranch: opts?.defaultBranch,
9994
- onLine
9995
- });
9996
- const exitCode = await attachAbort(handle, opts?.signal);
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
- ran: true,
9999
- exitCode,
10000
- source: found.source,
10001
- kill: handle.kill
9924
+ older: messages.slice(0, cut),
9925
+ recent: messages.slice(cut)
10002
9926
  };
10003
9927
  }
10004
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
10005
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
10006
- if (!setup.ran) {
10007
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
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
- if (!setup.ran) {
10010
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
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
- return setup;
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
- async function runArchiveScript(repoPath, worktreePath, onLine) {
10015
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
10016
- if (!settings?.archive) return { ran: false, exitCode: null };
10017
- const handle = await spawnWorkspaceScript(settings.archive, {
10018
- worktreePath,
10019
- repoPath,
10020
- onLine
10021
- });
10022
- const exitCode = await handle.done;
10023
- return { ran: true, exitCode };
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 listRunScripts(worktreePath, repoPath) {
10026
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
10027
- if (!settings?.runScripts.length) return [];
10028
- return settings.runScripts.filter((s) => {
10029
- if (!s.availableIn?.length) return true;
10030
- return s.availableIn.includes("local");
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 getDefaultRunScript(worktreePath, repoPath) {
10034
- const scripts = listRunScripts(worktreePath, repoPath);
10035
- if (!scripts.length) return null;
10036
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
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 getRunScript(worktreePath, repoPath, name) {
10039
- const scripts = listRunScripts(worktreePath, repoPath);
10040
- if (!scripts.length) return null;
10041
- if (name) {
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 getDefaultRunScript(worktreePath, repoPath);
10020
+ return 0;
10045
10021
  }
10046
- function getRunMode(worktreePath, repoPath) {
10047
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
10022
+ function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
10023
+ return lastRequestOccupancy(thread) >= occupancyTokens;
10048
10024
  }
10049
- async function allocatePort() {
10050
- return new Promise((resolve, reject) => {
10051
- const server = (0, import_node_net.createServer)();
10052
- server.listen(0, "127.0.0.1", () => {
10053
- const addr = server.address();
10054
- if (!addr || typeof addr === "string") {
10055
- server.close();
10056
- reject(new Error("Failed to allocate port"));
10057
- return;
10058
- }
10059
- const port = addr.port;
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
- async function allocatePortRange(size = PORT_RANGE_SIZE) {
10066
- const base = await allocatePort();
10067
- const ports = [base];
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
- import_node_fs26 = require("fs");
10110
- import_node_net = require("net");
10111
- import_node_path25 = require("path");
10112
- import_execa4 = require("execa");
10113
- import_node_readline3 = require("readline");
10114
- init_settings();
10115
- init_nested_electron_env();
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/git/orphan-cleanup.ts
10126
- function isSideboardWorktreePath(path) {
10127
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
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
- async function cleanupOrphanWorktrees(opts) {
10191
- const settings = loadAppSettings();
10192
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
10193
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
10194
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
10195
- const budget = Math.max(0, maxCount - activeCount);
10196
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
10197
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
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
- if (!opts?.dryRun) {
10217
- updateAdvancedSettings({
10218
- worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
10219
- });
10220
- }
10221
- return { removed, kept, orphans };
10081
+ return [...taken];
10222
10082
  }
10223
- function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
10224
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
10225
- const last = settings.advanced.worktreeLastCleanupAt;
10226
- if (!last) return true;
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
- var import_node_fs27, import_node_path26;
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
- applied: true,
10318
- method: "merge",
10319
- targetBranch: target,
10320
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
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
- var import_execa5;
10324
- var init_apply_into_main = __esm({
10325
- "src/git/apply-into-main.ts"() {
10326
- "use strict";
10327
- import_execa5 = require("execa");
10328
- init_worktree();
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
- async function addWorkspace(repoPath) {
10394
- const root = await resolveRepoRoot(repoPath);
10395
- if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
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 removeWorkspace(repoPath) {
10411
- writeAll(readAll().filter((w) => w.path !== repoPath));
10412
- rememberRemoved(repoPath);
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
- async function ensureWorkspace(repoPath) {
10415
- return addWorkspace(repoPath);
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 syncWorkspacesFromThreads(repoPaths) {
10418
- const current = readAll();
10419
- const removed = readRemoved();
10420
- const byPath = new Map(current.map((w) => [w.path, w]));
10421
- let dirty = false;
10422
- for (const path of repoPaths) {
10423
- if (!path || path === "/" || isGlobalRepoPath(path) || byPath.has(path) || removed.has(path)) {
10424
- continue;
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 next = [...byPath.values()];
10436
- if (dirty) writeAll(next);
10437
- return next.sort((a, b) => a.name.localeCompare(b.name));
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
- var import_node_fs28, import_node_path27;
10440
- var init_workspaces2 = __esm({
10441
- "src/store/workspaces.ts"() {
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
- import_node_fs28 = require("fs");
10444
- import_node_path27 = require("path");
10445
- init_paths();
10186
+ import_node_crypto4 = require("crypto");
10187
+ init_context_compact();
10188
+ init_teams();
10189
+ init_worktree_labels();
10446
10190
  init_global_workspace();
10447
- init_worktree();
10191
+ init_orchestrator_capable();
10192
+ init_thread_store();
10193
+ init_worktree_labels();
10448
10194
  }
10449
10195
  });
10450
10196
 
10451
- // src/git/clone-repo.ts
10452
- async function cloneRepoIntoSideboard(opts) {
10453
- const url = opts.url.trim();
10454
- if (!url) throw new Error("Clone URL is required");
10455
- let name = opts.name?.trim();
10456
- if (!name) {
10457
- const leaf = (0, import_node_path28.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
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
- const repoPath = await resolveRepoRoot(dest);
10474
- const workspace = await ensureWorkspace(repoPath);
10475
- return { repoPath, workspace };
10205
+ return `${REVIEW_SKILL_FRONTMATTER}${source}
10206
+ `;
10476
10207
  }
10477
- var import_node_fs29, import_node_path28, import_execa6;
10478
- var init_clone_repo = __esm({
10479
- "src/git/clone-repo.ts"() {
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
- import_node_fs29 = require("fs");
10482
- import_node_path28 = require("path");
10483
- import_execa6 = require("execa");
10484
- init_paths();
10485
- init_workspaces2();
10486
- init_worktree();
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/store/desktop-host.ts
10491
- function desktopHostPidPath() {
10492
- return (0, import_node_path29.join)(appDataDir(), "desktop-host.pid");
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 readDesktopHostPid() {
10355
+ function readTextIfPresent(abs) {
10356
+ if (!(0, import_node_fs26.existsSync)(abs)) return null;
10495
10357
  try {
10496
- const pid = Number.parseInt((0, import_node_fs30.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
10497
- if (!Number.isFinite(pid) || pid <= 0) return null;
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 pidAlive(pid) {
10504
- try {
10505
- process.kill(pid, 0);
10506
- return true;
10507
- } catch {
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 isDesktopHostAlive() {
10512
- const pid = readDesktopHostPid();
10513
- return pid != null && pidAlive(pid);
10514
- }
10515
- function isThisProcessDesktopHost() {
10516
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
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
- var import_node_fs30, import_node_path29;
10523
- var init_desktop_host = __esm({
10524
- "src/store/desktop-host.ts"() {
10525
- "use strict";
10526
- import_node_fs30 = require("fs");
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
- // src/detect/detect.ts
10533
- async function requireAgent(agent, opts) {
10534
- ensureAgentPath();
10535
- enrichPathWithNpmGlobalBin();
10536
- const status = await Promise.race([
10537
- getAdapter(agent).detect(),
10538
- new Promise((_, reject) => {
10539
- setTimeout(() => {
10540
- reject(
10541
- new Error(
10542
- `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
10543
- )
10544
- );
10545
- }, REQUIRE_AGENT_TIMEOUT_MS);
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
- if (opts?.requireLinear && !status.linearMcp) {
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
- `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
10429
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
10554
10430
  );
10555
10431
  }
10556
- return status;
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 REQUIRE_AGENT_TIMEOUT_MS;
10559
- var init_detect = __esm({
10560
- "src/detect/detect.ts"() {
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
- init_agents();
10563
- REQUIRE_AGENT_TIMEOUT_MS = 8e3;
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/threads/create.ts
10568
- async function createThread(input, _onSetupLine) {
10569
- const resolved = resolveNewThreadOptions({
10570
- agent: input.agent,
10571
- model: input.model,
10572
- effort: input.effort,
10573
- fast: input.fast
10574
- });
10575
- await requireAgent(resolved.agent);
10576
- const repoPath = await resolveRepoRoot(input.repoPath);
10577
- if (!(0, import_node_fs31.existsSync)(repoPath)) {
10578
- throw new Error(`Repo not found: ${repoPath}`);
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
- let sourceRef = input.sourceRef;
10581
- let sourceIsFork = false;
10582
- let prUrl = null;
10583
- let prTitle = null;
10584
- if (input.sourceType === "pr") {
10585
- const num2 = Number(input.sourceRef.replace(/^#/, ""));
10586
- if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
10587
- const pr = await getPr(repoPath, num2);
10588
- if (!pr) throw new Error(`PR #${num2} not found`);
10589
- sourceIsFork = pr.isCrossRepository;
10590
- prUrl = pr.url;
10591
- const localFetchBranch = `sideboard-pr-${pr.number}`;
10592
- await fetchPrHead(repoPath, pr.number, localFetchBranch);
10593
- sourceRef = localFetchBranch;
10594
- } else if (input.sourceType === "ticket") {
10595
- sourceRef = await resolveDefaultBranch(repoPath);
10596
- } else if (input.sourceType === "branch") {
10597
- if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
10598
- sourceRef = await resolveDefaultBranch(repoPath);
10599
- } else {
10600
- const existing = await getPrForHeadBranch(repoPath, sourceRef);
10601
- if (existing?.url) {
10602
- prUrl = existing.url;
10603
- prTitle = existing.title;
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
- const team = allocateTeamSlug(repoPath);
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
- async function listLinearIssues(agent, repoPath) {
10643
- const { getAdapter: getAdapter2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
10644
- await requireAgent(agent, { requireLinear: true });
10645
- const adapter = getAdapter2(agent);
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
- var import_node_fs31;
10652
- var init_create = __esm({
10653
- "src/threads/create.ts"() {
10654
- "use strict";
10655
- import_node_fs31 = require("fs");
10656
- init_detect();
10657
- init_worktree();
10658
- init_conductor();
10659
- init_app_settings();
10660
- init_thread_store();
10661
- init_workspaces2();
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
- // src/composer/summarize.ts
10666
- async function summarizeConversation(transcript, opts) {
10667
- const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
10668
-
10669
- [\u2026truncated\u2026]` : transcript;
10670
- const claudeSummary = await tryClaudeSummary(clipped, opts);
10671
- if (claudeSummary) {
10672
- return { summary: clipSummary(claudeSummary), method: "claude" };
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 tryClaudeSummary(transcript, opts) {
10677
- ensureAgentPath();
10678
- const prompt = [
10679
- "Summarize this coding-agent conversation for continuity in a fresh session.",
10680
- "Use concise markdown bullets covering:",
10681
- "- Goal / request",
10682
- "- Key decisions and constraints",
10683
- "- Files / areas touched",
10684
- "- Current state and open tasks",
10685
- "- Anything the next turn must remember",
10686
- "Do not invent details. No preamble.",
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
- const { stdout, exitCode } = await run(
10693
- "claude",
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 extractiveSummary(transcript) {
10715
- const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
10716
- const users = [];
10717
- const tools = [];
10718
- const agents = [];
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
- parts.push(
10753
- "",
10754
- "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
10755
- );
10756
- return parts.join("\n");
10677
+ return handle.done;
10757
10678
  }
10758
- function clipSummary(text4) {
10759
- if (text4.length <= MAX_SUMMARY_CHARS) return text4;
10760
- return `${text4.slice(0, MAX_SUMMARY_CHARS)}
10761
-
10762
- [\u2026summary truncated\u2026]`;
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
- var MAX_TRANSCRIPT_CHARS, MAX_SUMMARY_CHARS;
10765
- var init_summarize = __esm({
10766
- "src/composer/summarize.ts"() {
10767
- "use strict";
10768
- init_run();
10769
- init_path();
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
- // src/composer/context-compact.ts
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
- return n;
10725
+ if (!setup.ran) {
10726
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
10727
+ }
10728
+ return setup;
10782
10729
  }
10783
- function estimatePartChars(part) {
10784
- switch (part.type) {
10785
- case "text":
10786
- case "thinking":
10787
- return part.text.length;
10788
- case "tool": {
10789
- const input = part.input ? JSON.stringify(part.input) : "";
10790
- return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
10791
- }
10792
- default:
10793
- return 0;
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 estimateThreadChars(messages) {
10797
- return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
10762
+ function getRunMode(worktreePath, repoPath) {
10763
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
10798
10764
  }
10799
- function shouldCompactContext(messages, thresholds = {}) {
10800
- const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
10801
- const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
10802
- if (messages.length < minMessages) return false;
10803
- return estimateThreadChars(messages) >= maxChars;
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 splitForCompaction(messages, thresholds = {}) {
10806
- const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
10807
- const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
10808
- if (messages.length === 0) return { older: [], recent: [] };
10809
- let recentChars = 0;
10810
- let cut = messages.length;
10811
- for (let i = messages.length - 1; i >= 0; i--) {
10812
- const next = recentChars + estimateMessageChars(messages[i]);
10813
- const kept = messages.length - i;
10814
- if (kept >= keepCount && next > keepChars) break;
10815
- recentChars = next;
10816
- cut = i;
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
- const minRecent = Math.min(keepCount, messages.length);
10819
- cut = Math.min(cut, messages.length - minRecent);
10820
- if (cut <= 0) return { older: [], recent: messages };
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
- older: messages.slice(0, cut),
10823
- recent: messages.slice(cut)
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
- function formatToolPart(t, detail) {
10827
- if (detail === "summary") {
10828
- const label = t.description || t.detail || t.name;
10829
- const path = t.filePath ? ` (${t.filePath})` : "";
10830
- return `- ${t.name}: ${label}${path}`;
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
- const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
10833
- if (t.description) lines.push(`Description: ${t.description}`);
10834
- if (t.filePath) lines.push(`Path: ${t.filePath}`);
10835
- if (t.input && Object.keys(t.input).length > 0) {
10836
- lines.push("Input:");
10837
- lines.push("```json");
10838
- lines.push(JSON.stringify(t.input, null, 2));
10839
- lines.push("```");
10840
- } else if (t.detail) {
10841
- lines.push(`Detail: ${t.detail}`);
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
- if (t.result != null && t.result !== "") {
10844
- lines.push("Result:");
10845
- lines.push("```");
10846
- lines.push(t.result);
10847
- lines.push("```");
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 lines.join("\n");
10905
+ return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
10850
10906
  }
10851
- function formatMessagesAsTranscript(messages, opts) {
10852
- const tools = opts?.tools ?? "full";
10853
- const blocks = [];
10854
- for (const m of messages) {
10855
- if (m.role === "summary") {
10856
- blocks.push(`## Prior summary
10857
- ${m.text}`);
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 (m.role === "user") {
10861
- blocks.push(`### User
10862
- ${m.text}`);
10922
+ if (opts?.dryRun) {
10923
+ removed.push(orphan.path);
10863
10924
  continue;
10864
10925
  }
10865
- const bits = [];
10866
- if (m.text?.trim()) {
10867
- bits.push(`### Agent
10868
- ${m.text}`);
10869
- } else {
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
- return blocks.join("\n\n");
10890
- }
10891
- function buildSessionSeed(messages, opts) {
10892
- if (messages.length === 0) return null;
10893
- const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
10894
- if (!body.trim()) return null;
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 applyCompaction(messages, summaryText, thresholds = {}) {
10904
- const { older, recent } = splitForCompaction(messages, thresholds);
10905
- if (older.length === 0) return messages;
10906
- const summary = {
10907
- role: "summary",
10908
- text: summaryText.trim(),
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
- async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
10914
- if (!shouldCompactContext(thread.messages, thresholds)) {
10915
- return { didCompact: false, thread };
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
- const { older } = splitForCompaction(thread.messages, thresholds);
10918
- if (older.length === 0) {
10919
- return { didCompact: false, thread };
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 transcript = formatMessagesAsTranscript(older, { tools: "summary" });
10922
- const { summary, method } = await summarize(transcript, {
10923
- cwd: thread.worktreePath
10973
+ const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
10974
+ cwd: thread.repoPath,
10975
+ reject: false
10924
10976
  });
10925
- const messages = applyCompaction(thread.messages, summary, thresholds);
10926
- const next = {
10927
- ...thread,
10928
- messages,
10929
- sessionId: null,
10930
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
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
- didCompact: true,
10934
- thread: next,
10935
- summary,
10936
- method,
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 CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAGES, CONTEXT_MIN_MESSAGES;
10941
- var init_context_compact = __esm({
10942
- "src/composer/context-compact.ts"() {
11040
+ var import_execa5;
11041
+ var init_apply_into_main = __esm({
11042
+ "src/git/apply-into-main.ts"() {
10943
11043
  "use strict";
10944
- init_summarize();
10945
- CONTEXT_COMPACT_CHARS = 1e5;
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/threads/chat-tabs.ts
10953
- function sameWorktreePath(a, b) {
10954
- return normalizeWorktreePath(a) === normalizeWorktreePath(b);
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 takenTeamSlugsForChatTab(worktreePath) {
10957
- const key = normalizeWorktreePath(worktreePath);
10958
- const taken = /* @__PURE__ */ new Set();
10959
- const dir = worktreeNameFromPath(key);
10960
- if (dir) taken.add(dir.toLowerCase());
10961
- for (const sibling of threadsSharingWorktree(key)) {
10962
- for (const slug of takenSlugsFromThread(sibling)) {
10963
- taken.add(slug);
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 requireThread(idOrRef) {
10969
- const thread = findThreadByRef(idOrRef) ?? null;
10970
- if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
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 worktreeBindingFrom(from) {
10974
- return {
10975
- worktreePath: normalizeWorktreePath(from.worktreePath),
10976
- repoPath: from.repoPath,
10977
- branchName: from.branchName,
10978
- sourceRef: from.sourceRef,
10979
- // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
10980
- // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
10981
- sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
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 threadsSharingWorktree(worktreePath) {
10993
- if (!worktreePath) return [];
10994
- const key = normalizeWorktreePath(worktreePath);
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 formatTranscriptMarkdown(title, messages) {
11000
- const body = formatMessagesAsTranscript(messages, { tools: "full" });
11001
- return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
11092
+ function rememberRemoved(repoPath) {
11093
+ const next = readRemoved();
11094
+ next.add(repoPath);
11095
+ writeRemoved(next);
11002
11096
  }
11003
- function forkMessageSlice(from, throughIndex) {
11004
- const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
11005
- return through >= 0 ? from.messages.slice(0, through + 1) : [];
11097
+ function forgetRemoved(repoPath) {
11098
+ const next = readRemoved();
11099
+ if (!next.delete(repoPath)) return;
11100
+ writeRemoved(next);
11006
11101
  }
11007
- function buildForkTranscriptAttachment(baseTitle, messages) {
11008
- const title = baseTitle || "Chat";
11009
- return {
11010
- id: (0, import_node_crypto4.randomUUID)(),
11011
- name: `Transcript of ${title}.md`,
11012
- kind: "transcript",
11013
- content: formatTranscriptMarkdown(title, messages)
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 createChatTab(input) {
11017
- const from = requireThread(input.fromThreadId);
11018
- const binding = worktreeBindingFrom(from);
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 forkChatTab(input) {
11050
- const from = requireThread(input.threadId);
11051
- const slice = forkMessageSlice(from, input.throughIndex);
11052
- const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
11053
- const tab = createChatTab({
11054
- fromThreadId: input.threadId,
11055
- agent: input.agent ?? from.agent,
11056
- model: input.model,
11057
- title: input.title?.trim() || void 0,
11058
- attachments: [attachment]
11059
- });
11060
- if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
11061
- const next = { ...tab, parentThreadId: from.id };
11062
- writeThread(next);
11063
- return next;
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
- return tab;
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 import_node_crypto4;
11068
- var init_chat_tabs = __esm({
11069
- "src/threads/chat-tabs.ts"() {
11156
+ var import_node_fs29, import_node_path28;
11157
+ var init_workspaces2 = __esm({
11158
+ "src/store/workspaces.ts"() {
11070
11159
  "use strict";
11071
- import_node_crypto4 = require("crypto");
11072
- init_context_compact();
11073
- init_teams();
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
- init_orchestrator_capable();
11077
- init_thread_store();
11078
- init_worktree_labels();
11164
+ init_worktree();
11079
11165
  }
11080
11166
  });
11081
11167
 
11082
- // src/store/turn-live.ts
11083
- function trimParts(parts) {
11084
- if (parts.length <= MAX_PARTS) return parts;
11085
- return parts.slice(-Math.floor(MAX_PARTS / 2));
11086
- }
11087
- function lastText(parts, type) {
11088
- for (let i = parts.length - 1; i >= 0; i--) {
11089
- const p = parts[i];
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
- writeTurnLive(threadId, summarizeTurnLive(buf.parts));
11130
- }
11131
- function scheduleFlush(threadId) {
11132
- const buf = buffers.get(threadId);
11133
- if (!buf) return;
11134
- if (buf.timer) return;
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 prev = buffers.get(threadId) ?? { parts: [], timer: null };
11146
- prev.parts = trimParts(applyAgentEvent(prev.parts, event));
11147
- buffers.set(threadId, prev);
11148
- if (event.type === "exit") {
11149
- flush(threadId);
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 immediate = event.type === "tool_use" || event.type === "tool_result";
11156
- if (immediate) flush(threadId);
11157
- else scheduleFlush(threadId);
11190
+ const repoPath = await resolveRepoRoot(dest);
11191
+ const workspace = await ensureWorkspace(repoPath);
11192
+ return { repoPath, workspace };
11158
11193
  }
11159
- function writeTurnLive(threadId, progress) {
11160
- const path = threadLivePath(threadId);
11161
- const tmp = `${path}.${process.pid}.tmp`;
11162
- try {
11163
- (0, import_node_fs32.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
11164
- (0, import_node_fs32.renameSync)(tmp, path);
11165
- } catch {
11166
- try {
11167
- (0, import_node_fs32.unlinkSync)(tmp);
11168
- } catch {
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 readTurnLive(threadId) {
11173
- const path = threadLivePath(threadId);
11174
- if (!(0, import_node_fs32.existsSync)(path)) return null;
11175
- try {
11176
- const raw = JSON.parse((0, import_node_fs32.readFileSync)(path, "utf8"));
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 clearTurnLive(threadId) {
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
- (0, import_node_fs32.unlinkSync)(path);
11222
+ process.kill(pid, 0);
11223
+ return true;
11191
11224
  } catch {
11225
+ return false;
11192
11226
  }
11193
11227
  }
11194
- var import_node_fs32, buffers, FLUSH_MS, MAX_PARTS;
11195
- var init_turn_live = __esm({
11196
- "src/store/turn-live.ts"() {
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
- import_node_fs32 = require("fs");
11199
- init_message_parts();
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/review/review-request-template.ts
11208
- var REVIEW_REQUEST_TEMPLATE;
11209
- var init_review_request_template = __esm({
11210
- "src/review/review-request-template.ts"() {
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
- REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
11213
-
11214
- 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.
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
- If a blocking issue is a missing or ambiguous repo rule that will recur, say so and propose one sentence for \`.sideboard/review.md\` or a \`.claude/skills/<name>/SKILL.md\`. Do not only patch this diff when the same miss will happen again. New skills go under \`.claude/skills\` (Claude Code / attach) \u2014 not \`.sideboard/skills\`.
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/review/request-review.ts
11337
- function shouldRefreshReviewRequestTemplate(content) {
11338
- const trimmed = content.trim();
11339
- if (!trimmed) return true;
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 readTextIfPresent(abs) {
11346
- if (!(0, import_node_fs33.existsSync)(abs)) return null;
11347
- try {
11348
- const content = (0, import_node_fs33.readFileSync)(abs, "utf8");
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 ensureAttachmentsGitignore(worktreePath) {
11355
- const gitignoreAbs = (0, import_node_path30.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
11356
- if ((0, import_node_fs33.existsSync)(gitignoreAbs)) return;
11357
- (0, import_node_fs33.mkdirSync)((0, import_node_path30.dirname)(gitignoreAbs), { recursive: true });
11358
- (0, import_node_fs33.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
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 resolveReviewGuidelines(worktreePath) {
11361
- const repoAbs = (0, import_node_path30.join)(worktreePath, REPO_REVIEW_PATH);
11362
- const repoContent = readTextIfPresent(repoAbs);
11363
- if (repoContent) {
11364
- return {
11365
- path: REPO_REVIEW_PATH,
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
- const localAbs = (0, import_node_path30.join)(worktreePath, REVIEW_REQUEST_PATH);
11372
- const localContent = readTextIfPresent(localAbs);
11373
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
11374
- return {
11375
- path: REVIEW_REQUEST_PATH,
11376
- name: REVIEW_REQUEST_NAME,
11377
- content: localContent,
11378
- source: "local"
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 legacyAbs = (0, import_node_path30.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
11382
- const legacyContent = readTextIfPresent(legacyAbs);
11383
- if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
11384
- return {
11385
- path: LEGACY_REVIEW_REQUEST_PATH,
11386
- name: REVIEW_REQUEST_NAME,
11387
- content: legacyContent,
11388
- source: "local"
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
- ensureAttachmentsGitignore(worktreePath);
11392
- (0, import_node_fs33.mkdirSync)((0, import_node_path30.dirname)(localAbs), { recursive: true });
11393
- (0, import_node_fs33.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
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 buildReviewRequestAttachment(content, opts) {
11402
- const path = opts?.path ?? REVIEW_REQUEST_PATH;
11403
- const name = opts?.name ?? (path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
11404
- return {
11405
- id: (0, import_node_crypto5.randomUUID)(),
11406
- name,
11407
- kind: "file",
11408
- path,
11409
- content
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
- async function requestReview(threadRef, send) {
11413
- const from = findThreadByRef(threadRef);
11414
- if (!from) throw new Error(`Thread not found: ${threadRef}`);
11415
- if (isOrchestratorThread(from)) {
11416
- throw new Error(
11417
- "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
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
- if (from.status === "archived") {
11421
- throw new Error(`Thread is archived: ${from.id}`);
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 import_node_crypto5, import_node_fs33, import_node_path30, REPO_REVIEW_PATH, REPO_REVIEW_NAME, REVIEW_REQUEST_PATH, LEGACY_REVIEW_REQUEST_PATH, REVIEW_REQUEST_NAME, REVIEW_REQUEST_PREFILL, LEGACY_REVIEW_TEMPLATE_MARKERS;
11438
- var init_request_review = __esm({
11439
- "src/review/request-review.ts"() {
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
- import_node_path30 = require("path");
11444
- init_global_workspace();
11445
- init_chat_tabs();
11446
- init_thread_store();
11447
- init_review_request_template();
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
- "- Do not write new skills under `.sideboard/skills` \u2014 native agents do not scan it. Point Codex/OpenCode at the file from `AGENTS.md`. Optional: symlink `.cursor/skills/<name>` to the Claude skill.",
13599
- "- Skip a guide for a one-off. If a matching skill exists, follow it. Same miss twice \u2192 edit the skill (or `.sideboard/review.md`), do not patch around it."
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 ensureAttachmentsGitignore2(worktreePath) {
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
- ensureAttachmentsGitignore2(worktreePath);
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: null
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
- server.tool(
17605
- "list_workspaces",
17606
- "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.",
17607
- {},
17608
- async () => {
17609
- const workspaces = orch.listWorkspaces();
17610
- const lines = await Promise.all(
17611
- workspaces.map(async (w) => {
17612
- const slug = await resolveGithubRepoSlug(w.path).catch(() => null);
17613
- return slug ? `${w.name} ${w.path} github:${slug}` : `${w.name} ${w.path}`;
17614
- })
17615
- );
17616
- return {
17617
- content: [
17618
- { type: "text", text: lines.join("\n") || "(no workspaces)" }
17619
- ]
17620
- };
17621
- }
17622
- );
17623
- server.tool(
17624
- "list_threads",
17625
- "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.",
17626
- {},
17627
- async () => {
17628
- const threads = orch.getThreads(true);
17629
- const lines = threads.map((t) => {
17630
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path40.basename)(t.repoPath) || t.repoPath;
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 progress = live?.summary ? ` ${live.summary}` : "";
17633
- 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}`;
17634
- });
17635
- return {
17636
- content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
17637
- };
17638
- }
17639
- );
17640
- server.tool(
17641
- "get_thread",
17642
- "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
17643
- { ref: import_zod4.z.string() },
17644
- async ({ ref }) => {
17645
- const t = orch.getThread(ref);
17646
- if (!t) {
17647
- return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
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
- const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
17650
- const summary = {
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
- registerSlackTools(server);
17801
- registerLinearTools(server);
17802
- if (sideboardMcpProfile() !== "worktree") {
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 .sideboard/review.md when present (else local Review request.md / 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.',
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 {