@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
package/dist/index.cjs CHANGED
@@ -555,7 +555,7 @@ function isElectronLikeCommand(command) {
555
555
  const name = command.trim();
556
556
  if (!name) return false;
557
557
  if (process.versions.electron && name === process.execPath) return true;
558
- return /(?:^|[/\\])Electron(?:\.exe)?$/i.test(name) || /Sideboard\.app[/\\]/i.test(name) || /Electron\.app[/\\]/i.test(name);
558
+ return /(?:^|[/\\])Electron(?:\.exe)?$/i.test(name) || /(?:^|[/\\])Sideboard(?:\.exe)?$/i.test(name) || /(?:Sideboard|Electron)\.app[/\\]Contents[/\\]MacOS[/\\]/i.test(name);
559
559
  }
560
560
  function unwrapStrippedElectronLaunch(command, args) {
561
561
  if (!isStrippedElectronLaunch(command, args) || !args || args.length < 4) {
@@ -5496,19 +5496,11 @@ function coordinatorTurnReminder(opts) {
5496
5496
  const goal = opts.goal?.trim();
5497
5497
  return [
5498
5498
  "Sideboard Orchestration (mandatory):",
5499
- "- You oversee Sideboard worktree agents. You are not yourself checked out in a project worktree.",
5500
- "- This cwd is a synthetic empty home (not a git repo). Emptiness here is expected \u2014 it is not a problem to fix.",
5501
- "- Registered workspaces / child threads are the fleet you manage via Sideboard MCP.",
5502
- `- YOUR orchestration thread id is ${opts.parentId}`,
5503
- `- Always pass parentThreadId="${opts.parentId}" on create_thread (never invent or reuse another uuid).`,
5504
- `- Or omit parentThreadId \u2014 Sideboard MCP already binds children to ${opts.parentId}.`,
5499
+ "- You oversee worktree agents from a synthetic empty cwd (not a git repo). Follow AGENTS.md / CLAUDE.md.",
5500
+ `- YOUR orchestration thread id is ${opts.parentId} \u2014 pass parentThreadId="${opts.parentId}" on create_thread, or omit it.`,
5505
5501
  goal ? `- Goal / title: ${goal}` : null,
5506
5502
  accountDefaultsPlaybookLine(),
5507
- `- 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.`,
5508
- "- 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.",
5509
- "- 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.",
5510
- "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
5511
- "- 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."
5503
+ "- Status: list_threads. Link chats as `[Title](sideboard://thread/<id>)`. Merge only if the user asked."
5512
5504
  ].filter(Boolean).join("\n");
5513
5505
  }
5514
5506
  function ensureGlobalCoordinatorCwd(opts) {
@@ -5637,14 +5629,14 @@ var init_coordinator_prompt = __esm({
5637
5629
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
5638
5630
  "Inspect / review / PRs:",
5639
5631
  "- get_diff \u2014 compact diff summary",
5640
- '- 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',
5632
+ '- 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',
5641
5633
  "- 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.",
5642
5634
  '- 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.',
5643
5635
  '- 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.',
5644
5636
  "Process guides:",
5645
5637
  "- 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.",
5646
- "- 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.",
5647
- "- 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.",
5638
+ "- 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.",
5639
+ "- 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.",
5648
5640
  "Human-only (do not attempt): ready-for-review land (confirm_land), purge_thread.",
5649
5641
  "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.",
5650
5642
  "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."
@@ -6582,6 +6574,8 @@ function summarizeTurnStderr(tail, maxChars = 500) {
6582
6574
  if (tail.length === 0) return "";
6583
6575
  const cursorStartup = [...tail].reverse().find((line) => /cursor startup failed:/i.test(line));
6584
6576
  if (cursorStartup) return clipStderr(cursorStartup, maxChars);
6577
+ const cursorRunFailed = [...tail].reverse().find((line) => /^cursor run failed\b/i.test(line.trim()));
6578
+ if (cursorRunFailed) return clipStderr(cursorRunFailed, maxChars);
6585
6579
  if (tail.some(looksLikeNestedElectronCrash)) return NESTED_ELECTRON_SUMMARY;
6586
6580
  if (tail.some(looksLikeHomebrewLibuvCrash)) return HOMEBREW_LIBUV_SUMMARY;
6587
6581
  if (tail.some(
@@ -6612,7 +6606,7 @@ function looksLikeRetryableRunnerCrash(text4) {
6612
6606
  const lower = text4.trim().toLowerCase();
6613
6607
  if (/cannot find (?:package|module)|err_module_not_found/.test(lower)) return false;
6614
6608
  if (!lower) return true;
6615
- 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(
6609
+ 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(
6616
6610
  lower
6617
6611
  );
6618
6612
  }
@@ -6670,6 +6664,9 @@ function humanizeAgentFailDetail(detail) {
6670
6664
  if (/corrupt local agent checkpoint|missing root blob|truncated crash dump/.test(lower)) {
6671
6665
  return `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
6672
6666
  }
6667
+ if (/connection stalled/.test(lower)) {
6668
+ return /retry the turn/i.test(raw) ? raw : `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
6669
+ }
6673
6670
  return raw;
6674
6671
  }
6675
6672
  function turnFailChatText(opts) {
@@ -7538,11 +7535,18 @@ var init_claude_mcp = __esm({
7538
7535
  function sideboardMcpProfile(env = process.env) {
7539
7536
  return env[SIDEBOARD_MCP_PROFILE_ENV]?.trim().toLowerCase() === "worktree" ? "worktree" : "orchestration";
7540
7537
  }
7541
- var SIDEBOARD_MCP_PROFILE_ENV;
7538
+ var SIDEBOARD_MCP_PROFILE_ENV, WORKTREE_MCP_TOOLS;
7542
7539
  var init_profile = __esm({
7543
7540
  "src/mcp/profile.ts"() {
7544
7541
  "use strict";
7545
7542
  SIDEBOARD_MCP_PROFILE_ENV = "SIDEBOARD_MCP_PROFILE";
7543
+ WORKTREE_MCP_TOOLS = [
7544
+ "present_artifact",
7545
+ "ask_user",
7546
+ "present_plan",
7547
+ "present_schema",
7548
+ "present_files"
7549
+ ];
7546
7550
  }
7547
7551
  });
7548
7552
 
@@ -8063,14 +8067,7 @@ var init_injected_mcp = __esm({
8063
8067
  "mcp__sideboard__present_schema",
8064
8068
  "mcp__sideboard__present_files",
8065
8069
  "mcp__sideboard__ask_user",
8066
- "mcp__sideboard__present_plan",
8067
- "mcp__sideboard__list_teams",
8068
- "mcp__sideboard__slack_list_channels",
8069
- "mcp__sideboard__slack_list_users",
8070
- "mcp__sideboard__slack_search",
8071
- "mcp__sideboard__slack_read",
8072
- "mcp__sideboard__slack_post",
8073
- "mcp__sideboard__slack_replies"
8070
+ "mcp__sideboard__present_plan"
8074
8071
  ];
8075
8072
  BRIGHTSY_MCP_ALLOWED_TOOLS = [
8076
8073
  "mcp__brightsy",
@@ -10358,6 +10355,14 @@ var init_cursor_stream_coalesce = __esm({
10358
10355
  });
10359
10356
 
10360
10357
  // src/agents/spawn.ts
10358
+ function applyPromptCacheTtlEnv(agent, env) {
10359
+ if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
10360
+ env.ENABLE_PROMPT_CACHING_1H ??= "1";
10361
+ }
10362
+ if (agent === "opencode" && env.OPENCODE_ANTHROPIC_FORCE_PROMPT_CACHING_5M !== "1") {
10363
+ env.OPENCODE_ANTHROPIC_PROMPT_CACHING_1H ??= "1";
10364
+ }
10365
+ }
10361
10366
  async function spawnAgentTurn(thread, input, onEvent) {
10362
10367
  ensureAgentPath();
10363
10368
  if (!thread.worktreePath?.trim()) {
@@ -10383,6 +10388,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
10383
10388
  );
10384
10389
  }
10385
10390
  const env = childEnvWithAppSettings(cmd.env);
10391
+ applyPromptCacheTtlEnv(thread.agent, env);
10386
10392
  try {
10387
10393
  if (isOrchestratorThread(thread)) {
10388
10394
  mergeAgentGitAuthEnv(env, await resolveAgentGitAuthEnv(env));
@@ -10628,1707 +10634,1722 @@ var init_pr_merge_archive = __esm({
10628
10634
  }
10629
10635
  });
10630
10636
 
10631
- // src/hook/conductor.ts
10632
- function matchSimpleGlob(pattern, name) {
10633
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
10634
- return new RegExp(`^${escaped}$`).test(name);
10635
- }
10636
- function readWorktreeInclude(repoPath) {
10637
- const path2 = (0, import_node_path28.join)(repoPath, ".worktreeinclude");
10638
- if (!(0, import_node_fs29.existsSync)(path2)) return [];
10639
- return (0, import_node_fs29.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
10640
- }
10641
- function resolveFilesToCopy(repoPath) {
10642
- const fromInclude = readWorktreeInclude(repoPath);
10643
- if (fromInclude.length) return fromInclude;
10644
- const settings = loadRepoSettings(repoPath);
10645
- if (settings?.filesToCopy?.length) return settings.filesToCopy;
10646
- if (settings?.fileIncludeGlobs?.length) {
10647
- const matched = [];
10648
- try {
10649
- for (const entry of (0, import_node_fs29.readdirSync)(repoPath, { withFileTypes: true })) {
10650
- if (!entry.isFile()) continue;
10651
- for (const glob of settings.fileIncludeGlobs) {
10652
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path28.basename)(glob), entry.name)) {
10653
- matched.push(entry.name);
10654
- break;
10655
- }
10656
- }
10657
- }
10658
- } catch {
10659
- }
10660
- if (matched.length) return [...new Set(matched)];
10661
- }
10662
- const defaults = [];
10663
- try {
10664
- for (const entry of (0, import_node_fs29.readdirSync)(repoPath, { withFileTypes: true })) {
10665
- if (entry.isFile() && entry.name.startsWith(".env")) {
10666
- defaults.push(entry.name);
10667
- }
10668
- }
10669
- } catch {
10670
- }
10671
- if (defaults.length) return defaults;
10672
- return [".env.local", ".env"];
10673
- }
10674
- function copyConfiguredFiles(repoPath, worktreePath) {
10675
- const patterns = resolveFilesToCopy(repoPath);
10676
- const copied = [];
10677
- for (const rel of patterns) {
10678
- const src = (0, import_node_path28.join)(repoPath, rel);
10679
- if (!(0, import_node_fs29.existsSync)(src)) continue;
10680
- const dest = (0, import_node_path28.join)(worktreePath, rel);
10681
- (0, import_node_fs29.mkdirSync)((0, import_node_path28.dirname)(dest), { recursive: true });
10682
- (0, import_node_fs29.copyFileSync)(src, dest);
10683
- copied.push(rel);
10637
+ // src/composer/summarize.ts
10638
+ async function summarizeConversation(transcript, opts) {
10639
+ const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
10640
+
10641
+ [\u2026truncated\u2026]` : transcript;
10642
+ const claudeSummary = await tryClaudeSummary(clipped, opts);
10643
+ if (claudeSummary) {
10644
+ return { summary: clipSummary(claudeSummary), method: "claude" };
10684
10645
  }
10685
- return copied;
10646
+ return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
10686
10647
  }
10687
- async function captureLoginEnv() {
10688
- if (cachedLoginEnv) return { ...cachedLoginEnv };
10689
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
10648
+ async function tryClaudeSummary(transcript, opts) {
10649
+ ensureAgentPath();
10650
+ const prompt = [
10651
+ "Summarize this coding-agent conversation for continuity in a fresh session.",
10652
+ "Use concise markdown bullets covering:",
10653
+ "- Goal / request",
10654
+ "- Key decisions and constraints",
10655
+ "- Files / areas touched",
10656
+ "- Current state and open tasks",
10657
+ "- Anything the next turn must remember",
10658
+ "Do not invent details. No preamble.",
10659
+ "",
10660
+ "Transcript:",
10661
+ transcript
10662
+ ].join("\n");
10690
10663
  try {
10691
- const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
10692
- reject: false,
10693
- timeout: 5e3
10694
- });
10695
- if (result.exitCode === 0 && result.stdout) {
10696
- const env = { ...process.env };
10697
- for (const entry of result.stdout.split("\0")) {
10698
- if (!entry) continue;
10699
- const eq = entry.indexOf("=");
10700
- if (eq <= 0) continue;
10701
- const key = entry.slice(0, eq);
10702
- const value = entry.slice(eq + 1);
10703
- if (key) env[key] = value;
10704
- }
10705
- cachedLoginEnv = env;
10706
- return { ...env };
10707
- }
10664
+ const { stdout, exitCode } = await run(
10665
+ "claude",
10666
+ [
10667
+ "-p",
10668
+ prompt,
10669
+ "--output-format",
10670
+ "text",
10671
+ "--permission-mode",
10672
+ "bypassPermissions",
10673
+ "--model",
10674
+ "haiku"
10675
+ ],
10676
+ { cwd: opts?.cwd, reject: false }
10677
+ );
10678
+ if (exitCode !== 0) return null;
10679
+ const text4 = stdout.trim();
10680
+ if (text4.length < 40) return null;
10681
+ return text4;
10708
10682
  } catch {
10683
+ return null;
10709
10684
  }
10710
- cachedLoginEnv = { ...process.env };
10711
- return { ...cachedLoginEnv };
10712
10685
  }
10713
- function buildWorkspaceScriptEnv(opts, baseEnv) {
10714
- const env = stripNestedElectronEnv({
10715
- ...baseEnv ?? process.env
10716
- });
10717
- const name = opts.workspaceName ?? (0, import_node_path28.basename)(opts.worktreePath);
10718
- const ports = opts.ports ?? [];
10719
- const primary = ports[0];
10720
- env.SIDEBOARD_WORKSPACE_NAME = name;
10721
- env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
10722
- env.SIDEBOARD_ROOT_PATH = opts.repoPath;
10723
- env.SIDEBOARD_IS_LOCAL = "1";
10724
- if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
10725
- env.CONDUCTOR_WORKSPACE_NAME = name;
10726
- env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
10727
- env.CONDUCTOR_ROOT_PATH = opts.repoPath;
10728
- env.CONDUCTOR_IS_LOCAL = "1";
10729
- if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
10730
- if (primary != null) {
10731
- env.SIDEBOARD_PORT = String(primary);
10732
- env.CONDUCTOR_PORT = String(primary);
10733
- env.PORT = String(primary);
10734
- for (let i = 1; i < ports.length; i++) {
10735
- const p = ports[i];
10736
- env[`SIDEBOARD_PORT_${i}`] = String(p);
10737
- env[`CONDUCTOR_PORT_${i}`] = String(p);
10686
+ function extractiveSummary(transcript) {
10687
+ const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
10688
+ const users = [];
10689
+ const tools = [];
10690
+ const agents = [];
10691
+ let mode = "none";
10692
+ for (const line of lines) {
10693
+ if (line.startsWith("### User")) {
10694
+ mode = "user";
10695
+ continue;
10696
+ }
10697
+ if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
10698
+ mode = "agent";
10699
+ continue;
10700
+ }
10701
+ if (line.startsWith("- ") && line.includes(":")) {
10702
+ tools.push(line.replace(/^- /, ""));
10703
+ continue;
10704
+ }
10705
+ if (mode === "user" && users.length < 8) {
10706
+ users.push(line.slice(0, 240));
10707
+ } else if (mode === "agent" && agents.length < 6) {
10708
+ agents.push(line.slice(0, 240));
10738
10709
  }
10739
10710
  }
10740
- return env;
10711
+ const uniqueTools = [...new Set(tools)].slice(0, 20);
10712
+ const parts = [
10713
+ "## Compacted context (extractive)",
10714
+ "",
10715
+ "### User goals",
10716
+ ...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
10717
+ "",
10718
+ "### Recent agent notes",
10719
+ ...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
10720
+ ];
10721
+ if (uniqueTools.length) {
10722
+ parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
10723
+ }
10724
+ parts.push(
10725
+ "",
10726
+ "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
10727
+ );
10728
+ return parts.join("\n");
10741
10729
  }
10742
- function pipeLines(stream, onLine) {
10743
- if (!stream || !onLine) return;
10744
- const rl = (0, import_node_readline3.createInterface)({ input: stream });
10745
- rl.on("line", onLine);
10730
+ function clipSummary(text4) {
10731
+ if (text4.length <= MAX_SUMMARY_CHARS) return text4;
10732
+ return `${text4.slice(0, MAX_SUMMARY_CHARS)}
10733
+
10734
+ [\u2026summary truncated\u2026]`;
10746
10735
  }
10747
- function killScriptTree(child, ports = []) {
10748
- const pid = child.pid;
10749
- if (pid) {
10750
- try {
10751
- if (process.platform === "win32") {
10752
- void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
10753
- } else {
10754
- process.kill(-pid, "SIGTERM");
10755
- setTimeout(() => {
10756
- try {
10757
- process.kill(-pid, "SIGKILL");
10758
- } catch {
10759
- }
10760
- }, 2500).unref?.();
10761
- }
10762
- } catch {
10763
- try {
10764
- child.kill("SIGTERM");
10765
- } catch {
10766
- }
10767
- }
10736
+ var MAX_TRANSCRIPT_CHARS, MAX_SUMMARY_CHARS;
10737
+ var init_summarize = __esm({
10738
+ "src/composer/summarize.ts"() {
10739
+ "use strict";
10740
+ init_run();
10741
+ init_path();
10742
+ MAX_TRANSCRIPT_CHARS = 12e4;
10743
+ MAX_SUMMARY_CHARS = 6e3;
10768
10744
  }
10769
- for (const port of ports) {
10770
- if (!Number.isFinite(port) || port <= 0) continue;
10771
- if (process.platform === "win32") {
10772
- void (0, import_execa4.execa)(
10773
- "powershell",
10774
- [
10775
- "-NoProfile",
10776
- "-Command",
10777
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
10778
- ],
10779
- { reject: false }
10780
- );
10781
- } else {
10782
- void (0, import_execa4.execa)(
10783
- "zsh",
10784
- ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
10785
- { reject: false }
10786
- );
10745
+ });
10746
+
10747
+ // src/composer/context-compact.ts
10748
+ function estimateMessageChars(message) {
10749
+ let n = message.text.length + 16;
10750
+ for (const part of message.parts ?? []) {
10751
+ n += estimatePartChars(part);
10752
+ }
10753
+ return n;
10754
+ }
10755
+ function estimatePartChars(part) {
10756
+ switch (part.type) {
10757
+ case "text":
10758
+ case "thinking":
10759
+ return part.text.length;
10760
+ case "tool": {
10761
+ const input = part.input ? JSON.stringify(part.input) : "";
10762
+ return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
10787
10763
  }
10764
+ default:
10765
+ return 0;
10788
10766
  }
10789
10767
  }
10790
- async function spawnWorkspaceScript(command, opts) {
10791
- const loginEnv = await captureLoginEnv();
10792
- const env = buildWorkspaceScriptEnv(
10793
- {
10794
- worktreePath: opts.worktreePath,
10795
- repoPath: opts.repoPath,
10796
- workspaceName: opts.workspaceName,
10797
- defaultBranch: opts.defaultBranch,
10798
- ports: opts.ports
10799
- },
10800
- loginEnv
10801
- );
10802
- try {
10803
- mergeAgentGitAuthEnv(
10804
- env,
10805
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
10806
- );
10807
- } catch {
10808
- }
10809
- const shell = process.platform === "darwin" ? "zsh" : "bash";
10810
- const child = (0, import_execa4.execa)(shell, ["-lc", command], {
10811
- cwd: opts.worktreePath,
10812
- reject: false,
10813
- env,
10814
- // Own process group so Stop can tear down the whole tree (not just the shell).
10815
- // There is no settings.toml `stop=` / teardown hook for run scripts.
10816
- ...process.platform === "win32" ? {} : { detached: true }
10817
- });
10818
- pipeLines(child.stdout, opts.onLine);
10819
- pipeLines(child.stderr, opts.onLine);
10820
- const ports = opts.ports ?? [];
10821
- return {
10822
- pid: child.pid,
10823
- kill: () => killScriptTree(child, ports),
10824
- done: child.then((r) => r.exitCode ?? null),
10825
- child
10826
- };
10827
- }
10828
- async function attachAbort(handle, signal) {
10829
- if (signal) {
10830
- const onAbort = () => handle.kill();
10831
- if (signal.aborted) onAbort();
10832
- else signal.addEventListener("abort", onAbort, { once: true });
10833
- }
10834
- return handle.done;
10768
+ function estimateThreadChars(messages) {
10769
+ return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
10835
10770
  }
10836
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
10837
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
10838
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
10839
- const handle = await spawnWorkspaceScript(settings.setup, {
10840
- worktreePath,
10841
- repoPath,
10842
- defaultBranch: opts?.defaultBranch,
10843
- onLine
10844
- });
10845
- const exitCode = await attachAbort(handle, opts?.signal);
10846
- return {
10847
- ran: true,
10848
- exitCode,
10849
- source: workspaceSettingsSourceLabel(worktreePath, repoPath),
10850
- kill: handle.kill
10851
- };
10771
+ function shouldCompactContext(messages, thresholds = {}) {
10772
+ const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
10773
+ const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
10774
+ if (messages.length < minMessages) return false;
10775
+ return estimateThreadChars(messages) >= maxChars;
10852
10776
  }
10853
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
10854
- const found = findConventionSetup(worktreePath, repoPath);
10855
- if (!found) return { ran: false, exitCode: null, source: null };
10856
- onLine?.(`[setup] ${found.source}`);
10857
- const handle = await spawnWorkspaceScript(found.command, {
10858
- worktreePath,
10859
- repoPath,
10860
- defaultBranch: opts?.defaultBranch,
10861
- onLine
10862
- });
10863
- const exitCode = await attachAbort(handle, opts?.signal);
10777
+ function splitForCompaction(messages, thresholds = {}) {
10778
+ const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
10779
+ const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
10780
+ if (messages.length === 0) return { older: [], recent: [] };
10781
+ let recentChars = 0;
10782
+ let cut = messages.length;
10783
+ for (let i = messages.length - 1; i >= 0; i--) {
10784
+ const next = recentChars + estimateMessageChars(messages[i]);
10785
+ const kept = messages.length - i;
10786
+ if (kept >= keepCount && next > keepChars) break;
10787
+ recentChars = next;
10788
+ cut = i;
10789
+ }
10790
+ const minRecent = Math.min(keepCount, messages.length);
10791
+ cut = Math.min(cut, messages.length - minRecent);
10792
+ if (cut <= 0) return { older: [], recent: messages };
10864
10793
  return {
10865
- ran: true,
10866
- exitCode,
10867
- source: found.source,
10868
- kill: handle.kill
10794
+ older: messages.slice(0, cut),
10795
+ recent: messages.slice(cut)
10869
10796
  };
10870
10797
  }
10871
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
10872
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
10873
- if (!setup.ran) {
10874
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
10798
+ function formatToolPart(t, detail) {
10799
+ if (detail === "summary") {
10800
+ const label = t.description || t.detail || t.name;
10801
+ const path2 = t.filePath ? ` (${t.filePath})` : "";
10802
+ return `- ${t.name}: ${label}${path2}`;
10875
10803
  }
10876
- if (!setup.ran) {
10877
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
10804
+ const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
10805
+ if (t.description) lines.push(`Description: ${t.description}`);
10806
+ if (t.filePath) lines.push(`Path: ${t.filePath}`);
10807
+ if (t.input && Object.keys(t.input).length > 0) {
10808
+ lines.push("Input:");
10809
+ lines.push("```json");
10810
+ lines.push(JSON.stringify(t.input, null, 2));
10811
+ lines.push("```");
10812
+ } else if (t.detail) {
10813
+ lines.push(`Detail: ${t.detail}`);
10878
10814
  }
10879
- return setup;
10815
+ if (t.result != null && t.result !== "") {
10816
+ lines.push("Result:");
10817
+ lines.push("```");
10818
+ lines.push(t.result);
10819
+ lines.push("```");
10820
+ }
10821
+ return lines.join("\n");
10880
10822
  }
10881
- async function runArchiveScript(repoPath, worktreePath, onLine) {
10882
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
10883
- if (!settings?.archive) return { ran: false, exitCode: null };
10884
- const handle = await spawnWorkspaceScript(settings.archive, {
10885
- worktreePath,
10886
- repoPath,
10887
- onLine
10888
- });
10889
- const exitCode = await handle.done;
10890
- return { ran: true, exitCode };
10823
+ function formatMessagesAsTranscript(messages, opts) {
10824
+ const tools = opts?.tools ?? "full";
10825
+ const blocks = [];
10826
+ for (const m of messages) {
10827
+ if (m.role === "summary") {
10828
+ blocks.push(`## Prior summary
10829
+ ${m.text}`);
10830
+ continue;
10831
+ }
10832
+ if (m.role === "user") {
10833
+ blocks.push(`### User
10834
+ ${m.text}`);
10835
+ continue;
10836
+ }
10837
+ const bits = [];
10838
+ if (m.text?.trim()) {
10839
+ bits.push(`### Agent
10840
+ ${m.text}`);
10841
+ } else {
10842
+ bits.push("### Agent");
10843
+ }
10844
+ const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
10845
+ for (const th of thinking) {
10846
+ if (th.type !== "thinking" || !th.text.trim()) continue;
10847
+ bits.push("Thinking:");
10848
+ bits.push(th.text);
10849
+ }
10850
+ const toolParts = (m.parts ?? []).filter(
10851
+ (p) => p.type === "tool"
10852
+ );
10853
+ if (tools !== "none" && toolParts.length > 0) {
10854
+ if (tools === "summary") bits.push("Tools:");
10855
+ for (const t of toolParts) {
10856
+ bits.push(formatToolPart(t, tools));
10857
+ }
10858
+ }
10859
+ blocks.push(bits.join("\n"));
10860
+ }
10861
+ return blocks.join("\n\n");
10891
10862
  }
10892
- function listRunScripts(worktreePath, repoPath) {
10893
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
10894
- if (!settings?.runScripts.length) return [];
10895
- return settings.runScripts.filter((s) => {
10896
- if (!s.availableIn?.length) return true;
10897
- return s.availableIn.includes("local");
10898
- });
10863
+ function buildSessionSeed(messages, opts) {
10864
+ if (messages.length === 0) return null;
10865
+ const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
10866
+ if (!body.trim()) return null;
10867
+ return [
10868
+ "Sideboard conversation context (restored after compaction or a new session):",
10869
+ "",
10870
+ body,
10871
+ "",
10872
+ "Continue from this context. Do not repeat the summary unless asked."
10873
+ ].join("\n");
10899
10874
  }
10900
- function getDefaultRunScript(worktreePath, repoPath) {
10901
- const scripts = listRunScripts(worktreePath, repoPath);
10902
- if (!scripts.length) return null;
10903
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
10875
+ function applyCompaction(messages, summaryText, thresholds = {}) {
10876
+ const { older, recent } = splitForCompaction(messages, thresholds);
10877
+ if (older.length === 0) return messages;
10878
+ const summary = {
10879
+ role: "summary",
10880
+ text: summaryText.trim(),
10881
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10882
+ };
10883
+ return [summary, ...recent];
10904
10884
  }
10905
- function getRunScript(worktreePath, repoPath, name) {
10906
- const scripts = listRunScripts(worktreePath, repoPath);
10907
- if (!scripts.length) return null;
10908
- if (name) {
10909
- return scripts.find((s) => s.name === name) ?? null;
10885
+ function lastRequestOccupancy(thread) {
10886
+ for (let i = thread.messages.length - 1; i >= 0; i--) {
10887
+ const usage = thread.messages[i]?.usage;
10888
+ if (usage) return contextTokens(usage);
10910
10889
  }
10911
- return getDefaultRunScript(worktreePath, repoPath);
10890
+ return 0;
10912
10891
  }
10913
- function getRunMode(worktreePath, repoPath) {
10914
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
10892
+ function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
10893
+ return lastRequestOccupancy(thread) >= occupancyTokens;
10915
10894
  }
10916
- async function allocatePort() {
10917
- return new Promise((resolve, reject) => {
10918
- const server = (0, import_node_net.createServer)();
10919
- server.listen(0, "127.0.0.1", () => {
10920
- const addr = server.address();
10921
- if (!addr || typeof addr === "string") {
10922
- server.close();
10923
- reject(new Error("Failed to allocate port"));
10924
- return;
10925
- }
10926
- const port = addr.port;
10927
- server.close((err) => err ? reject(err) : resolve(port));
10928
- });
10929
- server.on("error", reject);
10895
+ async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
10896
+ if (!shouldCompactContext(thread.messages, thresholds)) {
10897
+ return { didCompact: false, thread };
10898
+ }
10899
+ const { older } = splitForCompaction(thread.messages, thresholds);
10900
+ if (older.length === 0) {
10901
+ return { didCompact: false, thread };
10902
+ }
10903
+ const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
10904
+ const { summary, method } = await summarize(transcript, {
10905
+ cwd: thread.worktreePath
10930
10906
  });
10907
+ const messages = applyCompaction(thread.messages, summary, thresholds);
10908
+ const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
10909
+ const next = {
10910
+ ...thread,
10911
+ messages,
10912
+ sessionId: resetSession ? null : thread.sessionId,
10913
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10914
+ };
10915
+ return {
10916
+ didCompact: true,
10917
+ thread: next,
10918
+ summary,
10919
+ method,
10920
+ olderCount: older.length
10921
+ };
10931
10922
  }
10932
- async function allocatePortRange(size = PORT_RANGE_SIZE) {
10933
- const base = await allocatePort();
10934
- const ports = [base];
10935
- for (let i = 1; i < size; i++) {
10936
- const candidate = base + i;
10937
- const free = await new Promise((resolve) => {
10938
- const server = (0, import_node_net.createServer)();
10939
- server.once("error", () => resolve(false));
10940
- server.listen(candidate, "127.0.0.1", () => {
10941
- server.close(() => resolve(true));
10942
- });
10943
- });
10944
- if (free) {
10945
- ports.push(candidate);
10946
- } else {
10947
- ports.push(await allocatePort());
10948
- }
10949
- }
10950
- return ports;
10951
- }
10952
- async function startDevServer(repoPath, worktreePath, onLine, opts) {
10953
- const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
10954
- if (!script) return null;
10955
- const ports = await allocatePortRange(PORT_RANGE_SIZE);
10956
- const handle = await spawnWorkspaceScript(script.command, {
10957
- worktreePath,
10958
- repoPath,
10959
- ports,
10960
- defaultBranch: opts?.defaultBranch,
10961
- onLine
10962
- });
10963
- return {
10964
- pid: handle.pid,
10965
- port: ports[0],
10966
- ports,
10967
- scriptName: script.name,
10968
- kill: handle.kill,
10969
- done: handle.done
10970
- };
10971
- }
10972
- var import_node_fs29, import_node_net, import_node_path28, import_execa4, import_node_readline3, PORT_RANGE_SIZE, cachedLoginEnv;
10973
- var init_conductor = __esm({
10974
- "src/hook/conductor.ts"() {
10923
+ var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAGES, CONTEXT_MIN_MESSAGES, SESSION_RESET_OCCUPANCY_TOKENS;
10924
+ var init_context_compact = __esm({
10925
+ "src/composer/context-compact.ts"() {
10975
10926
  "use strict";
10976
- import_node_fs29 = require("fs");
10977
- import_node_net = require("net");
10978
- import_node_path28 = require("path");
10979
- import_execa4 = require("execa");
10980
- import_node_readline3 = require("readline");
10981
- init_settings();
10982
- init_nested_electron_env();
10983
- init_convention_setup();
10984
- init_cursor_worktrees();
10985
- init_git_auth_mode();
10986
- init_nested_electron_env();
10987
- PORT_RANGE_SIZE = 10;
10988
- cachedLoginEnv = null;
10927
+ init_usage();
10928
+ init_summarize();
10929
+ CONTEXT_COMPACT_CHARS = 4e5;
10930
+ CONTEXT_KEEP_RECENT_CHARS = 24e3;
10931
+ CONTEXT_KEEP_RECENT_MESSAGES = 12;
10932
+ CONTEXT_MIN_MESSAGES = 10;
10933
+ SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
10989
10934
  }
10990
10935
  });
10991
10936
 
10992
- // src/git/orphan-cleanup.ts
10993
- function isSideboardWorktreePath(path2) {
10994
- return path2.includes("/.sideboard/worktrees/") || path2.includes("/sideboard/workspaces/");
10937
+ // src/threads/chat-tabs.ts
10938
+ function sameWorktreePath(a, b) {
10939
+ return normalizeWorktreePath(a) === normalizeWorktreePath(b);
10995
10940
  }
10996
- async function findOrphanWorktrees(repoPaths) {
10997
- const threads = listThreads({ includeArchived: true });
10998
- const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
10999
- const repos = new Set(
11000
- repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
11001
- );
11002
- const homeRoot = sideboardWorkspacesDir();
11003
- if ((0, import_node_fs30.existsSync)(homeRoot)) {
11004
- try {
11005
- for (const entry of (0, import_node_fs30.readdirSync)(homeRoot, { withFileTypes: true })) {
11006
- if (!entry.isDirectory()) continue;
11007
- void entry;
11008
- }
11009
- } catch {
11010
- }
11011
- }
11012
- const orphans = [];
11013
- const seen = /* @__PURE__ */ new Set();
11014
- for (const repoPath of repos) {
11015
- if (!repoPath || !(0, import_node_fs30.existsSync)(repoPath)) continue;
11016
- try {
11017
- const wts = await listWorktrees(repoPath);
11018
- for (const wt of wts) {
11019
- const path2 = wt.path.replace(/\/$/, "");
11020
- if (!isSideboardWorktreePath(path2)) continue;
11021
- if (known.has(path2)) continue;
11022
- if (seen.has(path2)) continue;
11023
- seen.add(path2);
11024
- let mtimeMs = 0;
11025
- try {
11026
- mtimeMs = (0, import_node_fs30.statSync)(path2).mtimeMs;
11027
- } catch {
11028
- mtimeMs = 0;
11029
- }
11030
- orphans.push({ path: path2, repoPath, mtimeMs });
11031
- }
11032
- } catch {
11033
- }
11034
- try {
11035
- const root = worktreesRoot(repoPath);
11036
- if ((0, import_node_fs30.existsSync)(root)) {
11037
- for (const entry of (0, import_node_fs30.readdirSync)(root, { withFileTypes: true })) {
11038
- if (!entry.isDirectory()) continue;
11039
- const path2 = (0, import_node_path29.join)(root, entry.name).replace(/\/$/, "");
11040
- if (known.has(path2) || seen.has(path2)) continue;
11041
- if (!(0, import_node_fs30.existsSync)((0, import_node_path29.join)(path2, ".git"))) continue;
11042
- seen.add(path2);
11043
- let mtimeMs = 0;
11044
- try {
11045
- mtimeMs = (0, import_node_fs30.statSync)(path2).mtimeMs;
11046
- } catch {
11047
- mtimeMs = Date.now();
11048
- }
11049
- orphans.push({ path: path2, repoPath, mtimeMs });
11050
- }
11051
- }
11052
- } catch {
10941
+ function takenTeamSlugsForChatTab(worktreePath) {
10942
+ const key = normalizeWorktreePath(worktreePath);
10943
+ const taken = /* @__PURE__ */ new Set();
10944
+ const dir = worktreeNameFromPath(key);
10945
+ if (dir) taken.add(dir.toLowerCase());
10946
+ for (const sibling of threadsSharingWorktree(key)) {
10947
+ for (const slug of takenSlugsFromThread(sibling)) {
10948
+ taken.add(slug);
11053
10949
  }
11054
10950
  }
11055
- return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
10951
+ return [...taken];
11056
10952
  }
11057
- async function cleanupOrphanWorktrees(opts) {
11058
- const settings = loadAppSettings();
11059
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
11060
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
11061
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
11062
- const budget = Math.max(0, maxCount - activeCount);
11063
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
11064
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
11065
- const removed = [];
11066
- const kept = [];
11067
- for (const orphan of orphans) {
11068
- if (keep.has(orphan.path)) {
11069
- kept.push(orphan.path);
11070
- continue;
11071
- }
11072
- if (opts?.dryRun) {
11073
- removed.push(orphan.path);
11074
- continue;
11075
- }
11076
- try {
11077
- await removeWorktree(orphan.repoPath, orphan.path);
11078
- removed.push(orphan.path);
11079
- } catch {
11080
- kept.push(orphan.path);
11081
- }
11082
- }
11083
- if (!opts?.dryRun) {
11084
- updateAdvancedSettings({
11085
- worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
11086
- });
11087
- }
11088
- return { removed, kept, orphans };
10953
+ function requireThread(idOrRef) {
10954
+ const thread = findThreadByRef(idOrRef) ?? null;
10955
+ if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
10956
+ return thread;
11089
10957
  }
11090
- function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
11091
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
11092
- const last = settings.advanced.worktreeLastCleanupAt;
11093
- if (!last) return true;
11094
- const elapsed = Date.now() - Date.parse(last);
11095
- return elapsed >= intervalHours * 36e5;
10958
+ function worktreeBindingFrom(from) {
10959
+ return {
10960
+ worktreePath: normalizeWorktreePath(from.worktreePath),
10961
+ repoPath: from.repoPath,
10962
+ branchName: from.branchName,
10963
+ sourceRef: from.sourceRef,
10964
+ // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
10965
+ // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
10966
+ sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
10967
+ sourceIsFork: from.sourceIsFork,
10968
+ parentThreadId: from.parentThreadId,
10969
+ prUrl: from.prUrl,
10970
+ prTitle: from.prTitle,
10971
+ prState: from.prState,
10972
+ skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
10973
+ stackId: from.stackId,
10974
+ stackLayer: from.stackLayer
10975
+ };
11096
10976
  }
11097
- function worktreeCleanupSettings() {
11098
- const a = loadAppSettings().advanced;
10977
+ function threadsSharingWorktree(worktreePath) {
10978
+ if (!worktreePath) return [];
10979
+ const key = normalizeWorktreePath(worktreePath);
10980
+ return listThreads({ includeArchived: true }).filter(
10981
+ (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
10982
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
10983
+ }
10984
+ function formatTranscriptMarkdown(title, messages) {
10985
+ const body = formatMessagesAsTranscript(messages, { tools: "full" });
10986
+ return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
10987
+ }
10988
+ function forkMessageSlice(from, throughIndex) {
10989
+ const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
10990
+ return through >= 0 ? from.messages.slice(0, through + 1) : [];
10991
+ }
10992
+ function buildForkTranscriptAttachment(baseTitle, messages) {
10993
+ const title = baseTitle || "Chat";
11099
10994
  return {
11100
- worktreeMaxCount: a.worktreeMaxCount,
11101
- worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
11102
- worktreeLastCleanupAt: a.worktreeLastCleanupAt,
11103
- autoCleanupOrphans: a.autoCleanupOrphans
10995
+ id: (0, import_node_crypto5.randomUUID)(),
10996
+ name: `Transcript of ${title}.md`,
10997
+ kind: "transcript",
10998
+ content: formatTranscriptMarkdown(title, messages)
11104
10999
  };
11105
11000
  }
11106
- var import_node_fs30, import_node_path29;
11107
- var init_orphan_cleanup = __esm({
11108
- "src/git/orphan-cleanup.ts"() {
11109
- "use strict";
11110
- import_node_fs30 = require("fs");
11111
- import_node_path29 = require("path");
11112
- init_worktree();
11113
- init_thread_store();
11114
- init_paths();
11115
- init_app_settings();
11116
- }
11117
- });
11118
-
11119
- // src/git/apply-into-main.ts
11120
- async function applyThreadIntoMain(thread, opts) {
11121
- const method = opts?.method ?? "merge";
11122
- const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
11123
- const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
11124
- cwd: thread.repoPath,
11125
- reject: false
11126
- });
11127
- if (status.stdout.trim()) {
11128
- throw new Error(
11129
- `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
11130
- );
11131
- }
11132
- const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
11133
- cwd: thread.repoPath,
11134
- reject: false
11135
- });
11136
- if (checkout.exitCode !== 0) {
11137
- throw new Error(
11138
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
11139
- );
11001
+ function createChatTab(input) {
11002
+ const from = requireThread(input.fromThreadId);
11003
+ const binding = worktreeBindingFrom(from);
11004
+ const explicitTitle = input.title?.trim();
11005
+ const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
11006
+ const nextAgent = input.agent ?? from.agent;
11007
+ if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
11008
+ assertOrchestratorCapableAgent(nextAgent);
11140
11009
  }
11141
- if (method === "cherry-pick") {
11142
- const log = await (0, import_execa5.execa)(
11143
- "git",
11144
- ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
11145
- { cwd: thread.repoPath, reject: false }
11146
- );
11147
- const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
11148
- if (!shas.length) {
11149
- return {
11150
- applied: false,
11151
- method,
11152
- targetBranch: target,
11153
- message: `No commits on ${thread.branchName} not already in ${target}`
11154
- };
11155
- }
11156
- for (const sha of shas) {
11157
- const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
11158
- cwd: thread.repoPath,
11159
- reject: false
11160
- });
11161
- if (cp.exitCode !== 0) {
11162
- await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
11163
- cwd: thread.repoPath,
11164
- reject: false
11165
- });
11166
- throw new Error(
11167
- `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
11168
- );
11169
- }
11170
- }
11171
- return {
11172
- applied: true,
11173
- method,
11174
- targetBranch: target,
11175
- message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
11176
- };
11177
- }
11178
- const merge = await (0, import_execa5.execa)(
11179
- "git",
11180
- ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
11181
- { cwd: thread.repoPath, reject: false }
11182
- );
11183
- if (merge.exitCode !== 0) {
11184
- await (0, import_execa5.execa)("git", ["merge", "--abort"], {
11185
- cwd: thread.repoPath,
11186
- reject: false
11187
- });
11188
- throw new Error(
11189
- `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
11190
- );
11191
- }
11192
- return {
11193
- applied: true,
11194
- method: "merge",
11195
- targetBranch: target,
11196
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
11197
- };
11198
- }
11199
- var import_execa5;
11200
- var init_apply_into_main = __esm({
11201
- "src/git/apply-into-main.ts"() {
11202
- "use strict";
11203
- import_execa5 = require("execa");
11204
- init_worktree();
11205
- }
11206
- });
11207
-
11208
- // src/store/workspaces.ts
11209
- var workspaces_exports = {};
11210
- __export(workspaces_exports, {
11211
- addWorkspace: () => addWorkspace,
11212
- ensureWorkspace: () => ensureWorkspace,
11213
- listWorkspaces: () => listWorkspaces,
11214
- removeWorkspace: () => removeWorkspace,
11215
- syncWorkspacesFromThreads: () => syncWorkspacesFromThreads
11216
- });
11217
- function workspacesFile() {
11218
- return (0, import_node_path30.join)(appDataDir(), "workspaces.json");
11219
- }
11220
- function removedWorkspacesFile() {
11221
- return (0, import_node_path30.join)(appDataDir(), "removed-workspaces.json");
11222
- }
11223
- function readAll2() {
11224
- const path2 = workspacesFile();
11225
- if (!(0, import_node_fs31.existsSync)(path2)) return [];
11226
- try {
11227
- const raw = JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8"));
11228
- return Array.isArray(raw) ? raw : [];
11229
- } catch {
11230
- return [];
11231
- }
11232
- }
11233
- function writeAll2(list) {
11234
- (0, import_node_fs31.mkdirSync)(appDataDir(), { recursive: true });
11235
- (0, import_node_fs31.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
11236
- }
11237
- function readRemoved() {
11238
- const path2 = removedWorkspacesFile();
11239
- if (!(0, import_node_fs31.existsSync)(path2)) return /* @__PURE__ */ new Set();
11240
- try {
11241
- const raw = JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8"));
11242
- return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
11243
- } catch {
11244
- return /* @__PURE__ */ new Set();
11245
- }
11246
- }
11247
- function writeRemoved(paths) {
11248
- (0, import_node_fs31.mkdirSync)(appDataDir(), { recursive: true });
11249
- (0, import_node_fs31.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
11250
- }
11251
- function rememberRemoved(repoPath) {
11252
- const next = readRemoved();
11253
- next.add(repoPath);
11254
- writeRemoved(next);
11255
- }
11256
- function forgetRemoved(repoPath) {
11257
- const next = readRemoved();
11258
- if (!next.delete(repoPath)) return;
11259
- writeRemoved(next);
11260
- }
11261
- function listWorkspaces() {
11262
- const all = readAll2();
11263
- const valid = all.filter(
11264
- (w) => Boolean(w.path) && w.path !== "/" && w.path !== "." && !isGlobalRepoPath(w.path)
11265
- );
11266
- if (valid.length !== all.length) writeAll2(valid);
11267
- return valid.sort((a, b) => a.name.localeCompare(b.name));
11268
- }
11269
- async function addWorkspace(repoPath) {
11270
- const root = await resolveRepoRoot(repoPath);
11271
- if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
11272
- if (!(0, import_node_fs31.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
11273
- forgetRemoved(root);
11274
- await ensureGhPreferOrigin(root);
11275
- const current = readAll2();
11276
- const existing = current.find((w) => w.path === root);
11277
- if (existing) return existing;
11278
- const next = {
11279
- path: root,
11280
- name: (0, import_node_path30.basename)(root),
11281
- addedAt: (/* @__PURE__ */ new Date()).toISOString()
11282
- };
11283
- writeAll2([...current, next]);
11284
- return next;
11285
- }
11286
- function removeWorkspace(repoPath) {
11287
- writeAll2(readAll2().filter((w) => w.path !== repoPath));
11288
- rememberRemoved(repoPath);
11289
- }
11290
- async function ensureWorkspace(repoPath) {
11291
- return addWorkspace(repoPath);
11010
+ const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
11011
+ const thread = createEmptyThread({
11012
+ title,
11013
+ // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
11014
+ // syncThreadBranchFromGit would otherwise rewrite every sibling to the
11015
+ // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
11016
+ userSetTitle: true,
11017
+ ...binding,
11018
+ // Slack / Brightsy cloud identity stays on the original chat. A + tab
11019
+ // that copies `slack:team:user` would steal inbound DMs after you close
11020
+ // the connected chat.
11021
+ sourceRef: singletonInbox ? title : binding.sourceRef,
11022
+ agent: nextAgent,
11023
+ model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
11024
+ effort: input.effort !== void 0 ? input.effort : from.effort,
11025
+ fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
11026
+ planMode: from.planMode,
11027
+ autonomy: input.autonomy ?? from.autonomy,
11028
+ attachments: input.attachments ?? [],
11029
+ status: "idle"
11030
+ });
11031
+ writeThread(thread);
11032
+ return thread;
11292
11033
  }
11293
- function syncWorkspacesFromThreads(repoPaths) {
11294
- const current = readAll2();
11295
- const removed = readRemoved();
11296
- const byPath = new Map(current.map((w) => [w.path, w]));
11297
- let dirty = false;
11298
- for (const path2 of repoPaths) {
11299
- if (!path2 || path2 === "/" || isGlobalRepoPath(path2) || byPath.has(path2) || removed.has(path2)) {
11300
- continue;
11301
- }
11302
- if (!(0, import_node_fs31.existsSync)(path2)) continue;
11303
- const ws = {
11304
- path: path2,
11305
- name: (0, import_node_path30.basename)(path2),
11306
- addedAt: (/* @__PURE__ */ new Date()).toISOString()
11307
- };
11308
- byPath.set(path2, ws);
11309
- dirty = true;
11034
+ function forkChatTab(input) {
11035
+ const from = requireThread(input.threadId);
11036
+ const slice = forkMessageSlice(from, input.throughIndex);
11037
+ const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
11038
+ const tab = createChatTab({
11039
+ fromThreadId: input.threadId,
11040
+ agent: input.agent ?? from.agent,
11041
+ model: input.model,
11042
+ title: input.title?.trim() || void 0,
11043
+ attachments: [attachment]
11044
+ });
11045
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
11046
+ const next = { ...tab, parentThreadId: from.id };
11047
+ writeThread(next);
11048
+ return next;
11310
11049
  }
11311
- const next = [...byPath.values()];
11312
- if (dirty) writeAll2(next);
11313
- return next.sort((a, b) => a.name.localeCompare(b.name));
11050
+ return tab;
11314
11051
  }
11315
- var import_node_fs31, import_node_path30;
11316
- var init_workspaces2 = __esm({
11317
- "src/store/workspaces.ts"() {
11052
+ var import_node_crypto5;
11053
+ var init_chat_tabs = __esm({
11054
+ "src/threads/chat-tabs.ts"() {
11318
11055
  "use strict";
11319
- import_node_fs31 = require("fs");
11320
- import_node_path30 = require("path");
11321
- init_paths();
11056
+ import_node_crypto5 = require("crypto");
11057
+ init_context_compact();
11058
+ init_teams();
11059
+ init_worktree_labels();
11322
11060
  init_global_workspace();
11323
- init_worktree();
11061
+ init_orchestrator_capable();
11062
+ init_thread_store();
11063
+ init_worktree_labels();
11324
11064
  }
11325
11065
  });
11326
11066
 
11327
- // src/git/clone-repo.ts
11328
- async function cloneRepoIntoSideboard(opts) {
11329
- const url = opts.url.trim();
11330
- if (!url) throw new Error("Clone URL is required");
11331
- let name = opts.name?.trim();
11332
- if (!name) {
11333
- const leaf = (0, import_node_path31.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
11334
- name = leaf || "repo";
11335
- }
11336
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
11337
- const dest = (0, import_node_path31.join)(sideboardReposDir(), name);
11338
- if ((0, import_node_fs32.existsSync)(dest)) {
11339
- const repoPath2 = await resolveRepoRoot(dest);
11340
- const workspace2 = await ensureWorkspace(repoPath2);
11341
- return { repoPath: repoPath2, workspace: workspace2 };
11342
- }
11343
- const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
11344
- if (result.exitCode !== 0) {
11345
- throw new Error(
11346
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
11347
- );
11067
+ // src/review/review-request-template.ts
11068
+ function wrapReviewSkillMarkdown(body) {
11069
+ const trimmed = body.replace(/^\uFEFF/, "").trim();
11070
+ const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
11071
+ if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
11072
+ return source.endsWith("\n") ? source : `${source}
11073
+ `;
11348
11074
  }
11349
- const repoPath = await resolveRepoRoot(dest);
11350
- const workspace = await ensureWorkspace(repoPath);
11351
- return { repoPath, workspace };
11075
+ return `${REVIEW_SKILL_FRONTMATTER}${source}
11076
+ `;
11352
11077
  }
11353
- var import_node_fs32, import_node_path31, import_execa6;
11354
- var init_clone_repo = __esm({
11355
- "src/git/clone-repo.ts"() {
11078
+ var REVIEW_REQUEST_TEMPLATE, REVIEW_SKILL_PATH, REVIEW_SKILL_NAME, REVIEW_SKILL_FRONTMATTER;
11079
+ var init_review_request_template = __esm({
11080
+ "src/review/review-request-template.ts"() {
11356
11081
  "use strict";
11357
- import_node_fs32 = require("fs");
11358
- import_node_path31 = require("path");
11359
- import_execa6 = require("execa");
11360
- init_paths();
11361
- init_workspaces2();
11362
- init_worktree();
11363
- }
11364
- });
11082
+ REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
11365
11083
 
11366
- // src/detect/detect.ts
11367
- async function detectAgents() {
11368
- ensureAgentPath();
11369
- enrichPathWithNpmGlobalBin();
11370
- return Promise.all(allAdapters().map((a) => a.detect()));
11371
- }
11372
- async function requireAgent(agent, opts) {
11373
- ensureAgentPath();
11374
- enrichPathWithNpmGlobalBin();
11375
- const status = await Promise.race([
11376
- getAdapter(agent).detect(),
11377
- new Promise((_, reject) => {
11378
- setTimeout(() => {
11379
- reject(
11380
- new Error(
11381
- `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
11382
- )
11383
- );
11384
- }, REQUIRE_AGENT_TIMEOUT_MS);
11385
- })
11386
- ]);
11387
- if (!status.installed || !status.authenticated) {
11388
- throw new Error(status.reason ?? `${agent} is not available`);
11084
+ 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.
11085
+
11086
+ ## Required outcome
11087
+
11088
+ Start your reply with a **Recommendation** section using exactly one of:
11089
+
11090
+ - **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
11091
+ - **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
11092
+ - **Request changes** \u2014 not ready; blocking issues must be fixed first.
11093
+ - **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
11094
+
11095
+ 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.
11096
+
11097
+ People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
11098
+
11099
+ ## Findings
11100
+
11101
+ Below are guidelines for determining whether an issue is worth flagging to the original author.
11102
+
11103
+ These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
11104
+
11105
+ Flag something as a bug / blocking finding only when:
11106
+
11107
+ 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
11108
+ 2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
11109
+ 3. Fixing it does not demand rigor absent from the rest of the codebase.
11110
+ 4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
11111
+ 5. The author would likely fix it if made aware.
11112
+ 6. It does not rely on unstated assumptions about the codebase or author intent.
11113
+ 7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
11114
+ 8. It is clearly not just an intentional change by the author.
11115
+
11116
+ When flagging an issue, include a short accompanying comment:
11117
+
11118
+ 1. Clear about why it is a problem.
11119
+ 2. Severity must match reality \u2014 do not inflate.
11120
+ 3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
11121
+ 4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
11122
+ 5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
11123
+ 6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
11124
+ 7. Skimmable on first read.
11125
+ 8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
11126
+
11127
+ HOW MANY FINDINGS TO RETURN:
11128
+
11129
+ 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.
11130
+
11131
+ GUIDELINES:
11132
+
11133
+ - Ignore trivial style unless it obscures meaning or violates documented standards.
11134
+ - One comment per distinct issue (or a short multi-line range if needed).
11135
+ - Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
11136
+ - In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
11137
+ - Do NOT introduce or remove outer indentation levels unless that is the actual fix.
11138
+ - Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
11139
+
11140
+ 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).
11141
+
11142
+ ## Getting the diff
11143
+
11144
+ 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.
11145
+
11146
+ 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.
11147
+
11148
+ ## Fallback: if you don't have access to the Sideboard diff tool
11149
+
11150
+ If you don't have access to \`get_diff\`, use the following git commands to get the diff:
11151
+
11152
+ \`\`\`bash
11153
+ # Get the merge base between this branch and the target
11154
+ MERGE_BASE=$(git merge-base origin/main HEAD)
11155
+
11156
+ # Get the committed diff against the merge base
11157
+ git diff $MERGE_BASE HEAD
11158
+
11159
+ # Get any uncommitted changes (staged and unstaged)
11160
+ git diff HEAD
11161
+ \`\`\`
11162
+
11163
+ 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.
11164
+
11165
+ No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
11166
+
11167
+ ## Output format
11168
+
11169
+ **1. Recommendation first** (required), then **2. Findings** (may be empty).
11170
+
11171
+ Only report ONE finding per unique issue.
11172
+
11173
+ <example>
11174
+ ## Recommendation
11175
+
11176
+ **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.
11177
+
11178
+ ## Findings
11179
+
11180
+ ### **#1 Empty input causes crash** (blocking)
11181
+
11182
+ If the input field is empty when the page loads, the app will crash.
11183
+
11184
+ File: src/client/frontends/desktop/ui/Input.tsx
11185
+
11186
+ ### **#2 Dead code** (nit)
11187
+
11188
+ The getUserData function is now unused. It should be deleted.
11189
+
11190
+ File: src/client/frontends/desktop/core/UserData.ts
11191
+ </example>
11192
+
11193
+ <example>
11194
+ ## Recommendation
11195
+
11196
+ **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
11197
+ </example>
11198
+
11199
+ ## Growing the rules
11200
+
11201
+ 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.
11202
+ `;
11203
+ REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
11204
+ REVIEW_SKILL_NAME = "review";
11205
+ REVIEW_SKILL_FRONTMATTER = `---
11206
+ name: review
11207
+ description: >-
11208
+ Merge-readiness review for this repository. Use when reviewing a PR, worktree
11209
+ diff, or Sideboard Review (Approve / Approve with nits / Request changes).
11210
+ ---
11211
+
11212
+ `;
11389
11213
  }
11390
- if (opts?.requireLinear && !status.linearMcp) {
11391
- throw new Error(
11392
- `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
11393
- );
11214
+ });
11215
+
11216
+ // src/review/request-review.ts
11217
+ function shouldRefreshReviewRequestTemplate(content) {
11218
+ const trimmed = content.trim();
11219
+ if (!trimmed) return true;
11220
+ if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
11221
+ return false;
11394
11222
  }
11395
- return status;
11223
+ return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
11396
11224
  }
11397
- var REQUIRE_AGENT_TIMEOUT_MS;
11398
- var init_detect = __esm({
11399
- "src/detect/detect.ts"() {
11400
- "use strict";
11401
- init_agents();
11402
- REQUIRE_AGENT_TIMEOUT_MS = 8e3;
11225
+ function readTextIfPresent(abs) {
11226
+ if (!(0, import_node_fs29.existsSync)(abs)) return null;
11227
+ try {
11228
+ const content = (0, import_node_fs29.readFileSync)(abs, "utf8");
11229
+ return content.trim() ? content : null;
11230
+ } catch {
11231
+ return null;
11403
11232
  }
11404
- });
11405
-
11406
- // src/threads/create.ts
11407
- async function createThread(input, _onSetupLine) {
11408
- const resolved = resolveNewThreadOptions({
11409
- agent: input.agent,
11410
- model: input.model,
11411
- effort: input.effort,
11412
- fast: input.fast
11413
- });
11414
- await requireAgent(resolved.agent);
11415
- const repoPath = await resolveRepoRoot(input.repoPath);
11416
- if (!(0, import_node_fs33.existsSync)(repoPath)) {
11417
- throw new Error(`Repo not found: ${repoPath}`);
11233
+ }
11234
+ function readLocalGuidelines(worktreePath) {
11235
+ const localAbs = (0, import_node_path28.join)(worktreePath, REVIEW_REQUEST_PATH);
11236
+ const localContent = readTextIfPresent(localAbs);
11237
+ if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
11238
+ return { path: REVIEW_REQUEST_PATH, content: localContent };
11418
11239
  }
11419
- let sourceRef = input.sourceRef;
11420
- let sourceIsFork = false;
11421
- let prUrl = null;
11422
- let prTitle = null;
11423
- if (input.sourceType === "pr") {
11424
- const num2 = Number(input.sourceRef.replace(/^#/, ""));
11425
- if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
11426
- const pr = await getPr(repoPath, num2);
11427
- if (!pr) throw new Error(`PR #${num2} not found`);
11428
- sourceIsFork = pr.isCrossRepository;
11429
- prUrl = pr.url;
11430
- const localFetchBranch = `sideboard-pr-${pr.number}`;
11431
- await fetchPrHead(repoPath, pr.number, localFetchBranch);
11432
- sourceRef = localFetchBranch;
11433
- } else if (input.sourceType === "ticket") {
11434
- sourceRef = await resolveDefaultBranch(repoPath);
11435
- } else if (input.sourceType === "branch") {
11436
- if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
11437
- sourceRef = await resolveDefaultBranch(repoPath);
11438
- } else {
11439
- const existing = await getPrForHeadBranch(repoPath, sourceRef);
11440
- if (existing?.url) {
11441
- prUrl = existing.url;
11442
- prTitle = existing.title;
11443
- }
11444
- }
11445
- } else if (input.sourceType === "adopt") {
11446
- throw new Error("Use adoptThread() for adopt sources");
11240
+ const legacyAbs = (0, import_node_path28.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
11241
+ const legacyContent = readTextIfPresent(legacyAbs);
11242
+ if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
11243
+ return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
11447
11244
  }
11448
- const team = allocateTeamSlug(repoPath);
11449
- const { branchName, worktreePath } = await createThreadWorktree({
11450
- repoPath,
11451
- sourceRef,
11452
- slug: team.slug
11453
- });
11454
- copyConfiguredFiles(repoPath, worktreePath);
11455
- const explicitTitle = input.title?.trim();
11456
- const thread = createEmptyThread({
11457
- title: explicitTitle || team.name,
11458
- userSetTitle: Boolean(explicitTitle),
11459
- sourceType: input.sourceType,
11460
- sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
11461
- branchName,
11462
- worktreePath,
11463
- repoPath,
11464
- agent: resolved.agent,
11465
- autonomy: input.autonomy ?? "default",
11466
- model: resolved.model,
11467
- effort: resolved.effort,
11468
- fast: resolved.fast,
11469
- planMode: Boolean(input.planMode),
11470
- attachments: input.attachments ?? [],
11471
- sourceIsFork,
11472
- parentThreadId: input.parentThreadId ?? null,
11473
- status: "idle",
11474
- prUrl,
11475
- prTitle
11476
- });
11477
- writeThread(thread);
11478
- await ensureWorkspace(repoPath);
11479
- return readThread(thread.id) ?? thread;
11245
+ return null;
11480
11246
  }
11481
- async function listLinearIssues(agent, repoPath) {
11482
- const { getAdapter: getAdapter2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
11483
- await requireAgent(agent, { requireLinear: true });
11484
- const adapter = getAdapter2(agent);
11485
- if (!adapter.listLinearIssues) {
11486
- throw new Error(`${agent} does not support Linear issue listing`);
11247
+ function skillGuidelines(content, source) {
11248
+ return {
11249
+ path: REVIEW_SKILL_PATH,
11250
+ name: REVIEW_SKILL_NAME,
11251
+ content,
11252
+ source
11253
+ };
11254
+ }
11255
+ function ensureReviewSkillFile(worktreePath) {
11256
+ const abs = (0, import_node_path28.join)(worktreePath, REVIEW_SKILL_PATH);
11257
+ const existing = readTextIfPresent(abs);
11258
+ if (existing) {
11259
+ return { path: REVIEW_SKILL_PATH, content: existing, wrote: false };
11487
11260
  }
11488
- return adapter.listLinearIssues(repoPath);
11261
+ const fromRepo = readTextIfPresent((0, import_node_path28.join)(worktreePath, REPO_REVIEW_PATH));
11262
+ const fromLocal = readLocalGuidelines(worktreePath)?.content ?? null;
11263
+ const content = wrapReviewSkillMarkdown(fromRepo ?? fromLocal ?? REVIEW_REQUEST_TEMPLATE);
11264
+ (0, import_node_fs29.mkdirSync)((0, import_node_path28.dirname)(abs), { recursive: true });
11265
+ (0, import_node_fs29.writeFileSync)(abs, content, "utf8");
11266
+ return { path: REVIEW_SKILL_PATH, content, wrote: true };
11489
11267
  }
11490
- var import_node_fs33;
11491
- var init_create = __esm({
11492
- "src/threads/create.ts"() {
11493
- "use strict";
11494
- import_node_fs33 = require("fs");
11495
- init_detect();
11496
- init_worktree();
11497
- init_conductor();
11498
- init_app_settings();
11499
- init_thread_store();
11500
- init_workspaces2();
11501
- }
11502
- });
11503
-
11504
- // src/composer/summarize.ts
11505
- async function summarizeConversation(transcript, opts) {
11506
- const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
11507
-
11508
- [\u2026truncated\u2026]` : transcript;
11509
- const claudeSummary = await tryClaudeSummary(clipped, opts);
11510
- if (claudeSummary) {
11511
- return { summary: clipSummary(claudeSummary), method: "claude" };
11268
+ function resolveReviewGuidelines(worktreePath) {
11269
+ const skillContent = readTextIfPresent((0, import_node_path28.join)(worktreePath, REVIEW_SKILL_PATH));
11270
+ if (skillContent) return skillGuidelines(skillContent, "skill");
11271
+ const local = readLocalGuidelines(worktreePath);
11272
+ if (local) {
11273
+ return {
11274
+ path: local.path,
11275
+ name: REVIEW_REQUEST_NAME,
11276
+ content: local.content,
11277
+ source: "local"
11278
+ };
11512
11279
  }
11513
- return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
11280
+ const seeded = ensureReviewSkillFile(worktreePath);
11281
+ return skillGuidelines(seeded.content, seeded.wrote ? "stock" : "skill");
11514
11282
  }
11515
- async function tryClaudeSummary(transcript, opts) {
11516
- ensureAgentPath();
11517
- const prompt = [
11518
- "Summarize this coding-agent conversation for continuity in a fresh session.",
11519
- "Use concise markdown bullets covering:",
11520
- "- Goal / request",
11521
- "- Key decisions and constraints",
11522
- "- Files / areas touched",
11523
- "- Current state and open tasks",
11524
- "- Anything the next turn must remember",
11525
- "Do not invent details. No preamble.",
11526
- "",
11527
- "Transcript:",
11528
- transcript
11529
- ].join("\n");
11530
- try {
11531
- const { stdout, exitCode } = await run(
11532
- "claude",
11533
- [
11534
- "-p",
11535
- prompt,
11536
- "--output-format",
11537
- "text",
11538
- "--permission-mode",
11539
- "bypassPermissions",
11540
- "--model",
11541
- "haiku"
11542
- ],
11543
- { cwd: opts?.cwd, reject: false }
11544
- );
11545
- if (exitCode !== 0) return null;
11546
- const text4 = stdout.trim();
11547
- if (text4.length < 40) return null;
11548
- return text4;
11549
- } catch {
11550
- return null;
11551
- }
11283
+ function ensureReviewRequestFile(worktreePath) {
11284
+ const seeded = ensureReviewSkillFile(worktreePath);
11285
+ return skillGuidelines(seeded.content, "skill");
11552
11286
  }
11553
- function extractiveSummary(transcript) {
11554
- const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
11555
- const users = [];
11556
- const tools = [];
11557
- const agents = [];
11558
- let mode = "none";
11559
- for (const line of lines) {
11560
- if (line.startsWith("### User")) {
11561
- mode = "user";
11562
- continue;
11563
- }
11564
- if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
11565
- mode = "agent";
11566
- continue;
11567
- }
11568
- if (line.startsWith("- ") && line.includes(":")) {
11569
- tools.push(line.replace(/^- /, ""));
11570
- continue;
11571
- }
11572
- if (mode === "user" && users.length < 8) {
11573
- users.push(line.slice(0, 240));
11574
- } else if (mode === "agent" && agents.length < 6) {
11575
- agents.push(line.slice(0, 240));
11576
- }
11287
+ function buildReviewRequestAttachment(content, opts) {
11288
+ const path2 = opts?.path ?? REVIEW_SKILL_PATH;
11289
+ const name = opts?.name ?? (path2 === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path2 === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
11290
+ return {
11291
+ id: (0, import_node_crypto6.randomUUID)(),
11292
+ name,
11293
+ kind: "file",
11294
+ path: path2,
11295
+ content
11296
+ };
11297
+ }
11298
+ function readExistingReviewRequestFile(worktreePath) {
11299
+ return readTextIfPresent((0, import_node_path28.join)(worktreePath, REVIEW_SKILL_PATH)) ?? readTextIfPresent((0, import_node_path28.join)(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent((0, import_node_path28.join)(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent((0, import_node_path28.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
11300
+ }
11301
+ async function requestReview(threadRef, send2) {
11302
+ const from = findThreadByRef(threadRef);
11303
+ if (!from) throw new Error(`Thread not found: ${threadRef}`);
11304
+ if (isOrchestratorThread(from)) {
11305
+ throw new Error(
11306
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
11307
+ );
11577
11308
  }
11578
- const uniqueTools = [...new Set(tools)].slice(0, 20);
11579
- const parts = [
11580
- "## Compacted context (extractive)",
11581
- "",
11582
- "### User goals",
11583
- ...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
11584
- "",
11585
- "### Recent agent notes",
11586
- ...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
11587
- ];
11588
- if (uniqueTools.length) {
11589
- parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
11309
+ if (from.status === "archived") {
11310
+ throw new Error(`Thread is archived: ${from.id}`);
11590
11311
  }
11591
- parts.push(
11592
- "",
11593
- "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
11594
- );
11595
- return parts.join("\n");
11596
- }
11597
- function clipSummary(text4) {
11598
- if (text4.length <= MAX_SUMMARY_CHARS) return text4;
11599
- return `${text4.slice(0, MAX_SUMMARY_CHARS)}
11600
-
11601
- [\u2026summary truncated\u2026]`;
11312
+ const guidelines = resolveReviewGuidelines(from.worktreePath);
11313
+ const tab = createChatTab({
11314
+ fromThreadId: from.id,
11315
+ title: "Review",
11316
+ attachments: [
11317
+ buildReviewRequestAttachment(guidelines.content, {
11318
+ path: guidelines.path,
11319
+ name: guidelines.name
11320
+ })
11321
+ ]
11322
+ });
11323
+ const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
11324
+ return { tab: started, from };
11602
11325
  }
11603
- var MAX_TRANSCRIPT_CHARS, MAX_SUMMARY_CHARS;
11604
- var init_summarize = __esm({
11605
- "src/composer/summarize.ts"() {
11326
+ var import_node_crypto6, import_node_fs29, import_node_path28, REPO_REVIEW_PATH, REPO_REVIEW_NAME, REVIEW_REQUEST_PATH, LEGACY_REVIEW_REQUEST_PATH, REVIEW_REQUEST_NAME, REVIEW_REQUEST_PREFILL, LEGACY_REVIEW_TEMPLATE_MARKERS;
11327
+ var init_request_review = __esm({
11328
+ "src/review/request-review.ts"() {
11606
11329
  "use strict";
11607
- init_run();
11608
- init_path();
11609
- MAX_TRANSCRIPT_CHARS = 12e4;
11610
- MAX_SUMMARY_CHARS = 6e3;
11330
+ import_node_crypto6 = require("crypto");
11331
+ import_node_fs29 = require("fs");
11332
+ import_node_path28 = require("path");
11333
+ init_global_workspace();
11334
+ init_chat_tabs();
11335
+ init_thread_store();
11336
+ init_review_request_template();
11337
+ init_workspace_scratch();
11338
+ REPO_REVIEW_PATH = ".sideboard/review.md";
11339
+ REPO_REVIEW_NAME = "review.md";
11340
+ REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
11341
+ LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
11342
+ REVIEW_REQUEST_NAME = "Review request.md";
11343
+ REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
11344
+ LEGACY_REVIEW_TEMPLATE_MARKERS = [
11345
+ "You are acting as a reviewer for a proposed code change made by another engineer.",
11346
+ "HOW MANY FINDINGS TO RETURN:"
11347
+ ];
11611
11348
  }
11612
11349
  });
11613
11350
 
11614
- // src/composer/context-compact.ts
11615
- function estimateMessageChars(message) {
11616
- let n = message.text.length + 16;
11617
- for (const part of message.parts ?? []) {
11618
- n += estimatePartChars(part);
11619
- }
11620
- return n;
11351
+ // src/hook/conductor.ts
11352
+ function matchSimpleGlob(pattern, name) {
11353
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
11354
+ return new RegExp(`^${escaped}$`).test(name);
11621
11355
  }
11622
- function estimatePartChars(part) {
11623
- switch (part.type) {
11624
- case "text":
11625
- case "thinking":
11626
- return part.text.length;
11627
- case "tool": {
11628
- const input = part.input ? JSON.stringify(part.input) : "";
11629
- return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
11356
+ function readWorktreeInclude(repoPath) {
11357
+ const path2 = (0, import_node_path29.join)(repoPath, ".worktreeinclude");
11358
+ if (!(0, import_node_fs30.existsSync)(path2)) return [];
11359
+ return (0, import_node_fs30.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
11360
+ }
11361
+ function resolveFilesToCopy(repoPath) {
11362
+ const fromInclude = readWorktreeInclude(repoPath);
11363
+ if (fromInclude.length) return fromInclude;
11364
+ const settings = loadRepoSettings(repoPath);
11365
+ if (settings?.filesToCopy?.length) return settings.filesToCopy;
11366
+ if (settings?.fileIncludeGlobs?.length) {
11367
+ const matched = [];
11368
+ try {
11369
+ for (const entry of (0, import_node_fs30.readdirSync)(repoPath, { withFileTypes: true })) {
11370
+ if (!entry.isFile()) continue;
11371
+ for (const glob of settings.fileIncludeGlobs) {
11372
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path29.basename)(glob), entry.name)) {
11373
+ matched.push(entry.name);
11374
+ break;
11375
+ }
11376
+ }
11377
+ }
11378
+ } catch {
11630
11379
  }
11631
- default:
11632
- return 0;
11380
+ if (matched.length) return [...new Set(matched)];
11633
11381
  }
11634
- }
11635
- function estimateThreadChars(messages) {
11636
- return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
11637
- }
11638
- function shouldCompactContext(messages, thresholds = {}) {
11639
- const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
11640
- const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
11641
- if (messages.length < minMessages) return false;
11642
- return estimateThreadChars(messages) >= maxChars;
11643
- }
11644
- function splitForCompaction(messages, thresholds = {}) {
11645
- const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
11646
- const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
11647
- if (messages.length === 0) return { older: [], recent: [] };
11648
- let recentChars = 0;
11649
- let cut = messages.length;
11650
- for (let i = messages.length - 1; i >= 0; i--) {
11651
- const next = recentChars + estimateMessageChars(messages[i]);
11652
- const kept = messages.length - i;
11653
- if (kept >= keepCount && next > keepChars) break;
11654
- recentChars = next;
11655
- cut = i;
11382
+ const defaults = [];
11383
+ try {
11384
+ for (const entry of (0, import_node_fs30.readdirSync)(repoPath, { withFileTypes: true })) {
11385
+ if (entry.isFile() && entry.name.startsWith(".env")) {
11386
+ defaults.push(entry.name);
11387
+ }
11388
+ }
11389
+ } catch {
11656
11390
  }
11657
- const minRecent = Math.min(keepCount, messages.length);
11658
- cut = Math.min(cut, messages.length - minRecent);
11659
- if (cut <= 0) return { older: [], recent: messages };
11660
- return {
11661
- older: messages.slice(0, cut),
11662
- recent: messages.slice(cut)
11663
- };
11391
+ if (defaults.length) return defaults;
11392
+ return [".env.local", ".env"];
11664
11393
  }
11665
- function formatToolPart(t, detail) {
11666
- if (detail === "summary") {
11667
- const label = t.description || t.detail || t.name;
11668
- const path2 = t.filePath ? ` (${t.filePath})` : "";
11669
- return `- ${t.name}: ${label}${path2}`;
11394
+ function copyConfiguredFiles(repoPath, worktreePath) {
11395
+ const patterns = resolveFilesToCopy(repoPath);
11396
+ const copied = [];
11397
+ for (const rel of patterns) {
11398
+ const src = (0, import_node_path29.join)(repoPath, rel);
11399
+ if (!(0, import_node_fs30.existsSync)(src)) continue;
11400
+ const dest = (0, import_node_path29.join)(worktreePath, rel);
11401
+ (0, import_node_fs30.mkdirSync)((0, import_node_path29.dirname)(dest), { recursive: true });
11402
+ (0, import_node_fs30.copyFileSync)(src, dest);
11403
+ copied.push(rel);
11670
11404
  }
11671
- const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
11672
- if (t.description) lines.push(`Description: ${t.description}`);
11673
- if (t.filePath) lines.push(`Path: ${t.filePath}`);
11674
- if (t.input && Object.keys(t.input).length > 0) {
11675
- lines.push("Input:");
11676
- lines.push("```json");
11677
- lines.push(JSON.stringify(t.input, null, 2));
11678
- lines.push("```");
11679
- } else if (t.detail) {
11680
- lines.push(`Detail: ${t.detail}`);
11681
- }
11682
- if (t.result != null && t.result !== "") {
11683
- lines.push("Result:");
11684
- lines.push("```");
11685
- lines.push(t.result);
11686
- lines.push("```");
11687
- }
11688
- return lines.join("\n");
11405
+ return copied;
11689
11406
  }
11690
- function formatMessagesAsTranscript(messages, opts) {
11691
- const tools = opts?.tools ?? "full";
11692
- const blocks = [];
11693
- for (const m of messages) {
11694
- if (m.role === "summary") {
11695
- blocks.push(`## Prior summary
11696
- ${m.text}`);
11697
- continue;
11698
- }
11699
- if (m.role === "user") {
11700
- blocks.push(`### User
11701
- ${m.text}`);
11702
- continue;
11703
- }
11704
- const bits = [];
11705
- if (m.text?.trim()) {
11706
- bits.push(`### Agent
11707
- ${m.text}`);
11708
- } else {
11709
- bits.push("### Agent");
11710
- }
11711
- const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
11712
- for (const th of thinking) {
11713
- if (th.type !== "thinking" || !th.text.trim()) continue;
11714
- bits.push("Thinking:");
11715
- bits.push(th.text);
11716
- }
11717
- const toolParts = (m.parts ?? []).filter(
11718
- (p) => p.type === "tool"
11719
- );
11720
- if (tools !== "none" && toolParts.length > 0) {
11721
- if (tools === "summary") bits.push("Tools:");
11722
- for (const t of toolParts) {
11723
- bits.push(formatToolPart(t, tools));
11407
+ async function captureLoginEnv() {
11408
+ if (cachedLoginEnv) return { ...cachedLoginEnv };
11409
+ const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
11410
+ try {
11411
+ const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
11412
+ reject: false,
11413
+ timeout: 5e3
11414
+ });
11415
+ if (result.exitCode === 0 && result.stdout) {
11416
+ const env = { ...process.env };
11417
+ for (const entry of result.stdout.split("\0")) {
11418
+ if (!entry) continue;
11419
+ const eq = entry.indexOf("=");
11420
+ if (eq <= 0) continue;
11421
+ const key = entry.slice(0, eq);
11422
+ const value = entry.slice(eq + 1);
11423
+ if (key) env[key] = value;
11724
11424
  }
11425
+ cachedLoginEnv = env;
11426
+ return { ...env };
11725
11427
  }
11726
- blocks.push(bits.join("\n"));
11428
+ } catch {
11727
11429
  }
11728
- return blocks.join("\n\n");
11430
+ cachedLoginEnv = { ...process.env };
11431
+ return { ...cachedLoginEnv };
11729
11432
  }
11730
- function buildSessionSeed(messages, opts) {
11731
- if (messages.length === 0) return null;
11732
- const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
11733
- if (!body.trim()) return null;
11734
- return [
11735
- "Sideboard conversation context (restored after compaction or a new session):",
11736
- "",
11737
- body,
11738
- "",
11739
- "Continue from this context. Do not repeat the summary unless asked."
11740
- ].join("\n");
11433
+ function buildWorkspaceScriptEnv(opts, baseEnv) {
11434
+ const env = stripNestedElectronEnv({
11435
+ ...baseEnv ?? process.env
11436
+ });
11437
+ const name = opts.workspaceName ?? (0, import_node_path29.basename)(opts.worktreePath);
11438
+ const ports = opts.ports ?? [];
11439
+ const primary = ports[0];
11440
+ env.SIDEBOARD_WORKSPACE_NAME = name;
11441
+ env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
11442
+ env.SIDEBOARD_ROOT_PATH = opts.repoPath;
11443
+ env.SIDEBOARD_IS_LOCAL = "1";
11444
+ if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
11445
+ env.CONDUCTOR_WORKSPACE_NAME = name;
11446
+ env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
11447
+ env.CONDUCTOR_ROOT_PATH = opts.repoPath;
11448
+ env.CONDUCTOR_IS_LOCAL = "1";
11449
+ if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
11450
+ if (primary != null) {
11451
+ env.SIDEBOARD_PORT = String(primary);
11452
+ env.CONDUCTOR_PORT = String(primary);
11453
+ env.PORT = String(primary);
11454
+ for (let i = 1; i < ports.length; i++) {
11455
+ const p = ports[i];
11456
+ env[`SIDEBOARD_PORT_${i}`] = String(p);
11457
+ env[`CONDUCTOR_PORT_${i}`] = String(p);
11458
+ }
11459
+ }
11460
+ return env;
11741
11461
  }
11742
- function applyCompaction(messages, summaryText, thresholds = {}) {
11743
- const { older, recent } = splitForCompaction(messages, thresholds);
11744
- if (older.length === 0) return messages;
11745
- const summary = {
11746
- role: "summary",
11747
- text: summaryText.trim(),
11748
- ts: (/* @__PURE__ */ new Date()).toISOString()
11749
- };
11750
- return [summary, ...recent];
11462
+ function pipeLines(stream, onLine) {
11463
+ if (!stream || !onLine) return;
11464
+ const rl = (0, import_node_readline3.createInterface)({ input: stream });
11465
+ rl.on("line", onLine);
11751
11466
  }
11752
- async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
11753
- if (!shouldCompactContext(thread.messages, thresholds)) {
11754
- return { didCompact: false, thread };
11467
+ function killScriptTree(child, ports = []) {
11468
+ const pid = child.pid;
11469
+ if (pid) {
11470
+ try {
11471
+ if (process.platform === "win32") {
11472
+ void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
11473
+ } else {
11474
+ process.kill(-pid, "SIGTERM");
11475
+ setTimeout(() => {
11476
+ try {
11477
+ process.kill(-pid, "SIGKILL");
11478
+ } catch {
11479
+ }
11480
+ }, 2500).unref?.();
11481
+ }
11482
+ } catch {
11483
+ try {
11484
+ child.kill("SIGTERM");
11485
+ } catch {
11486
+ }
11487
+ }
11755
11488
  }
11756
- const { older } = splitForCompaction(thread.messages, thresholds);
11757
- if (older.length === 0) {
11758
- return { didCompact: false, thread };
11489
+ for (const port of ports) {
11490
+ if (!Number.isFinite(port) || port <= 0) continue;
11491
+ if (process.platform === "win32") {
11492
+ void (0, import_execa4.execa)(
11493
+ "powershell",
11494
+ [
11495
+ "-NoProfile",
11496
+ "-Command",
11497
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
11498
+ ],
11499
+ { reject: false }
11500
+ );
11501
+ } else {
11502
+ void (0, import_execa4.execa)(
11503
+ "zsh",
11504
+ ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
11505
+ { reject: false }
11506
+ );
11507
+ }
11759
11508
  }
11760
- const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
11761
- const { summary, method } = await summarize(transcript, {
11762
- cwd: thread.worktreePath
11509
+ }
11510
+ async function spawnWorkspaceScript(command, opts) {
11511
+ const loginEnv = await captureLoginEnv();
11512
+ const env = buildWorkspaceScriptEnv(
11513
+ {
11514
+ worktreePath: opts.worktreePath,
11515
+ repoPath: opts.repoPath,
11516
+ workspaceName: opts.workspaceName,
11517
+ defaultBranch: opts.defaultBranch,
11518
+ ports: opts.ports
11519
+ },
11520
+ loginEnv
11521
+ );
11522
+ try {
11523
+ mergeAgentGitAuthEnv(
11524
+ env,
11525
+ await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
11526
+ );
11527
+ } catch {
11528
+ }
11529
+ const shell = process.platform === "darwin" ? "zsh" : "bash";
11530
+ const child = (0, import_execa4.execa)(shell, ["-lc", command], {
11531
+ cwd: opts.worktreePath,
11532
+ reject: false,
11533
+ env,
11534
+ // Own process group so Stop can tear down the whole tree (not just the shell).
11535
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
11536
+ ...process.platform === "win32" ? {} : { detached: true }
11763
11537
  });
11764
- const messages = applyCompaction(thread.messages, summary, thresholds);
11765
- const next = {
11766
- ...thread,
11767
- messages,
11768
- sessionId: null,
11769
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
11770
- };
11538
+ pipeLines(child.stdout, opts.onLine);
11539
+ pipeLines(child.stderr, opts.onLine);
11540
+ const ports = opts.ports ?? [];
11771
11541
  return {
11772
- didCompact: true,
11773
- thread: next,
11774
- summary,
11775
- method,
11776
- olderCount: older.length
11542
+ pid: child.pid,
11543
+ kill: () => killScriptTree(child, ports),
11544
+ done: child.then((r) => r.exitCode ?? null),
11545
+ child
11777
11546
  };
11778
11547
  }
11779
- var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAGES, CONTEXT_MIN_MESSAGES;
11780
- var init_context_compact = __esm({
11781
- "src/composer/context-compact.ts"() {
11782
- "use strict";
11783
- init_summarize();
11784
- CONTEXT_COMPACT_CHARS = 1e5;
11785
- CONTEXT_KEEP_RECENT_CHARS = 24e3;
11786
- CONTEXT_KEEP_RECENT_MESSAGES = 12;
11787
- CONTEXT_MIN_MESSAGES = 10;
11788
- }
11789
- });
11790
-
11791
- // src/threads/chat-tabs.ts
11792
- function sameWorktreePath(a, b) {
11793
- return normalizeWorktreePath(a) === normalizeWorktreePath(b);
11794
- }
11795
- function takenTeamSlugsForChatTab(worktreePath) {
11796
- const key = normalizeWorktreePath(worktreePath);
11797
- const taken = /* @__PURE__ */ new Set();
11798
- const dir = worktreeNameFromPath(key);
11799
- if (dir) taken.add(dir.toLowerCase());
11800
- for (const sibling of threadsSharingWorktree(key)) {
11801
- for (const slug of takenSlugsFromThread(sibling)) {
11802
- taken.add(slug);
11803
- }
11548
+ async function attachAbort(handle, signal) {
11549
+ if (signal) {
11550
+ const onAbort = () => handle.kill();
11551
+ if (signal.aborted) onAbort();
11552
+ else signal.addEventListener("abort", onAbort, { once: true });
11804
11553
  }
11805
- return [...taken];
11806
- }
11807
- function requireThread(idOrRef) {
11808
- const thread = findThreadByRef(idOrRef) ?? null;
11809
- if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
11810
- return thread;
11554
+ return handle.done;
11811
11555
  }
11812
- function worktreeBindingFrom(from) {
11556
+ async function runSetupScript(repoPath, worktreePath, onLine, opts) {
11557
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
11558
+ if (!settings?.setup) return { ran: false, exitCode: null, source: null };
11559
+ const handle = await spawnWorkspaceScript(settings.setup, {
11560
+ worktreePath,
11561
+ repoPath,
11562
+ defaultBranch: opts?.defaultBranch,
11563
+ onLine
11564
+ });
11565
+ const exitCode = await attachAbort(handle, opts?.signal);
11813
11566
  return {
11814
- worktreePath: normalizeWorktreePath(from.worktreePath),
11815
- repoPath: from.repoPath,
11816
- branchName: from.branchName,
11817
- sourceRef: from.sourceRef,
11818
- // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
11819
- // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
11820
- sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
11821
- sourceIsFork: from.sourceIsFork,
11822
- parentThreadId: from.parentThreadId,
11823
- prUrl: from.prUrl,
11824
- prTitle: from.prTitle,
11825
- prState: from.prState,
11826
- skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
11827
- stackId: from.stackId,
11828
- stackLayer: from.stackLayer
11567
+ ran: true,
11568
+ exitCode,
11569
+ source: workspaceSettingsSourceLabel(worktreePath, repoPath),
11570
+ kill: handle.kill
11829
11571
  };
11830
11572
  }
11831
- function threadsSharingWorktree(worktreePath) {
11832
- if (!worktreePath) return [];
11833
- const key = normalizeWorktreePath(worktreePath);
11834
- return listThreads({ includeArchived: true }).filter(
11835
- (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
11836
- ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
11837
- }
11838
- function formatTranscriptMarkdown(title, messages) {
11839
- const body = formatMessagesAsTranscript(messages, { tools: "full" });
11840
- return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
11841
- }
11842
- function forkMessageSlice(from, throughIndex) {
11843
- const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
11844
- return through >= 0 ? from.messages.slice(0, through + 1) : [];
11845
- }
11846
- function buildForkTranscriptAttachment(baseTitle, messages) {
11847
- const title = baseTitle || "Chat";
11573
+ async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
11574
+ const found = findConventionSetup(worktreePath, repoPath);
11575
+ if (!found) return { ran: false, exitCode: null, source: null };
11576
+ onLine?.(`[setup] ${found.source}`);
11577
+ const handle = await spawnWorkspaceScript(found.command, {
11578
+ worktreePath,
11579
+ repoPath,
11580
+ defaultBranch: opts?.defaultBranch,
11581
+ onLine
11582
+ });
11583
+ const exitCode = await attachAbort(handle, opts?.signal);
11848
11584
  return {
11849
- id: (0, import_node_crypto5.randomUUID)(),
11850
- name: `Transcript of ${title}.md`,
11851
- kind: "transcript",
11852
- content: formatTranscriptMarkdown(title, messages)
11585
+ ran: true,
11586
+ exitCode,
11587
+ source: found.source,
11588
+ kill: handle.kill
11853
11589
  };
11854
11590
  }
11855
- function createChatTab(input) {
11856
- const from = requireThread(input.fromThreadId);
11857
- const binding = worktreeBindingFrom(from);
11858
- const explicitTitle = input.title?.trim();
11859
- const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
11860
- const nextAgent = input.agent ?? from.agent;
11861
- if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
11862
- assertOrchestratorCapableAgent(nextAgent);
11591
+ async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
11592
+ const reviewSkill = ensureReviewSkillFile(worktreePath);
11593
+ if (reviewSkill.wrote) {
11594
+ onLine?.(
11595
+ `[setup] wrote ${REVIEW_SKILL_PATH} \u2014 commit it so later worktrees inherit Review guidelines`
11596
+ );
11863
11597
  }
11864
- const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
11865
- const thread = createEmptyThread({
11866
- title,
11867
- // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
11868
- // syncThreadBranchFromGit would otherwise rewrite every sibling to the
11869
- // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
11870
- userSetTitle: true,
11871
- ...binding,
11872
- // Slack / Brightsy cloud identity stays on the original chat. A + tab
11873
- // that copies `slack:team:user` would steal inbound DMs after you close
11874
- // the connected chat.
11875
- sourceRef: singletonInbox ? title : binding.sourceRef,
11876
- agent: nextAgent,
11877
- model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
11878
- effort: input.effort !== void 0 ? input.effort : from.effort,
11879
- fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
11880
- planMode: from.planMode,
11881
- autonomy: input.autonomy ?? from.autonomy,
11882
- attachments: input.attachments ?? [],
11883
- status: "idle"
11598
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
11599
+ if (!setup.ran) {
11600
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
11601
+ }
11602
+ if (!setup.ran) {
11603
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
11604
+ }
11605
+ return setup;
11606
+ }
11607
+ async function runArchiveScript(repoPath, worktreePath, onLine) {
11608
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
11609
+ if (!settings?.archive) return { ran: false, exitCode: null };
11610
+ const handle = await spawnWorkspaceScript(settings.archive, {
11611
+ worktreePath,
11612
+ repoPath,
11613
+ onLine
11884
11614
  });
11885
- writeThread(thread);
11886
- return thread;
11615
+ const exitCode = await handle.done;
11616
+ return { ran: true, exitCode };
11887
11617
  }
11888
- function forkChatTab(input) {
11889
- const from = requireThread(input.threadId);
11890
- const slice = forkMessageSlice(from, input.throughIndex);
11891
- const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
11892
- const tab = createChatTab({
11893
- fromThreadId: input.threadId,
11894
- agent: input.agent ?? from.agent,
11895
- model: input.model,
11896
- title: input.title?.trim() || void 0,
11897
- attachments: [attachment]
11618
+ function listRunScripts(worktreePath, repoPath) {
11619
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
11620
+ if (!settings?.runScripts.length) return [];
11621
+ return settings.runScripts.filter((s) => {
11622
+ if (!s.availableIn?.length) return true;
11623
+ return s.availableIn.includes("local");
11898
11624
  });
11899
- if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
11900
- const next = { ...tab, parentThreadId: from.id };
11901
- writeThread(next);
11902
- return next;
11903
- }
11904
- return tab;
11905
11625
  }
11906
- var import_node_crypto5;
11907
- var init_chat_tabs = __esm({
11908
- "src/threads/chat-tabs.ts"() {
11909
- "use strict";
11910
- import_node_crypto5 = require("crypto");
11911
- init_context_compact();
11912
- init_teams();
11913
- init_worktree_labels();
11914
- init_global_workspace();
11915
- init_orchestrator_capable();
11916
- init_thread_store();
11917
- init_worktree_labels();
11918
- }
11919
- });
11920
-
11921
- // src/store/turn-live.ts
11922
- function trimParts(parts) {
11923
- if (parts.length <= MAX_PARTS) return parts;
11924
- return parts.slice(-Math.floor(MAX_PARTS / 2));
11626
+ function getDefaultRunScript(worktreePath, repoPath) {
11627
+ const scripts = listRunScripts(worktreePath, repoPath);
11628
+ if (!scripts.length) return null;
11629
+ return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
11925
11630
  }
11926
- function lastText(parts, type) {
11927
- for (let i = parts.length - 1; i >= 0; i--) {
11928
- const p = parts[i];
11929
- if (p.type === type && p.text.trim()) return p.text.trim();
11631
+ function getRunScript(worktreePath, repoPath, name) {
11632
+ const scripts = listRunScripts(worktreePath, repoPath);
11633
+ if (!scripts.length) return null;
11634
+ if (name) {
11635
+ return scripts.find((s) => s.name === name) ?? null;
11930
11636
  }
11931
- return "";
11637
+ return getDefaultRunScript(worktreePath, repoPath);
11932
11638
  }
11933
- function summarizeTurnLive(parts) {
11934
- const tools = parts.filter(
11935
- (p) => p.type === "tool"
11936
- );
11937
- const lastTool = tools[tools.length - 1];
11938
- const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
11939
- const running = lastTool?.status === "running";
11940
- const thinking = lastText(parts, "thinking");
11941
- const text4 = lastText(parts, "text");
11942
- const excerptRaw = thinking || text4;
11943
- const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
11944
- let summary = "Working\u2026";
11945
- if (lastToolLabel) {
11946
- const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
11947
- summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
11948
- } else if (thinking) {
11949
- summary = "Thinking\u2026";
11950
- } else if (text4) {
11951
- summary = "Writing reply\u2026";
11639
+ function getRunMode(worktreePath, repoPath) {
11640
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
11641
+ }
11642
+ async function allocatePort() {
11643
+ return new Promise((resolve, reject) => {
11644
+ const server = (0, import_node_net.createServer)();
11645
+ server.listen(0, "127.0.0.1", () => {
11646
+ const addr = server.address();
11647
+ if (!addr || typeof addr === "string") {
11648
+ server.close();
11649
+ reject(new Error("Failed to allocate port"));
11650
+ return;
11651
+ }
11652
+ const port = addr.port;
11653
+ server.close((err) => err ? reject(err) : resolve(port));
11654
+ });
11655
+ server.on("error", reject);
11656
+ });
11657
+ }
11658
+ async function allocatePortRange(size = PORT_RANGE_SIZE) {
11659
+ const base = await allocatePort();
11660
+ const ports = [base];
11661
+ for (let i = 1; i < size; i++) {
11662
+ const candidate = base + i;
11663
+ const free = await new Promise((resolve) => {
11664
+ const server = (0, import_node_net.createServer)();
11665
+ server.once("error", () => resolve(false));
11666
+ server.listen(candidate, "127.0.0.1", () => {
11667
+ server.close(() => resolve(true));
11668
+ });
11669
+ });
11670
+ if (free) {
11671
+ ports.push(candidate);
11672
+ } else {
11673
+ ports.push(await allocatePort());
11674
+ }
11952
11675
  }
11676
+ return ports;
11677
+ }
11678
+ async function startDevServer(repoPath, worktreePath, onLine, opts) {
11679
+ const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
11680
+ if (!script) return null;
11681
+ const ports = await allocatePortRange(PORT_RANGE_SIZE);
11682
+ const handle = await spawnWorkspaceScript(script.command, {
11683
+ worktreePath,
11684
+ repoPath,
11685
+ ports,
11686
+ defaultBranch: opts?.defaultBranch,
11687
+ onLine
11688
+ });
11953
11689
  return {
11954
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11955
- summary,
11956
- lastTool: lastToolLabel,
11957
- toolCount: tools.length,
11958
- excerpt
11690
+ pid: handle.pid,
11691
+ port: ports[0],
11692
+ ports,
11693
+ scriptName: script.name,
11694
+ kill: handle.kill,
11695
+ done: handle.done
11959
11696
  };
11960
11697
  }
11961
- function flush(threadId) {
11962
- const buf = buffers.get(threadId);
11963
- if (!buf) return;
11964
- if (buf.timer) {
11965
- clearTimeout(buf.timer);
11966
- buf.timer = null;
11698
+ var import_node_fs30, import_node_net, import_node_path29, import_execa4, import_node_readline3, PORT_RANGE_SIZE, cachedLoginEnv;
11699
+ var init_conductor = __esm({
11700
+ "src/hook/conductor.ts"() {
11701
+ "use strict";
11702
+ import_node_fs30 = require("fs");
11703
+ import_node_net = require("net");
11704
+ import_node_path29 = require("path");
11705
+ import_execa4 = require("execa");
11706
+ import_node_readline3 = require("readline");
11707
+ init_settings();
11708
+ init_nested_electron_env();
11709
+ init_convention_setup();
11710
+ init_cursor_worktrees();
11711
+ init_git_auth_mode();
11712
+ init_request_review();
11713
+ init_nested_electron_env();
11714
+ PORT_RANGE_SIZE = 10;
11715
+ cachedLoginEnv = null;
11967
11716
  }
11968
- writeTurnLive(threadId, summarizeTurnLive(buf.parts));
11969
- }
11970
- function scheduleFlush(threadId) {
11971
- const buf = buffers.get(threadId);
11972
- if (!buf) return;
11973
- if (buf.timer) return;
11974
- buf.timer = setTimeout(() => {
11975
- const live = buffers.get(threadId);
11976
- if (live) live.timer = null;
11977
- flush(threadId);
11978
- }, FLUSH_MS);
11717
+ });
11718
+
11719
+ // src/git/orphan-cleanup.ts
11720
+ function isSideboardWorktreePath(path2) {
11721
+ return path2.includes("/.sideboard/worktrees/") || path2.includes("/sideboard/workspaces/");
11979
11722
  }
11980
- function noteTurnLiveEvent(threadId, event) {
11981
- if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
11982
- return;
11723
+ async function findOrphanWorktrees(repoPaths) {
11724
+ const threads = listThreads({ includeArchived: true });
11725
+ const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
11726
+ const repos = new Set(
11727
+ repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
11728
+ );
11729
+ const homeRoot = sideboardWorkspacesDir();
11730
+ if ((0, import_node_fs31.existsSync)(homeRoot)) {
11731
+ try {
11732
+ for (const entry of (0, import_node_fs31.readdirSync)(homeRoot, { withFileTypes: true })) {
11733
+ if (!entry.isDirectory()) continue;
11734
+ void entry;
11735
+ }
11736
+ } catch {
11737
+ }
11983
11738
  }
11984
- const prev = buffers.get(threadId) ?? { parts: [], timer: null };
11985
- prev.parts = trimParts(applyAgentEvent(prev.parts, event));
11986
- buffers.set(threadId, prev);
11987
- if (event.type === "exit") {
11988
- flush(threadId);
11989
- const cur = buffers.get(threadId);
11990
- if (cur?.timer) clearTimeout(cur.timer);
11991
- buffers.delete(threadId);
11992
- return;
11739
+ const orphans = [];
11740
+ const seen = /* @__PURE__ */ new Set();
11741
+ for (const repoPath of repos) {
11742
+ if (!repoPath || !(0, import_node_fs31.existsSync)(repoPath)) continue;
11743
+ try {
11744
+ const wts = await listWorktrees(repoPath);
11745
+ for (const wt of wts) {
11746
+ const path2 = wt.path.replace(/\/$/, "");
11747
+ if (!isSideboardWorktreePath(path2)) continue;
11748
+ if (known.has(path2)) continue;
11749
+ if (seen.has(path2)) continue;
11750
+ seen.add(path2);
11751
+ let mtimeMs = 0;
11752
+ try {
11753
+ mtimeMs = (0, import_node_fs31.statSync)(path2).mtimeMs;
11754
+ } catch {
11755
+ mtimeMs = 0;
11756
+ }
11757
+ orphans.push({ path: path2, repoPath, mtimeMs });
11758
+ }
11759
+ } catch {
11760
+ }
11761
+ try {
11762
+ const root = worktreesRoot(repoPath);
11763
+ if ((0, import_node_fs31.existsSync)(root)) {
11764
+ for (const entry of (0, import_node_fs31.readdirSync)(root, { withFileTypes: true })) {
11765
+ if (!entry.isDirectory()) continue;
11766
+ const path2 = (0, import_node_path30.join)(root, entry.name).replace(/\/$/, "");
11767
+ if (known.has(path2) || seen.has(path2)) continue;
11768
+ if (!(0, import_node_fs31.existsSync)((0, import_node_path30.join)(path2, ".git"))) continue;
11769
+ seen.add(path2);
11770
+ let mtimeMs = 0;
11771
+ try {
11772
+ mtimeMs = (0, import_node_fs31.statSync)(path2).mtimeMs;
11773
+ } catch {
11774
+ mtimeMs = Date.now();
11775
+ }
11776
+ orphans.push({ path: path2, repoPath, mtimeMs });
11777
+ }
11778
+ }
11779
+ } catch {
11780
+ }
11993
11781
  }
11994
- const immediate = event.type === "tool_use" || event.type === "tool_result";
11995
- if (immediate) flush(threadId);
11996
- else scheduleFlush(threadId);
11782
+ return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
11997
11783
  }
11998
- function writeTurnLive(threadId, progress) {
11999
- const path2 = threadLivePath(threadId);
12000
- const tmp = `${path2}.${process.pid}.tmp`;
12001
- try {
12002
- (0, import_node_fs34.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
12003
- (0, import_node_fs34.renameSync)(tmp, path2);
12004
- } catch {
11784
+ async function cleanupOrphanWorktrees(opts) {
11785
+ const settings = loadAppSettings();
11786
+ const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
11787
+ const orphans = await findOrphanWorktrees(opts?.repoPaths);
11788
+ const activeCount = listThreads().filter((t) => t.status !== "archived").length;
11789
+ const budget = Math.max(0, maxCount - activeCount);
11790
+ const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
11791
+ const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
11792
+ const removed = [];
11793
+ const kept = [];
11794
+ for (const orphan of orphans) {
11795
+ if (keep.has(orphan.path)) {
11796
+ kept.push(orphan.path);
11797
+ continue;
11798
+ }
11799
+ if (opts?.dryRun) {
11800
+ removed.push(orphan.path);
11801
+ continue;
11802
+ }
12005
11803
  try {
12006
- (0, import_node_fs34.unlinkSync)(tmp);
11804
+ await removeWorktree(orphan.repoPath, orphan.path);
11805
+ removed.push(orphan.path);
12007
11806
  } catch {
11807
+ kept.push(orphan.path);
12008
11808
  }
12009
11809
  }
12010
- }
12011
- function readTurnLive(threadId) {
12012
- const path2 = threadLivePath(threadId);
12013
- if (!(0, import_node_fs34.existsSync)(path2)) return null;
12014
- try {
12015
- const raw = JSON.parse((0, import_node_fs34.readFileSync)(path2, "utf8"));
12016
- if (!raw || typeof raw.summary !== "string") return null;
12017
- return raw;
12018
- } catch {
12019
- return null;
11810
+ if (!opts?.dryRun) {
11811
+ updateAdvancedSettings({
11812
+ worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
11813
+ });
12020
11814
  }
11815
+ return { removed, kept, orphans };
12021
11816
  }
12022
- function clearTurnLive(threadId) {
12023
- const buf = buffers.get(threadId);
12024
- if (buf?.timer) clearTimeout(buf.timer);
12025
- buffers.delete(threadId);
12026
- const path2 = threadLivePath(threadId);
12027
- if (!(0, import_node_fs34.existsSync)(path2)) return;
12028
- try {
12029
- (0, import_node_fs34.unlinkSync)(path2);
12030
- } catch {
12031
- }
11817
+ function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
11818
+ const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
11819
+ const last = settings.advanced.worktreeLastCleanupAt;
11820
+ if (!last) return true;
11821
+ const elapsed = Date.now() - Date.parse(last);
11822
+ return elapsed >= intervalHours * 36e5;
12032
11823
  }
12033
- var import_node_fs34, buffers, FLUSH_MS, MAX_PARTS;
12034
- var init_turn_live = __esm({
12035
- "src/store/turn-live.ts"() {
11824
+ function worktreeCleanupSettings() {
11825
+ const a = loadAppSettings().advanced;
11826
+ return {
11827
+ worktreeMaxCount: a.worktreeMaxCount,
11828
+ worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
11829
+ worktreeLastCleanupAt: a.worktreeLastCleanupAt,
11830
+ autoCleanupOrphans: a.autoCleanupOrphans
11831
+ };
11832
+ }
11833
+ var import_node_fs31, import_node_path30;
11834
+ var init_orphan_cleanup = __esm({
11835
+ "src/git/orphan-cleanup.ts"() {
12036
11836
  "use strict";
12037
- import_node_fs34 = require("fs");
12038
- init_message_parts();
11837
+ import_node_fs31 = require("fs");
11838
+ import_node_path30 = require("path");
11839
+ init_worktree();
11840
+ init_thread_store();
12039
11841
  init_paths();
12040
- buffers = /* @__PURE__ */ new Map();
12041
- FLUSH_MS = 800;
12042
- MAX_PARTS = 80;
11842
+ init_app_settings();
12043
11843
  }
12044
11844
  });
12045
11845
 
12046
- // src/review/review-request-template.ts
12047
- var REVIEW_REQUEST_TEMPLATE;
12048
- var init_review_request_template = __esm({
12049
- "src/review/review-request-template.ts"() {
12050
- "use strict";
12051
- REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
12052
-
12053
- 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.
12054
-
12055
- ## Required outcome
12056
-
12057
- Start your reply with a **Recommendation** section using exactly one of:
12058
-
12059
- - **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
12060
- - **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
12061
- - **Request changes** \u2014 not ready; blocking issues must be fixed first.
12062
- - **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
12063
-
12064
- 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.
12065
-
12066
- People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
12067
-
12068
- ## Findings
12069
-
12070
- Below are guidelines for determining whether an issue is worth flagging to the original author.
12071
-
12072
- These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
12073
-
12074
- Flag something as a bug / blocking finding only when:
12075
-
12076
- 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
12077
- 2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
12078
- 3. Fixing it does not demand rigor absent from the rest of the codebase.
12079
- 4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
12080
- 5. The author would likely fix it if made aware.
12081
- 6. It does not rely on unstated assumptions about the codebase or author intent.
12082
- 7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
12083
- 8. It is clearly not just an intentional change by the author.
12084
-
12085
- When flagging an issue, include a short accompanying comment:
12086
-
12087
- 1. Clear about why it is a problem.
12088
- 2. Severity must match reality \u2014 do not inflate.
12089
- 3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
12090
- 4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
12091
- 5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
12092
- 6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
12093
- 7. Skimmable on first read.
12094
- 8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
12095
-
12096
- HOW MANY FINDINGS TO RETURN:
12097
-
12098
- 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.
12099
-
12100
- GUIDELINES:
12101
-
12102
- - Ignore trivial style unless it obscures meaning or violates documented standards.
12103
- - One comment per distinct issue (or a short multi-line range if needed).
12104
- - Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
12105
- - In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
12106
- - Do NOT introduce or remove outer indentation levels unless that is the actual fix.
12107
- - Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
12108
-
12109
- 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).
12110
-
12111
- ## Getting the diff
12112
-
12113
- 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.
12114
-
12115
- 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.
12116
-
12117
- ## Fallback: if you don't have access to the Sideboard diff tool
12118
-
12119
- If you don't have access to \`get_diff\`, use the following git commands to get the diff:
12120
-
12121
- \`\`\`bash
12122
- # Get the merge base between this branch and the target
12123
- MERGE_BASE=$(git merge-base origin/main HEAD)
12124
-
12125
- # Get the committed diff against the merge base
12126
- git diff $MERGE_BASE HEAD
12127
-
12128
- # Get any uncommitted changes (staged and unstaged)
12129
- git diff HEAD
12130
- \`\`\`
12131
-
12132
- 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.
12133
-
12134
- No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
12135
-
12136
- ## Output format
12137
-
12138
- **1. Recommendation first** (required), then **2. Findings** (may be empty).
12139
-
12140
- Only report ONE finding per unique issue.
12141
-
12142
- <example>
12143
- ## Recommendation
12144
-
12145
- **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.
12146
-
12147
- ## Findings
12148
-
12149
- ### **#1 Empty input causes crash** (blocking)
12150
-
12151
- If the input field is empty when the page loads, the app will crash.
12152
-
12153
- File: src/client/frontends/desktop/ui/Input.tsx
12154
-
12155
- ### **#2 Dead code** (nit)
12156
-
12157
- The getUserData function is now unused. It should be deleted.
12158
-
12159
- File: src/client/frontends/desktop/core/UserData.ts
12160
- </example>
11846
+ // src/git/apply-into-main.ts
11847
+ async function applyThreadIntoMain(thread, opts) {
11848
+ const method = opts?.method ?? "merge";
11849
+ const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
11850
+ const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
11851
+ cwd: thread.repoPath,
11852
+ reject: false
11853
+ });
11854
+ if (status.stdout.trim()) {
11855
+ throw new Error(
11856
+ `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
11857
+ );
11858
+ }
11859
+ const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
11860
+ cwd: thread.repoPath,
11861
+ reject: false
11862
+ });
11863
+ if (checkout.exitCode !== 0) {
11864
+ throw new Error(
11865
+ `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
11866
+ );
11867
+ }
11868
+ if (method === "cherry-pick") {
11869
+ const log = await (0, import_execa5.execa)(
11870
+ "git",
11871
+ ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
11872
+ { cwd: thread.repoPath, reject: false }
11873
+ );
11874
+ const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
11875
+ if (!shas.length) {
11876
+ return {
11877
+ applied: false,
11878
+ method,
11879
+ targetBranch: target,
11880
+ message: `No commits on ${thread.branchName} not already in ${target}`
11881
+ };
11882
+ }
11883
+ for (const sha of shas) {
11884
+ const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
11885
+ cwd: thread.repoPath,
11886
+ reject: false
11887
+ });
11888
+ if (cp.exitCode !== 0) {
11889
+ await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
11890
+ cwd: thread.repoPath,
11891
+ reject: false
11892
+ });
11893
+ throw new Error(
11894
+ `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
11895
+ );
11896
+ }
11897
+ }
11898
+ return {
11899
+ applied: true,
11900
+ method,
11901
+ targetBranch: target,
11902
+ message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
11903
+ };
11904
+ }
11905
+ const merge = await (0, import_execa5.execa)(
11906
+ "git",
11907
+ ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
11908
+ { cwd: thread.repoPath, reject: false }
11909
+ );
11910
+ if (merge.exitCode !== 0) {
11911
+ await (0, import_execa5.execa)("git", ["merge", "--abort"], {
11912
+ cwd: thread.repoPath,
11913
+ reject: false
11914
+ });
11915
+ throw new Error(
11916
+ `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
11917
+ );
11918
+ }
11919
+ return {
11920
+ applied: true,
11921
+ method: "merge",
11922
+ targetBranch: target,
11923
+ message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
11924
+ };
11925
+ }
11926
+ var import_execa5;
11927
+ var init_apply_into_main = __esm({
11928
+ "src/git/apply-into-main.ts"() {
11929
+ "use strict";
11930
+ import_execa5 = require("execa");
11931
+ init_worktree();
11932
+ }
11933
+ });
12161
11934
 
12162
- <example>
12163
- ## Recommendation
11935
+ // src/store/workspaces.ts
11936
+ var workspaces_exports = {};
11937
+ __export(workspaces_exports, {
11938
+ addWorkspace: () => addWorkspace,
11939
+ ensureWorkspace: () => ensureWorkspace,
11940
+ listWorkspaces: () => listWorkspaces,
11941
+ removeWorkspace: () => removeWorkspace,
11942
+ syncWorkspacesFromThreads: () => syncWorkspacesFromThreads
11943
+ });
11944
+ function workspacesFile() {
11945
+ return (0, import_node_path31.join)(appDataDir(), "workspaces.json");
11946
+ }
11947
+ function removedWorkspacesFile() {
11948
+ return (0, import_node_path31.join)(appDataDir(), "removed-workspaces.json");
11949
+ }
11950
+ function readAll2() {
11951
+ const path2 = workspacesFile();
11952
+ if (!(0, import_node_fs32.existsSync)(path2)) return [];
11953
+ try {
11954
+ const raw = JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8"));
11955
+ return Array.isArray(raw) ? raw : [];
11956
+ } catch {
11957
+ return [];
11958
+ }
11959
+ }
11960
+ function writeAll2(list) {
11961
+ (0, import_node_fs32.mkdirSync)(appDataDir(), { recursive: true });
11962
+ (0, import_node_fs32.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
11963
+ }
11964
+ function readRemoved() {
11965
+ const path2 = removedWorkspacesFile();
11966
+ if (!(0, import_node_fs32.existsSync)(path2)) return /* @__PURE__ */ new Set();
11967
+ try {
11968
+ const raw = JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8"));
11969
+ return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
11970
+ } catch {
11971
+ return /* @__PURE__ */ new Set();
11972
+ }
11973
+ }
11974
+ function writeRemoved(paths) {
11975
+ (0, import_node_fs32.mkdirSync)(appDataDir(), { recursive: true });
11976
+ (0, import_node_fs32.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
11977
+ }
11978
+ function rememberRemoved(repoPath) {
11979
+ const next = readRemoved();
11980
+ next.add(repoPath);
11981
+ writeRemoved(next);
11982
+ }
11983
+ function forgetRemoved(repoPath) {
11984
+ const next = readRemoved();
11985
+ if (!next.delete(repoPath)) return;
11986
+ writeRemoved(next);
11987
+ }
11988
+ function listWorkspaces() {
11989
+ const all = readAll2();
11990
+ const valid = all.filter(
11991
+ (w) => Boolean(w.path) && w.path !== "/" && w.path !== "." && !isGlobalRepoPath(w.path)
11992
+ );
11993
+ if (valid.length !== all.length) writeAll2(valid);
11994
+ return valid.sort((a, b) => a.name.localeCompare(b.name));
11995
+ }
11996
+ async function addWorkspace(repoPath) {
11997
+ const root = await resolveRepoRoot(repoPath);
11998
+ if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
11999
+ if (!(0, import_node_fs32.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
12000
+ forgetRemoved(root);
12001
+ await ensureGhPreferOrigin(root);
12002
+ const current = readAll2();
12003
+ const existing = current.find((w) => w.path === root);
12004
+ if (existing) return existing;
12005
+ const next = {
12006
+ path: root,
12007
+ name: (0, import_node_path31.basename)(root),
12008
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
12009
+ };
12010
+ writeAll2([...current, next]);
12011
+ return next;
12012
+ }
12013
+ function removeWorkspace(repoPath) {
12014
+ writeAll2(readAll2().filter((w) => w.path !== repoPath));
12015
+ rememberRemoved(repoPath);
12016
+ }
12017
+ async function ensureWorkspace(repoPath) {
12018
+ return addWorkspace(repoPath);
12019
+ }
12020
+ function syncWorkspacesFromThreads(repoPaths) {
12021
+ const current = readAll2();
12022
+ const removed = readRemoved();
12023
+ const byPath = new Map(current.map((w) => [w.path, w]));
12024
+ let dirty = false;
12025
+ for (const path2 of repoPaths) {
12026
+ if (!path2 || path2 === "/" || isGlobalRepoPath(path2) || byPath.has(path2) || removed.has(path2)) {
12027
+ continue;
12028
+ }
12029
+ if (!(0, import_node_fs32.existsSync)(path2)) continue;
12030
+ const ws = {
12031
+ path: path2,
12032
+ name: (0, import_node_path31.basename)(path2),
12033
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
12034
+ };
12035
+ byPath.set(path2, ws);
12036
+ dirty = true;
12037
+ }
12038
+ const next = [...byPath.values()];
12039
+ if (dirty) writeAll2(next);
12040
+ return next.sort((a, b) => a.name.localeCompare(b.name));
12041
+ }
12042
+ var import_node_fs32, import_node_path31;
12043
+ var init_workspaces2 = __esm({
12044
+ "src/store/workspaces.ts"() {
12045
+ "use strict";
12046
+ import_node_fs32 = require("fs");
12047
+ import_node_path31 = require("path");
12048
+ init_paths();
12049
+ init_global_workspace();
12050
+ init_worktree();
12051
+ }
12052
+ });
12164
12053
 
12165
- **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
12166
- </example>
12054
+ // src/git/clone-repo.ts
12055
+ async function cloneRepoIntoSideboard(opts) {
12056
+ const url = opts.url.trim();
12057
+ if (!url) throw new Error("Clone URL is required");
12058
+ let name = opts.name?.trim();
12059
+ if (!name) {
12060
+ const leaf = (0, import_node_path32.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
12061
+ name = leaf || "repo";
12062
+ }
12063
+ name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
12064
+ const dest = (0, import_node_path32.join)(sideboardReposDir(), name);
12065
+ if ((0, import_node_fs33.existsSync)(dest)) {
12066
+ const repoPath2 = await resolveRepoRoot(dest);
12067
+ const workspace2 = await ensureWorkspace(repoPath2);
12068
+ return { repoPath: repoPath2, workspace: workspace2 };
12069
+ }
12070
+ const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
12071
+ if (result.exitCode !== 0) {
12072
+ throw new Error(
12073
+ `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
12074
+ );
12075
+ }
12076
+ const repoPath = await resolveRepoRoot(dest);
12077
+ const workspace = await ensureWorkspace(repoPath);
12078
+ return { repoPath, workspace };
12079
+ }
12080
+ var import_node_fs33, import_node_path32, import_execa6;
12081
+ var init_clone_repo = __esm({
12082
+ "src/git/clone-repo.ts"() {
12083
+ "use strict";
12084
+ import_node_fs33 = require("fs");
12085
+ import_node_path32 = require("path");
12086
+ import_execa6 = require("execa");
12087
+ init_paths();
12088
+ init_workspaces2();
12089
+ init_worktree();
12090
+ }
12091
+ });
12167
12092
 
12168
- ## Growing the rules
12093
+ // src/detect/detect.ts
12094
+ async function detectAgents() {
12095
+ ensureAgentPath();
12096
+ enrichPathWithNpmGlobalBin();
12097
+ return Promise.all(allAdapters().map((a) => a.detect()));
12098
+ }
12099
+ async function requireAgent(agent, opts) {
12100
+ ensureAgentPath();
12101
+ enrichPathWithNpmGlobalBin();
12102
+ const status = await Promise.race([
12103
+ getAdapter(agent).detect(),
12104
+ new Promise((_, reject) => {
12105
+ setTimeout(() => {
12106
+ reject(
12107
+ new Error(
12108
+ `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
12109
+ )
12110
+ );
12111
+ }, REQUIRE_AGENT_TIMEOUT_MS);
12112
+ })
12113
+ ]);
12114
+ if (!status.installed || !status.authenticated) {
12115
+ throw new Error(status.reason ?? `${agent} is not available`);
12116
+ }
12117
+ if (opts?.requireLinear && !status.linearMcp) {
12118
+ throw new Error(
12119
+ `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
12120
+ );
12121
+ }
12122
+ return status;
12123
+ }
12124
+ var REQUIRE_AGENT_TIMEOUT_MS;
12125
+ var init_detect = __esm({
12126
+ "src/detect/detect.ts"() {
12127
+ "use strict";
12128
+ init_agents();
12129
+ REQUIRE_AGENT_TIMEOUT_MS = 8e3;
12130
+ }
12131
+ });
12169
12132
 
12170
- 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\`.
12171
- `;
12133
+ // src/threads/create.ts
12134
+ async function createThread(input, _onSetupLine) {
12135
+ const resolved = resolveNewThreadOptions({
12136
+ agent: input.agent,
12137
+ model: input.model,
12138
+ effort: input.effort,
12139
+ fast: input.fast
12140
+ });
12141
+ await requireAgent(resolved.agent);
12142
+ const repoPath = await resolveRepoRoot(input.repoPath);
12143
+ if (!(0, import_node_fs34.existsSync)(repoPath)) {
12144
+ throw new Error(`Repo not found: ${repoPath}`);
12145
+ }
12146
+ let sourceRef = input.sourceRef;
12147
+ let sourceIsFork = false;
12148
+ let prUrl = null;
12149
+ let prTitle = null;
12150
+ if (input.sourceType === "pr") {
12151
+ const num2 = Number(input.sourceRef.replace(/^#/, ""));
12152
+ if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
12153
+ const pr = await getPr(repoPath, num2);
12154
+ if (!pr) throw new Error(`PR #${num2} not found`);
12155
+ sourceIsFork = pr.isCrossRepository;
12156
+ prUrl = pr.url;
12157
+ const localFetchBranch = `sideboard-pr-${pr.number}`;
12158
+ await fetchPrHead(repoPath, pr.number, localFetchBranch);
12159
+ sourceRef = localFetchBranch;
12160
+ } else if (input.sourceType === "ticket") {
12161
+ sourceRef = await resolveDefaultBranch(repoPath);
12162
+ } else if (input.sourceType === "branch") {
12163
+ if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
12164
+ sourceRef = await resolveDefaultBranch(repoPath);
12165
+ } else {
12166
+ const existing = await getPrForHeadBranch(repoPath, sourceRef);
12167
+ if (existing?.url) {
12168
+ prUrl = existing.url;
12169
+ prTitle = existing.title;
12170
+ }
12171
+ }
12172
+ } else if (input.sourceType === "adopt") {
12173
+ throw new Error("Use adoptThread() for adopt sources");
12174
+ }
12175
+ const team = allocateTeamSlug(repoPath);
12176
+ const { branchName, worktreePath } = await createThreadWorktree({
12177
+ repoPath,
12178
+ sourceRef,
12179
+ slug: team.slug
12180
+ });
12181
+ copyConfiguredFiles(repoPath, worktreePath);
12182
+ const explicitTitle = input.title?.trim();
12183
+ const thread = createEmptyThread({
12184
+ title: explicitTitle || team.name,
12185
+ userSetTitle: Boolean(explicitTitle),
12186
+ sourceType: input.sourceType,
12187
+ sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
12188
+ branchName,
12189
+ worktreePath,
12190
+ repoPath,
12191
+ agent: resolved.agent,
12192
+ autonomy: input.autonomy ?? "default",
12193
+ model: resolved.model,
12194
+ effort: resolved.effort,
12195
+ fast: resolved.fast,
12196
+ planMode: Boolean(input.planMode),
12197
+ attachments: input.attachments ?? [],
12198
+ sourceIsFork,
12199
+ parentThreadId: input.parentThreadId ?? null,
12200
+ status: "idle",
12201
+ prUrl,
12202
+ prTitle
12203
+ });
12204
+ writeThread(thread);
12205
+ await ensureWorkspace(repoPath);
12206
+ return readThread(thread.id) ?? thread;
12207
+ }
12208
+ async function listLinearIssues(agent, repoPath) {
12209
+ const { getAdapter: getAdapter2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
12210
+ await requireAgent(agent, { requireLinear: true });
12211
+ const adapter = getAdapter2(agent);
12212
+ if (!adapter.listLinearIssues) {
12213
+ throw new Error(`${agent} does not support Linear issue listing`);
12214
+ }
12215
+ return adapter.listLinearIssues(repoPath);
12216
+ }
12217
+ var import_node_fs34;
12218
+ var init_create = __esm({
12219
+ "src/threads/create.ts"() {
12220
+ "use strict";
12221
+ import_node_fs34 = require("fs");
12222
+ init_detect();
12223
+ init_worktree();
12224
+ init_conductor();
12225
+ init_app_settings();
12226
+ init_thread_store();
12227
+ init_workspaces2();
12172
12228
  }
12173
12229
  });
12174
12230
 
12175
- // src/review/request-review.ts
12176
- function shouldRefreshReviewRequestTemplate(content) {
12177
- const trimmed = content.trim();
12178
- if (!trimmed) return true;
12179
- if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
12180
- return false;
12181
- }
12182
- return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
12231
+ // src/store/turn-live.ts
12232
+ function trimParts(parts) {
12233
+ if (parts.length <= MAX_PARTS) return parts;
12234
+ return parts.slice(-Math.floor(MAX_PARTS / 2));
12183
12235
  }
12184
- function readTextIfPresent(abs) {
12185
- if (!(0, import_node_fs35.existsSync)(abs)) return null;
12186
- try {
12187
- const content = (0, import_node_fs35.readFileSync)(abs, "utf8");
12188
- return content.trim() ? content : null;
12189
- } catch {
12190
- return null;
12236
+ function lastText(parts, type) {
12237
+ for (let i = parts.length - 1; i >= 0; i--) {
12238
+ const p = parts[i];
12239
+ if (p.type === type && p.text.trim()) return p.text.trim();
12191
12240
  }
12241
+ return "";
12192
12242
  }
12193
- function ensureAttachmentsGitignore(worktreePath) {
12194
- const gitignoreAbs = (0, import_node_path32.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
12195
- if ((0, import_node_fs35.existsSync)(gitignoreAbs)) return;
12196
- (0, import_node_fs35.mkdirSync)((0, import_node_path32.dirname)(gitignoreAbs), { recursive: true });
12197
- (0, import_node_fs35.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
12198
- }
12199
- function resolveReviewGuidelines(worktreePath) {
12200
- const repoAbs = (0, import_node_path32.join)(worktreePath, REPO_REVIEW_PATH);
12201
- const repoContent = readTextIfPresent(repoAbs);
12202
- if (repoContent) {
12203
- return {
12204
- path: REPO_REVIEW_PATH,
12205
- name: REPO_REVIEW_NAME,
12206
- content: repoContent,
12207
- source: "repo"
12208
- };
12209
- }
12210
- const localAbs = (0, import_node_path32.join)(worktreePath, REVIEW_REQUEST_PATH);
12211
- const localContent = readTextIfPresent(localAbs);
12212
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
12213
- return {
12214
- path: REVIEW_REQUEST_PATH,
12215
- name: REVIEW_REQUEST_NAME,
12216
- content: localContent,
12217
- source: "local"
12218
- };
12219
- }
12220
- const legacyAbs = (0, import_node_path32.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
12221
- const legacyContent = readTextIfPresent(legacyAbs);
12222
- if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
12223
- return {
12224
- path: LEGACY_REVIEW_REQUEST_PATH,
12225
- name: REVIEW_REQUEST_NAME,
12226
- content: legacyContent,
12227
- source: "local"
12228
- };
12243
+ function summarizeTurnLive(parts) {
12244
+ const tools = parts.filter(
12245
+ (p) => p.type === "tool"
12246
+ );
12247
+ const lastTool = tools[tools.length - 1];
12248
+ const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
12249
+ const running = lastTool?.status === "running";
12250
+ const thinking = lastText(parts, "thinking");
12251
+ const text4 = lastText(parts, "text");
12252
+ const excerptRaw = thinking || text4;
12253
+ const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
12254
+ let summary = "Working\u2026";
12255
+ if (lastToolLabel) {
12256
+ const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
12257
+ summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
12258
+ } else if (thinking) {
12259
+ summary = "Thinking\u2026";
12260
+ } else if (text4) {
12261
+ summary = "Writing reply\u2026";
12229
12262
  }
12230
- ensureAttachmentsGitignore(worktreePath);
12231
- (0, import_node_fs35.mkdirSync)((0, import_node_path32.dirname)(localAbs), { recursive: true });
12232
- (0, import_node_fs35.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
12233
12263
  return {
12234
- path: REVIEW_REQUEST_PATH,
12235
- name: REVIEW_REQUEST_NAME,
12236
- content: REVIEW_REQUEST_TEMPLATE,
12237
- source: "stock"
12264
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
12265
+ summary,
12266
+ lastTool: lastToolLabel,
12267
+ toolCount: tools.length,
12268
+ excerpt
12238
12269
  };
12239
12270
  }
12240
- function ensureReviewRequestFile(worktreePath) {
12241
- const repoAbs = (0, import_node_path32.join)(worktreePath, REPO_REVIEW_PATH);
12242
- const repoContent = readTextIfPresent(repoAbs);
12243
- if (repoContent) {
12244
- return {
12245
- path: REPO_REVIEW_PATH,
12246
- name: REPO_REVIEW_NAME,
12247
- content: repoContent,
12248
- source: "repo"
12249
- };
12250
- }
12251
- const localAbs = (0, import_node_path32.join)(worktreePath, REVIEW_REQUEST_PATH);
12252
- const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent((0, import_node_path32.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
12253
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
12254
- const path2 = (0, import_node_fs35.existsSync)(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
12255
- return {
12256
- path: path2,
12257
- name: REVIEW_REQUEST_NAME,
12258
- content: localContent,
12259
- source: "local"
12260
- };
12271
+ function flush(threadId) {
12272
+ const buf = buffers.get(threadId);
12273
+ if (!buf) return;
12274
+ if (buf.timer) {
12275
+ clearTimeout(buf.timer);
12276
+ buf.timer = null;
12261
12277
  }
12262
- (0, import_node_fs35.mkdirSync)((0, import_node_path32.dirname)(repoAbs), { recursive: true });
12263
- (0, import_node_fs35.writeFileSync)(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
12264
- return {
12265
- path: REPO_REVIEW_PATH,
12266
- name: REPO_REVIEW_NAME,
12267
- content: REVIEW_REQUEST_TEMPLATE,
12268
- source: "repo"
12269
- };
12278
+ writeTurnLive(threadId, summarizeTurnLive(buf.parts));
12270
12279
  }
12271
- function buildReviewRequestAttachment(content, opts) {
12272
- const path2 = opts?.path ?? REVIEW_REQUEST_PATH;
12273
- const name = opts?.name ?? (path2 === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
12274
- return {
12275
- id: (0, import_node_crypto6.randomUUID)(),
12276
- name,
12277
- kind: "file",
12278
- path: path2,
12279
- content
12280
- };
12280
+ function scheduleFlush(threadId) {
12281
+ const buf = buffers.get(threadId);
12282
+ if (!buf) return;
12283
+ if (buf.timer) return;
12284
+ buf.timer = setTimeout(() => {
12285
+ const live = buffers.get(threadId);
12286
+ if (live) live.timer = null;
12287
+ flush(threadId);
12288
+ }, FLUSH_MS);
12281
12289
  }
12282
- function readExistingReviewRequestFile(worktreePath) {
12283
- return readTextIfPresent((0, import_node_path32.join)(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent((0, import_node_path32.join)(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent((0, import_node_path32.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
12290
+ function noteTurnLiveEvent(threadId, event) {
12291
+ if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
12292
+ return;
12293
+ }
12294
+ const prev = buffers.get(threadId) ?? { parts: [], timer: null };
12295
+ prev.parts = trimParts(applyAgentEvent(prev.parts, event));
12296
+ buffers.set(threadId, prev);
12297
+ if (event.type === "exit") {
12298
+ flush(threadId);
12299
+ const cur = buffers.get(threadId);
12300
+ if (cur?.timer) clearTimeout(cur.timer);
12301
+ buffers.delete(threadId);
12302
+ return;
12303
+ }
12304
+ const immediate = event.type === "tool_use" || event.type === "tool_result";
12305
+ if (immediate) flush(threadId);
12306
+ else scheduleFlush(threadId);
12284
12307
  }
12285
- async function requestReview(threadRef, send2) {
12286
- const from = findThreadByRef(threadRef);
12287
- if (!from) throw new Error(`Thread not found: ${threadRef}`);
12288
- if (isOrchestratorThread(from)) {
12289
- throw new Error(
12290
- "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
12291
- );
12308
+ function writeTurnLive(threadId, progress) {
12309
+ const path2 = threadLivePath(threadId);
12310
+ const tmp = `${path2}.${process.pid}.tmp`;
12311
+ try {
12312
+ (0, import_node_fs35.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
12313
+ (0, import_node_fs35.renameSync)(tmp, path2);
12314
+ } catch {
12315
+ try {
12316
+ (0, import_node_fs35.unlinkSync)(tmp);
12317
+ } catch {
12318
+ }
12292
12319
  }
12293
- if (from.status === "archived") {
12294
- throw new Error(`Thread is archived: ${from.id}`);
12320
+ }
12321
+ function readTurnLive(threadId) {
12322
+ const path2 = threadLivePath(threadId);
12323
+ if (!(0, import_node_fs35.existsSync)(path2)) return null;
12324
+ try {
12325
+ const raw = JSON.parse((0, import_node_fs35.readFileSync)(path2, "utf8"));
12326
+ if (!raw || typeof raw.summary !== "string") return null;
12327
+ return raw;
12328
+ } catch {
12329
+ return null;
12295
12330
  }
12296
- const guidelines = resolveReviewGuidelines(from.worktreePath);
12297
- const tab = createChatTab({
12298
- fromThreadId: from.id,
12299
- title: "Review",
12300
- attachments: [
12301
- buildReviewRequestAttachment(guidelines.content, {
12302
- path: guidelines.path,
12303
- name: guidelines.name
12304
- })
12305
- ]
12306
- });
12307
- const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
12308
- return { tab: started, from };
12309
12331
  }
12310
- var import_node_crypto6, import_node_fs35, import_node_path32, REPO_REVIEW_PATH, REPO_REVIEW_NAME, REVIEW_REQUEST_PATH, LEGACY_REVIEW_REQUEST_PATH, REVIEW_REQUEST_NAME, REVIEW_REQUEST_PREFILL, LEGACY_REVIEW_TEMPLATE_MARKERS;
12311
- var init_request_review = __esm({
12312
- "src/review/request-review.ts"() {
12332
+ function clearTurnLive(threadId) {
12333
+ const buf = buffers.get(threadId);
12334
+ if (buf?.timer) clearTimeout(buf.timer);
12335
+ buffers.delete(threadId);
12336
+ const path2 = threadLivePath(threadId);
12337
+ if (!(0, import_node_fs35.existsSync)(path2)) return;
12338
+ try {
12339
+ (0, import_node_fs35.unlinkSync)(path2);
12340
+ } catch {
12341
+ }
12342
+ }
12343
+ var import_node_fs35, buffers, FLUSH_MS, MAX_PARTS;
12344
+ var init_turn_live = __esm({
12345
+ "src/store/turn-live.ts"() {
12313
12346
  "use strict";
12314
- import_node_crypto6 = require("crypto");
12315
12347
  import_node_fs35 = require("fs");
12316
- import_node_path32 = require("path");
12317
- init_global_workspace();
12318
- init_chat_tabs();
12319
- init_thread_store();
12320
- init_review_request_template();
12321
- init_workspace_scratch();
12322
- REPO_REVIEW_PATH = ".sideboard/review.md";
12323
- REPO_REVIEW_NAME = "review.md";
12324
- REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
12325
- LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
12326
- REVIEW_REQUEST_NAME = "Review request.md";
12327
- REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
12328
- LEGACY_REVIEW_TEMPLATE_MARKERS = [
12329
- "You are acting as a reviewer for a proposed code change made by another engineer.",
12330
- "HOW MANY FINDINGS TO RETURN:"
12331
- ];
12348
+ init_message_parts();
12349
+ init_paths();
12350
+ buffers = /* @__PURE__ */ new Map();
12351
+ FLUSH_MS = 800;
12352
+ MAX_PARTS = 80;
12332
12353
  }
12333
12354
  });
12334
12355
 
@@ -14520,8 +14541,9 @@ function formatProcessGuideDirective() {
14520
14541
  "Process guides (recurring work only):",
14521
14542
  "- 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.",
14522
14543
  "- 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.",
14523
- "- 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.",
14524
- "- 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."
14544
+ "- 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).",
14545
+ "- 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.",
14546
+ "- 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."
14525
14547
  ].join("\n");
14526
14548
  }
14527
14549
  function formatArtifactDirective() {
@@ -14548,15 +14570,7 @@ function formatArtifactDirective() {
14548
14570
  ].join("\n");
14549
14571
  }
14550
14572
  function formatUiReminder() {
14551
- return [
14552
- "Sideboard side column (important):",
14553
- "There is no claude.ai Artifact tool here \u2014 that is normal.",
14554
- "HTML/SVG/markdown: ```html fence or MCP present_artifact.",
14555
- "CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
14556
- "Files column: MCP present_files (brightsy account storage or memory).",
14557
- "Do not say artifacts/CMS UI are unavailable.",
14558
- "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."
14559
- ].join(" ");
14573
+ 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.";
14560
14574
  }
14561
14575
  function loadAgentInstructions(worktreePath, agent) {
14562
14576
  const candidates = FILES_BY_AGENT[agent] ?? FILES_BY_AGENT.claude;
@@ -14703,7 +14717,7 @@ __export(plan_file_exports, {
14703
14717
  resolvePlanMarkdown: () => resolvePlanMarkdown,
14704
14718
  writePlanFile: () => writePlanFile
14705
14719
  });
14706
- function ensureAttachmentsGitignore2(worktreePath) {
14720
+ function ensureAttachmentsGitignore(worktreePath) {
14707
14721
  const gitignoreAbs = (0, import_node_path38.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
14708
14722
  if ((0, import_node_fs42.existsSync)(gitignoreAbs)) return;
14709
14723
  (0, import_node_fs42.mkdirSync)((0, import_node_path38.dirname)(gitignoreAbs), { recursive: true });
@@ -14725,7 +14739,7 @@ function readPlanFile(worktreePath) {
14725
14739
  return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path38.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path38.join)(worktreePath, LEGACY_PLAN_FILE_REL));
14726
14740
  }
14727
14741
  function writePlanFile(worktreePath, content) {
14728
- ensureAttachmentsGitignore2(worktreePath);
14742
+ ensureAttachmentsGitignore(worktreePath);
14729
14743
  const abs = planFileAbs(worktreePath);
14730
14744
  (0, import_node_fs42.mkdirSync)((0, import_node_path38.dirname)(abs), { recursive: true });
14731
14745
  const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
@@ -15480,7 +15494,7 @@ var init_orchestrator = __esm({
15480
15494
  if (compact.didCompact) {
15481
15495
  thread = updateThread(threadId, {
15482
15496
  messages: compact.thread.messages,
15483
- sessionId: null
15497
+ sessionId: compact.thread.sessionId
15484
15498
  });
15485
15499
  this.emit({
15486
15500
  type: "context_compacted",
@@ -16897,6 +16911,9 @@ __export(index_exports, {
16897
16911
  REVIEW_REQUEST_PATH: () => REVIEW_REQUEST_PATH,
16898
16912
  REVIEW_REQUEST_PREFILL: () => REVIEW_REQUEST_PREFILL,
16899
16913
  REVIEW_REQUEST_TEMPLATE: () => REVIEW_REQUEST_TEMPLATE,
16914
+ REVIEW_SKILL_NAME: () => REVIEW_SKILL_NAME,
16915
+ REVIEW_SKILL_PATH: () => REVIEW_SKILL_PATH,
16916
+ SESSION_RESET_OCCUPANCY_TOKENS: () => SESSION_RESET_OCCUPANCY_TOKENS,
16900
16917
  SIDEBOARD_FORCE_STOP: () => SIDEBOARD_FORCE_STOP,
16901
16918
  SIDEBOARD_MCP_ALLOWED_TOOLS: () => SIDEBOARD_MCP_ALLOWED_TOOLS,
16902
16919
  SIDEBOARD_MCP_PROFILE_ENV: () => SIDEBOARD_MCP_PROFILE_ENV,
@@ -16911,6 +16928,7 @@ __export(index_exports, {
16911
16928
  SlackOAuthCancelledError: () => SlackOAuthCancelledError,
16912
16929
  SlackRelayHub: () => SlackRelayHub,
16913
16930
  THINKING_EFFORTS: () => THINKING_EFFORTS,
16931
+ WORKTREE_MCP_TOOLS: () => WORKTREE_MCP_TOOLS,
16914
16932
  ackSlackInboundSeen: () => ackSlackInboundSeen,
16915
16933
  addPrStackLayer: () => addPrStackLayer,
16916
16934
  addStackLayerFromThread: () => addStackLayerFromThread,
@@ -16930,6 +16948,7 @@ __export(index_exports, {
16930
16948
  applyAppEnvironment: () => applyAppEnvironment,
16931
16949
  applyCompaction: () => applyCompaction,
16932
16950
  applyGithubGitAuthEnv: () => applyGithubGitAuthEnv,
16951
+ applyPromptCacheTtlEnv: () => applyPromptCacheTtlEnv,
16933
16952
  applyThreadIntoMain: () => applyThreadIntoMain,
16934
16953
  applyTurnUsage: () => applyTurnUsage,
16935
16954
  armSchedules: () => armSchedules,
@@ -17029,6 +17048,7 @@ __export(index_exports, {
17029
17048
  ensureGhPreferOrigin: () => ensureGhPreferOrigin,
17030
17049
  ensureGlobalCoordinatorCwd: () => ensureGlobalCoordinatorCwd,
17031
17050
  ensureReviewRequestFile: () => ensureReviewRequestFile,
17051
+ ensureReviewSkillFile: () => ensureReviewSkillFile,
17032
17052
  ensureSlackCoordinator: () => ensureSlackCoordinator,
17033
17053
  ensureSlackDeviceIdentity: () => ensureSlackDeviceIdentity,
17034
17054
  ensureWorkspace: () => ensureWorkspace,
@@ -17167,6 +17187,7 @@ __export(index_exports, {
17167
17187
  isThisProcessDesktopHost: () => isThisProcessDesktopHost,
17168
17188
  isThreadCaffeinated: () => isThreadCaffeinated,
17169
17189
  isWorkspaceScratchPath: () => isWorkspaceScratchPath,
17190
+ lastRequestOccupancy: () => lastRequestOccupancy,
17170
17191
  linearAuthorizationHeader: () => linearAuthorizationHeader,
17171
17192
  linearGraphql: () => linearGraphql,
17172
17193
  linearOAuthAuthorizeUrl: () => linearOAuthAuthorizeUrl,
@@ -17334,6 +17355,7 @@ __export(index_exports, {
17334
17355
  shouldCompactContext: () => shouldCompactContext,
17335
17356
  shouldInjectBrightsyMcp: () => shouldInjectBrightsyMcp,
17336
17357
  shouldRefreshReviewRequestTemplate: () => shouldRefreshReviewRequestTemplate,
17358
+ shouldResetSessionForOccupancy: () => shouldResetSessionForOccupancy,
17337
17359
  shouldRunWorktreeCleanup: () => shouldRunWorktreeCleanup,
17338
17360
  sideboardHomeDir: () => sideboardHomeDir,
17339
17361
  sideboardMcpProfile: () => sideboardMcpProfile,
@@ -17411,6 +17433,7 @@ __export(index_exports, {
17411
17433
  worktreeDisplayLabelForGroup: () => worktreeDisplayLabelForGroup,
17412
17434
  worktreeNameFromPath: () => worktreeNameFromPath,
17413
17435
  worktreesRoot: () => worktreesRoot,
17436
+ wrapReviewSkillMarkdown: () => wrapReviewSkillMarkdown,
17414
17437
  writeInjectedMcpConfig: () => writeInjectedMcpConfig,
17415
17438
  writePlanFile: () => writePlanFile,
17416
17439
  writeThread: () => writeThread,
@@ -19218,74 +19241,77 @@ async function startMcpServer() {
19218
19241
  name: "sideboard",
19219
19242
  version: "0.1.0"
19220
19243
  });
19221
- server.tool(
19222
- "list_workspaces",
19223
- "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.",
19224
- {},
19225
- async () => {
19226
- const workspaces = orch.listWorkspaces();
19227
- const lines = await Promise.all(
19228
- workspaces.map(async (w) => {
19229
- const slug = await resolveGithubRepoSlug(w.path).catch(() => null);
19230
- return slug ? `${w.name} ${w.path} github:${slug}` : `${w.name} ${w.path}`;
19231
- })
19232
- );
19233
- return {
19234
- content: [
19235
- { type: "text", text: lines.join("\n") || "(no workspaces)" }
19236
- ]
19237
- };
19238
- }
19239
- );
19240
- server.tool(
19241
- "list_threads",
19242
- "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.",
19243
- {},
19244
- async () => {
19245
- const threads = orch.getThreads(true);
19246
- const lines = threads.map((t) => {
19247
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path40.basename)(t.repoPath) || t.repoPath;
19244
+ const worktreeProfile = sideboardMcpProfile() === "worktree";
19245
+ if (!worktreeProfile) {
19246
+ server.tool(
19247
+ "list_workspaces",
19248
+ "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.",
19249
+ {},
19250
+ async () => {
19251
+ const workspaces = orch.listWorkspaces();
19252
+ const lines = await Promise.all(
19253
+ workspaces.map(async (w) => {
19254
+ const slug = await resolveGithubRepoSlug(w.path).catch(() => null);
19255
+ return slug ? `${w.name} ${w.path} github:${slug}` : `${w.name} ${w.path}`;
19256
+ })
19257
+ );
19258
+ return {
19259
+ content: [
19260
+ { type: "text", text: lines.join("\n") || "(no workspaces)" }
19261
+ ]
19262
+ };
19263
+ }
19264
+ );
19265
+ server.tool(
19266
+ "list_threads",
19267
+ "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.",
19268
+ {},
19269
+ async () => {
19270
+ const threads = orch.getThreads(true);
19271
+ const lines = threads.map((t) => {
19272
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path40.basename)(t.repoPath) || t.repoPath;
19273
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
19274
+ const progress = live?.summary ? ` ${live.summary}` : "";
19275
+ 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}`;
19276
+ });
19277
+ return {
19278
+ content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
19279
+ };
19280
+ }
19281
+ );
19282
+ server.tool(
19283
+ "get_thread",
19284
+ "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
19285
+ { ref: import_zod4.z.string() },
19286
+ async ({ ref }) => {
19287
+ const t = orch.getThread(ref);
19288
+ if (!t) {
19289
+ return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
19290
+ }
19248
19291
  const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
19249
- const progress = live?.summary ? ` ${live.summary}` : "";
19250
- 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}`;
19251
- });
19252
- return {
19253
- content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
19254
- };
19255
- }
19256
- );
19257
- server.tool(
19258
- "get_thread",
19259
- "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
19260
- { ref: import_zod4.z.string() },
19261
- async ({ ref }) => {
19262
- const t = orch.getThread(ref);
19263
- if (!t) {
19264
- return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
19265
- }
19266
- const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
19267
- const summary = {
19268
- id: t.id,
19269
- title: t.title,
19270
- status: t.status,
19271
- agent: t.agent,
19272
- sourceType: t.sourceType,
19273
- sourceRef: t.sourceRef,
19274
- branchName: t.branchName,
19275
- worktreePath: t.worktreePath,
19276
- sessionId: t.sessionId,
19277
- queueLength: t.queue.length,
19278
- messageCount: t.messages.length,
19279
- devPort: t.devPort,
19280
- prUrl: t.prUrl,
19281
- lastError: t.lastError ?? null,
19282
- stillRunning: t.status === "running" || t.status === "queued",
19283
- progress: live?.summary ?? null,
19284
- lastActivityAt: live?.updatedAt ?? null
19285
- };
19286
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
19287
- }
19288
- );
19292
+ const summary = {
19293
+ id: t.id,
19294
+ title: t.title,
19295
+ status: t.status,
19296
+ agent: t.agent,
19297
+ sourceType: t.sourceType,
19298
+ sourceRef: t.sourceRef,
19299
+ branchName: t.branchName,
19300
+ worktreePath: t.worktreePath,
19301
+ sessionId: t.sessionId,
19302
+ queueLength: t.queue.length,
19303
+ messageCount: t.messages.length,
19304
+ devPort: t.devPort,
19305
+ prUrl: t.prUrl,
19306
+ lastError: t.lastError ?? null,
19307
+ stillRunning: t.status === "running" || t.status === "queued",
19308
+ progress: live?.summary ?? null,
19309
+ lastActivityAt: live?.updatedAt ?? null
19310
+ };
19311
+ return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
19312
+ }
19313
+ );
19314
+ }
19289
19315
  server.tool(
19290
19316
  "present_artifact",
19291
19317
  "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.",
@@ -19414,9 +19440,9 @@ async function startMcpServer() {
19414
19440
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
19415
19441
  }
19416
19442
  );
19417
- registerSlackTools(server);
19418
- registerLinearTools(server);
19419
- if (sideboardMcpProfile() !== "worktree") {
19443
+ if (!worktreeProfile) {
19444
+ registerSlackTools(server);
19445
+ registerLinearTools(server);
19420
19446
  registerScheduleTools(server);
19421
19447
  const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await Promise.resolve().then(() => (init_caffeinate_hold(), caffeinate_hold_exports));
19422
19448
  const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
@@ -19770,7 +19796,7 @@ async function startMcpServer() {
19770
19796
  );
19771
19797
  server.tool(
19772
19798
  "request_review",
19773
- '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.',
19799
+ '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.',
19774
19800
  { ref: import_zod4.z.string().describe("Worktree thread id/ref to review") },
19775
19801
  async ({ ref }) => {
19776
19802
  try {
@@ -21550,10 +21576,11 @@ function slackReplyThreadTs(msg) {
21550
21576
  }
21551
21577
  var SLACK_PROGRESS_DELAY_MS = 2e4;
21552
21578
  var SLACK_PROGRESS_EDIT_MS = 15e3;
21579
+ var SLACK_PROGRESS_PLACEHOLDER = "Thinking\u2026";
21553
21580
  function formatSlackWorkingText(summary) {
21554
21581
  const line = (summary ?? "").trim();
21555
- if (!line || /^working/i.test(line)) return "Working\u2026";
21556
- return `Working\u2026 ${line}`;
21582
+ if (!line || /^(working|thinking)/i.test(line)) return SLACK_PROGRESS_PLACEHOLDER;
21583
+ return line;
21557
21584
  }
21558
21585
  var SLACK_SEEN_REACTION = "eyes";
21559
21586
  function writeTokenForTeam(teamId) {
@@ -21689,7 +21716,7 @@ function startSlackTurnProgress(msg, threadId, opts) {
21689
21716
  };
21690
21717
  const postWorking = async () => {
21691
21718
  if (!stillLive()) return;
21692
- const summary = readTurnLive(threadId())?.summary ?? "Working\u2026";
21719
+ const summary = readTurnLive(threadId())?.summary ?? SLACK_PROGRESS_PLACEHOLDER;
21693
21720
  const body = formatSlackWorkingText(summary);
21694
21721
  try {
21695
21722
  const posted = await postSlackText(target, signForThisMac(body), opts);
@@ -21707,7 +21734,7 @@ function startSlackTurnProgress(msg, threadId, opts) {
21707
21734
  };
21708
21735
  const editWorking = async () => {
21709
21736
  if (!postedTs || !stillLive()) return;
21710
- const summary = readTurnLive(threadId())?.summary ?? "Working\u2026";
21737
+ const summary = readTurnLive(threadId())?.summary ?? SLACK_PROGRESS_PLACEHOLDER;
21711
21738
  const body = formatSlackWorkingText(summary);
21712
21739
  if (body === lastBody) return;
21713
21740
  lastBody = body;
@@ -22615,6 +22642,9 @@ init_outbound_watch();
22615
22642
  REVIEW_REQUEST_PATH,
22616
22643
  REVIEW_REQUEST_PREFILL,
22617
22644
  REVIEW_REQUEST_TEMPLATE,
22645
+ REVIEW_SKILL_NAME,
22646
+ REVIEW_SKILL_PATH,
22647
+ SESSION_RESET_OCCUPANCY_TOKENS,
22618
22648
  SIDEBOARD_FORCE_STOP,
22619
22649
  SIDEBOARD_MCP_ALLOWED_TOOLS,
22620
22650
  SIDEBOARD_MCP_PROFILE_ENV,
@@ -22629,6 +22659,7 @@ init_outbound_watch();
22629
22659
  SlackOAuthCancelledError,
22630
22660
  SlackRelayHub,
22631
22661
  THINKING_EFFORTS,
22662
+ WORKTREE_MCP_TOOLS,
22632
22663
  ackSlackInboundSeen,
22633
22664
  addPrStackLayer,
22634
22665
  addStackLayerFromThread,
@@ -22648,6 +22679,7 @@ init_outbound_watch();
22648
22679
  applyAppEnvironment,
22649
22680
  applyCompaction,
22650
22681
  applyGithubGitAuthEnv,
22682
+ applyPromptCacheTtlEnv,
22651
22683
  applyThreadIntoMain,
22652
22684
  applyTurnUsage,
22653
22685
  armSchedules,
@@ -22747,6 +22779,7 @@ init_outbound_watch();
22747
22779
  ensureGhPreferOrigin,
22748
22780
  ensureGlobalCoordinatorCwd,
22749
22781
  ensureReviewRequestFile,
22782
+ ensureReviewSkillFile,
22750
22783
  ensureSlackCoordinator,
22751
22784
  ensureSlackDeviceIdentity,
22752
22785
  ensureWorkspace,
@@ -22885,6 +22918,7 @@ init_outbound_watch();
22885
22918
  isThisProcessDesktopHost,
22886
22919
  isThreadCaffeinated,
22887
22920
  isWorkspaceScratchPath,
22921
+ lastRequestOccupancy,
22888
22922
  linearAuthorizationHeader,
22889
22923
  linearGraphql,
22890
22924
  linearOAuthAuthorizeUrl,
@@ -23052,6 +23086,7 @@ init_outbound_watch();
23052
23086
  shouldCompactContext,
23053
23087
  shouldInjectBrightsyMcp,
23054
23088
  shouldRefreshReviewRequestTemplate,
23089
+ shouldResetSessionForOccupancy,
23055
23090
  shouldRunWorktreeCleanup,
23056
23091
  sideboardHomeDir,
23057
23092
  sideboardMcpProfile,
@@ -23129,6 +23164,7 @@ init_outbound_watch();
23129
23164
  worktreeDisplayLabelForGroup,
23130
23165
  worktreeNameFromPath,
23131
23166
  worktreesRoot,
23167
+ wrapReviewSkillMarkdown,
23132
23168
  writeInjectedMcpConfig,
23133
23169
  writePlanFile,
23134
23170
  writeThread,