@sideboard-ai/core 0.1.107 → 0.1.110

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 (44) hide show
  1. package/dist/agents/cursor-runner.cjs +87 -12
  2. package/dist/agents/cursor-runner.js +38 -12
  3. package/dist/{agents-T4RHL5UX.js → agents-MQDW3YXL.js} +6 -6
  4. package/dist/{agents-4XQANGFO.js → agents-WK54VP7J.js} +8 -8
  5. package/dist/{app-settings-2WH6DVOW.js → app-settings-5XAGNDX7.js} +3 -1
  6. package/dist/{app-settings-6AXBWM3K.js → app-settings-J4LUWIRN.js} +4 -2
  7. package/dist/{chunk-HUKCGRAT.js → chunk-3UOKB6VQ.js} +47 -2
  8. package/dist/{chunk-SMWVSSE5.js → chunk-4EWPKYOU.js} +4 -4
  9. package/dist/{chunk-7ZTQHC2Q.js → chunk-4NR5FL46.js} +11 -1
  10. package/dist/{chunk-TLPJHLLM.js → chunk-4TR3HZFT.js} +1 -1
  11. package/dist/{chunk-VTTTV2LM.js → chunk-7KWYXGIU.js} +2 -2
  12. package/dist/{chunk-KGSVLZV6.js → chunk-A4VZXJEJ.js} +2 -0
  13. package/dist/{chunk-5XUKY2JY.js → chunk-AROMOP3C.js} +8 -16
  14. package/dist/{chunk-PZVA2VDF.js → chunk-C4KHDW3U.js} +2 -2
  15. package/dist/{chunk-3SIFRHJ3.js → chunk-EAAB4EK5.js} +1597 -1704
  16. package/dist/{chunk-GYFNFU62.js → chunk-KQBI5HNT.js} +8 -16
  17. package/dist/{chunk-P3BQ5DOL.js → chunk-KYOTBZ6S.js} +2 -0
  18. package/dist/{chunk-6HRR4T4P.js → chunk-RSIWPLRG.js} +483 -46
  19. package/dist/{chunk-EX4NZN3O.js → chunk-SGEVS5SY.js} +11 -1
  20. package/dist/{chunk-THXSFETX.js → chunk-VOD3HFLP.js} +4 -4
  21. package/dist/{chunk-XOYH3OMI.js → chunk-VOJSO3RM.js} +484 -55
  22. package/dist/{chunk-MMI4RJ5I.js → chunk-WIHKHR5R.js} +14 -3
  23. package/dist/{chunk-XEFHG6VG.js → chunk-XOZDAVOP.js} +1449 -1518
  24. package/dist/{chunk-LRO7YVLJ.js → chunk-YFAXVUY2.js} +2 -2
  25. package/dist/{chunk-6JJTMI7G.js → chunk-YXDR43ZC.js} +2 -2
  26. package/dist/{coordinator-prompt-4TUQUEHY.js → coordinator-prompt-AR66L3N4.js} +5 -5
  27. package/dist/{coordinator-prompt-YQWRXCPX.js → coordinator-prompt-BHMLWL64.js} +4 -4
  28. package/dist/{global-workspace-H37LFFXI.js → global-workspace-SKFODUQQ.js} +5 -5
  29. package/dist/{global-workspace-V6TMECDU.js → global-workspace-XSUFEIAQ.js} +6 -6
  30. package/dist/index.cjs +2388 -2006
  31. package/dist/index.d.cts +115 -26
  32. package/dist/index.d.ts +115 -26
  33. package/dist/index.js +141 -98
  34. package/dist/mcp/run-stdio.cjs +2354 -1988
  35. package/dist/mcp/run-stdio.js +90 -82
  36. package/dist/{orchestrator-DEHSKERK.js → orchestrator-FCZVCKBK.js} +11 -11
  37. package/dist/{orchestrator-2P7DX44Q.js → orchestrator-SCATSB3O.js} +8 -8
  38. package/dist/{thread-store-DQJGDKIJ.js → thread-store-LPTBVW5C.js} +1 -1
  39. package/dist/{thread-store-4OUEQ2DB.js → thread-store-OEALWU7Y.js} +1 -1
  40. package/dist/{workspaces-UPEVANU4.js → workspaces-HV3J4TTW.js} +7 -7
  41. package/dist/{workspaces-JU3P6FHB.js → workspaces-XAQVKTLO.js} +6 -6
  42. package/dist/{worktree-6MFKE465.js → worktree-N2EV24EE.js} +3 -3
  43. package/dist/{worktree-NR5YJG2E.js → worktree-XFJED3VU.js} +4 -4
  44. package/package.json +1 -1
@@ -2,27 +2,35 @@
2
2
 
3
3
  import {
4
4
  PLAN_MODE_INSTRUCTION,
5
+ applyAgentEvent,
5
6
  applyTurnUsage,
7
+ contextTokens,
6
8
  fallbackTurnFailDetail,
9
+ finalizeParts,
7
10
  formatTurnExitError,
8
11
  getAdapter,
12
+ isBrightsyNdjsonLine,
9
13
  isSessionQuotaLimit,
10
14
  looksLikeAgentFailureMessage,
11
15
  looksLikeInvalidAgentSession,
16
+ normalizeParseResult,
12
17
  parseBrightsyCliLine,
13
18
  parseSessionQuotaResetAt,
19
+ partsToAssistantText,
14
20
  pushTurnStderr,
15
21
  resolveQuotaFallbackAgent,
16
22
  shouldRetryFailedAgentTurn,
23
+ stripBrightsyNdjsonNoise,
17
24
  summarizeTurnStderr,
25
+ toolDescription,
18
26
  turnFailChatText
19
- } from "./chunk-XOYH3OMI.js";
27
+ } from "./chunk-VOJSO3RM.js";
20
28
  import {
21
29
  addWorkspace,
22
30
  ensureWorkspace,
23
31
  removeWorkspace,
24
32
  syncWorkspacesFromThreads
25
- } from "./chunk-LRO7YVLJ.js";
33
+ } from "./chunk-YFAXVUY2.js";
26
34
  import {
27
35
  extractPresentedPlan,
28
36
  readPlanFile,
@@ -43,14 +51,14 @@ import {
43
51
  isOrchestratorThread,
44
52
  isSlackCoordinatorThread,
45
53
  orchestratorSessionPoisonedByBuiltins
46
- } from "./chunk-THXSFETX.js";
54
+ } from "./chunk-VOD3HFLP.js";
47
55
  import {
48
56
  SLACK_REPLY_FORMATTING,
49
57
  coordinatorSystemPrompt,
50
58
  coordinatorTurnReminder,
51
59
  enrichWorkspacesWithGithub,
52
60
  ensureGlobalCoordinatorCwd
53
- } from "./chunk-5XUKY2JY.js";
61
+ } from "./chunk-AROMOP3C.js";
54
62
  import {
55
63
  addPrStackLayer,
56
64
  allocateTeamName,
@@ -93,7 +101,7 @@ import {
93
101
  threadDisplayLabel,
94
102
  withRepoGitLock,
95
103
  worktreeNameFromPath
96
- } from "./chunk-6JJTMI7G.js";
104
+ } from "./chunk-YXDR43ZC.js";
97
105
  import {
98
106
  ATTACHMENTS_DIR,
99
107
  LEGACY_ATTACHMENTS_DIR,
@@ -110,7 +118,7 @@ import {
110
118
  updateThread,
111
119
  withThreadLock,
112
120
  writeThread
113
- } from "./chunk-P3BQ5DOL.js";
121
+ } from "./chunk-KYOTBZ6S.js";
114
122
  import {
115
123
  childEnvWithAppSettings,
116
124
  isSecureFileEncrypted,
@@ -124,7 +132,7 @@ import {
124
132
  stripNestedElectronEnv,
125
133
  updateAdvancedSettings,
126
134
  writeSecureJson
127
- } from "./chunk-EX4NZN3O.js";
135
+ } from "./chunk-SGEVS5SY.js";
128
136
  import {
129
137
  writePrivateFile
130
138
  } from "./chunk-NSTQ6QKD.js";
@@ -653,238 +661,13 @@ import { existsSync as existsSync16 } from "fs";
653
661
  import { createInterface } from "readline";
654
662
  import { execa } from "execa";
655
663
 
656
- // src/agents/message-parts.ts
657
- function asRecord(input) {
658
- if (input && typeof input === "object" && !Array.isArray(input)) {
659
- return input;
660
- }
661
- return void 0;
662
- }
663
- function str(v) {
664
- return typeof v === "string" && v.trim() ? v : void 0;
665
- }
666
- function toolDetail(name, input) {
667
- if (!input) return void 0;
668
- const command = str(input.command) ?? str(input.cmd);
669
- if (command) return command;
670
- const path = str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
671
- if (path) return path;
672
- const pattern = str(input.pattern) ?? str(input.glob) ?? str(input.glob_pattern);
673
- if (pattern) return pattern;
674
- const query = str(input.query) ?? str(input.prompt);
675
- if (query) return query.length > 80 ? `${query.slice(0, 77)}\u2026` : query;
676
- try {
677
- const raw = JSON.stringify(input);
678
- return raw.length > 80 ? `${raw.slice(0, 77)}\u2026` : raw;
679
- } catch {
680
- return name;
681
- }
682
- }
683
- function toolDescription(name, input) {
684
- const n = name.replace(/^mcp__/, "").replace(/__/g, " \xB7 ");
685
- if (/^get_record_types$|recordTypes/i.test(name)) return "List record types";
686
- if (/connectedAgentRequest/i.test(name)) {
687
- return str(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
688
- }
689
- if (/present_artifact$/i.test(name)) {
690
- return str(input?.title) ? `Present ${str(input?.title)}` : "Present artifact";
691
- }
692
- if (/present_plan$/i.test(name)) {
693
- return str(input?.title) ? `Plan ${str(input?.title)}` : "Present plan";
694
- }
695
- if (/present_schema$/i.test(name)) {
696
- return str(input?.title) ? `Schema ${str(input?.title)}` : "Present schema";
697
- }
698
- if (/present_files$/i.test(name)) {
699
- return str(input?.title) ? `Files ${str(input?.title)}` : "Present files";
700
- }
701
- if (/^(create|update)_artifact$/i.test(name.replace(/^mcp__[^_]+__/, ""))) {
702
- return str(input?.title) ? `Artifact ${str(input?.title)}` : "Artifact";
703
- }
704
- if (!input) return n;
705
- if (/bash|shell|terminal/i.test(name) && str(input.command)) {
706
- const cmd = str(input.command);
707
- if (/git\s+fetch/i.test(cmd)) return "Fetch latest from origin and check status";
708
- if (/git\s+status/i.test(cmd)) return "Check git status";
709
- if (/git\s+log/i.test(cmd)) return "Inspect recent commits";
710
- if (/git\s+branch/i.test(cmd)) return "List branches";
711
- if (/grep|rg\b/i.test(cmd)) return "Search repository";
712
- return "Run shell command";
713
- }
714
- if (/edit|write|apply/i.test(name)) {
715
- const path = str(input.file_path) ?? str(input.path);
716
- return path ? `Edit ${path.split("/").pop()}` : "Edit file";
717
- }
718
- if (/read/i.test(name)) {
719
- const path = str(input.file_path) ?? str(input.path);
720
- return path ? `Read ${path.split("/").pop()}` : "Read file";
721
- }
722
- if (/grep/i.test(name)) return "Search files";
723
- if (/glob/i.test(name)) return "Find files";
724
- return n;
725
- }
726
- function toolFilePath(input) {
727
- if (!input) return void 0;
728
- return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
729
- }
730
- function countLines(text) {
731
- if (!text) return 0;
732
- return text.split("\n").length;
733
- }
734
- function diffFromInput(input) {
735
- if (!input) return {};
736
- const oldS = str(input.old_string) ?? str(input.oldString);
737
- const newS = str(input.new_string) ?? str(input.newString) ?? str(input.content);
738
- if (oldS != null || newS != null) {
739
- return {
740
- additions: countLines(newS),
741
- deletions: countLines(oldS)
742
- };
743
- }
744
- return {};
745
- }
746
- function parseDiffStat(result) {
747
- if (!result) return {};
748
- const paired = result.match(/\+(\d+)\s+-(\d+)/);
749
- if (paired) {
750
- return {
751
- additions: Number(paired[1]),
752
- deletions: Number(paired[2])
753
- };
754
- }
755
- const verbose = result.match(
756
- /(\d+)\s+insertions?(?:,\s*(\d+)\s+deletions?)?/i
757
- );
758
- if (verbose) {
759
- return {
760
- additions: Number(verbose[1]),
761
- deletions: verbose[2] != null ? Number(verbose[2]) : void 0
762
- };
763
- }
764
- return {};
765
- }
766
- function applyAgentEvent(parts, event) {
767
- if (event.type === "stdout") {
768
- const data = event.data;
769
- if (!data) return parts;
770
- const next = [...parts];
771
- const last = next[next.length - 1];
772
- if (last?.type === "text") {
773
- next[next.length - 1] = { type: "text", text: last.text + data };
774
- } else {
775
- next.push({ type: "text", text: data });
776
- }
777
- return next;
778
- }
779
- if (event.type === "thinking") {
780
- const data = event.data;
781
- if (!data) return parts;
782
- const next = [...parts];
783
- const last = next[next.length - 1];
784
- if (last?.type === "thinking") {
785
- next[next.length - 1] = { type: "thinking", text: last.text + data };
786
- } else {
787
- next.push({ type: "thinking", text: data });
788
- }
789
- return next;
790
- }
791
- if (event.type === "tool_use") {
792
- const input = asRecord(event.input);
793
- const diff = diffFromInput(input);
794
- const existing = parts.findIndex((p) => p.type === "tool" && p.id === event.id);
795
- if (existing >= 0) {
796
- const prev = parts[existing];
797
- const next = [...parts];
798
- next[existing] = {
799
- ...prev,
800
- name: event.name || prev.name,
801
- input: input ?? prev.input,
802
- description: toolDescription(event.name || prev.name, input ?? prev.input),
803
- detail: toolDetail(event.name || prev.name, input ?? prev.input) ?? prev.detail,
804
- filePath: toolFilePath(input) ?? prev.filePath,
805
- additions: diff.additions ?? prev.additions,
806
- deletions: diff.deletions ?? prev.deletions
807
- };
808
- return next;
809
- }
810
- return [
811
- ...parts,
812
- {
813
- type: "tool",
814
- id: event.id,
815
- name: event.name,
816
- input,
817
- description: toolDescription(event.name, input),
818
- detail: toolDetail(event.name, input),
819
- status: "running",
820
- filePath: toolFilePath(input),
821
- ...diff
822
- }
823
- ];
824
- }
825
- if (event.type === "tool_result") {
826
- const existing = parts.findIndex((p) => p.type === "tool" && p.id === event.id);
827
- const fromResult = parseDiffStat(event.content);
828
- if (existing < 0) {
829
- return [
830
- ...parts,
831
- {
832
- type: "tool",
833
- id: event.id,
834
- name: "tool",
835
- description: "tool",
836
- status: event.isError ? "error" : "done",
837
- result: event.content,
838
- ...fromResult.additions != null ? { additions: fromResult.additions } : {},
839
- ...fromResult.deletions != null ? { deletions: fromResult.deletions } : {}
840
- }
841
- ];
842
- }
843
- return parts.map((p, i) => {
844
- if (i !== existing || p.type !== "tool") return p;
845
- return {
846
- ...p,
847
- status: event.isError ? "error" : "done",
848
- result: event.content,
849
- ...fromResult.additions != null ? { additions: fromResult.additions } : {},
850
- ...fromResult.deletions != null ? { deletions: fromResult.deletions } : {}
851
- };
852
- });
853
- }
854
- return parts;
855
- }
856
- function partsToAssistantText(parts) {
857
- return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
858
- }
859
- function stripBrightsyNdjsonNoise(text) {
860
- if (!text || !text.includes('"type"')) return text;
861
- let out = text;
862
- out = out.replace(
863
- /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
864
- ""
865
- );
866
- out = out.replace(/\{"type":"(?:tool_use|tool_result|tool)"[\s\S]*$/g, "");
867
- return out.trim();
868
- }
869
- function isBrightsyNdjsonLine(line) {
870
- const t = line.trim();
871
- if (!t.startsWith("{")) return false;
872
- return /"type"\s*:\s*"(tool_use|tool_result|tool|thinking|usage|done|error|text)"/.test(t);
873
- }
874
- function finalizeParts(parts) {
875
- return parts.map(
876
- (p) => p.type === "tool" && p.status === "running" ? { ...p, status: "done" } : p
877
- );
878
- }
879
- function normalizeParseResult(parsed) {
880
- if (!parsed) return [];
881
- return Array.isArray(parsed) ? parsed : [parsed];
882
- }
883
-
884
664
  // src/agents/cursor-stream-coalesce.ts
885
665
  function isTextEvent(event) {
886
666
  return event.type === "stdout" || event.type === "thinking";
887
667
  }
668
+ function parentKey(event) {
669
+ return event.parentId ?? "";
670
+ }
888
671
  function createAgentStreamCoalescer(emit, opts) {
889
672
  const intervalMs = opts?.intervalMs ?? 32;
890
673
  let pending = null;
@@ -897,7 +680,11 @@ function createAgentStreamCoalescer(emit, opts) {
897
680
  if (!pending) return;
898
681
  const event = pending;
899
682
  pending = null;
900
- emit({ type: event.type, data: event.data });
683
+ emit({
684
+ type: event.type,
685
+ data: event.data,
686
+ ...event.parentId ? { parentId: event.parentId } : {}
687
+ });
901
688
  };
902
689
  const schedule = () => {
903
690
  if (timer) return;
@@ -912,11 +699,15 @@ function createAgentStreamCoalescer(emit, opts) {
912
699
  return;
913
700
  }
914
701
  if (!event.data) return;
915
- if (pending && pending.type === event.type) {
702
+ if (pending && pending.type === event.type && parentKey(pending) === parentKey(event)) {
916
703
  pending.data += event.data;
917
704
  } else {
918
705
  flush2();
919
- pending = { type: event.type, data: event.data };
706
+ pending = {
707
+ type: event.type,
708
+ data: event.data,
709
+ ...event.parentId ? { parentId: event.parentId } : {}
710
+ };
920
711
  }
921
712
  schedule();
922
713
  },
@@ -925,6 +716,14 @@ function createAgentStreamCoalescer(emit, opts) {
925
716
  }
926
717
 
927
718
  // src/agents/spawn.ts
719
+ function applyPromptCacheTtlEnv(agent, env) {
720
+ if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
721
+ env.ENABLE_PROMPT_CACHING_1H ??= "1";
722
+ }
723
+ if (agent === "opencode" && env.OPENCODE_ANTHROPIC_FORCE_PROMPT_CACHING_5M !== "1") {
724
+ env.OPENCODE_ANTHROPIC_PROMPT_CACHING_1H ??= "1";
725
+ }
726
+ }
928
727
  async function spawnAgentTurn(thread, input, onEvent) {
929
728
  ensureAgentPath();
930
729
  if (!thread.worktreePath?.trim()) {
@@ -932,9 +731,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
932
731
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
933
732
  );
934
733
  }
935
- const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-H37LFFXI.js");
734
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-SKFODUQQ.js");
936
735
  if (isGlobalThread2(thread)) {
937
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-YQWRXCPX.js");
736
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-BHMLWL64.js");
938
737
  ensureGlobalCoordinatorCwd2(
939
738
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
940
739
  );
@@ -950,6 +749,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
950
749
  );
951
750
  }
952
751
  const env = childEnvWithAppSettings(cmd.env);
752
+ applyPromptCacheTtlEnv(thread.agent, env);
953
753
  try {
954
754
  if (isOrchestratorThread(thread)) {
955
755
  mergeAgentGitAuthEnv(env, await resolveAgentGitAuthEnv(env));
@@ -1161,1127 +961,1471 @@ function shouldAutoArchiveOnPrMerge(opts) {
1161
961
  // src/hook/conductor.ts
1162
962
  import {
1163
963
  copyFileSync,
1164
- existsSync as existsSync3,
1165
- mkdirSync,
964
+ existsSync as existsSync4,
965
+ mkdirSync as mkdirSync2,
1166
966
  readdirSync,
1167
- readFileSync as readFileSync3
967
+ readFileSync as readFileSync4
1168
968
  } from "fs";
1169
969
  import { createServer } from "net";
1170
- import { basename, dirname, join as join4 } from "path";
970
+ import { basename, dirname as dirname2, join as join5 } from "path";
1171
971
  import { execa as execa2 } from "execa";
1172
972
  import { createInterface as createInterface2 } from "readline";
1173
- var PORT_RANGE_SIZE = 10;
1174
- function matchSimpleGlob(pattern, name) {
1175
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1176
- return new RegExp(`^${escaped}$`).test(name);
973
+
974
+ // src/review/request-review.ts
975
+ import { randomUUID as randomUUID2 } from "crypto";
976
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
977
+ import { dirname, join as join4 } from "path";
978
+
979
+ // src/threads/chat-tabs.ts
980
+ import { randomUUID } from "crypto";
981
+
982
+ // src/composer/summarize.ts
983
+ var MAX_TRANSCRIPT_CHARS = 12e4;
984
+ var MAX_SUMMARY_CHARS = 6e3;
985
+ async function summarizeConversation(transcript, opts) {
986
+ const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
987
+
988
+ [\u2026truncated\u2026]` : transcript;
989
+ const claudeSummary = await tryClaudeSummary(clipped, opts);
990
+ if (claudeSummary) {
991
+ return { summary: clipSummary(claudeSummary), method: "claude" };
992
+ }
993
+ return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
1177
994
  }
1178
- function readWorktreeInclude(repoPath) {
1179
- const path = join4(repoPath, ".worktreeinclude");
1180
- if (!existsSync3(path)) return [];
1181
- return readFileSync3(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
995
+ async function tryClaudeSummary(transcript, opts) {
996
+ ensureAgentPath();
997
+ const prompt = [
998
+ "Summarize this coding-agent conversation for continuity in a fresh session.",
999
+ "Use concise markdown bullets covering:",
1000
+ "- Goal / request",
1001
+ "- Key decisions and constraints",
1002
+ "- Files / areas touched",
1003
+ "- Current state and open tasks",
1004
+ "- Anything the next turn must remember",
1005
+ "Do not invent details. No preamble.",
1006
+ "",
1007
+ "Transcript:",
1008
+ transcript
1009
+ ].join("\n");
1010
+ try {
1011
+ const { stdout, exitCode } = await run(
1012
+ "claude",
1013
+ [
1014
+ "-p",
1015
+ prompt,
1016
+ "--output-format",
1017
+ "text",
1018
+ "--permission-mode",
1019
+ "bypassPermissions",
1020
+ "--model",
1021
+ "haiku"
1022
+ ],
1023
+ { cwd: opts?.cwd, reject: false }
1024
+ );
1025
+ if (exitCode !== 0) return null;
1026
+ const text = stdout.trim();
1027
+ if (text.length < 40) return null;
1028
+ return text;
1029
+ } catch {
1030
+ return null;
1031
+ }
1182
1032
  }
1183
- function resolveFilesToCopy(repoPath) {
1184
- const fromInclude = readWorktreeInclude(repoPath);
1185
- if (fromInclude.length) return fromInclude;
1186
- const settings = loadRepoSettings(repoPath);
1187
- if (settings?.filesToCopy?.length) return settings.filesToCopy;
1188
- if (settings?.fileIncludeGlobs?.length) {
1189
- const matched = [];
1190
- try {
1191
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1192
- if (!entry.isFile()) continue;
1193
- for (const glob of settings.fileIncludeGlobs) {
1194
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
1195
- matched.push(entry.name);
1196
- break;
1197
- }
1198
- }
1199
- }
1200
- } catch {
1033
+ function extractiveSummary(transcript) {
1034
+ const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
1035
+ const users = [];
1036
+ const tools = [];
1037
+ const agents = [];
1038
+ let mode = "none";
1039
+ for (const line of lines) {
1040
+ if (line.startsWith("### User")) {
1041
+ mode = "user";
1042
+ continue;
1201
1043
  }
1202
- if (matched.length) return [...new Set(matched)];
1203
- }
1204
- const defaults = [];
1205
- try {
1206
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1207
- if (entry.isFile() && entry.name.startsWith(".env")) {
1208
- defaults.push(entry.name);
1209
- }
1044
+ if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
1045
+ mode = "agent";
1046
+ continue;
1047
+ }
1048
+ if (line.startsWith("- ") && line.includes(":")) {
1049
+ tools.push(line.replace(/^- /, ""));
1050
+ continue;
1051
+ }
1052
+ if (mode === "user" && users.length < 8) {
1053
+ users.push(line.slice(0, 240));
1054
+ } else if (mode === "agent" && agents.length < 6) {
1055
+ agents.push(line.slice(0, 240));
1210
1056
  }
1211
- } catch {
1212
1057
  }
1213
- if (defaults.length) return defaults;
1214
- return [".env.local", ".env"];
1058
+ const uniqueTools = [...new Set(tools)].slice(0, 20);
1059
+ const parts = [
1060
+ "## Compacted context (extractive)",
1061
+ "",
1062
+ "### User goals",
1063
+ ...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
1064
+ "",
1065
+ "### Recent agent notes",
1066
+ ...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
1067
+ ];
1068
+ if (uniqueTools.length) {
1069
+ parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
1070
+ }
1071
+ parts.push(
1072
+ "",
1073
+ "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
1074
+ );
1075
+ return parts.join("\n");
1215
1076
  }
1216
- function copyConfiguredFiles(repoPath, worktreePath) {
1217
- const patterns = resolveFilesToCopy(repoPath);
1218
- const copied = [];
1219
- for (const rel of patterns) {
1220
- const src = join4(repoPath, rel);
1221
- if (!existsSync3(src)) continue;
1222
- const dest = join4(worktreePath, rel);
1223
- mkdirSync(dirname(dest), { recursive: true });
1224
- copyFileSync(src, dest);
1225
- copied.push(rel);
1077
+ function clipSummary(text) {
1078
+ if (text.length <= MAX_SUMMARY_CHARS) return text;
1079
+ return `${text.slice(0, MAX_SUMMARY_CHARS)}
1080
+
1081
+ [\u2026summary truncated\u2026]`;
1082
+ }
1083
+
1084
+ // src/composer/context-compact.ts
1085
+ var CONTEXT_COMPACT_CHARS = 4e5;
1086
+ var CONTEXT_KEEP_RECENT_CHARS = 24e3;
1087
+ var CONTEXT_KEEP_RECENT_MESSAGES = 12;
1088
+ var CONTEXT_MIN_MESSAGES = 10;
1089
+ var SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
1090
+ function estimateMessageChars(message) {
1091
+ let n = message.text.length + 16;
1092
+ for (const part of message.parts ?? []) {
1093
+ n += estimatePartChars(part);
1226
1094
  }
1227
- return copied;
1095
+ return n;
1228
1096
  }
1229
- var cachedLoginEnv = null;
1230
- async function captureLoginEnv() {
1231
- if (cachedLoginEnv) return { ...cachedLoginEnv };
1232
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
1233
- try {
1234
- const result = await execa2(shell, ["-l", "-c", "env -0"], {
1235
- reject: false,
1236
- timeout: 5e3
1237
- });
1238
- if (result.exitCode === 0 && result.stdout) {
1239
- const env = { ...process.env };
1240
- for (const entry of result.stdout.split("\0")) {
1241
- if (!entry) continue;
1242
- const eq = entry.indexOf("=");
1243
- if (eq <= 0) continue;
1244
- const key = entry.slice(0, eq);
1245
- const value = entry.slice(eq + 1);
1246
- if (key) env[key] = value;
1247
- }
1248
- cachedLoginEnv = env;
1249
- return { ...env };
1097
+ function estimatePartChars(part) {
1098
+ switch (part.type) {
1099
+ case "text":
1100
+ case "thinking":
1101
+ return part.text.length;
1102
+ case "tool": {
1103
+ const input = part.input ? JSON.stringify(part.input) : "";
1104
+ return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
1250
1105
  }
1251
- } catch {
1106
+ default:
1107
+ return 0;
1252
1108
  }
1253
- cachedLoginEnv = { ...process.env };
1254
- return { ...cachedLoginEnv };
1255
1109
  }
1256
- function buildWorkspaceScriptEnv(opts, baseEnv) {
1257
- const env = stripNestedElectronEnv({
1258
- ...baseEnv ?? process.env
1259
- });
1260
- const name = opts.workspaceName ?? basename(opts.worktreePath);
1261
- const ports = opts.ports ?? [];
1262
- const primary = ports[0];
1263
- env.SIDEBOARD_WORKSPACE_NAME = name;
1264
- env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
1265
- env.SIDEBOARD_ROOT_PATH = opts.repoPath;
1266
- env.SIDEBOARD_IS_LOCAL = "1";
1267
- if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
1268
- env.CONDUCTOR_WORKSPACE_NAME = name;
1269
- env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
1270
- env.CONDUCTOR_ROOT_PATH = opts.repoPath;
1271
- env.CONDUCTOR_IS_LOCAL = "1";
1272
- if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
1273
- if (primary != null) {
1274
- env.SIDEBOARD_PORT = String(primary);
1275
- env.CONDUCTOR_PORT = String(primary);
1276
- env.PORT = String(primary);
1277
- for (let i = 1; i < ports.length; i++) {
1278
- const p = ports[i];
1279
- env[`SIDEBOARD_PORT_${i}`] = String(p);
1280
- env[`CONDUCTOR_PORT_${i}`] = String(p);
1281
- }
1110
+ function estimateThreadChars(messages) {
1111
+ return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
1112
+ }
1113
+ function shouldCompactContext(messages, thresholds = {}) {
1114
+ const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
1115
+ const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
1116
+ if (messages.length < minMessages) return false;
1117
+ return estimateThreadChars(messages) >= maxChars;
1118
+ }
1119
+ function splitForCompaction(messages, thresholds = {}) {
1120
+ const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
1121
+ const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
1122
+ if (messages.length === 0) return { older: [], recent: [] };
1123
+ let recentChars = 0;
1124
+ let cut = messages.length;
1125
+ for (let i = messages.length - 1; i >= 0; i--) {
1126
+ const next = recentChars + estimateMessageChars(messages[i]);
1127
+ const kept = messages.length - i;
1128
+ if (kept >= keepCount && next > keepChars) break;
1129
+ recentChars = next;
1130
+ cut = i;
1282
1131
  }
1283
- return env;
1132
+ const minRecent = Math.min(keepCount, messages.length);
1133
+ cut = Math.min(cut, messages.length - minRecent);
1134
+ if (cut <= 0) return { older: [], recent: messages };
1135
+ return {
1136
+ older: messages.slice(0, cut),
1137
+ recent: messages.slice(cut)
1138
+ };
1284
1139
  }
1285
- function pipeLines(stream, onLine) {
1286
- if (!stream || !onLine) return;
1287
- const rl = createInterface2({ input: stream });
1288
- rl.on("line", onLine);
1140
+ function formatToolPart(t, detail) {
1141
+ if (detail === "summary") {
1142
+ const label = t.description || t.detail || t.name;
1143
+ const path = t.filePath ? ` (${t.filePath})` : "";
1144
+ return `- ${t.name}: ${label}${path}`;
1145
+ }
1146
+ const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
1147
+ if (t.description) lines.push(`Description: ${t.description}`);
1148
+ if (t.filePath) lines.push(`Path: ${t.filePath}`);
1149
+ if (t.input && Object.keys(t.input).length > 0) {
1150
+ lines.push("Input:");
1151
+ lines.push("```json");
1152
+ lines.push(JSON.stringify(t.input, null, 2));
1153
+ lines.push("```");
1154
+ } else if (t.detail) {
1155
+ lines.push(`Detail: ${t.detail}`);
1156
+ }
1157
+ if (t.result != null && t.result !== "") {
1158
+ lines.push("Result:");
1159
+ lines.push("```");
1160
+ lines.push(t.result);
1161
+ lines.push("```");
1162
+ }
1163
+ return lines.join("\n");
1289
1164
  }
1290
- function killScriptTree(child, ports = []) {
1291
- const pid = child.pid;
1292
- if (pid) {
1293
- try {
1294
- if (process.platform === "win32") {
1295
- void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
1296
- } else {
1297
- process.kill(-pid, "SIGTERM");
1298
- setTimeout(() => {
1299
- try {
1300
- process.kill(-pid, "SIGKILL");
1301
- } catch {
1302
- }
1303
- }, 2500).unref?.();
1304
- }
1305
- } catch {
1306
- try {
1307
- child.kill("SIGTERM");
1308
- } catch {
1309
- }
1165
+ function formatMessagesAsTranscript(messages, opts) {
1166
+ const tools = opts?.tools ?? "full";
1167
+ const blocks = [];
1168
+ for (const m of messages) {
1169
+ if (m.role === "summary") {
1170
+ blocks.push(`## Prior summary
1171
+ ${m.text}`);
1172
+ continue;
1310
1173
  }
1311
- }
1312
- for (const port of ports) {
1313
- if (!Number.isFinite(port) || port <= 0) continue;
1314
- if (process.platform === "win32") {
1315
- void execa2(
1316
- "powershell",
1317
- [
1318
- "-NoProfile",
1319
- "-Command",
1320
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
1321
- ],
1322
- { reject: false }
1323
- );
1174
+ if (m.role === "user") {
1175
+ blocks.push(`### User
1176
+ ${m.text}`);
1177
+ continue;
1178
+ }
1179
+ const bits = [];
1180
+ if (m.text?.trim()) {
1181
+ bits.push(`### Agent
1182
+ ${m.text}`);
1324
1183
  } else {
1325
- void execa2(
1326
- "zsh",
1327
- ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
1328
- { reject: false }
1329
- );
1184
+ bits.push("### Agent");
1330
1185
  }
1331
- }
1332
- }
1333
- async function spawnWorkspaceScript(command, opts) {
1334
- const loginEnv = await captureLoginEnv();
1335
- const env = buildWorkspaceScriptEnv(
1336
- {
1337
- worktreePath: opts.worktreePath,
1338
- repoPath: opts.repoPath,
1339
- workspaceName: opts.workspaceName,
1340
- defaultBranch: opts.defaultBranch,
1341
- ports: opts.ports
1342
- },
1343
- loginEnv
1344
- );
1345
- try {
1346
- mergeAgentGitAuthEnv(
1347
- env,
1348
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
1186
+ const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
1187
+ for (const th of thinking) {
1188
+ if (th.type !== "thinking" || !th.text.trim()) continue;
1189
+ bits.push("Thinking:");
1190
+ bits.push(th.text);
1191
+ }
1192
+ const toolParts = (m.parts ?? []).filter(
1193
+ (p) => p.type === "tool"
1349
1194
  );
1350
- } catch {
1195
+ if (tools !== "none" && toolParts.length > 0) {
1196
+ if (tools === "summary") bits.push("Tools:");
1197
+ for (const t of toolParts) {
1198
+ bits.push(formatToolPart(t, tools));
1199
+ }
1200
+ }
1201
+ blocks.push(bits.join("\n"));
1351
1202
  }
1352
- const shell = process.platform === "darwin" ? "zsh" : "bash";
1353
- const child = execa2(shell, ["-lc", command], {
1354
- cwd: opts.worktreePath,
1355
- reject: false,
1356
- env,
1357
- // Own process group so Stop can tear down the whole tree (not just the shell).
1358
- // There is no settings.toml `stop=` / teardown hook for run scripts.
1359
- ...process.platform === "win32" ? {} : { detached: true }
1360
- });
1361
- pipeLines(child.stdout, opts.onLine);
1362
- pipeLines(child.stderr, opts.onLine);
1363
- const ports = opts.ports ?? [];
1364
- return {
1365
- pid: child.pid,
1366
- kill: () => killScriptTree(child, ports),
1367
- done: child.then((r) => r.exitCode ?? null),
1368
- child
1203
+ return blocks.join("\n\n");
1204
+ }
1205
+ function buildSessionSeed(messages, opts) {
1206
+ if (messages.length === 0) return null;
1207
+ const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
1208
+ if (!body.trim()) return null;
1209
+ return [
1210
+ "Sideboard conversation context (restored after compaction or a new session):",
1211
+ "",
1212
+ body,
1213
+ "",
1214
+ "Continue from this context. Do not repeat the summary unless asked."
1215
+ ].join("\n");
1216
+ }
1217
+ function applyCompaction(messages, summaryText, thresholds = {}) {
1218
+ const { older, recent } = splitForCompaction(messages, thresholds);
1219
+ if (older.length === 0) return messages;
1220
+ const summary = {
1221
+ role: "summary",
1222
+ text: summaryText.trim(),
1223
+ ts: (/* @__PURE__ */ new Date()).toISOString()
1369
1224
  };
1225
+ return [summary, ...recent];
1370
1226
  }
1371
- async function attachAbort(handle, signal) {
1372
- if (signal) {
1373
- const onAbort = () => handle.kill();
1374
- if (signal.aborted) onAbort();
1375
- else signal.addEventListener("abort", onAbort, { once: true });
1227
+ function lastRequestOccupancy(thread) {
1228
+ for (let i = thread.messages.length - 1; i >= 0; i--) {
1229
+ const usage = thread.messages[i]?.usage;
1230
+ if (usage) return contextTokens(usage);
1376
1231
  }
1377
- return handle.done;
1232
+ return 0;
1378
1233
  }
1379
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
1380
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1381
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
1382
- const handle = await spawnWorkspaceScript(settings.setup, {
1383
- worktreePath,
1384
- repoPath,
1385
- defaultBranch: opts?.defaultBranch,
1386
- onLine
1234
+ function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
1235
+ return lastRequestOccupancy(thread) >= occupancyTokens;
1236
+ }
1237
+ async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
1238
+ if (!shouldCompactContext(thread.messages, thresholds)) {
1239
+ return { didCompact: false, thread };
1240
+ }
1241
+ const { older } = splitForCompaction(thread.messages, thresholds);
1242
+ if (older.length === 0) {
1243
+ return { didCompact: false, thread };
1244
+ }
1245
+ const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
1246
+ const { summary, method } = await summarize(transcript, {
1247
+ cwd: thread.worktreePath
1387
1248
  });
1388
- const exitCode = await attachAbort(handle, opts?.signal);
1249
+ const messages = applyCompaction(thread.messages, summary, thresholds);
1250
+ const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
1251
+ const next = {
1252
+ ...thread,
1253
+ messages,
1254
+ sessionId: resetSession ? null : thread.sessionId,
1255
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1256
+ };
1389
1257
  return {
1390
- ran: true,
1391
- exitCode,
1392
- source: workspaceSettingsSourceLabel(worktreePath, repoPath),
1393
- kill: handle.kill
1258
+ didCompact: true,
1259
+ thread: next,
1260
+ summary,
1261
+ method,
1262
+ olderCount: older.length
1394
1263
  };
1395
1264
  }
1396
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
1397
- const found = findConventionSetup(worktreePath, repoPath);
1398
- if (!found) return { ran: false, exitCode: null, source: null };
1399
- onLine?.(`[setup] ${found.source}`);
1400
- const handle = await spawnWorkspaceScript(found.command, {
1401
- worktreePath,
1402
- repoPath,
1403
- defaultBranch: opts?.defaultBranch,
1404
- onLine
1405
- });
1406
- const exitCode = await attachAbort(handle, opts?.signal);
1407
- return {
1408
- ran: true,
1409
- exitCode,
1410
- source: found.source,
1411
- kill: handle.kill
1412
- };
1265
+
1266
+ // src/threads/chat-tabs.ts
1267
+ function sameWorktreePath(a, b) {
1268
+ return normalizeWorktreePath(a) === normalizeWorktreePath(b);
1413
1269
  }
1414
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
1415
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
1416
- if (!setup.ran) {
1417
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
1418
- }
1419
- if (!setup.ran) {
1420
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
1270
+ function takenTeamSlugsForChatTab(worktreePath) {
1271
+ const key = normalizeWorktreePath(worktreePath);
1272
+ const taken = /* @__PURE__ */ new Set();
1273
+ const dir = worktreeNameFromPath(key);
1274
+ if (dir) taken.add(dir.toLowerCase());
1275
+ for (const sibling of threadsSharingWorktree(key)) {
1276
+ for (const slug of takenSlugsFromThread(sibling)) {
1277
+ taken.add(slug);
1278
+ }
1421
1279
  }
1422
- return setup;
1280
+ return [...taken];
1423
1281
  }
1424
- async function runArchiveScript(repoPath, worktreePath, onLine) {
1425
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1426
- if (!settings?.archive) return { ran: false, exitCode: null };
1427
- const handle = await spawnWorkspaceScript(settings.archive, {
1428
- worktreePath,
1429
- repoPath,
1430
- onLine
1431
- });
1432
- const exitCode = await handle.done;
1433
- return { ran: true, exitCode };
1282
+ function requireThread(idOrRef) {
1283
+ const thread = findThreadByRef(idOrRef) ?? null;
1284
+ if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
1285
+ return thread;
1434
1286
  }
1435
- function listRunScripts(worktreePath, repoPath) {
1436
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1437
- if (!settings?.runScripts.length) return [];
1438
- return settings.runScripts.filter((s) => {
1439
- if (!s.availableIn?.length) return true;
1440
- return s.availableIn.includes("local");
1441
- });
1287
+ function worktreeBindingFrom(from) {
1288
+ return {
1289
+ worktreePath: normalizeWorktreePath(from.worktreePath),
1290
+ repoPath: from.repoPath,
1291
+ branchName: from.branchName,
1292
+ sourceRef: from.sourceRef,
1293
+ // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
1294
+ // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
1295
+ sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
1296
+ sourceIsFork: from.sourceIsFork,
1297
+ parentThreadId: from.parentThreadId,
1298
+ prUrl: from.prUrl,
1299
+ prTitle: from.prTitle,
1300
+ prState: from.prState,
1301
+ skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
1302
+ stackId: from.stackId,
1303
+ stackLayer: from.stackLayer,
1304
+ cowboy: from.cowboy
1305
+ };
1442
1306
  }
1443
- function getDefaultRunScript(worktreePath, repoPath) {
1444
- const scripts = listRunScripts(worktreePath, repoPath);
1445
- if (!scripts.length) return null;
1446
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
1307
+ function threadsSharingWorktree(worktreePath) {
1308
+ if (!worktreePath) return [];
1309
+ const key = normalizeWorktreePath(worktreePath);
1310
+ return listThreads({ includeArchived: true }).filter(
1311
+ (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
1312
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1447
1313
  }
1448
- function getRunScript(worktreePath, repoPath, name) {
1449
- const scripts = listRunScripts(worktreePath, repoPath);
1450
- if (!scripts.length) return null;
1451
- if (name) {
1452
- return scripts.find((s) => s.name === name) ?? null;
1453
- }
1454
- return getDefaultRunScript(worktreePath, repoPath);
1314
+ function formatTranscriptMarkdown(title, messages) {
1315
+ const body = formatMessagesAsTranscript(messages, { tools: "full" });
1316
+ return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
1455
1317
  }
1456
- function getRunMode(worktreePath, repoPath) {
1457
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
1318
+ function forkMessageSlice(from, throughIndex) {
1319
+ const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
1320
+ return through >= 0 ? from.messages.slice(0, through + 1) : [];
1458
1321
  }
1459
- async function allocatePort() {
1460
- return new Promise((resolve, reject) => {
1461
- const server = createServer();
1462
- server.listen(0, "127.0.0.1", () => {
1463
- const addr = server.address();
1464
- if (!addr || typeof addr === "string") {
1465
- server.close();
1466
- reject(new Error("Failed to allocate port"));
1467
- return;
1468
- }
1469
- const port = addr.port;
1470
- server.close((err) => err ? reject(err) : resolve(port));
1471
- });
1472
- server.on("error", reject);
1473
- });
1322
+ function buildForkTranscriptAttachment(baseTitle, messages) {
1323
+ const title = baseTitle || "Chat";
1324
+ return {
1325
+ id: randomUUID(),
1326
+ name: `Transcript of ${title}.md`,
1327
+ kind: "transcript",
1328
+ content: formatTranscriptMarkdown(title, messages)
1329
+ };
1474
1330
  }
1475
- async function allocatePortRange(size = PORT_RANGE_SIZE) {
1476
- const base = await allocatePort();
1477
- const ports = [base];
1478
- for (let i = 1; i < size; i++) {
1479
- const candidate = base + i;
1480
- const free = await new Promise((resolve) => {
1481
- const server = createServer();
1482
- server.once("error", () => resolve(false));
1483
- server.listen(candidate, "127.0.0.1", () => {
1484
- server.close(() => resolve(true));
1485
- });
1486
- });
1487
- if (free) {
1488
- ports.push(candidate);
1489
- } else {
1490
- ports.push(await allocatePort());
1491
- }
1331
+ function createChatTab(input) {
1332
+ const from = requireThread(input.fromThreadId);
1333
+ const binding = worktreeBindingFrom(from);
1334
+ const explicitTitle = input.title?.trim();
1335
+ const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
1336
+ const nextAgent = input.agent ?? from.agent;
1337
+ if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
1338
+ assertOrchestratorCapableAgent(nextAgent);
1492
1339
  }
1493
- return ports;
1340
+ const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
1341
+ const thread = createEmptyThread({
1342
+ title,
1343
+ // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
1344
+ // syncThreadBranchFromGit would otherwise rewrite every sibling to the
1345
+ // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
1346
+ userSetTitle: true,
1347
+ ...binding,
1348
+ // Slack / Brightsy cloud identity stays on the original chat. A + tab
1349
+ // that copies `slack:team:user` would steal inbound DMs after you close
1350
+ // the connected chat.
1351
+ sourceRef: singletonInbox ? title : binding.sourceRef,
1352
+ agent: nextAgent,
1353
+ model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
1354
+ effort: input.effort !== void 0 ? input.effort : from.effort,
1355
+ fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
1356
+ planMode: from.planMode,
1357
+ autonomy: input.autonomy ?? from.autonomy,
1358
+ attachments: input.attachments ?? [],
1359
+ status: "idle"
1360
+ });
1361
+ writeThread(thread);
1362
+ return thread;
1494
1363
  }
1495
- async function startDevServer(repoPath, worktreePath, onLine, opts) {
1496
- const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
1497
- if (!script) return null;
1498
- const ports = await allocatePortRange(PORT_RANGE_SIZE);
1499
- const handle = await spawnWorkspaceScript(script.command, {
1500
- worktreePath,
1501
- repoPath,
1502
- ports,
1503
- defaultBranch: opts?.defaultBranch,
1504
- onLine
1364
+ function forkChatTab(input) {
1365
+ const from = requireThread(input.threadId);
1366
+ const slice = forkMessageSlice(from, input.throughIndex);
1367
+ const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
1368
+ const tab = createChatTab({
1369
+ fromThreadId: input.threadId,
1370
+ agent: input.agent ?? from.agent,
1371
+ model: input.model,
1372
+ title: input.title?.trim() || void 0,
1373
+ attachments: [attachment]
1505
1374
  });
1506
- return {
1507
- pid: handle.pid,
1508
- port: ports[0],
1509
- ports,
1510
- scriptName: script.name,
1511
- kill: handle.kill,
1512
- done: handle.done
1513
- };
1375
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
1376
+ const next = { ...tab, parentThreadId: from.id };
1377
+ writeThread(next);
1378
+ return next;
1379
+ }
1380
+ return tab;
1514
1381
  }
1515
1382
 
1516
- // src/git/orphan-cleanup.ts
1517
- import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "fs";
1518
- import { join as join5 } from "path";
1519
- function isSideboardWorktreePath(path) {
1520
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
1521
- }
1522
- async function findOrphanWorktrees(repoPaths) {
1523
- const threads = listThreads({ includeArchived: true });
1524
- const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
1525
- const repos = new Set(
1526
- repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
1527
- );
1528
- const homeRoot = sideboardWorkspacesDir();
1529
- if (existsSync4(homeRoot)) {
1530
- try {
1531
- for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
1532
- if (!entry.isDirectory()) continue;
1533
- void entry;
1534
- }
1535
- } catch {
1536
- }
1537
- }
1538
- const orphans = [];
1539
- const seen = /* @__PURE__ */ new Set();
1540
- for (const repoPath of repos) {
1541
- if (!repoPath || !existsSync4(repoPath)) continue;
1542
- try {
1543
- const wts = await listWorktrees(repoPath);
1544
- for (const wt of wts) {
1545
- const path = wt.path.replace(/\/$/, "");
1546
- if (!isSideboardWorktreePath(path)) continue;
1547
- if (known.has(path)) continue;
1548
- if (seen.has(path)) continue;
1549
- seen.add(path);
1550
- let mtimeMs = 0;
1551
- try {
1552
- mtimeMs = statSync(path).mtimeMs;
1553
- } catch {
1554
- mtimeMs = 0;
1555
- }
1556
- orphans.push({ path, repoPath, mtimeMs });
1557
- }
1558
- } catch {
1559
- }
1560
- try {
1561
- const root = worktreesRoot(repoPath);
1562
- if (existsSync4(root)) {
1563
- for (const entry of readdirSync2(root, { withFileTypes: true })) {
1564
- if (!entry.isDirectory()) continue;
1565
- const path = join5(root, entry.name).replace(/\/$/, "");
1566
- if (known.has(path) || seen.has(path)) continue;
1567
- if (!existsSync4(join5(path, ".git"))) continue;
1568
- seen.add(path);
1569
- let mtimeMs = 0;
1570
- try {
1571
- mtimeMs = statSync(path).mtimeMs;
1572
- } catch {
1573
- mtimeMs = Date.now();
1574
- }
1575
- orphans.push({ path, repoPath, mtimeMs });
1576
- }
1577
- }
1578
- } catch {
1579
- }
1383
+ // src/review/review-request-template.ts
1384
+ var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
1385
+
1386
+ 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.
1387
+
1388
+ ## Required outcome
1389
+
1390
+ Start your reply with a **Recommendation** section using exactly one of:
1391
+
1392
+ - **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
1393
+ - **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
1394
+ - **Request changes** \u2014 not ready; blocking issues must be fixed first.
1395
+ - **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
1396
+
1397
+ 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.
1398
+
1399
+ People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
1400
+
1401
+ ## Findings
1402
+
1403
+ Below are guidelines for determining whether an issue is worth flagging to the original author.
1404
+
1405
+ These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
1406
+
1407
+ Flag something as a bug / blocking finding only when:
1408
+
1409
+ 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
1410
+ 2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
1411
+ 3. Fixing it does not demand rigor absent from the rest of the codebase.
1412
+ 4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
1413
+ 5. The author would likely fix it if made aware.
1414
+ 6. It does not rely on unstated assumptions about the codebase or author intent.
1415
+ 7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
1416
+ 8. It is clearly not just an intentional change by the author.
1417
+
1418
+ When flagging an issue, include a short accompanying comment:
1419
+
1420
+ 1. Clear about why it is a problem.
1421
+ 2. Severity must match reality \u2014 do not inflate.
1422
+ 3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
1423
+ 4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
1424
+ 5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
1425
+ 6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
1426
+ 7. Skimmable on first read.
1427
+ 8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
1428
+
1429
+ HOW MANY FINDINGS TO RETURN:
1430
+
1431
+ 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.
1432
+
1433
+ GUIDELINES:
1434
+
1435
+ - Ignore trivial style unless it obscures meaning or violates documented standards.
1436
+ - One comment per distinct issue (or a short multi-line range if needed).
1437
+ - Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
1438
+ - In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
1439
+ - Do NOT introduce or remove outer indentation levels unless that is the actual fix.
1440
+ - Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
1441
+
1442
+ 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).
1443
+
1444
+ ## Getting the diff
1445
+
1446
+ 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.
1447
+
1448
+ 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.
1449
+
1450
+ ## Fallback: if you don't have access to the Sideboard diff tool
1451
+
1452
+ If you don't have access to \`get_diff\`, use the following git commands to get the diff:
1453
+
1454
+ \`\`\`bash
1455
+ # Get the merge base between this branch and the target
1456
+ MERGE_BASE=$(git merge-base origin/main HEAD)
1457
+
1458
+ # Get the committed diff against the merge base
1459
+ git diff $MERGE_BASE HEAD
1460
+
1461
+ # Get any uncommitted changes (staged and unstaged)
1462
+ git diff HEAD
1463
+ \`\`\`
1464
+
1465
+ 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.
1466
+
1467
+ No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
1468
+
1469
+ ## Output format
1470
+
1471
+ **1. Recommendation first** (required), then **2. Findings** (may be empty).
1472
+
1473
+ Only report ONE finding per unique issue.
1474
+
1475
+ <example>
1476
+ ## Recommendation
1477
+
1478
+ **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.
1479
+
1480
+ ## Findings
1481
+
1482
+ ### **#1 Empty input causes crash** (blocking)
1483
+
1484
+ If the input field is empty when the page loads, the app will crash.
1485
+
1486
+ File: src/client/frontends/desktop/ui/Input.tsx
1487
+
1488
+ ### **#2 Dead code** (nit)
1489
+
1490
+ The getUserData function is now unused. It should be deleted.
1491
+
1492
+ File: src/client/frontends/desktop/core/UserData.ts
1493
+ </example>
1494
+
1495
+ <example>
1496
+ ## Recommendation
1497
+
1498
+ **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
1499
+ </example>
1500
+
1501
+ ## Growing the rules
1502
+
1503
+ 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.
1504
+ `;
1505
+ var REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
1506
+ var REVIEW_SKILL_NAME = "review";
1507
+ var REVIEW_SKILL_FRONTMATTER = `---
1508
+ name: review
1509
+ description: >-
1510
+ Merge-readiness review for this repository. Use when reviewing a PR, worktree
1511
+ diff, or Sideboard Review (Approve / Approve with nits / Request changes).
1512
+ ---
1513
+
1514
+ `;
1515
+ function wrapReviewSkillMarkdown(body) {
1516
+ const trimmed = body.replace(/^\uFEFF/, "").trim();
1517
+ const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
1518
+ if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
1519
+ return source.endsWith("\n") ? source : `${source}
1520
+ `;
1580
1521
  }
1581
- return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
1522
+ return `${REVIEW_SKILL_FRONTMATTER}${source}
1523
+ `;
1582
1524
  }
1583
- async function cleanupOrphanWorktrees(opts) {
1584
- const settings = loadAppSettings();
1585
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
1586
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
1587
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
1588
- const budget = Math.max(0, maxCount - activeCount);
1589
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
1590
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
1591
- const removed = [];
1592
- const kept = [];
1593
- for (const orphan of orphans) {
1594
- if (keep.has(orphan.path)) {
1595
- kept.push(orphan.path);
1596
- continue;
1597
- }
1598
- if (opts?.dryRun) {
1599
- removed.push(orphan.path);
1600
- continue;
1601
- }
1602
- try {
1603
- await removeWorktree(orphan.repoPath, orphan.path);
1604
- removed.push(orphan.path);
1605
- } catch {
1606
- kept.push(orphan.path);
1607
- }
1608
- }
1609
- if (!opts?.dryRun) {
1610
- updateAdvancedSettings({
1611
- worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
1612
- });
1525
+
1526
+ // src/review/request-review.ts
1527
+ var REPO_REVIEW_PATH = ".sideboard/review.md";
1528
+ var REPO_REVIEW_NAME = "review.md";
1529
+ var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
1530
+ var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
1531
+ var REVIEW_REQUEST_NAME = "Review request.md";
1532
+ var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
1533
+ var LEGACY_REVIEW_TEMPLATE_MARKERS = [
1534
+ "You are acting as a reviewer for a proposed code change made by another engineer.",
1535
+ "HOW MANY FINDINGS TO RETURN:"
1536
+ ];
1537
+ function shouldRefreshReviewRequestTemplate(content) {
1538
+ const trimmed = content.trim();
1539
+ if (!trimmed) return true;
1540
+ if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
1541
+ return false;
1613
1542
  }
1614
- return { removed, kept, orphans };
1543
+ return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
1615
1544
  }
1616
- function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
1617
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
1618
- const last = settings.advanced.worktreeLastCleanupAt;
1619
- if (!last) return true;
1620
- const elapsed = Date.now() - Date.parse(last);
1621
- return elapsed >= intervalHours * 36e5;
1545
+ function readTextIfPresent(abs) {
1546
+ if (!existsSync3(abs)) return null;
1547
+ try {
1548
+ const content = readFileSync3(abs, "utf8");
1549
+ return content.trim() ? content : null;
1550
+ } catch {
1551
+ return null;
1552
+ }
1622
1553
  }
1623
-
1624
- // src/git/apply-into-main.ts
1625
- import { execa as execa3 } from "execa";
1626
- async function applyThreadIntoMain(thread, opts) {
1627
- const method = opts?.method ?? "merge";
1628
- const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
1629
- const status = await execa3("git", ["status", "--porcelain"], {
1630
- cwd: thread.repoPath,
1631
- reject: false
1632
- });
1633
- if (status.stdout.trim()) {
1634
- throw new Error(
1635
- `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
1636
- );
1554
+ function readLocalGuidelines(worktreePath) {
1555
+ const localAbs = join4(worktreePath, REVIEW_REQUEST_PATH);
1556
+ const localContent = readTextIfPresent(localAbs);
1557
+ if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
1558
+ return { path: REVIEW_REQUEST_PATH, content: localContent };
1637
1559
  }
1638
- const checkout = await execa3("git", ["checkout", target], {
1639
- cwd: thread.repoPath,
1640
- reject: false
1641
- });
1642
- if (checkout.exitCode !== 0) {
1643
- throw new Error(
1644
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
1645
- );
1560
+ const legacyAbs = join4(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
1561
+ const legacyContent = readTextIfPresent(legacyAbs);
1562
+ if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
1563
+ return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
1646
1564
  }
1647
- if (method === "cherry-pick") {
1648
- const log = await execa3(
1649
- "git",
1650
- ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
1651
- { cwd: thread.repoPath, reject: false }
1652
- );
1653
- const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
1654
- if (!shas.length) {
1655
- return {
1656
- applied: false,
1657
- method,
1658
- targetBranch: target,
1659
- message: `No commits on ${thread.branchName} not already in ${target}`
1660
- };
1661
- }
1662
- for (const sha of shas) {
1663
- const cp = await execa3("git", ["cherry-pick", sha], {
1664
- cwd: thread.repoPath,
1665
- reject: false
1666
- });
1667
- if (cp.exitCode !== 0) {
1668
- await execa3("git", ["cherry-pick", "--abort"], {
1669
- cwd: thread.repoPath,
1670
- reject: false
1671
- });
1672
- throw new Error(
1673
- `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
1674
- );
1675
- }
1676
- }
1565
+ return null;
1566
+ }
1567
+ function skillGuidelines(content, source) {
1568
+ return {
1569
+ path: REVIEW_SKILL_PATH,
1570
+ name: REVIEW_SKILL_NAME,
1571
+ content,
1572
+ source
1573
+ };
1574
+ }
1575
+ function ensureReviewSkillFile(worktreePath) {
1576
+ const abs = join4(worktreePath, REVIEW_SKILL_PATH);
1577
+ const existing = readTextIfPresent(abs);
1578
+ if (existing) {
1579
+ return { path: REVIEW_SKILL_PATH, content: existing, wrote: false };
1580
+ }
1581
+ const fromRepo = readTextIfPresent(join4(worktreePath, REPO_REVIEW_PATH));
1582
+ const fromLocal = readLocalGuidelines(worktreePath)?.content ?? null;
1583
+ const content = wrapReviewSkillMarkdown(fromRepo ?? fromLocal ?? REVIEW_REQUEST_TEMPLATE);
1584
+ mkdirSync(dirname(abs), { recursive: true });
1585
+ writeFileSync(abs, content, "utf8");
1586
+ return { path: REVIEW_SKILL_PATH, content, wrote: true };
1587
+ }
1588
+ function resolveReviewGuidelines(worktreePath) {
1589
+ const skillContent = readTextIfPresent(join4(worktreePath, REVIEW_SKILL_PATH));
1590
+ if (skillContent) return skillGuidelines(skillContent, "skill");
1591
+ const local = readLocalGuidelines(worktreePath);
1592
+ if (local) {
1677
1593
  return {
1678
- applied: true,
1679
- method,
1680
- targetBranch: target,
1681
- message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
1594
+ path: local.path,
1595
+ name: REVIEW_REQUEST_NAME,
1596
+ content: local.content,
1597
+ source: "local"
1682
1598
  };
1683
1599
  }
1684
- const merge = await execa3(
1685
- "git",
1686
- ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
1687
- { cwd: thread.repoPath, reject: false }
1688
- );
1689
- if (merge.exitCode !== 0) {
1690
- await execa3("git", ["merge", "--abort"], {
1691
- cwd: thread.repoPath,
1692
- reject: false
1693
- });
1694
- throw new Error(
1695
- `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
1696
- );
1697
- }
1600
+ const seeded = ensureReviewSkillFile(worktreePath);
1601
+ return skillGuidelines(seeded.content, seeded.wrote ? "stock" : "skill");
1602
+ }
1603
+ function buildReviewRequestAttachment(content, opts) {
1604
+ const path = opts?.path ?? REVIEW_SKILL_PATH;
1605
+ const name = opts?.name ?? (path === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
1698
1606
  return {
1699
- applied: true,
1700
- method: "merge",
1701
- targetBranch: target,
1702
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
1607
+ id: randomUUID2(),
1608
+ name,
1609
+ kind: "file",
1610
+ path,
1611
+ content
1703
1612
  };
1704
1613
  }
1705
-
1706
- // src/git/clone-repo.ts
1707
- import { existsSync as existsSync5 } from "fs";
1708
- import { basename as basename2, join as join6 } from "path";
1709
- import { execa as execa4 } from "execa";
1710
- async function cloneRepoIntoSideboard(opts) {
1711
- const url = opts.url.trim();
1712
- if (!url) throw new Error("Clone URL is required");
1713
- let name = opts.name?.trim();
1714
- if (!name) {
1715
- const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
1716
- name = leaf || "repo";
1717
- }
1718
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
1719
- const dest = join6(sideboardReposDir(), name);
1720
- if (existsSync5(dest)) {
1721
- const repoPath2 = await resolveRepoRoot(dest);
1722
- const workspace2 = await ensureWorkspace(repoPath2);
1723
- return { repoPath: repoPath2, workspace: workspace2 };
1724
- }
1725
- const result = await execa4("git", ["clone", url, dest], { reject: false });
1726
- if (result.exitCode !== 0) {
1614
+ async function requestReview(threadRef, send) {
1615
+ const from = findThreadByRef(threadRef);
1616
+ if (!from) throw new Error(`Thread not found: ${threadRef}`);
1617
+ if (isOrchestratorThread(from)) {
1727
1618
  throw new Error(
1728
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
1619
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
1729
1620
  );
1730
1621
  }
1731
- const repoPath = await resolveRepoRoot(dest);
1732
- const workspace = await ensureWorkspace(repoPath);
1733
- return { repoPath, workspace };
1734
- }
1735
-
1736
- // src/store/desktop-host.ts
1737
- import { readFileSync as readFileSync4, unlinkSync, writeFileSync } from "fs";
1738
- import { join as join7 } from "path";
1739
- function desktopHostPidPath() {
1740
- return join7(appDataDir(), "desktop-host.pid");
1741
- }
1742
- function readDesktopHostPid() {
1743
- try {
1744
- const pid = Number.parseInt(readFileSync4(desktopHostPidPath(), "utf8").trim(), 10);
1745
- if (!Number.isFinite(pid) || pid <= 0) return null;
1746
- return pid;
1747
- } catch {
1748
- return null;
1749
- }
1750
- }
1751
- function pidAlive(pid) {
1752
- try {
1753
- process.kill(pid, 0);
1754
- return true;
1755
- } catch {
1756
- return false;
1622
+ if (from.status === "archived") {
1623
+ throw new Error(`Thread is archived: ${from.id}`);
1757
1624
  }
1625
+ const guidelines = resolveReviewGuidelines(from.worktreePath);
1626
+ const tab = createChatTab({
1627
+ fromThreadId: from.id,
1628
+ title: "Review",
1629
+ attachments: [
1630
+ buildReviewRequestAttachment(guidelines.content, {
1631
+ path: guidelines.path,
1632
+ name: guidelines.name
1633
+ })
1634
+ ]
1635
+ });
1636
+ const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
1637
+ return { tab: started, from };
1758
1638
  }
1759
- function isDesktopHostAlive() {
1760
- const pid = readDesktopHostPid();
1761
- return pid != null && pidAlive(pid);
1762
- }
1763
- function isThisProcessDesktopHost() {
1764
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
1639
+
1640
+ // src/hook/conductor.ts
1641
+ var PORT_RANGE_SIZE = 10;
1642
+ function matchSimpleGlob(pattern, name) {
1643
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1644
+ return new RegExp(`^${escaped}$`).test(name);
1765
1645
  }
1766
- function thisProcessShouldDrainAgentQueues() {
1767
- if (isThisProcessDesktopHost()) return true;
1768
- return !isDesktopHostAlive();
1646
+ function readWorktreeInclude(repoPath) {
1647
+ const path = join5(repoPath, ".worktreeinclude");
1648
+ if (!existsSync4(path)) return [];
1649
+ return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
1769
1650
  }
1770
-
1771
- // src/threads/create.ts
1772
- import { existsSync as existsSync6 } from "fs";
1773
-
1774
- // src/detect/detect.ts
1775
- var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
1776
- async function requireAgent(agent, opts) {
1777
- ensureAgentPath();
1778
- enrichPathWithNpmGlobalBin();
1779
- const status = await Promise.race([
1780
- getAdapter(agent).detect(),
1781
- new Promise((_, reject) => {
1782
- setTimeout(() => {
1783
- reject(
1784
- new Error(
1785
- `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
1786
- )
1787
- );
1788
- }, REQUIRE_AGENT_TIMEOUT_MS);
1789
- })
1790
- ]);
1791
- if (!status.installed || !status.authenticated) {
1792
- throw new Error(status.reason ?? `${agent} is not available`);
1651
+ function resolveFilesToCopy(repoPath) {
1652
+ const fromInclude = readWorktreeInclude(repoPath);
1653
+ if (fromInclude.length) return fromInclude;
1654
+ const settings = loadRepoSettings(repoPath);
1655
+ if (settings?.filesToCopy?.length) return settings.filesToCopy;
1656
+ if (settings?.fileIncludeGlobs?.length) {
1657
+ const matched = [];
1658
+ try {
1659
+ for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1660
+ if (!entry.isFile()) continue;
1661
+ for (const glob of settings.fileIncludeGlobs) {
1662
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
1663
+ matched.push(entry.name);
1664
+ break;
1665
+ }
1666
+ }
1667
+ }
1668
+ } catch {
1669
+ }
1670
+ if (matched.length) return [...new Set(matched)];
1793
1671
  }
1794
- if (opts?.requireLinear && !status.linearMcp) {
1795
- throw new Error(
1796
- `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
1797
- );
1672
+ const defaults = [];
1673
+ try {
1674
+ for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1675
+ if (entry.isFile() && entry.name.startsWith(".env")) {
1676
+ defaults.push(entry.name);
1677
+ }
1678
+ }
1679
+ } catch {
1798
1680
  }
1799
- return status;
1681
+ if (defaults.length) return defaults;
1682
+ return [".env.local", ".env"];
1800
1683
  }
1801
-
1802
- // src/threads/create.ts
1803
- async function createThread(input, _onSetupLine) {
1804
- const resolved = resolveNewThreadOptions({
1805
- agent: input.agent,
1806
- model: input.model,
1807
- effort: input.effort,
1808
- fast: input.fast
1809
- });
1810
- await requireAgent(resolved.agent);
1811
- const repoPath = await resolveRepoRoot(input.repoPath);
1812
- if (!existsSync6(repoPath)) {
1813
- throw new Error(`Repo not found: ${repoPath}`);
1684
+ function copyConfiguredFiles(repoPath, worktreePath) {
1685
+ const patterns = resolveFilesToCopy(repoPath);
1686
+ const copied = [];
1687
+ for (const rel of patterns) {
1688
+ const src = join5(repoPath, rel);
1689
+ if (!existsSync4(src)) continue;
1690
+ const dest = join5(worktreePath, rel);
1691
+ mkdirSync2(dirname2(dest), { recursive: true });
1692
+ copyFileSync(src, dest);
1693
+ copied.push(rel);
1814
1694
  }
1815
- let sourceRef = input.sourceRef;
1816
- let sourceIsFork = false;
1817
- let prUrl = null;
1818
- let prTitle = null;
1819
- if (input.sourceType === "pr") {
1820
- const num = Number(input.sourceRef.replace(/^#/, ""));
1821
- if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
1822
- const pr = await getPr(repoPath, num);
1823
- if (!pr) throw new Error(`PR #${num} not found`);
1824
- sourceIsFork = pr.isCrossRepository;
1825
- prUrl = pr.url;
1826
- const localFetchBranch = `sideboard-pr-${pr.number}`;
1827
- await fetchPrHead(repoPath, pr.number, localFetchBranch);
1828
- sourceRef = localFetchBranch;
1829
- } else if (input.sourceType === "ticket") {
1830
- sourceRef = await resolveDefaultBranch(repoPath);
1831
- } else if (input.sourceType === "branch") {
1832
- if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
1833
- sourceRef = await resolveDefaultBranch(repoPath);
1834
- } else {
1835
- const existing = await getPrForHeadBranch(repoPath, sourceRef);
1836
- if (existing?.url) {
1837
- prUrl = existing.url;
1838
- prTitle = existing.title;
1695
+ return copied;
1696
+ }
1697
+ var cachedLoginEnv = null;
1698
+ async function captureLoginEnv() {
1699
+ if (cachedLoginEnv) return { ...cachedLoginEnv };
1700
+ const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
1701
+ try {
1702
+ const result = await execa2(shell, ["-l", "-c", "env -0"], {
1703
+ reject: false,
1704
+ timeout: 5e3
1705
+ });
1706
+ if (result.exitCode === 0 && result.stdout) {
1707
+ const env = { ...process.env };
1708
+ for (const entry of result.stdout.split("\0")) {
1709
+ if (!entry) continue;
1710
+ const eq = entry.indexOf("=");
1711
+ if (eq <= 0) continue;
1712
+ const key = entry.slice(0, eq);
1713
+ const value = entry.slice(eq + 1);
1714
+ if (key) env[key] = value;
1839
1715
  }
1716
+ cachedLoginEnv = env;
1717
+ return { ...env };
1840
1718
  }
1841
- } else if (input.sourceType === "adopt") {
1842
- throw new Error("Use adoptThread() for adopt sources");
1719
+ } catch {
1843
1720
  }
1844
- const team = allocateTeamSlug(repoPath);
1845
- const { branchName, worktreePath } = await createThreadWorktree({
1846
- repoPath,
1847
- sourceRef,
1848
- slug: team.slug
1849
- });
1850
- copyConfiguredFiles(repoPath, worktreePath);
1851
- const explicitTitle = input.title?.trim();
1852
- const thread = createEmptyThread({
1853
- title: explicitTitle || team.name,
1854
- userSetTitle: Boolean(explicitTitle),
1855
- sourceType: input.sourceType,
1856
- sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
1857
- branchName,
1858
- worktreePath,
1859
- repoPath,
1860
- agent: resolved.agent,
1861
- autonomy: input.autonomy ?? "default",
1862
- model: resolved.model,
1863
- effort: resolved.effort,
1864
- fast: resolved.fast,
1865
- planMode: Boolean(input.planMode),
1866
- attachments: input.attachments ?? [],
1867
- sourceIsFork,
1868
- parentThreadId: input.parentThreadId ?? null,
1869
- status: "idle",
1870
- prUrl,
1871
- prTitle
1872
- });
1873
- writeThread(thread);
1874
- await ensureWorkspace(repoPath);
1875
- return readThread(thread.id) ?? thread;
1721
+ cachedLoginEnv = { ...process.env };
1722
+ return { ...cachedLoginEnv };
1876
1723
  }
1877
- async function listLinearIssues(agent, repoPath) {
1878
- const { getAdapter: getAdapter2 } = await import("./agents-T4RHL5UX.js");
1879
- await requireAgent(agent, { requireLinear: true });
1880
- const adapter = getAdapter2(agent);
1881
- if (!adapter.listLinearIssues) {
1882
- throw new Error(`${agent} does not support Linear issue listing`);
1724
+ function buildWorkspaceScriptEnv(opts, baseEnv) {
1725
+ const env = stripNestedElectronEnv({
1726
+ ...baseEnv ?? process.env
1727
+ });
1728
+ const name = opts.workspaceName ?? basename(opts.worktreePath);
1729
+ const ports = opts.ports ?? [];
1730
+ const primary = ports[0];
1731
+ env.SIDEBOARD_WORKSPACE_NAME = name;
1732
+ env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
1733
+ env.SIDEBOARD_ROOT_PATH = opts.repoPath;
1734
+ env.SIDEBOARD_IS_LOCAL = "1";
1735
+ if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
1736
+ env.CONDUCTOR_WORKSPACE_NAME = name;
1737
+ env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
1738
+ env.CONDUCTOR_ROOT_PATH = opts.repoPath;
1739
+ env.CONDUCTOR_IS_LOCAL = "1";
1740
+ if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
1741
+ if (primary != null) {
1742
+ env.SIDEBOARD_PORT = String(primary);
1743
+ env.CONDUCTOR_PORT = String(primary);
1744
+ env.PORT = String(primary);
1745
+ for (let i = 1; i < ports.length; i++) {
1746
+ const p = ports[i];
1747
+ env[`SIDEBOARD_PORT_${i}`] = String(p);
1748
+ env[`CONDUCTOR_PORT_${i}`] = String(p);
1749
+ }
1883
1750
  }
1884
- return adapter.listLinearIssues(repoPath);
1751
+ return env;
1885
1752
  }
1886
-
1887
- // src/threads/chat-tabs.ts
1888
- import { randomUUID } from "crypto";
1889
-
1890
- // src/composer/summarize.ts
1891
- var MAX_TRANSCRIPT_CHARS = 12e4;
1892
- var MAX_SUMMARY_CHARS = 6e3;
1893
- async function summarizeConversation(transcript, opts) {
1894
- const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
1895
-
1896
- [\u2026truncated\u2026]` : transcript;
1897
- const claudeSummary = await tryClaudeSummary(clipped, opts);
1898
- if (claudeSummary) {
1899
- return { summary: clipSummary(claudeSummary), method: "claude" };
1753
+ function pipeLines(stream, onLine) {
1754
+ if (!stream || !onLine) return;
1755
+ const rl = createInterface2({ input: stream });
1756
+ rl.on("line", onLine);
1757
+ }
1758
+ function killScriptTree(child, ports = []) {
1759
+ const pid = child.pid;
1760
+ if (pid) {
1761
+ try {
1762
+ if (process.platform === "win32") {
1763
+ void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
1764
+ } else {
1765
+ process.kill(-pid, "SIGTERM");
1766
+ setTimeout(() => {
1767
+ try {
1768
+ process.kill(-pid, "SIGKILL");
1769
+ } catch {
1770
+ }
1771
+ }, 2500).unref?.();
1772
+ }
1773
+ } catch {
1774
+ try {
1775
+ child.kill("SIGTERM");
1776
+ } catch {
1777
+ }
1778
+ }
1779
+ }
1780
+ for (const port of ports) {
1781
+ if (!Number.isFinite(port) || port <= 0) continue;
1782
+ if (process.platform === "win32") {
1783
+ void execa2(
1784
+ "powershell",
1785
+ [
1786
+ "-NoProfile",
1787
+ "-Command",
1788
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
1789
+ ],
1790
+ { reject: false }
1791
+ );
1792
+ } else {
1793
+ void execa2(
1794
+ "zsh",
1795
+ ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
1796
+ { reject: false }
1797
+ );
1798
+ }
1900
1799
  }
1901
- return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
1902
1800
  }
1903
- async function tryClaudeSummary(transcript, opts) {
1904
- ensureAgentPath();
1905
- const prompt = [
1906
- "Summarize this coding-agent conversation for continuity in a fresh session.",
1907
- "Use concise markdown bullets covering:",
1908
- "- Goal / request",
1909
- "- Key decisions and constraints",
1910
- "- Files / areas touched",
1911
- "- Current state and open tasks",
1912
- "- Anything the next turn must remember",
1913
- "Do not invent details. No preamble.",
1914
- "",
1915
- "Transcript:",
1916
- transcript
1917
- ].join("\n");
1801
+ async function spawnWorkspaceScript(command, opts) {
1802
+ const loginEnv = await captureLoginEnv();
1803
+ const env = buildWorkspaceScriptEnv(
1804
+ {
1805
+ worktreePath: opts.worktreePath,
1806
+ repoPath: opts.repoPath,
1807
+ workspaceName: opts.workspaceName,
1808
+ defaultBranch: opts.defaultBranch,
1809
+ ports: opts.ports
1810
+ },
1811
+ loginEnv
1812
+ );
1918
1813
  try {
1919
- const { stdout, exitCode } = await run(
1920
- "claude",
1921
- [
1922
- "-p",
1923
- prompt,
1924
- "--output-format",
1925
- "text",
1926
- "--permission-mode",
1927
- "bypassPermissions",
1928
- "--model",
1929
- "haiku"
1930
- ],
1931
- { cwd: opts?.cwd, reject: false }
1814
+ mergeAgentGitAuthEnv(
1815
+ env,
1816
+ await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
1932
1817
  );
1933
- if (exitCode !== 0) return null;
1934
- const text = stdout.trim();
1935
- if (text.length < 40) return null;
1936
- return text;
1937
1818
  } catch {
1938
- return null;
1939
1819
  }
1820
+ const shell = process.platform === "darwin" ? "zsh" : "bash";
1821
+ const child = execa2(shell, ["-lc", command], {
1822
+ cwd: opts.worktreePath,
1823
+ reject: false,
1824
+ env,
1825
+ // Own process group so Stop can tear down the whole tree (not just the shell).
1826
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
1827
+ ...process.platform === "win32" ? {} : { detached: true }
1828
+ });
1829
+ pipeLines(child.stdout, opts.onLine);
1830
+ pipeLines(child.stderr, opts.onLine);
1831
+ const ports = opts.ports ?? [];
1832
+ return {
1833
+ pid: child.pid,
1834
+ kill: () => killScriptTree(child, ports),
1835
+ done: child.then((r) => r.exitCode ?? null),
1836
+ child
1837
+ };
1940
1838
  }
1941
- function extractiveSummary(transcript) {
1942
- const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
1943
- const users = [];
1944
- const tools = [];
1945
- const agents = [];
1946
- let mode = "none";
1947
- for (const line of lines) {
1948
- if (line.startsWith("### User")) {
1949
- mode = "user";
1950
- continue;
1951
- }
1952
- if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
1953
- mode = "agent";
1954
- continue;
1955
- }
1956
- if (line.startsWith("- ") && line.includes(":")) {
1957
- tools.push(line.replace(/^- /, ""));
1958
- continue;
1959
- }
1960
- if (mode === "user" && users.length < 8) {
1961
- users.push(line.slice(0, 240));
1962
- } else if (mode === "agent" && agents.length < 6) {
1963
- agents.push(line.slice(0, 240));
1964
- }
1965
- }
1966
- const uniqueTools = [...new Set(tools)].slice(0, 20);
1967
- const parts = [
1968
- "## Compacted context (extractive)",
1969
- "",
1970
- "### User goals",
1971
- ...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
1972
- "",
1973
- "### Recent agent notes",
1974
- ...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
1975
- ];
1976
- if (uniqueTools.length) {
1977
- parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
1839
+ async function attachAbort(handle, signal) {
1840
+ if (signal) {
1841
+ const onAbort = () => handle.kill();
1842
+ if (signal.aborted) onAbort();
1843
+ else signal.addEventListener("abort", onAbort, { once: true });
1978
1844
  }
1979
- parts.push(
1980
- "",
1981
- "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
1982
- );
1983
- return parts.join("\n");
1845
+ return handle.done;
1984
1846
  }
1985
- function clipSummary(text) {
1986
- if (text.length <= MAX_SUMMARY_CHARS) return text;
1987
- return `${text.slice(0, MAX_SUMMARY_CHARS)}
1988
-
1989
- [\u2026summary truncated\u2026]`;
1847
+ async function runSetupScript(repoPath, worktreePath, onLine, opts) {
1848
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
1849
+ if (!settings?.setup) return { ran: false, exitCode: null, source: null };
1850
+ const handle = await spawnWorkspaceScript(settings.setup, {
1851
+ worktreePath,
1852
+ repoPath,
1853
+ defaultBranch: opts?.defaultBranch,
1854
+ onLine
1855
+ });
1856
+ const exitCode = await attachAbort(handle, opts?.signal);
1857
+ return {
1858
+ ran: true,
1859
+ exitCode,
1860
+ source: workspaceSettingsSourceLabel(worktreePath, repoPath),
1861
+ kill: handle.kill
1862
+ };
1990
1863
  }
1991
-
1992
- // src/composer/context-compact.ts
1993
- var CONTEXT_COMPACT_CHARS = 1e5;
1994
- var CONTEXT_KEEP_RECENT_CHARS = 24e3;
1995
- var CONTEXT_KEEP_RECENT_MESSAGES = 12;
1996
- var CONTEXT_MIN_MESSAGES = 10;
1997
- function estimateMessageChars(message) {
1998
- let n = message.text.length + 16;
1999
- for (const part of message.parts ?? []) {
2000
- n += estimatePartChars(part);
1864
+ async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
1865
+ const found = findConventionSetup(worktreePath, repoPath);
1866
+ if (!found) return { ran: false, exitCode: null, source: null };
1867
+ onLine?.(`[setup] ${found.source}`);
1868
+ const handle = await spawnWorkspaceScript(found.command, {
1869
+ worktreePath,
1870
+ repoPath,
1871
+ defaultBranch: opts?.defaultBranch,
1872
+ onLine
1873
+ });
1874
+ const exitCode = await attachAbort(handle, opts?.signal);
1875
+ return {
1876
+ ran: true,
1877
+ exitCode,
1878
+ source: found.source,
1879
+ kill: handle.kill
1880
+ };
1881
+ }
1882
+ async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
1883
+ const reviewSkill = ensureReviewSkillFile(worktreePath);
1884
+ if (reviewSkill.wrote) {
1885
+ onLine?.(
1886
+ `[setup] wrote ${REVIEW_SKILL_PATH} \u2014 commit it so later worktrees inherit Review guidelines`
1887
+ );
2001
1888
  }
2002
- return n;
1889
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
1890
+ if (!setup.ran) {
1891
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
1892
+ }
1893
+ if (!setup.ran) {
1894
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
1895
+ }
1896
+ return setup;
2003
1897
  }
2004
- function estimatePartChars(part) {
2005
- switch (part.type) {
2006
- case "text":
2007
- case "thinking":
2008
- return part.text.length;
2009
- case "tool": {
2010
- const input = part.input ? JSON.stringify(part.input) : "";
2011
- return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
2012
- }
2013
- default:
2014
- return 0;
1898
+ async function runArchiveScript(repoPath, worktreePath, onLine) {
1899
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
1900
+ if (!settings?.archive) return { ran: false, exitCode: null };
1901
+ const handle = await spawnWorkspaceScript(settings.archive, {
1902
+ worktreePath,
1903
+ repoPath,
1904
+ onLine
1905
+ });
1906
+ const exitCode = await handle.done;
1907
+ return { ran: true, exitCode };
1908
+ }
1909
+ function listRunScripts(worktreePath, repoPath) {
1910
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
1911
+ if (!settings?.runScripts.length) return [];
1912
+ return settings.runScripts.filter((s) => {
1913
+ if (!s.availableIn?.length) return true;
1914
+ return s.availableIn.includes("local");
1915
+ });
1916
+ }
1917
+ function getDefaultRunScript(worktreePath, repoPath) {
1918
+ const scripts = listRunScripts(worktreePath, repoPath);
1919
+ if (!scripts.length) return null;
1920
+ return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
1921
+ }
1922
+ function getRunScript(worktreePath, repoPath, name) {
1923
+ const scripts = listRunScripts(worktreePath, repoPath);
1924
+ if (!scripts.length) return null;
1925
+ if (name) {
1926
+ return scripts.find((s) => s.name === name) ?? null;
2015
1927
  }
1928
+ return getDefaultRunScript(worktreePath, repoPath);
2016
1929
  }
2017
- function estimateThreadChars(messages) {
2018
- return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
1930
+ function getRunMode(worktreePath, repoPath) {
1931
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
2019
1932
  }
2020
- function shouldCompactContext(messages, thresholds = {}) {
2021
- const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
2022
- const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
2023
- if (messages.length < minMessages) return false;
2024
- return estimateThreadChars(messages) >= maxChars;
1933
+ async function allocatePort() {
1934
+ return new Promise((resolve, reject) => {
1935
+ const server = createServer();
1936
+ server.listen(0, "127.0.0.1", () => {
1937
+ const addr = server.address();
1938
+ if (!addr || typeof addr === "string") {
1939
+ server.close();
1940
+ reject(new Error("Failed to allocate port"));
1941
+ return;
1942
+ }
1943
+ const port = addr.port;
1944
+ server.close((err) => err ? reject(err) : resolve(port));
1945
+ });
1946
+ server.on("error", reject);
1947
+ });
2025
1948
  }
2026
- function splitForCompaction(messages, thresholds = {}) {
2027
- const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
2028
- const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
2029
- if (messages.length === 0) return { older: [], recent: [] };
2030
- let recentChars = 0;
2031
- let cut = messages.length;
2032
- for (let i = messages.length - 1; i >= 0; i--) {
2033
- const next = recentChars + estimateMessageChars(messages[i]);
2034
- const kept = messages.length - i;
2035
- if (kept >= keepCount && next > keepChars) break;
2036
- recentChars = next;
2037
- cut = i;
1949
+ async function allocatePortRange(size = PORT_RANGE_SIZE) {
1950
+ const base = await allocatePort();
1951
+ const ports = [base];
1952
+ for (let i = 1; i < size; i++) {
1953
+ const candidate = base + i;
1954
+ const free = await new Promise((resolve) => {
1955
+ const server = createServer();
1956
+ server.once("error", () => resolve(false));
1957
+ server.listen(candidate, "127.0.0.1", () => {
1958
+ server.close(() => resolve(true));
1959
+ });
1960
+ });
1961
+ if (free) {
1962
+ ports.push(candidate);
1963
+ } else {
1964
+ ports.push(await allocatePort());
1965
+ }
2038
1966
  }
2039
- const minRecent = Math.min(keepCount, messages.length);
2040
- cut = Math.min(cut, messages.length - minRecent);
2041
- if (cut <= 0) return { older: [], recent: messages };
1967
+ return ports;
1968
+ }
1969
+ async function startDevServer(repoPath, worktreePath, onLine, opts) {
1970
+ const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
1971
+ if (!script) return null;
1972
+ const ports = await allocatePortRange(PORT_RANGE_SIZE);
1973
+ const handle = await spawnWorkspaceScript(script.command, {
1974
+ worktreePath,
1975
+ repoPath,
1976
+ ports,
1977
+ defaultBranch: opts?.defaultBranch,
1978
+ onLine
1979
+ });
2042
1980
  return {
2043
- older: messages.slice(0, cut),
2044
- recent: messages.slice(cut)
1981
+ pid: handle.pid,
1982
+ port: ports[0],
1983
+ ports,
1984
+ scriptName: script.name,
1985
+ kill: handle.kill,
1986
+ done: handle.done
2045
1987
  };
2046
1988
  }
2047
- function formatToolPart(t, detail) {
2048
- if (detail === "summary") {
2049
- const label = t.description || t.detail || t.name;
2050
- const path = t.filePath ? ` (${t.filePath})` : "";
2051
- return `- ${t.name}: ${label}${path}`;
2052
- }
2053
- const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
2054
- if (t.description) lines.push(`Description: ${t.description}`);
2055
- if (t.filePath) lines.push(`Path: ${t.filePath}`);
2056
- if (t.input && Object.keys(t.input).length > 0) {
2057
- lines.push("Input:");
2058
- lines.push("```json");
2059
- lines.push(JSON.stringify(t.input, null, 2));
2060
- lines.push("```");
2061
- } else if (t.detail) {
2062
- lines.push(`Detail: ${t.detail}`);
1989
+
1990
+ // src/git/orphan-cleanup.ts
1991
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync } from "fs";
1992
+ import { join as join6 } from "path";
1993
+ function isSideboardWorktreePath(path) {
1994
+ return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
1995
+ }
1996
+ async function findOrphanWorktrees(repoPaths) {
1997
+ const threads = listThreads({ includeArchived: true });
1998
+ const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
1999
+ const repos = new Set(
2000
+ repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
2001
+ );
2002
+ const homeRoot = sideboardWorkspacesDir();
2003
+ if (existsSync5(homeRoot)) {
2004
+ try {
2005
+ for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
2006
+ if (!entry.isDirectory()) continue;
2007
+ void entry;
2008
+ }
2009
+ } catch {
2010
+ }
2063
2011
  }
2064
- if (t.result != null && t.result !== "") {
2065
- lines.push("Result:");
2066
- lines.push("```");
2067
- lines.push(t.result);
2068
- lines.push("```");
2012
+ const orphans = [];
2013
+ const seen = /* @__PURE__ */ new Set();
2014
+ for (const repoPath of repos) {
2015
+ if (!repoPath || !existsSync5(repoPath)) continue;
2016
+ try {
2017
+ const wts = await listWorktrees(repoPath);
2018
+ for (const wt of wts) {
2019
+ const path = wt.path.replace(/\/$/, "");
2020
+ if (!isSideboardWorktreePath(path)) continue;
2021
+ if (known.has(path)) continue;
2022
+ if (seen.has(path)) continue;
2023
+ seen.add(path);
2024
+ let mtimeMs = 0;
2025
+ try {
2026
+ mtimeMs = statSync(path).mtimeMs;
2027
+ } catch {
2028
+ mtimeMs = 0;
2029
+ }
2030
+ orphans.push({ path, repoPath, mtimeMs });
2031
+ }
2032
+ } catch {
2033
+ }
2034
+ try {
2035
+ const root = worktreesRoot(repoPath);
2036
+ if (existsSync5(root)) {
2037
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
2038
+ if (!entry.isDirectory()) continue;
2039
+ const path = join6(root, entry.name).replace(/\/$/, "");
2040
+ if (known.has(path) || seen.has(path)) continue;
2041
+ if (!existsSync5(join6(path, ".git"))) continue;
2042
+ seen.add(path);
2043
+ let mtimeMs = 0;
2044
+ try {
2045
+ mtimeMs = statSync(path).mtimeMs;
2046
+ } catch {
2047
+ mtimeMs = Date.now();
2048
+ }
2049
+ orphans.push({ path, repoPath, mtimeMs });
2050
+ }
2051
+ }
2052
+ } catch {
2053
+ }
2069
2054
  }
2070
- return lines.join("\n");
2055
+ return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
2071
2056
  }
2072
- function formatMessagesAsTranscript(messages, opts) {
2073
- const tools = opts?.tools ?? "full";
2074
- const blocks = [];
2075
- for (const m of messages) {
2076
- if (m.role === "summary") {
2077
- blocks.push(`## Prior summary
2078
- ${m.text}`);
2057
+ async function cleanupOrphanWorktrees(opts) {
2058
+ const settings = loadAppSettings();
2059
+ const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
2060
+ const orphans = await findOrphanWorktrees(opts?.repoPaths);
2061
+ const activeCount = listThreads().filter((t) => t.status !== "archived").length;
2062
+ const budget = Math.max(0, maxCount - activeCount);
2063
+ const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
2064
+ const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
2065
+ const removed = [];
2066
+ const kept = [];
2067
+ for (const orphan of orphans) {
2068
+ if (keep.has(orphan.path)) {
2069
+ kept.push(orphan.path);
2079
2070
  continue;
2080
2071
  }
2081
- if (m.role === "user") {
2082
- blocks.push(`### User
2083
- ${m.text}`);
2072
+ if (opts?.dryRun) {
2073
+ removed.push(orphan.path);
2084
2074
  continue;
2085
2075
  }
2086
- const bits = [];
2087
- if (m.text?.trim()) {
2088
- bits.push(`### Agent
2089
- ${m.text}`);
2090
- } else {
2091
- bits.push("### Agent");
2092
- }
2093
- const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
2094
- for (const th of thinking) {
2095
- if (th.type !== "thinking" || !th.text.trim()) continue;
2096
- bits.push("Thinking:");
2097
- bits.push(th.text);
2076
+ try {
2077
+ await removeWorktree(orphan.repoPath, orphan.path);
2078
+ removed.push(orphan.path);
2079
+ } catch {
2080
+ kept.push(orphan.path);
2098
2081
  }
2099
- const toolParts = (m.parts ?? []).filter(
2100
- (p) => p.type === "tool"
2082
+ }
2083
+ if (!opts?.dryRun) {
2084
+ updateAdvancedSettings({
2085
+ worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
2086
+ });
2087
+ }
2088
+ return { removed, kept, orphans };
2089
+ }
2090
+ function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
2091
+ const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
2092
+ const last = settings.advanced.worktreeLastCleanupAt;
2093
+ if (!last) return true;
2094
+ const elapsed = Date.now() - Date.parse(last);
2095
+ return elapsed >= intervalHours * 36e5;
2096
+ }
2097
+
2098
+ // src/git/apply-into-main.ts
2099
+ import { execa as execa3 } from "execa";
2100
+ async function applyThreadIntoMain(thread, opts) {
2101
+ const method = opts?.method ?? "merge";
2102
+ const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
2103
+ const status = await execa3("git", ["status", "--porcelain"], {
2104
+ cwd: thread.repoPath,
2105
+ reject: false
2106
+ });
2107
+ if (status.stdout.trim()) {
2108
+ throw new Error(
2109
+ `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
2101
2110
  );
2102
- if (tools !== "none" && toolParts.length > 0) {
2103
- if (tools === "summary") bits.push("Tools:");
2104
- for (const t of toolParts) {
2105
- bits.push(formatToolPart(t, tools));
2111
+ }
2112
+ const checkout = await execa3("git", ["checkout", target], {
2113
+ cwd: thread.repoPath,
2114
+ reject: false
2115
+ });
2116
+ if (checkout.exitCode !== 0) {
2117
+ throw new Error(
2118
+ `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
2119
+ );
2120
+ }
2121
+ if (method === "cherry-pick") {
2122
+ const log = await execa3(
2123
+ "git",
2124
+ ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
2125
+ { cwd: thread.repoPath, reject: false }
2126
+ );
2127
+ const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
2128
+ if (!shas.length) {
2129
+ return {
2130
+ applied: false,
2131
+ method,
2132
+ targetBranch: target,
2133
+ message: `No commits on ${thread.branchName} not already in ${target}`
2134
+ };
2135
+ }
2136
+ for (const sha of shas) {
2137
+ const cp = await execa3("git", ["cherry-pick", sha], {
2138
+ cwd: thread.repoPath,
2139
+ reject: false
2140
+ });
2141
+ if (cp.exitCode !== 0) {
2142
+ await execa3("git", ["cherry-pick", "--abort"], {
2143
+ cwd: thread.repoPath,
2144
+ reject: false
2145
+ });
2146
+ throw new Error(
2147
+ `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
2148
+ );
2106
2149
  }
2107
2150
  }
2108
- blocks.push(bits.join("\n"));
2151
+ return {
2152
+ applied: true,
2153
+ method,
2154
+ targetBranch: target,
2155
+ message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
2156
+ };
2109
2157
  }
2110
- return blocks.join("\n\n");
2111
- }
2112
- function buildSessionSeed(messages, opts) {
2113
- if (messages.length === 0) return null;
2114
- const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
2115
- if (!body.trim()) return null;
2116
- return [
2117
- "Sideboard conversation context (restored after compaction or a new session):",
2118
- "",
2119
- body,
2120
- "",
2121
- "Continue from this context. Do not repeat the summary unless asked."
2122
- ].join("\n");
2123
- }
2124
- function applyCompaction(messages, summaryText, thresholds = {}) {
2125
- const { older, recent } = splitForCompaction(messages, thresholds);
2126
- if (older.length === 0) return messages;
2127
- const summary = {
2128
- role: "summary",
2129
- text: summaryText.trim(),
2130
- ts: (/* @__PURE__ */ new Date()).toISOString()
2158
+ const merge = await execa3(
2159
+ "git",
2160
+ ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
2161
+ { cwd: thread.repoPath, reject: false }
2162
+ );
2163
+ if (merge.exitCode !== 0) {
2164
+ await execa3("git", ["merge", "--abort"], {
2165
+ cwd: thread.repoPath,
2166
+ reject: false
2167
+ });
2168
+ throw new Error(
2169
+ `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
2170
+ );
2171
+ }
2172
+ return {
2173
+ applied: true,
2174
+ method: "merge",
2175
+ targetBranch: target,
2176
+ message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
2131
2177
  };
2132
- return [summary, ...recent];
2133
2178
  }
2134
- async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
2135
- if (!shouldCompactContext(thread.messages, thresholds)) {
2136
- return { didCompact: false, thread };
2179
+
2180
+ // src/git/clone-repo.ts
2181
+ import { existsSync as existsSync6 } from "fs";
2182
+ import { basename as basename2, join as join7 } from "path";
2183
+ import { execa as execa4 } from "execa";
2184
+ async function cloneRepoIntoSideboard(opts) {
2185
+ const url = opts.url.trim();
2186
+ if (!url) throw new Error("Clone URL is required");
2187
+ let name = opts.name?.trim();
2188
+ if (!name) {
2189
+ const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
2190
+ name = leaf || "repo";
2191
+ }
2192
+ name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
2193
+ const dest = join7(sideboardReposDir(), name);
2194
+ if (existsSync6(dest)) {
2195
+ const repoPath2 = await resolveRepoRoot(dest);
2196
+ const workspace2 = await ensureWorkspace(repoPath2);
2197
+ return { repoPath: repoPath2, workspace: workspace2 };
2137
2198
  }
2138
- const { older } = splitForCompaction(thread.messages, thresholds);
2139
- if (older.length === 0) {
2140
- return { didCompact: false, thread };
2199
+ const result = await execa4("git", ["clone", url, dest], { reject: false });
2200
+ if (result.exitCode !== 0) {
2201
+ throw new Error(
2202
+ `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
2203
+ );
2141
2204
  }
2142
- const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
2143
- const { summary, method } = await summarize(transcript, {
2144
- cwd: thread.worktreePath
2145
- });
2146
- const messages = applyCompaction(thread.messages, summary, thresholds);
2147
- const next = {
2148
- ...thread,
2149
- messages,
2150
- sessionId: null,
2151
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2152
- };
2153
- return {
2154
- didCompact: true,
2155
- thread: next,
2156
- summary,
2157
- method,
2158
- olderCount: older.length
2159
- };
2205
+ const repoPath = await resolveRepoRoot(dest);
2206
+ const workspace = await ensureWorkspace(repoPath);
2207
+ return { repoPath, workspace };
2160
2208
  }
2161
2209
 
2162
- // src/threads/chat-tabs.ts
2163
- function sameWorktreePath(a, b) {
2164
- return normalizeWorktreePath(a) === normalizeWorktreePath(b);
2210
+ // src/store/desktop-host.ts
2211
+ import { readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2212
+ import { join as join8 } from "path";
2213
+ function desktopHostPidPath() {
2214
+ return join8(appDataDir(), "desktop-host.pid");
2165
2215
  }
2166
- function takenTeamSlugsForChatTab(worktreePath) {
2167
- const key = normalizeWorktreePath(worktreePath);
2168
- const taken = /* @__PURE__ */ new Set();
2169
- const dir = worktreeNameFromPath(key);
2170
- if (dir) taken.add(dir.toLowerCase());
2171
- for (const sibling of threadsSharingWorktree(key)) {
2172
- for (const slug of takenSlugsFromThread(sibling)) {
2173
- taken.add(slug);
2174
- }
2216
+ function readDesktopHostPid() {
2217
+ try {
2218
+ const pid = Number.parseInt(readFileSync5(desktopHostPidPath(), "utf8").trim(), 10);
2219
+ if (!Number.isFinite(pid) || pid <= 0) return null;
2220
+ return pid;
2221
+ } catch {
2222
+ return null;
2175
2223
  }
2176
- return [...taken];
2177
- }
2178
- function requireThread(idOrRef) {
2179
- const thread = findThreadByRef(idOrRef) ?? null;
2180
- if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
2181
- return thread;
2182
2224
  }
2183
- function worktreeBindingFrom(from) {
2184
- return {
2185
- worktreePath: normalizeWorktreePath(from.worktreePath),
2186
- repoPath: from.repoPath,
2187
- branchName: from.branchName,
2188
- sourceRef: from.sourceRef,
2189
- // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
2190
- // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
2191
- sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
2192
- sourceIsFork: from.sourceIsFork,
2193
- parentThreadId: from.parentThreadId,
2194
- prUrl: from.prUrl,
2195
- prTitle: from.prTitle,
2196
- prState: from.prState,
2197
- skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
2198
- stackId: from.stackId,
2199
- stackLayer: from.stackLayer
2200
- };
2225
+ function pidAlive(pid) {
2226
+ try {
2227
+ process.kill(pid, 0);
2228
+ return true;
2229
+ } catch {
2230
+ return false;
2231
+ }
2201
2232
  }
2202
- function threadsSharingWorktree(worktreePath) {
2203
- if (!worktreePath) return [];
2204
- const key = normalizeWorktreePath(worktreePath);
2205
- return listThreads({ includeArchived: true }).filter(
2206
- (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
2207
- ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2233
+ function isDesktopHostAlive() {
2234
+ const pid = readDesktopHostPid();
2235
+ return pid != null && pidAlive(pid);
2208
2236
  }
2209
- function formatTranscriptMarkdown(title, messages) {
2210
- const body = formatMessagesAsTranscript(messages, { tools: "full" });
2211
- return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
2237
+ function isThisProcessDesktopHost() {
2238
+ return readDesktopHostPid() === process.pid && pidAlive(process.pid);
2212
2239
  }
2213
- function forkMessageSlice(from, throughIndex) {
2214
- const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
2215
- return through >= 0 ? from.messages.slice(0, through + 1) : [];
2240
+ function thisProcessShouldDrainAgentQueues() {
2241
+ if (isThisProcessDesktopHost()) return true;
2242
+ return !isDesktopHostAlive();
2216
2243
  }
2217
- function buildForkTranscriptAttachment(baseTitle, messages) {
2218
- const title = baseTitle || "Chat";
2219
- return {
2220
- id: randomUUID(),
2221
- name: `Transcript of ${title}.md`,
2222
- kind: "transcript",
2223
- content: formatTranscriptMarkdown(title, messages)
2224
- };
2244
+
2245
+ // src/threads/create.ts
2246
+ import { existsSync as existsSync7 } from "fs";
2247
+
2248
+ // src/detect/detect.ts
2249
+ var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
2250
+ async function requireAgent(agent, opts) {
2251
+ ensureAgentPath();
2252
+ enrichPathWithNpmGlobalBin();
2253
+ const status = await Promise.race([
2254
+ getAdapter(agent).detect(),
2255
+ new Promise((_, reject) => {
2256
+ setTimeout(() => {
2257
+ reject(
2258
+ new Error(
2259
+ `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
2260
+ )
2261
+ );
2262
+ }, REQUIRE_AGENT_TIMEOUT_MS);
2263
+ })
2264
+ ]);
2265
+ if (!status.installed || !status.authenticated) {
2266
+ throw new Error(status.reason ?? `${agent} is not available`);
2267
+ }
2268
+ if (opts?.requireLinear && !status.linearMcp) {
2269
+ throw new Error(
2270
+ `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
2271
+ );
2272
+ }
2273
+ return status;
2225
2274
  }
2226
- function createChatTab(input) {
2227
- const from = requireThread(input.fromThreadId);
2228
- const binding = worktreeBindingFrom(from);
2229
- const explicitTitle = input.title?.trim();
2230
- const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
2231
- const nextAgent = input.agent ?? from.agent;
2232
- if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
2233
- assertOrchestratorCapableAgent(nextAgent);
2275
+
2276
+ // src/threads/create.ts
2277
+ async function createThread(input, _onSetupLine) {
2278
+ const resolved = resolveNewThreadOptions({
2279
+ agent: input.agent,
2280
+ model: input.model,
2281
+ effort: input.effort,
2282
+ fast: input.fast
2283
+ });
2284
+ await requireAgent(resolved.agent);
2285
+ const repoPath = await resolveRepoRoot(input.repoPath);
2286
+ if (!existsSync7(repoPath)) {
2287
+ throw new Error(`Repo not found: ${repoPath}`);
2234
2288
  }
2235
- const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
2289
+ if (input.cowboy) {
2290
+ const { cowboyModeEnabled } = await import("./app-settings-5XAGNDX7.js");
2291
+ if (!cowboyModeEnabled()) {
2292
+ throw new Error(
2293
+ "Cowboy mode is off. Enable it in Settings \u2192 Advanced."
2294
+ );
2295
+ }
2296
+ if (input.sourceType === "pr" || input.sourceType === "ticket" || input.sourceType === "adopt") {
2297
+ throw new Error(
2298
+ "Cowboy mode works on the project folder (default branch), not a PR, ticket, or adopt."
2299
+ );
2300
+ }
2301
+ const defaultBranch = await resolveDefaultBranch(repoPath, { network: false });
2302
+ const head = await currentBranch(repoPath);
2303
+ if (head === "HEAD") {
2304
+ throw new Error(
2305
+ "Cowboy mode needs a named branch in the project folder (not a detached HEAD)."
2306
+ );
2307
+ }
2308
+ if (head !== defaultBranch) {
2309
+ throw new Error(
2310
+ `Cowboy mode uses ${defaultBranch} in the project folder. Switch that checkout to ${defaultBranch} first (currently ${head}).`
2311
+ );
2312
+ }
2313
+ const explicitTitle2 = input.title?.trim();
2314
+ const thread2 = createEmptyThread({
2315
+ title: explicitTitle2 || `Cowboy \xB7 ${head}`,
2316
+ userSetTitle: Boolean(explicitTitle2),
2317
+ sourceType: "branch",
2318
+ sourceRef: head,
2319
+ branchName: head,
2320
+ worktreePath: repoPath,
2321
+ repoPath,
2322
+ agent: resolved.agent,
2323
+ autonomy: input.autonomy ?? "default",
2324
+ model: resolved.model,
2325
+ effort: resolved.effort,
2326
+ fast: resolved.fast,
2327
+ planMode: Boolean(input.planMode),
2328
+ attachments: input.attachments ?? [],
2329
+ sourceIsFork: false,
2330
+ parentThreadId: input.parentThreadId ?? null,
2331
+ status: "idle",
2332
+ cowboy: true
2333
+ });
2334
+ writeThread(thread2);
2335
+ await ensureWorkspace(repoPath);
2336
+ return readThread(thread2.id) ?? thread2;
2337
+ }
2338
+ let sourceRef = input.sourceRef;
2339
+ let sourceIsFork = false;
2340
+ let prUrl = null;
2341
+ let prTitle = null;
2342
+ if (input.sourceType === "pr") {
2343
+ const num = Number(input.sourceRef.replace(/^#/, ""));
2344
+ if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
2345
+ const pr = await getPr(repoPath, num);
2346
+ if (!pr) throw new Error(`PR #${num} not found`);
2347
+ sourceIsFork = pr.isCrossRepository;
2348
+ prUrl = pr.url;
2349
+ const localFetchBranch = `sideboard-pr-${pr.number}`;
2350
+ await fetchPrHead(repoPath, pr.number, localFetchBranch);
2351
+ sourceRef = localFetchBranch;
2352
+ } else if (input.sourceType === "ticket") {
2353
+ sourceRef = await resolveDefaultBranch(repoPath);
2354
+ } else if (input.sourceType === "branch") {
2355
+ if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
2356
+ sourceRef = await resolveDefaultBranch(repoPath);
2357
+ } else {
2358
+ const existing = await getPrForHeadBranch(repoPath, sourceRef);
2359
+ if (existing?.url) {
2360
+ prUrl = existing.url;
2361
+ prTitle = existing.title;
2362
+ }
2363
+ }
2364
+ } else if (input.sourceType === "adopt") {
2365
+ throw new Error("Use adoptThread() for adopt sources");
2366
+ }
2367
+ const team = allocateTeamSlug(repoPath);
2368
+ const { branchName, worktreePath } = await createThreadWorktree({
2369
+ repoPath,
2370
+ sourceRef,
2371
+ slug: team.slug
2372
+ });
2373
+ copyConfiguredFiles(repoPath, worktreePath);
2374
+ const explicitTitle = input.title?.trim();
2236
2375
  const thread = createEmptyThread({
2237
- title,
2238
- // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
2239
- // syncThreadBranchFromGit would otherwise rewrite every sibling to the
2240
- // shared worktree folder name (e.g. fork "Arsenal" "Monaco").
2241
- userSetTitle: true,
2242
- ...binding,
2243
- // Slack / Brightsy cloud identity stays on the original chat. A + tab
2244
- // that copies `slack:team:user` would steal inbound DMs after you close
2245
- // the connected chat.
2246
- sourceRef: singletonInbox ? title : binding.sourceRef,
2247
- agent: nextAgent,
2248
- model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
2249
- effort: input.effort !== void 0 ? input.effort : from.effort,
2250
- fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
2251
- planMode: from.planMode,
2252
- autonomy: input.autonomy ?? from.autonomy,
2376
+ title: explicitTitle || team.name,
2377
+ userSetTitle: Boolean(explicitTitle),
2378
+ sourceType: input.sourceType,
2379
+ sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
2380
+ branchName,
2381
+ worktreePath,
2382
+ repoPath,
2383
+ agent: resolved.agent,
2384
+ autonomy: input.autonomy ?? "default",
2385
+ model: resolved.model,
2386
+ effort: resolved.effort,
2387
+ fast: resolved.fast,
2388
+ planMode: Boolean(input.planMode),
2253
2389
  attachments: input.attachments ?? [],
2254
- status: "idle"
2255
- });
2256
- writeThread(thread);
2257
- return thread;
2258
- }
2259
- function forkChatTab(input) {
2260
- const from = requireThread(input.threadId);
2261
- const slice = forkMessageSlice(from, input.throughIndex);
2262
- const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
2263
- const tab = createChatTab({
2264
- fromThreadId: input.threadId,
2265
- agent: input.agent ?? from.agent,
2266
- model: input.model,
2267
- title: input.title?.trim() || void 0,
2268
- attachments: [attachment]
2390
+ sourceIsFork,
2391
+ parentThreadId: input.parentThreadId ?? null,
2392
+ status: "idle",
2393
+ prUrl,
2394
+ prTitle
2269
2395
  });
2270
- if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
2271
- const next = { ...tab, parentThreadId: from.id };
2272
- writeThread(next);
2273
- return next;
2396
+ writeThread(thread);
2397
+ await ensureWorkspace(repoPath);
2398
+ return readThread(thread.id) ?? thread;
2399
+ }
2400
+ async function listLinearIssues(agent, repoPath) {
2401
+ const { getAdapter: getAdapter2 } = await import("./agents-MQDW3YXL.js");
2402
+ await requireAgent(agent, { requireLinear: true });
2403
+ const adapter = getAdapter2(agent);
2404
+ if (!adapter.listLinearIssues) {
2405
+ throw new Error(`${agent} does not support Linear issue listing`);
2274
2406
  }
2275
- return tab;
2407
+ return adapter.listLinearIssues(repoPath);
2408
+ }
2409
+
2410
+ // src/threads/cowboy.ts
2411
+ function isCowboyThread(thread) {
2412
+ return Boolean(thread?.cowboy);
2413
+ }
2414
+ function isPrimaryCheckoutThread(thread) {
2415
+ if (!thread?.worktreePath || !thread.repoPath) return false;
2416
+ return normalizeWorktreePath(thread.worktreePath) === normalizeWorktreePath(thread.repoPath);
2417
+ }
2418
+ function shouldRemoveWorktreeOnTeardown(thread) {
2419
+ return !isCowboyThread(thread) && !isPrimaryCheckoutThread(thread);
2276
2420
  }
2277
2421
 
2278
2422
  // src/store/turn-live.ts
2279
2423
  import {
2280
- existsSync as existsSync7,
2424
+ existsSync as existsSync8,
2281
2425
  renameSync,
2282
2426
  unlinkSync as unlinkSync2,
2283
- writeFileSync as writeFileSync2,
2284
- readFileSync as readFileSync5
2427
+ writeFileSync as writeFileSync3,
2428
+ readFileSync as readFileSync6
2285
2429
  } from "fs";
2286
2430
  var buffers = /* @__PURE__ */ new Map();
2287
2431
  var FLUSH_MS = 800;
@@ -2366,7 +2510,7 @@ function writeTurnLive(threadId, progress) {
2366
2510
  const path = threadLivePath(threadId);
2367
2511
  const tmp = `${path}.${process.pid}.tmp`;
2368
2512
  try {
2369
- writeFileSync2(tmp, JSON.stringify(progress), "utf8");
2513
+ writeFileSync3(tmp, JSON.stringify(progress), "utf8");
2370
2514
  renameSync(tmp, path);
2371
2515
  } catch {
2372
2516
  try {
@@ -2377,9 +2521,9 @@ function writeTurnLive(threadId, progress) {
2377
2521
  }
2378
2522
  function readTurnLive(threadId) {
2379
2523
  const path = threadLivePath(threadId);
2380
- if (!existsSync7(path)) return null;
2524
+ if (!existsSync8(path)) return null;
2381
2525
  try {
2382
- const raw = JSON.parse(readFileSync5(path, "utf8"));
2526
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
2383
2527
  if (!raw || typeof raw.summary !== "string") return null;
2384
2528
  return raw;
2385
2529
  } catch {
@@ -2391,253 +2535,13 @@ function clearTurnLive(threadId) {
2391
2535
  if (buf?.timer) clearTimeout(buf.timer);
2392
2536
  buffers.delete(threadId);
2393
2537
  const path = threadLivePath(threadId);
2394
- if (!existsSync7(path)) return;
2538
+ if (!existsSync8(path)) return;
2395
2539
  try {
2396
2540
  unlinkSync2(path);
2397
2541
  } catch {
2398
2542
  }
2399
2543
  }
2400
2544
 
2401
- // src/review/request-review.ts
2402
- import { randomUUID as randomUUID2 } from "crypto";
2403
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2404
- import { dirname as dirname2, join as join8 } from "path";
2405
-
2406
- // src/review/review-request-template.ts
2407
- var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
2408
-
2409
- 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.
2410
-
2411
- ## Required outcome
2412
-
2413
- Start your reply with a **Recommendation** section using exactly one of:
2414
-
2415
- - **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
2416
- - **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
2417
- - **Request changes** \u2014 not ready; blocking issues must be fixed first.
2418
- - **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
2419
-
2420
- 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.
2421
-
2422
- People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
2423
-
2424
- ## Findings
2425
-
2426
- Below are guidelines for determining whether an issue is worth flagging to the original author.
2427
-
2428
- These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
2429
-
2430
- Flag something as a bug / blocking finding only when:
2431
-
2432
- 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
2433
- 2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
2434
- 3. Fixing it does not demand rigor absent from the rest of the codebase.
2435
- 4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
2436
- 5. The author would likely fix it if made aware.
2437
- 6. It does not rely on unstated assumptions about the codebase or author intent.
2438
- 7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
2439
- 8. It is clearly not just an intentional change by the author.
2440
-
2441
- When flagging an issue, include a short accompanying comment:
2442
-
2443
- 1. Clear about why it is a problem.
2444
- 2. Severity must match reality \u2014 do not inflate.
2445
- 3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
2446
- 4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
2447
- 5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
2448
- 6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
2449
- 7. Skimmable on first read.
2450
- 8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
2451
-
2452
- HOW MANY FINDINGS TO RETURN:
2453
-
2454
- 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.
2455
-
2456
- GUIDELINES:
2457
-
2458
- - Ignore trivial style unless it obscures meaning or violates documented standards.
2459
- - One comment per distinct issue (or a short multi-line range if needed).
2460
- - Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
2461
- - In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
2462
- - Do NOT introduce or remove outer indentation levels unless that is the actual fix.
2463
- - Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
2464
-
2465
- 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).
2466
-
2467
- ## Getting the diff
2468
-
2469
- 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.
2470
-
2471
- 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.
2472
-
2473
- ## Fallback: if you don't have access to the Sideboard diff tool
2474
-
2475
- If you don't have access to \`get_diff\`, use the following git commands to get the diff:
2476
-
2477
- \`\`\`bash
2478
- # Get the merge base between this branch and the target
2479
- MERGE_BASE=$(git merge-base origin/main HEAD)
2480
-
2481
- # Get the committed diff against the merge base
2482
- git diff $MERGE_BASE HEAD
2483
-
2484
- # Get any uncommitted changes (staged and unstaged)
2485
- git diff HEAD
2486
- \`\`\`
2487
-
2488
- 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.
2489
-
2490
- No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
2491
-
2492
- ## Output format
2493
-
2494
- **1. Recommendation first** (required), then **2. Findings** (may be empty).
2495
-
2496
- Only report ONE finding per unique issue.
2497
-
2498
- <example>
2499
- ## Recommendation
2500
-
2501
- **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.
2502
-
2503
- ## Findings
2504
-
2505
- ### **#1 Empty input causes crash** (blocking)
2506
-
2507
- If the input field is empty when the page loads, the app will crash.
2508
-
2509
- File: src/client/frontends/desktop/ui/Input.tsx
2510
-
2511
- ### **#2 Dead code** (nit)
2512
-
2513
- The getUserData function is now unused. It should be deleted.
2514
-
2515
- File: src/client/frontends/desktop/core/UserData.ts
2516
- </example>
2517
-
2518
- <example>
2519
- ## Recommendation
2520
-
2521
- **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
2522
- </example>
2523
-
2524
- ## Growing the rules
2525
-
2526
- 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\`.
2527
- `;
2528
-
2529
- // src/review/request-review.ts
2530
- var REPO_REVIEW_PATH = ".sideboard/review.md";
2531
- var REPO_REVIEW_NAME = "review.md";
2532
- var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
2533
- var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
2534
- var REVIEW_REQUEST_NAME = "Review request.md";
2535
- var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
2536
- var LEGACY_REVIEW_TEMPLATE_MARKERS = [
2537
- "You are acting as a reviewer for a proposed code change made by another engineer.",
2538
- "HOW MANY FINDINGS TO RETURN:"
2539
- ];
2540
- function shouldRefreshReviewRequestTemplate(content) {
2541
- const trimmed = content.trim();
2542
- if (!trimmed) return true;
2543
- if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
2544
- return false;
2545
- }
2546
- return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
2547
- }
2548
- function readTextIfPresent(abs) {
2549
- if (!existsSync8(abs)) return null;
2550
- try {
2551
- const content = readFileSync6(abs, "utf8");
2552
- return content.trim() ? content : null;
2553
- } catch {
2554
- return null;
2555
- }
2556
- }
2557
- function ensureAttachmentsGitignore(worktreePath) {
2558
- const gitignoreAbs = join8(worktreePath, ATTACHMENTS_DIR, ".gitignore");
2559
- if (existsSync8(gitignoreAbs)) return;
2560
- mkdirSync2(dirname2(gitignoreAbs), { recursive: true });
2561
- writeFileSync3(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
2562
- }
2563
- function resolveReviewGuidelines(worktreePath) {
2564
- const repoAbs = join8(worktreePath, REPO_REVIEW_PATH);
2565
- const repoContent = readTextIfPresent(repoAbs);
2566
- if (repoContent) {
2567
- return {
2568
- path: REPO_REVIEW_PATH,
2569
- name: REPO_REVIEW_NAME,
2570
- content: repoContent,
2571
- source: "repo"
2572
- };
2573
- }
2574
- const localAbs = join8(worktreePath, REVIEW_REQUEST_PATH);
2575
- const localContent = readTextIfPresent(localAbs);
2576
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
2577
- return {
2578
- path: REVIEW_REQUEST_PATH,
2579
- name: REVIEW_REQUEST_NAME,
2580
- content: localContent,
2581
- source: "local"
2582
- };
2583
- }
2584
- const legacyAbs = join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
2585
- const legacyContent = readTextIfPresent(legacyAbs);
2586
- if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
2587
- return {
2588
- path: LEGACY_REVIEW_REQUEST_PATH,
2589
- name: REVIEW_REQUEST_NAME,
2590
- content: legacyContent,
2591
- source: "local"
2592
- };
2593
- }
2594
- ensureAttachmentsGitignore(worktreePath);
2595
- mkdirSync2(dirname2(localAbs), { recursive: true });
2596
- writeFileSync3(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
2597
- return {
2598
- path: REVIEW_REQUEST_PATH,
2599
- name: REVIEW_REQUEST_NAME,
2600
- content: REVIEW_REQUEST_TEMPLATE,
2601
- source: "stock"
2602
- };
2603
- }
2604
- function buildReviewRequestAttachment(content, opts) {
2605
- const path = opts?.path ?? REVIEW_REQUEST_PATH;
2606
- const name = opts?.name ?? (path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
2607
- return {
2608
- id: randomUUID2(),
2609
- name,
2610
- kind: "file",
2611
- path,
2612
- content
2613
- };
2614
- }
2615
- async function requestReview(threadRef, send) {
2616
- const from = findThreadByRef(threadRef);
2617
- if (!from) throw new Error(`Thread not found: ${threadRef}`);
2618
- if (isOrchestratorThread(from)) {
2619
- throw new Error(
2620
- "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
2621
- );
2622
- }
2623
- if (from.status === "archived") {
2624
- throw new Error(`Thread is archived: ${from.id}`);
2625
- }
2626
- const guidelines = resolveReviewGuidelines(from.worktreePath);
2627
- const tab = createChatTab({
2628
- fromThreadId: from.id,
2629
- title: "Review",
2630
- attachments: [
2631
- buildReviewRequestAttachment(guidelines.content, {
2632
- path: guidelines.path,
2633
- name: guidelines.name
2634
- })
2635
- ]
2636
- });
2637
- const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
2638
- return { tab: started, from };
2639
- }
2640
-
2641
2545
  // src/threads/fork-worktree.ts
2642
2546
  function requireThread2(idOrRef) {
2643
2547
  const thread = findThreadByRef(idOrRef) ?? null;
@@ -2879,7 +2783,7 @@ async function adoptThread(input) {
2879
2783
  messages: input.messages ?? []
2880
2784
  });
2881
2785
  writeThread(thread);
2882
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-JU3P6FHB.js");
2786
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-XAQVKTLO.js");
2883
2787
  await ensureWorkspace2(repoPath);
2884
2788
  return thread;
2885
2789
  }
@@ -4112,13 +4016,22 @@ async function previewLand(thread) {
4112
4016
  isFork: true
4113
4017
  };
4114
4018
  }
4115
- const target = await resolveDefaultBranch(thread.repoPath);
4116
- if (thread.branchName === target || thread.branchName === "main" || thread.branchName === "master") {
4019
+ if (isCowboyThread(thread)) {
4020
+ const current2 = await currentBranch(thread.worktreePath);
4021
+ const dirty = await isDirty(thread.worktreePath);
4022
+ const diff2 = await getDiff(thread.worktreePath, thread.repoPath, { base: current2 });
4023
+ return {
4024
+ branch: current2,
4025
+ target: current2,
4026
+ diffStat: diff2.stat,
4027
+ dirty,
4028
+ blocked: false,
4029
+ isFork: false,
4030
+ cowboy: true
4031
+ };
4117
4032
  }
4118
- const { stdout: head } = await import("./run-XKTAJRWF.js").then(
4119
- ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], thread.worktreePath)
4120
- );
4121
- const current = head.trim();
4033
+ const target = await resolveDefaultBranch(thread.repoPath);
4034
+ const current = await currentBranch(thread.worktreePath);
4122
4035
  if (current === target) {
4123
4036
  return {
4124
4037
  branch: current,
@@ -4145,6 +4058,19 @@ async function confirmLand(thread, opts) {
4145
4058
  if (preview.blocked) {
4146
4059
  throw new Error(preview.blockReason ?? "Land blocked");
4147
4060
  }
4061
+ if (preview.cowboy) {
4062
+ const meta2 = await suggestPrMetadata(thread.worktreePath, {
4063
+ base: preview.branch,
4064
+ fallbackTitle: thread.title,
4065
+ sourceLabel: `${thread.sourceType}:${thread.sourceRef}`
4066
+ });
4067
+ let committed2 = false;
4068
+ if (preview.dirty) {
4069
+ committed2 = await commitAll(thread.worktreePath, meta2.commitMessage);
4070
+ }
4071
+ await pushBranch(thread.worktreePath, preview.branch);
4072
+ return { prUrl: null, pushed: true, committed: committed2 };
4073
+ }
4148
4074
  const meta = await suggestPrMetadata(thread.worktreePath, {
4149
4075
  base: preview.target,
4150
4076
  fallbackTitle: thread.title,
@@ -4703,8 +4629,9 @@ function formatProcessGuideDirective() {
4703
4629
  "Process guides (recurring work only):",
4704
4630
  "- 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.",
4705
4631
  "- 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.",
4706
- "- 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.",
4707
- "- 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."
4632
+ "- 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).",
4633
+ "- 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.",
4634
+ "- 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."
4708
4635
  ].join("\n");
4709
4636
  }
4710
4637
  function formatArtifactDirective() {
@@ -4731,15 +4658,7 @@ function formatArtifactDirective() {
4731
4658
  ].join("\n");
4732
4659
  }
4733
4660
  function formatUiReminder() {
4734
- return [
4735
- "Sideboard side column (important):",
4736
- "There is no claude.ai Artifact tool here \u2014 that is normal.",
4737
- "HTML/SVG/markdown: ```html fence or MCP present_artifact.",
4738
- "CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
4739
- "Files column: MCP present_files (brightsy account storage or memory).",
4740
- "Do not say artifacts/CMS UI are unavailable.",
4741
- "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."
4742
- ].join(" ");
4661
+ 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.";
4743
4662
  }
4744
4663
 
4745
4664
  // src/threads/sync-branch.ts
@@ -5010,7 +4929,7 @@ function formatScheduledPrompt(name, prompt) {
5010
4929
  ${prompt}`;
5011
4930
  }
5012
4931
  async function defaultDeps() {
5013
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-2P7DX44Q.js");
4932
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-SCATSB3O.js");
5014
4933
  const orch = getOrchestrator2();
5015
4934
  return {
5016
4935
  findThread: (id) => findThreadByRef(id),
@@ -5228,7 +5147,7 @@ var Orchestrator = class {
5228
5147
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
5229
5148
  });
5230
5149
  }
5231
- const { autoCleanupOrphansEnabled } = await import("./app-settings-2WH6DVOW.js");
5150
+ const { autoCleanupOrphansEnabled } = await import("./app-settings-5XAGNDX7.js");
5232
5151
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
5233
5152
  await cleanupOrphanWorktrees({ repoPaths });
5234
5153
  }
@@ -5385,7 +5304,9 @@ var Orchestrator = class {
5385
5304
  return thread;
5386
5305
  }
5387
5306
  async finishCreateThread(threadId, prompt) {
5388
- const setup = this.runSetupAfterCreate(threadId);
5307
+ const created = this.getThread(threadId);
5308
+ const skipSetup = isCowboyThread(created);
5309
+ const setup = skipSetup ? Promise.resolve() : this.runSetupAfterCreate(threadId);
5389
5310
  if (prompt) {
5390
5311
  try {
5391
5312
  await this.send(threadId, prompt);
@@ -5397,7 +5318,8 @@ var Orchestrator = class {
5397
5318
  }
5398
5319
  }
5399
5320
  await setup;
5400
- const { autoRunAfterSetupEnabled } = await import("./app-settings-2WH6DVOW.js");
5321
+ if (skipSetup) return;
5322
+ const { autoRunAfterSetupEnabled } = await import("./app-settings-5XAGNDX7.js");
5401
5323
  if (autoRunAfterSetupEnabled()) {
5402
5324
  try {
5403
5325
  await this.startDev(threadId);
@@ -5611,7 +5533,7 @@ var Orchestrator = class {
5611
5533
  if (compact.didCompact) {
5612
5534
  thread = updateThread(threadId, {
5613
5535
  messages: compact.thread.messages,
5614
- sessionId: null
5536
+ sessionId: compact.thread.sessionId
5615
5537
  });
5616
5538
  this.emit({
5617
5539
  type: "context_compacted",
@@ -5682,7 +5604,7 @@ var Orchestrator = class {
5682
5604
  }
5683
5605
  const isBrightsy = fresh.agent === "brightsy";
5684
5606
  const isOrchestration = isOrchestratorThread(fresh);
5685
- const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-2WH6DVOW.js");
5607
+ const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-5XAGNDX7.js");
5686
5608
  const gitAuthMode = getGithubGitAuthMode();
5687
5609
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
5688
5610
  githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
@@ -6441,7 +6363,7 @@ var Orchestrator = class {
6441
6363
  updateThread(thread.id, { title: meta.title });
6442
6364
  }
6443
6365
  }
6444
- const { autoArchiveOnMergeEnabled } = await import("./app-settings-2WH6DVOW.js");
6366
+ const { autoArchiveOnMergeEnabled } = await import("./app-settings-5XAGNDX7.js");
6445
6367
  const latest = this.requireThread(thread.id);
6446
6368
  if (!shouldAutoArchiveOnPrMerge({
6447
6369
  previousPrState: prevState || null,
@@ -6728,23 +6650,25 @@ var Orchestrator = class {
6728
6650
  const siblings = threadsSharingWorktree(thread.worktreePath).filter((t) => t.id !== thread.id);
6729
6651
  if (siblings.length === 0) {
6730
6652
  this.stopDev(thread.id);
6731
- try {
6732
- await runArchiveScript(thread.repoPath, thread.worktreePath, (line) => {
6733
- this.emit({
6734
- type: "turn_output",
6735
- threadId: thread.id,
6736
- event: { type: "stdout", data: `[archive] ${line}` }
6653
+ if (shouldRemoveWorktreeOnTeardown(thread)) {
6654
+ try {
6655
+ await runArchiveScript(thread.repoPath, thread.worktreePath, (line) => {
6656
+ this.emit({
6657
+ type: "turn_output",
6658
+ threadId: thread.id,
6659
+ event: { type: "stdout", data: `[archive] ${line}` }
6660
+ });
6737
6661
  });
6738
- });
6739
- } catch {
6662
+ } catch {
6663
+ }
6664
+ await removeWorktree(thread.repoPath, thread.worktreePath);
6740
6665
  }
6741
- await removeWorktree(thread.repoPath, thread.worktreePath);
6742
6666
  }
6743
6667
  const archived = setStatus(thread.id, "archived");
6744
6668
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
6745
6669
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
6746
6670
  try {
6747
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-JU3P6FHB.js");
6671
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-XAQVKTLO.js");
6748
6672
  await ensureWorkspace2(thread.repoPath);
6749
6673
  } catch {
6750
6674
  }
@@ -6766,15 +6690,17 @@ var Orchestrator = class {
6766
6690
  const siblings = threadsSharingWorktree(thread.worktreePath).filter((t) => t.id !== thread.id);
6767
6691
  if (siblings.length === 0) {
6768
6692
  this.stopDev(thread.id);
6769
- try {
6770
- await runArchiveScript(thread.repoPath, thread.worktreePath);
6771
- } catch {
6693
+ if (shouldRemoveWorktreeOnTeardown(thread)) {
6694
+ try {
6695
+ await runArchiveScript(thread.repoPath, thread.worktreePath);
6696
+ } catch {
6697
+ }
6698
+ const { deleteBranchOnPurgeEnabled } = await import("./app-settings-5XAGNDX7.js");
6699
+ const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
6700
+ await removeWorktree(thread.repoPath, thread.worktreePath, {
6701
+ deleteBranch: deleteBranch ? thread.branchName : void 0
6702
+ });
6772
6703
  }
6773
- const { deleteBranchOnPurgeEnabled } = await import("./app-settings-2WH6DVOW.js");
6774
- const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
6775
- await removeWorktree(thread.repoPath, thread.worktreePath, {
6776
- deleteBranch: deleteBranch ? thread.branchName : void 0
6777
- });
6778
6704
  }
6779
6705
  deleteThreadRecord(thread.id);
6780
6706
  }
@@ -6791,7 +6717,12 @@ var Orchestrator = class {
6791
6717
  return restored2;
6792
6718
  }
6793
6719
  if (!existsSync16(thread.worktreePath)) {
6794
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-6MFKE465.js");
6720
+ if (isCowboyThread(thread) || isPrimaryCheckoutThread(thread)) {
6721
+ throw new Error(
6722
+ `Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
6723
+ );
6724
+ }
6725
+ const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-N2EV24EE.js");
6795
6726
  const slug = thread.worktreePath.split("/").pop();
6796
6727
  const dest = thread.worktreePath;
6797
6728
  await withRepoGitLock(thread.repoPath, async () => {
@@ -6899,7 +6830,7 @@ async function startOrchestration(opts) {
6899
6830
  sourceRef: "default",
6900
6831
  ...createOpts
6901
6832
  }).catch(async () => {
6902
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-6MFKE465.js");
6833
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-N2EV24EE.js");
6903
6834
  const repo = await resolveRepoRoot2(repoPath);
6904
6835
  const def = await resolveDefaultBranch2(repo);
6905
6836
  return orch.createThread({
@@ -6909,7 +6840,7 @@ async function startOrchestration(opts) {
6909
6840
  repoPath: repo
6910
6841
  });
6911
6842
  });
6912
- const { updateThread: upd } = await import("./thread-store-DQJGDKIJ.js");
6843
+ const { updateThread: upd } = await import("./thread-store-LPTBVW5C.js");
6913
6844
  const updated = upd(thread.id, {
6914
6845
  sourceType: "orchestration",
6915
6846
  sourceRef: goal