@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
@@ -1,19 +1,27 @@
1
1
  import {
2
2
  PLAN_MODE_INSTRUCTION,
3
3
  allAdapters,
4
+ applyAgentEvent,
4
5
  applyTurnUsage,
6
+ contextTokens,
7
+ finalizeParts,
5
8
  getAdapter,
9
+ isBrightsyNdjsonLine,
6
10
  isSessionQuotaLimit,
11
+ normalizeParseResult,
7
12
  parseBrightsyCliLine,
8
13
  parseSessionQuotaResetAt,
9
- resolveQuotaFallbackAgent
10
- } from "./chunk-6HRR4T4P.js";
14
+ partsToAssistantText,
15
+ resolveQuotaFallbackAgent,
16
+ stripBrightsyNdjsonNoise,
17
+ toolDescription
18
+ } from "./chunk-RSIWPLRG.js";
11
19
  import {
12
20
  addWorkspace,
13
21
  ensureWorkspace,
14
22
  removeWorkspace,
15
23
  syncWorkspacesFromThreads
16
- } from "./chunk-VTTTV2LM.js";
24
+ } from "./chunk-7KWYXGIU.js";
17
25
  import {
18
26
  assertOrchestratorCapableAgent,
19
27
  coerceOrchestratorAgent,
@@ -26,14 +34,14 @@ import {
26
34
  isOrchestratorThread,
27
35
  isSlackCoordinatorThread,
28
36
  orchestratorSessionPoisonedByBuiltins
29
- } from "./chunk-SMWVSSE5.js";
37
+ } from "./chunk-4EWPKYOU.js";
30
38
  import {
31
39
  SLACK_REPLY_FORMATTING,
32
40
  coordinatorSystemPrompt,
33
41
  coordinatorTurnReminder,
34
42
  enrichWorkspacesWithGithub,
35
43
  ensureGlobalCoordinatorCwd
36
- } from "./chunk-GYFNFU62.js";
44
+ } from "./chunk-KQBI5HNT.js";
37
45
  import {
38
46
  extractPresentedPlan,
39
47
  readPlanFile,
@@ -41,7 +49,7 @@ import {
41
49
  } from "./chunk-QTUESPAW.js";
42
50
  import {
43
51
  createAgentStreamCoalescer
44
- } from "./chunk-MMI4RJ5I.js";
52
+ } from "./chunk-WIHKHR5R.js";
45
53
  import {
46
54
  fallbackTurnFailDetail,
47
55
  formatTurnExitError,
@@ -51,7 +59,7 @@ import {
51
59
  shouldRetryFailedAgentTurn,
52
60
  summarizeTurnStderr,
53
61
  turnFailChatText
54
- } from "./chunk-HUKCGRAT.js";
62
+ } from "./chunk-3UOKB6VQ.js";
55
63
  import {
56
64
  releaseCaffeinateHoldForThread
57
65
  } from "./chunk-DCOZABNT.js";
@@ -97,7 +105,7 @@ import {
97
105
  threadDisplayLabel,
98
106
  withRepoGitLock,
99
107
  worktreeNameFromPath
100
- } from "./chunk-PZVA2VDF.js";
108
+ } from "./chunk-C4KHDW3U.js";
101
109
  import {
102
110
  ATTACHMENTS_DIR,
103
111
  LEGACY_ATTACHMENTS_DIR,
@@ -115,10 +123,10 @@ import {
115
123
  resolveVaultKey,
116
124
  updateAdvancedSettings,
117
125
  writeSecureJson
118
- } from "./chunk-7ZTQHC2Q.js";
126
+ } from "./chunk-4NR5FL46.js";
119
127
  import {
120
128
  stripNestedElectronEnv
121
- } from "./chunk-TLPJHLLM.js";
129
+ } from "./chunk-4TR3HZFT.js";
122
130
  import {
123
131
  writePrivateFile
124
132
  } from "./chunk-JT7R45JB.js";
@@ -133,7 +141,7 @@ import {
133
141
  updateThread,
134
142
  withThreadLock,
135
143
  writeThread
136
- } from "./chunk-KGSVLZV6.js";
144
+ } from "./chunk-A4VZXJEJ.js";
137
145
  import {
138
146
  appDataDir,
139
147
  findConventionSetup,
@@ -726,236 +734,14 @@ import { existsSync as existsSync16 } from "fs";
726
734
  // src/agents/spawn.ts
727
735
  import { createInterface } from "readline";
728
736
  import { execa } from "execa";
729
-
730
- // src/agents/message-parts.ts
731
- function asRecord(input) {
732
- if (input && typeof input === "object" && !Array.isArray(input)) {
733
- return input;
734
- }
735
- return void 0;
736
- }
737
- function str(v) {
738
- return typeof v === "string" && v.trim() ? v : void 0;
739
- }
740
- function toolDetail(name, input) {
741
- if (!input) return void 0;
742
- const command = str(input.command) ?? str(input.cmd);
743
- if (command) return command;
744
- const path = str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
745
- if (path) return path;
746
- const pattern = str(input.pattern) ?? str(input.glob) ?? str(input.glob_pattern);
747
- if (pattern) return pattern;
748
- const query = str(input.query) ?? str(input.prompt);
749
- if (query) return query.length > 80 ? `${query.slice(0, 77)}\u2026` : query;
750
- try {
751
- const raw = JSON.stringify(input);
752
- return raw.length > 80 ? `${raw.slice(0, 77)}\u2026` : raw;
753
- } catch {
754
- return name;
755
- }
756
- }
757
- function toolDescription(name, input) {
758
- const n = name.replace(/^mcp__/, "").replace(/__/g, " \xB7 ");
759
- if (/^get_record_types$|recordTypes/i.test(name)) return "List record types";
760
- if (/connectedAgentRequest/i.test(name)) {
761
- return str(input?.agent_id) ? `Ask connected agent` : "Ask connected agent";
762
- }
763
- if (/present_artifact$/i.test(name)) {
764
- return str(input?.title) ? `Present ${str(input?.title)}` : "Present artifact";
765
- }
766
- if (/present_plan$/i.test(name)) {
767
- return str(input?.title) ? `Plan ${str(input?.title)}` : "Present plan";
768
- }
769
- if (/present_schema$/i.test(name)) {
770
- return str(input?.title) ? `Schema ${str(input?.title)}` : "Present schema";
771
- }
772
- if (/present_files$/i.test(name)) {
773
- return str(input?.title) ? `Files ${str(input?.title)}` : "Present files";
774
- }
775
- if (/^(create|update)_artifact$/i.test(name.replace(/^mcp__[^_]+__/, ""))) {
776
- return str(input?.title) ? `Artifact ${str(input?.title)}` : "Artifact";
777
- }
778
- if (!input) return n;
779
- if (/bash|shell|terminal/i.test(name) && str(input.command)) {
780
- const cmd = str(input.command);
781
- if (/git\s+fetch/i.test(cmd)) return "Fetch latest from origin and check status";
782
- if (/git\s+status/i.test(cmd)) return "Check git status";
783
- if (/git\s+log/i.test(cmd)) return "Inspect recent commits";
784
- if (/git\s+branch/i.test(cmd)) return "List branches";
785
- if (/grep|rg\b/i.test(cmd)) return "Search repository";
786
- return "Run shell command";
787
- }
788
- if (/edit|write|apply/i.test(name)) {
789
- const path = str(input.file_path) ?? str(input.path);
790
- return path ? `Edit ${path.split("/").pop()}` : "Edit file";
791
- }
792
- if (/read/i.test(name)) {
793
- const path = str(input.file_path) ?? str(input.path);
794
- return path ? `Read ${path.split("/").pop()}` : "Read file";
795
- }
796
- if (/grep/i.test(name)) return "Search files";
797
- if (/glob/i.test(name)) return "Find files";
798
- return n;
799
- }
800
- function toolFilePath(input) {
801
- if (!input) return void 0;
802
- return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
803
- }
804
- function countLines(text) {
805
- if (!text) return 0;
806
- return text.split("\n").length;
807
- }
808
- function diffFromInput(input) {
809
- if (!input) return {};
810
- const oldS = str(input.old_string) ?? str(input.oldString);
811
- const newS = str(input.new_string) ?? str(input.newString) ?? str(input.content);
812
- if (oldS != null || newS != null) {
813
- return {
814
- additions: countLines(newS),
815
- deletions: countLines(oldS)
816
- };
817
- }
818
- return {};
819
- }
820
- function parseDiffStat(result) {
821
- if (!result) return {};
822
- const paired = result.match(/\+(\d+)\s+-(\d+)/);
823
- if (paired) {
824
- return {
825
- additions: Number(paired[1]),
826
- deletions: Number(paired[2])
827
- };
828
- }
829
- const verbose = result.match(
830
- /(\d+)\s+insertions?(?:,\s*(\d+)\s+deletions?)?/i
831
- );
832
- if (verbose) {
833
- return {
834
- additions: Number(verbose[1]),
835
- deletions: verbose[2] != null ? Number(verbose[2]) : void 0
836
- };
837
- }
838
- return {};
839
- }
840
- function applyAgentEvent(parts, event) {
841
- if (event.type === "stdout") {
842
- const data = event.data;
843
- if (!data) return parts;
844
- const next = [...parts];
845
- const last = next[next.length - 1];
846
- if (last?.type === "text") {
847
- next[next.length - 1] = { type: "text", text: last.text + data };
848
- } else {
849
- next.push({ type: "text", text: data });
850
- }
851
- return next;
852
- }
853
- if (event.type === "thinking") {
854
- const data = event.data;
855
- if (!data) return parts;
856
- const next = [...parts];
857
- const last = next[next.length - 1];
858
- if (last?.type === "thinking") {
859
- next[next.length - 1] = { type: "thinking", text: last.text + data };
860
- } else {
861
- next.push({ type: "thinking", text: data });
862
- }
863
- return next;
864
- }
865
- if (event.type === "tool_use") {
866
- const input = asRecord(event.input);
867
- const diff = diffFromInput(input);
868
- const existing = parts.findIndex((p) => p.type === "tool" && p.id === event.id);
869
- if (existing >= 0) {
870
- const prev = parts[existing];
871
- const next = [...parts];
872
- next[existing] = {
873
- ...prev,
874
- name: event.name || prev.name,
875
- input: input ?? prev.input,
876
- description: toolDescription(event.name || prev.name, input ?? prev.input),
877
- detail: toolDetail(event.name || prev.name, input ?? prev.input) ?? prev.detail,
878
- filePath: toolFilePath(input) ?? prev.filePath,
879
- additions: diff.additions ?? prev.additions,
880
- deletions: diff.deletions ?? prev.deletions
881
- };
882
- return next;
883
- }
884
- return [
885
- ...parts,
886
- {
887
- type: "tool",
888
- id: event.id,
889
- name: event.name,
890
- input,
891
- description: toolDescription(event.name, input),
892
- detail: toolDetail(event.name, input),
893
- status: "running",
894
- filePath: toolFilePath(input),
895
- ...diff
896
- }
897
- ];
737
+ function applyPromptCacheTtlEnv(agent, env) {
738
+ if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
739
+ env.ENABLE_PROMPT_CACHING_1H ??= "1";
898
740
  }
899
- if (event.type === "tool_result") {
900
- const existing = parts.findIndex((p) => p.type === "tool" && p.id === event.id);
901
- const fromResult = parseDiffStat(event.content);
902
- if (existing < 0) {
903
- return [
904
- ...parts,
905
- {
906
- type: "tool",
907
- id: event.id,
908
- name: "tool",
909
- description: "tool",
910
- status: event.isError ? "error" : "done",
911
- result: event.content,
912
- ...fromResult.additions != null ? { additions: fromResult.additions } : {},
913
- ...fromResult.deletions != null ? { deletions: fromResult.deletions } : {}
914
- }
915
- ];
916
- }
917
- return parts.map((p, i) => {
918
- if (i !== existing || p.type !== "tool") return p;
919
- return {
920
- ...p,
921
- status: event.isError ? "error" : "done",
922
- result: event.content,
923
- ...fromResult.additions != null ? { additions: fromResult.additions } : {},
924
- ...fromResult.deletions != null ? { deletions: fromResult.deletions } : {}
925
- };
926
- });
741
+ if (agent === "opencode" && env.OPENCODE_ANTHROPIC_FORCE_PROMPT_CACHING_5M !== "1") {
742
+ env.OPENCODE_ANTHROPIC_PROMPT_CACHING_1H ??= "1";
927
743
  }
928
- return parts;
929
- }
930
- function partsToAssistantText(parts) {
931
- return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
932
- }
933
- function stripBrightsyNdjsonNoise(text) {
934
- if (!text || !text.includes('"type"')) return text;
935
- let out = text;
936
- out = out.replace(
937
- /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
938
- ""
939
- );
940
- out = out.replace(/\{"type":"(?:tool_use|tool_result|tool)"[\s\S]*$/g, "");
941
- return out.trim();
942
- }
943
- function isBrightsyNdjsonLine(line) {
944
- const t = line.trim();
945
- if (!t.startsWith("{")) return false;
946
- return /"type"\s*:\s*"(tool_use|tool_result|tool|thinking|usage|done|error|text)"/.test(t);
947
- }
948
- function finalizeParts(parts) {
949
- return parts.map(
950
- (p) => p.type === "tool" && p.status === "running" ? { ...p, status: "done" } : p
951
- );
952
744
  }
953
- function normalizeParseResult(parsed) {
954
- if (!parsed) return [];
955
- return Array.isArray(parsed) ? parsed : [parsed];
956
- }
957
-
958
- // src/agents/spawn.ts
959
745
  async function spawnAgentTurn(thread, input, onEvent) {
960
746
  ensureAgentPath();
961
747
  if (!thread.worktreePath?.trim()) {
@@ -963,9 +749,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
963
749
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
964
750
  );
965
751
  }
966
- const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-V6TMECDU.js");
752
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-XSUFEIAQ.js");
967
753
  if (isGlobalThread2(thread)) {
968
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-4TUQUEHY.js");
754
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-AR66L3N4.js");
969
755
  ensureGlobalCoordinatorCwd2(
970
756
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
971
757
  );
@@ -981,6 +767,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
981
767
  );
982
768
  }
983
769
  const env = childEnvWithAppSettings(cmd.env);
770
+ applyPromptCacheTtlEnv(thread.agent, env);
984
771
  try {
985
772
  if (isOrchestratorThread(thread)) {
986
773
  mergeAgentGitAuthEnv(env, await resolveAgentGitAuthEnv(env));
@@ -1192,1540 +979,1617 @@ function shouldAutoArchiveOnPrMerge(opts) {
1192
979
  // src/hook/conductor.ts
1193
980
  import {
1194
981
  copyFileSync,
1195
- existsSync as existsSync3,
1196
- mkdirSync,
982
+ existsSync as existsSync4,
983
+ mkdirSync as mkdirSync2,
1197
984
  readdirSync,
1198
- readFileSync as readFileSync3
985
+ readFileSync as readFileSync4
1199
986
  } from "fs";
1200
987
  import { createServer } from "net";
1201
- import { basename, dirname, join as join4 } from "path";
988
+ import { basename, dirname as dirname2, join as join5 } from "path";
1202
989
  import { execa as execa2 } from "execa";
1203
990
  import { createInterface as createInterface2 } from "readline";
1204
- var PORT_RANGE_SIZE = 10;
1205
- function matchSimpleGlob(pattern, name) {
1206
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1207
- return new RegExp(`^${escaped}$`).test(name);
991
+
992
+ // src/review/request-review.ts
993
+ import { randomUUID as randomUUID2 } from "crypto";
994
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
995
+ import { dirname, join as join4 } from "path";
996
+
997
+ // src/threads/chat-tabs.ts
998
+ import { randomUUID } from "crypto";
999
+
1000
+ // src/composer/summarize.ts
1001
+ var MAX_TRANSCRIPT_CHARS = 12e4;
1002
+ var MAX_SUMMARY_CHARS = 6e3;
1003
+ async function summarizeConversation(transcript, opts) {
1004
+ const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
1005
+
1006
+ [\u2026truncated\u2026]` : transcript;
1007
+ const claudeSummary = await tryClaudeSummary(clipped, opts);
1008
+ if (claudeSummary) {
1009
+ return { summary: clipSummary(claudeSummary), method: "claude" };
1010
+ }
1011
+ return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
1208
1012
  }
1209
- function readWorktreeInclude(repoPath) {
1210
- const path = join4(repoPath, ".worktreeinclude");
1211
- if (!existsSync3(path)) return [];
1212
- return readFileSync3(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
1013
+ async function tryClaudeSummary(transcript, opts) {
1014
+ ensureAgentPath();
1015
+ const prompt = [
1016
+ "Summarize this coding-agent conversation for continuity in a fresh session.",
1017
+ "Use concise markdown bullets covering:",
1018
+ "- Goal / request",
1019
+ "- Key decisions and constraints",
1020
+ "- Files / areas touched",
1021
+ "- Current state and open tasks",
1022
+ "- Anything the next turn must remember",
1023
+ "Do not invent details. No preamble.",
1024
+ "",
1025
+ "Transcript:",
1026
+ transcript
1027
+ ].join("\n");
1028
+ try {
1029
+ const { stdout, exitCode } = await run(
1030
+ "claude",
1031
+ [
1032
+ "-p",
1033
+ prompt,
1034
+ "--output-format",
1035
+ "text",
1036
+ "--permission-mode",
1037
+ "bypassPermissions",
1038
+ "--model",
1039
+ "haiku"
1040
+ ],
1041
+ { cwd: opts?.cwd, reject: false }
1042
+ );
1043
+ if (exitCode !== 0) return null;
1044
+ const text = stdout.trim();
1045
+ if (text.length < 40) return null;
1046
+ return text;
1047
+ } catch {
1048
+ return null;
1049
+ }
1213
1050
  }
1214
- function resolveFilesToCopy(repoPath) {
1215
- const fromInclude = readWorktreeInclude(repoPath);
1216
- if (fromInclude.length) return fromInclude;
1217
- const settings = loadRepoSettings(repoPath);
1218
- if (settings?.filesToCopy?.length) return settings.filesToCopy;
1219
- if (settings?.fileIncludeGlobs?.length) {
1220
- const matched = [];
1221
- try {
1222
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1223
- if (!entry.isFile()) continue;
1224
- for (const glob of settings.fileIncludeGlobs) {
1225
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
1226
- matched.push(entry.name);
1227
- break;
1228
- }
1229
- }
1230
- }
1231
- } catch {
1051
+ function extractiveSummary(transcript) {
1052
+ const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
1053
+ const users = [];
1054
+ const tools = [];
1055
+ const agents = [];
1056
+ let mode = "none";
1057
+ for (const line of lines) {
1058
+ if (line.startsWith("### User")) {
1059
+ mode = "user";
1060
+ continue;
1232
1061
  }
1233
- if (matched.length) return [...new Set(matched)];
1234
- }
1235
- const defaults = [];
1236
- try {
1237
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1238
- if (entry.isFile() && entry.name.startsWith(".env")) {
1239
- defaults.push(entry.name);
1240
- }
1062
+ if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
1063
+ mode = "agent";
1064
+ continue;
1065
+ }
1066
+ if (line.startsWith("- ") && line.includes(":")) {
1067
+ tools.push(line.replace(/^- /, ""));
1068
+ continue;
1069
+ }
1070
+ if (mode === "user" && users.length < 8) {
1071
+ users.push(line.slice(0, 240));
1072
+ } else if (mode === "agent" && agents.length < 6) {
1073
+ agents.push(line.slice(0, 240));
1241
1074
  }
1242
- } catch {
1243
1075
  }
1244
- if (defaults.length) return defaults;
1245
- return [".env.local", ".env"];
1076
+ const uniqueTools = [...new Set(tools)].slice(0, 20);
1077
+ const parts = [
1078
+ "## Compacted context (extractive)",
1079
+ "",
1080
+ "### User goals",
1081
+ ...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
1082
+ "",
1083
+ "### Recent agent notes",
1084
+ ...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
1085
+ ];
1086
+ if (uniqueTools.length) {
1087
+ parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
1088
+ }
1089
+ parts.push(
1090
+ "",
1091
+ "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
1092
+ );
1093
+ return parts.join("\n");
1246
1094
  }
1247
- function copyConfiguredFiles(repoPath, worktreePath) {
1248
- const patterns = resolveFilesToCopy(repoPath);
1249
- const copied = [];
1250
- for (const rel of patterns) {
1251
- const src = join4(repoPath, rel);
1252
- if (!existsSync3(src)) continue;
1253
- const dest = join4(worktreePath, rel);
1254
- mkdirSync(dirname(dest), { recursive: true });
1255
- copyFileSync(src, dest);
1256
- copied.push(rel);
1095
+ function clipSummary(text) {
1096
+ if (text.length <= MAX_SUMMARY_CHARS) return text;
1097
+ return `${text.slice(0, MAX_SUMMARY_CHARS)}
1098
+
1099
+ [\u2026summary truncated\u2026]`;
1100
+ }
1101
+
1102
+ // src/composer/context-compact.ts
1103
+ var CONTEXT_COMPACT_CHARS = 4e5;
1104
+ var CONTEXT_KEEP_RECENT_CHARS = 24e3;
1105
+ var CONTEXT_KEEP_RECENT_MESSAGES = 12;
1106
+ var CONTEXT_MIN_MESSAGES = 10;
1107
+ var SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
1108
+ function estimateMessageChars(message) {
1109
+ let n = message.text.length + 16;
1110
+ for (const part of message.parts ?? []) {
1111
+ n += estimatePartChars(part);
1257
1112
  }
1258
- return copied;
1113
+ return n;
1259
1114
  }
1260
- var cachedLoginEnv = null;
1261
- async function captureLoginEnv() {
1262
- if (cachedLoginEnv) return { ...cachedLoginEnv };
1263
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
1264
- try {
1265
- const result = await execa2(shell, ["-l", "-c", "env -0"], {
1266
- reject: false,
1267
- timeout: 5e3
1268
- });
1269
- if (result.exitCode === 0 && result.stdout) {
1270
- const env = { ...process.env };
1271
- for (const entry of result.stdout.split("\0")) {
1272
- if (!entry) continue;
1273
- const eq = entry.indexOf("=");
1274
- if (eq <= 0) continue;
1275
- const key = entry.slice(0, eq);
1276
- const value = entry.slice(eq + 1);
1277
- if (key) env[key] = value;
1278
- }
1279
- cachedLoginEnv = env;
1280
- return { ...env };
1115
+ function estimatePartChars(part) {
1116
+ switch (part.type) {
1117
+ case "text":
1118
+ case "thinking":
1119
+ return part.text.length;
1120
+ case "tool": {
1121
+ const input = part.input ? JSON.stringify(part.input) : "";
1122
+ return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
1281
1123
  }
1282
- } catch {
1124
+ default:
1125
+ return 0;
1283
1126
  }
1284
- cachedLoginEnv = { ...process.env };
1285
- return { ...cachedLoginEnv };
1286
1127
  }
1287
- function buildWorkspaceScriptEnv(opts, baseEnv) {
1288
- const env = stripNestedElectronEnv({
1289
- ...baseEnv ?? process.env
1290
- });
1291
- const name = opts.workspaceName ?? basename(opts.worktreePath);
1292
- const ports = opts.ports ?? [];
1293
- const primary = ports[0];
1294
- env.SIDEBOARD_WORKSPACE_NAME = name;
1295
- env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
1296
- env.SIDEBOARD_ROOT_PATH = opts.repoPath;
1297
- env.SIDEBOARD_IS_LOCAL = "1";
1298
- if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
1299
- env.CONDUCTOR_WORKSPACE_NAME = name;
1300
- env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
1301
- env.CONDUCTOR_ROOT_PATH = opts.repoPath;
1302
- env.CONDUCTOR_IS_LOCAL = "1";
1303
- if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
1304
- if (primary != null) {
1305
- env.SIDEBOARD_PORT = String(primary);
1306
- env.CONDUCTOR_PORT = String(primary);
1307
- env.PORT = String(primary);
1308
- for (let i = 1; i < ports.length; i++) {
1309
- const p = ports[i];
1310
- env[`SIDEBOARD_PORT_${i}`] = String(p);
1311
- env[`CONDUCTOR_PORT_${i}`] = String(p);
1312
- }
1128
+ function estimateThreadChars(messages) {
1129
+ return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
1130
+ }
1131
+ function shouldCompactContext(messages, thresholds = {}) {
1132
+ const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
1133
+ const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
1134
+ if (messages.length < minMessages) return false;
1135
+ return estimateThreadChars(messages) >= maxChars;
1136
+ }
1137
+ function splitForCompaction(messages, thresholds = {}) {
1138
+ const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
1139
+ const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
1140
+ if (messages.length === 0) return { older: [], recent: [] };
1141
+ let recentChars = 0;
1142
+ let cut = messages.length;
1143
+ for (let i = messages.length - 1; i >= 0; i--) {
1144
+ const next = recentChars + estimateMessageChars(messages[i]);
1145
+ const kept = messages.length - i;
1146
+ if (kept >= keepCount && next > keepChars) break;
1147
+ recentChars = next;
1148
+ cut = i;
1313
1149
  }
1314
- return env;
1150
+ const minRecent = Math.min(keepCount, messages.length);
1151
+ cut = Math.min(cut, messages.length - minRecent);
1152
+ if (cut <= 0) return { older: [], recent: messages };
1153
+ return {
1154
+ older: messages.slice(0, cut),
1155
+ recent: messages.slice(cut)
1156
+ };
1315
1157
  }
1316
- function pipeLines(stream, onLine) {
1317
- if (!stream || !onLine) return;
1318
- const rl = createInterface2({ input: stream });
1319
- rl.on("line", onLine);
1158
+ function formatToolPart(t, detail) {
1159
+ if (detail === "summary") {
1160
+ const label = t.description || t.detail || t.name;
1161
+ const path = t.filePath ? ` (${t.filePath})` : "";
1162
+ return `- ${t.name}: ${label}${path}`;
1163
+ }
1164
+ const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
1165
+ if (t.description) lines.push(`Description: ${t.description}`);
1166
+ if (t.filePath) lines.push(`Path: ${t.filePath}`);
1167
+ if (t.input && Object.keys(t.input).length > 0) {
1168
+ lines.push("Input:");
1169
+ lines.push("```json");
1170
+ lines.push(JSON.stringify(t.input, null, 2));
1171
+ lines.push("```");
1172
+ } else if (t.detail) {
1173
+ lines.push(`Detail: ${t.detail}`);
1174
+ }
1175
+ if (t.result != null && t.result !== "") {
1176
+ lines.push("Result:");
1177
+ lines.push("```");
1178
+ lines.push(t.result);
1179
+ lines.push("```");
1180
+ }
1181
+ return lines.join("\n");
1320
1182
  }
1321
- function killScriptTree(child, ports = []) {
1322
- const pid = child.pid;
1323
- if (pid) {
1324
- try {
1325
- if (process.platform === "win32") {
1326
- void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
1327
- } else {
1328
- process.kill(-pid, "SIGTERM");
1329
- setTimeout(() => {
1330
- try {
1331
- process.kill(-pid, "SIGKILL");
1332
- } catch {
1333
- }
1334
- }, 2500).unref?.();
1335
- }
1336
- } catch {
1337
- try {
1338
- child.kill("SIGTERM");
1339
- } catch {
1340
- }
1183
+ function formatMessagesAsTranscript(messages, opts) {
1184
+ const tools = opts?.tools ?? "full";
1185
+ const blocks = [];
1186
+ for (const m of messages) {
1187
+ if (m.role === "summary") {
1188
+ blocks.push(`## Prior summary
1189
+ ${m.text}`);
1190
+ continue;
1341
1191
  }
1342
- }
1343
- for (const port of ports) {
1344
- if (!Number.isFinite(port) || port <= 0) continue;
1345
- if (process.platform === "win32") {
1346
- void execa2(
1347
- "powershell",
1348
- [
1349
- "-NoProfile",
1350
- "-Command",
1351
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
1352
- ],
1353
- { reject: false }
1354
- );
1192
+ if (m.role === "user") {
1193
+ blocks.push(`### User
1194
+ ${m.text}`);
1195
+ continue;
1196
+ }
1197
+ const bits = [];
1198
+ if (m.text?.trim()) {
1199
+ bits.push(`### Agent
1200
+ ${m.text}`);
1355
1201
  } else {
1356
- void execa2(
1357
- "zsh",
1358
- ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
1359
- { reject: false }
1360
- );
1202
+ bits.push("### Agent");
1361
1203
  }
1362
- }
1363
- }
1364
- async function spawnWorkspaceScript(command, opts) {
1365
- const loginEnv = await captureLoginEnv();
1366
- const env = buildWorkspaceScriptEnv(
1367
- {
1368
- worktreePath: opts.worktreePath,
1369
- repoPath: opts.repoPath,
1370
- workspaceName: opts.workspaceName,
1371
- defaultBranch: opts.defaultBranch,
1372
- ports: opts.ports
1373
- },
1374
- loginEnv
1375
- );
1376
- try {
1377
- mergeAgentGitAuthEnv(
1378
- env,
1379
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
1204
+ const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
1205
+ for (const th of thinking) {
1206
+ if (th.type !== "thinking" || !th.text.trim()) continue;
1207
+ bits.push("Thinking:");
1208
+ bits.push(th.text);
1209
+ }
1210
+ const toolParts = (m.parts ?? []).filter(
1211
+ (p) => p.type === "tool"
1380
1212
  );
1381
- } catch {
1213
+ if (tools !== "none" && toolParts.length > 0) {
1214
+ if (tools === "summary") bits.push("Tools:");
1215
+ for (const t of toolParts) {
1216
+ bits.push(formatToolPart(t, tools));
1217
+ }
1218
+ }
1219
+ blocks.push(bits.join("\n"));
1382
1220
  }
1383
- const shell = process.platform === "darwin" ? "zsh" : "bash";
1384
- const child = execa2(shell, ["-lc", command], {
1385
- cwd: opts.worktreePath,
1386
- reject: false,
1387
- env,
1388
- // Own process group so Stop can tear down the whole tree (not just the shell).
1389
- // There is no settings.toml `stop=` / teardown hook for run scripts.
1390
- ...process.platform === "win32" ? {} : { detached: true }
1391
- });
1392
- pipeLines(child.stdout, opts.onLine);
1393
- pipeLines(child.stderr, opts.onLine);
1394
- const ports = opts.ports ?? [];
1395
- return {
1396
- pid: child.pid,
1397
- kill: () => killScriptTree(child, ports),
1398
- done: child.then((r) => r.exitCode ?? null),
1399
- child
1221
+ return blocks.join("\n\n");
1222
+ }
1223
+ function buildSessionSeed(messages, opts) {
1224
+ if (messages.length === 0) return null;
1225
+ const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
1226
+ if (!body.trim()) return null;
1227
+ return [
1228
+ "Sideboard conversation context (restored after compaction or a new session):",
1229
+ "",
1230
+ body,
1231
+ "",
1232
+ "Continue from this context. Do not repeat the summary unless asked."
1233
+ ].join("\n");
1234
+ }
1235
+ function applyCompaction(messages, summaryText, thresholds = {}) {
1236
+ const { older, recent } = splitForCompaction(messages, thresholds);
1237
+ if (older.length === 0) return messages;
1238
+ const summary = {
1239
+ role: "summary",
1240
+ text: summaryText.trim(),
1241
+ ts: (/* @__PURE__ */ new Date()).toISOString()
1400
1242
  };
1243
+ return [summary, ...recent];
1401
1244
  }
1402
- async function attachAbort(handle, signal) {
1403
- if (signal) {
1404
- const onAbort = () => handle.kill();
1405
- if (signal.aborted) onAbort();
1406
- else signal.addEventListener("abort", onAbort, { once: true });
1245
+ function lastRequestOccupancy(thread) {
1246
+ for (let i = thread.messages.length - 1; i >= 0; i--) {
1247
+ const usage = thread.messages[i]?.usage;
1248
+ if (usage) return contextTokens(usage);
1407
1249
  }
1408
- return handle.done;
1250
+ return 0;
1409
1251
  }
1410
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
1411
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1412
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
1413
- const handle = await spawnWorkspaceScript(settings.setup, {
1414
- worktreePath,
1415
- repoPath,
1416
- defaultBranch: opts?.defaultBranch,
1417
- onLine
1252
+ function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
1253
+ return lastRequestOccupancy(thread) >= occupancyTokens;
1254
+ }
1255
+ async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
1256
+ if (!shouldCompactContext(thread.messages, thresholds)) {
1257
+ return { didCompact: false, thread };
1258
+ }
1259
+ const { older } = splitForCompaction(thread.messages, thresholds);
1260
+ if (older.length === 0) {
1261
+ return { didCompact: false, thread };
1262
+ }
1263
+ const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
1264
+ const { summary, method } = await summarize(transcript, {
1265
+ cwd: thread.worktreePath
1418
1266
  });
1419
- const exitCode = await attachAbort(handle, opts?.signal);
1267
+ const messages = applyCompaction(thread.messages, summary, thresholds);
1268
+ const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
1269
+ const next = {
1270
+ ...thread,
1271
+ messages,
1272
+ sessionId: resetSession ? null : thread.sessionId,
1273
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1274
+ };
1420
1275
  return {
1421
- ran: true,
1422
- exitCode,
1423
- source: workspaceSettingsSourceLabel(worktreePath, repoPath),
1424
- kill: handle.kill
1276
+ didCompact: true,
1277
+ thread: next,
1278
+ summary,
1279
+ method,
1280
+ olderCount: older.length
1425
1281
  };
1426
1282
  }
1427
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
1428
- const found = findConventionSetup(worktreePath, repoPath);
1429
- if (!found) return { ran: false, exitCode: null, source: null };
1430
- onLine?.(`[setup] ${found.source}`);
1431
- const handle = await spawnWorkspaceScript(found.command, {
1432
- worktreePath,
1433
- repoPath,
1434
- defaultBranch: opts?.defaultBranch,
1435
- onLine
1436
- });
1437
- const exitCode = await attachAbort(handle, opts?.signal);
1438
- return {
1439
- ran: true,
1440
- exitCode,
1441
- source: found.source,
1442
- kill: handle.kill
1443
- };
1283
+
1284
+ // src/threads/chat-tabs.ts
1285
+ function sameWorktreePath(a, b) {
1286
+ return normalizeWorktreePath(a) === normalizeWorktreePath(b);
1444
1287
  }
1445
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
1446
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
1447
- if (!setup.ran) {
1448
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
1449
- }
1450
- if (!setup.ran) {
1451
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
1288
+ function takenTeamSlugsForChatTab(worktreePath) {
1289
+ const key = normalizeWorktreePath(worktreePath);
1290
+ const taken = /* @__PURE__ */ new Set();
1291
+ const dir = worktreeNameFromPath(key);
1292
+ if (dir) taken.add(dir.toLowerCase());
1293
+ for (const sibling of threadsSharingWorktree(key)) {
1294
+ for (const slug of takenSlugsFromThread(sibling)) {
1295
+ taken.add(slug);
1296
+ }
1452
1297
  }
1453
- return setup;
1298
+ return [...taken];
1454
1299
  }
1455
- async function runArchiveScript(repoPath, worktreePath, onLine) {
1456
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1457
- if (!settings?.archive) return { ran: false, exitCode: null };
1458
- const handle = await spawnWorkspaceScript(settings.archive, {
1459
- worktreePath,
1460
- repoPath,
1461
- onLine
1462
- });
1463
- const exitCode = await handle.done;
1464
- return { ran: true, exitCode };
1300
+ function requireThread(idOrRef) {
1301
+ const thread = findThreadByRef(idOrRef) ?? null;
1302
+ if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
1303
+ return thread;
1465
1304
  }
1466
- function listRunScripts(worktreePath, repoPath) {
1467
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1468
- if (!settings?.runScripts.length) return [];
1469
- return settings.runScripts.filter((s) => {
1470
- if (!s.availableIn?.length) return true;
1471
- return s.availableIn.includes("local");
1472
- });
1305
+ function worktreeBindingFrom(from) {
1306
+ return {
1307
+ worktreePath: normalizeWorktreePath(from.worktreePath),
1308
+ repoPath: from.repoPath,
1309
+ branchName: from.branchName,
1310
+ sourceRef: from.sourceRef,
1311
+ // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
1312
+ // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
1313
+ sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
1314
+ sourceIsFork: from.sourceIsFork,
1315
+ parentThreadId: from.parentThreadId,
1316
+ prUrl: from.prUrl,
1317
+ prTitle: from.prTitle,
1318
+ prState: from.prState,
1319
+ skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
1320
+ stackId: from.stackId,
1321
+ stackLayer: from.stackLayer,
1322
+ cowboy: from.cowboy
1323
+ };
1473
1324
  }
1474
- function getDefaultRunScript(worktreePath, repoPath) {
1475
- const scripts = listRunScripts(worktreePath, repoPath);
1476
- if (!scripts.length) return null;
1477
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
1325
+ function threadsSharingWorktree(worktreePath) {
1326
+ if (!worktreePath) return [];
1327
+ const key = normalizeWorktreePath(worktreePath);
1328
+ return listThreads({ includeArchived: true }).filter(
1329
+ (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
1330
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1478
1331
  }
1479
- function getRunScript(worktreePath, repoPath, name) {
1480
- const scripts = listRunScripts(worktreePath, repoPath);
1481
- if (!scripts.length) return null;
1482
- if (name) {
1483
- return scripts.find((s) => s.name === name) ?? null;
1484
- }
1485
- return getDefaultRunScript(worktreePath, repoPath);
1332
+ function formatTranscriptMarkdown(title, messages) {
1333
+ const body = formatMessagesAsTranscript(messages, { tools: "full" });
1334
+ return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
1486
1335
  }
1487
- function getRunMode(worktreePath, repoPath) {
1488
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
1336
+ function forkMessageSlice(from, throughIndex) {
1337
+ const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
1338
+ return through >= 0 ? from.messages.slice(0, through + 1) : [];
1489
1339
  }
1490
- async function allocatePort() {
1491
- return new Promise((resolve, reject) => {
1492
- const server = createServer();
1493
- server.listen(0, "127.0.0.1", () => {
1494
- const addr = server.address();
1495
- if (!addr || typeof addr === "string") {
1496
- server.close();
1497
- reject(new Error("Failed to allocate port"));
1498
- return;
1499
- }
1500
- const port = addr.port;
1501
- server.close((err) => err ? reject(err) : resolve(port));
1502
- });
1503
- server.on("error", reject);
1504
- });
1340
+ function buildForkTranscriptAttachment(baseTitle, messages) {
1341
+ const title = baseTitle || "Chat";
1342
+ return {
1343
+ id: randomUUID(),
1344
+ name: `Transcript of ${title}.md`,
1345
+ kind: "transcript",
1346
+ content: formatTranscriptMarkdown(title, messages)
1347
+ };
1505
1348
  }
1506
- async function allocatePortRange(size = PORT_RANGE_SIZE) {
1507
- const base = await allocatePort();
1508
- const ports = [base];
1509
- for (let i = 1; i < size; i++) {
1510
- const candidate = base + i;
1511
- const free = await new Promise((resolve) => {
1512
- const server = createServer();
1513
- server.once("error", () => resolve(false));
1514
- server.listen(candidate, "127.0.0.1", () => {
1515
- server.close(() => resolve(true));
1516
- });
1517
- });
1518
- if (free) {
1519
- ports.push(candidate);
1520
- } else {
1521
- ports.push(await allocatePort());
1522
- }
1349
+ function createChatTab(input) {
1350
+ const from = requireThread(input.fromThreadId);
1351
+ const binding = worktreeBindingFrom(from);
1352
+ const explicitTitle = input.title?.trim();
1353
+ const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
1354
+ const nextAgent = input.agent ?? from.agent;
1355
+ if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
1356
+ assertOrchestratorCapableAgent(nextAgent);
1523
1357
  }
1524
- return ports;
1358
+ const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
1359
+ const thread = createEmptyThread({
1360
+ title,
1361
+ // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
1362
+ // syncThreadBranchFromGit would otherwise rewrite every sibling to the
1363
+ // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
1364
+ userSetTitle: true,
1365
+ ...binding,
1366
+ // Slack / Brightsy cloud identity stays on the original chat. A + tab
1367
+ // that copies `slack:team:user` would steal inbound DMs after you close
1368
+ // the connected chat.
1369
+ sourceRef: singletonInbox ? title : binding.sourceRef,
1370
+ agent: nextAgent,
1371
+ model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
1372
+ effort: input.effort !== void 0 ? input.effort : from.effort,
1373
+ fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
1374
+ planMode: from.planMode,
1375
+ autonomy: input.autonomy ?? from.autonomy,
1376
+ attachments: input.attachments ?? [],
1377
+ status: "idle"
1378
+ });
1379
+ writeThread(thread);
1380
+ return thread;
1525
1381
  }
1526
- async function startDevServer(repoPath, worktreePath, onLine, opts) {
1527
- const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
1528
- if (!script) return null;
1529
- const ports = await allocatePortRange(PORT_RANGE_SIZE);
1530
- const handle = await spawnWorkspaceScript(script.command, {
1531
- worktreePath,
1532
- repoPath,
1533
- ports,
1534
- defaultBranch: opts?.defaultBranch,
1535
- onLine
1382
+ function forkChatTab(input) {
1383
+ const from = requireThread(input.threadId);
1384
+ const slice = forkMessageSlice(from, input.throughIndex);
1385
+ const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
1386
+ const tab = createChatTab({
1387
+ fromThreadId: input.threadId,
1388
+ agent: input.agent ?? from.agent,
1389
+ model: input.model,
1390
+ title: input.title?.trim() || void 0,
1391
+ attachments: [attachment]
1536
1392
  });
1537
- return {
1538
- pid: handle.pid,
1539
- port: ports[0],
1540
- ports,
1541
- scriptName: script.name,
1542
- kill: handle.kill,
1543
- done: handle.done
1544
- };
1393
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
1394
+ const next = { ...tab, parentThreadId: from.id };
1395
+ writeThread(next);
1396
+ return next;
1397
+ }
1398
+ return tab;
1545
1399
  }
1546
1400
 
1547
- // src/git/orphan-cleanup.ts
1548
- import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "fs";
1549
- import { join as join5 } from "path";
1550
- function isSideboardWorktreePath(path) {
1551
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
1552
- }
1553
- async function findOrphanWorktrees(repoPaths) {
1554
- const threads = listThreads({ includeArchived: true });
1555
- const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
1556
- const repos = new Set(
1557
- repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
1558
- );
1559
- const homeRoot = sideboardWorkspacesDir();
1560
- if (existsSync4(homeRoot)) {
1561
- try {
1562
- for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
1563
- if (!entry.isDirectory()) continue;
1564
- void entry;
1565
- }
1566
- } catch {
1567
- }
1568
- }
1569
- const orphans = [];
1570
- const seen = /* @__PURE__ */ new Set();
1571
- for (const repoPath of repos) {
1572
- if (!repoPath || !existsSync4(repoPath)) continue;
1573
- try {
1574
- const wts = await listWorktrees(repoPath);
1575
- for (const wt of wts) {
1576
- const path = wt.path.replace(/\/$/, "");
1577
- if (!isSideboardWorktreePath(path)) continue;
1578
- if (known.has(path)) continue;
1579
- if (seen.has(path)) continue;
1580
- seen.add(path);
1581
- let mtimeMs = 0;
1582
- try {
1583
- mtimeMs = statSync(path).mtimeMs;
1584
- } catch {
1585
- mtimeMs = 0;
1586
- }
1587
- orphans.push({ path, repoPath, mtimeMs });
1588
- }
1589
- } catch {
1590
- }
1591
- try {
1592
- const root = worktreesRoot(repoPath);
1593
- if (existsSync4(root)) {
1594
- for (const entry of readdirSync2(root, { withFileTypes: true })) {
1595
- if (!entry.isDirectory()) continue;
1596
- const path = join5(root, entry.name).replace(/\/$/, "");
1597
- if (known.has(path) || seen.has(path)) continue;
1598
- if (!existsSync4(join5(path, ".git"))) continue;
1599
- seen.add(path);
1600
- let mtimeMs = 0;
1601
- try {
1602
- mtimeMs = statSync(path).mtimeMs;
1603
- } catch {
1604
- mtimeMs = Date.now();
1605
- }
1606
- orphans.push({ path, repoPath, mtimeMs });
1607
- }
1608
- }
1609
- } catch {
1610
- }
1401
+ // src/review/review-request-template.ts
1402
+ var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
1403
+
1404
+ 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.
1405
+
1406
+ ## Required outcome
1407
+
1408
+ Start your reply with a **Recommendation** section using exactly one of:
1409
+
1410
+ - **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
1411
+ - **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
1412
+ - **Request changes** \u2014 not ready; blocking issues must be fixed first.
1413
+ - **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
1414
+
1415
+ 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.
1416
+
1417
+ People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
1418
+
1419
+ ## Findings
1420
+
1421
+ Below are guidelines for determining whether an issue is worth flagging to the original author.
1422
+
1423
+ These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
1424
+
1425
+ Flag something as a bug / blocking finding only when:
1426
+
1427
+ 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
1428
+ 2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
1429
+ 3. Fixing it does not demand rigor absent from the rest of the codebase.
1430
+ 4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
1431
+ 5. The author would likely fix it if made aware.
1432
+ 6. It does not rely on unstated assumptions about the codebase or author intent.
1433
+ 7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
1434
+ 8. It is clearly not just an intentional change by the author.
1435
+
1436
+ When flagging an issue, include a short accompanying comment:
1437
+
1438
+ 1. Clear about why it is a problem.
1439
+ 2. Severity must match reality \u2014 do not inflate.
1440
+ 3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
1441
+ 4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
1442
+ 5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
1443
+ 6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
1444
+ 7. Skimmable on first read.
1445
+ 8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
1446
+
1447
+ HOW MANY FINDINGS TO RETURN:
1448
+
1449
+ 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.
1450
+
1451
+ GUIDELINES:
1452
+
1453
+ - Ignore trivial style unless it obscures meaning or violates documented standards.
1454
+ - One comment per distinct issue (or a short multi-line range if needed).
1455
+ - Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
1456
+ - In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
1457
+ - Do NOT introduce or remove outer indentation levels unless that is the actual fix.
1458
+ - Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
1459
+
1460
+ 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).
1461
+
1462
+ ## Getting the diff
1463
+
1464
+ 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.
1465
+
1466
+ 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.
1467
+
1468
+ ## Fallback: if you don't have access to the Sideboard diff tool
1469
+
1470
+ If you don't have access to \`get_diff\`, use the following git commands to get the diff:
1471
+
1472
+ \`\`\`bash
1473
+ # Get the merge base between this branch and the target
1474
+ MERGE_BASE=$(git merge-base origin/main HEAD)
1475
+
1476
+ # Get the committed diff against the merge base
1477
+ git diff $MERGE_BASE HEAD
1478
+
1479
+ # Get any uncommitted changes (staged and unstaged)
1480
+ git diff HEAD
1481
+ \`\`\`
1482
+
1483
+ 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.
1484
+
1485
+ No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
1486
+
1487
+ ## Output format
1488
+
1489
+ **1. Recommendation first** (required), then **2. Findings** (may be empty).
1490
+
1491
+ Only report ONE finding per unique issue.
1492
+
1493
+ <example>
1494
+ ## Recommendation
1495
+
1496
+ **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.
1497
+
1498
+ ## Findings
1499
+
1500
+ ### **#1 Empty input causes crash** (blocking)
1501
+
1502
+ If the input field is empty when the page loads, the app will crash.
1503
+
1504
+ File: src/client/frontends/desktop/ui/Input.tsx
1505
+
1506
+ ### **#2 Dead code** (nit)
1507
+
1508
+ The getUserData function is now unused. It should be deleted.
1509
+
1510
+ File: src/client/frontends/desktop/core/UserData.ts
1511
+ </example>
1512
+
1513
+ <example>
1514
+ ## Recommendation
1515
+
1516
+ **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
1517
+ </example>
1518
+
1519
+ ## Growing the rules
1520
+
1521
+ 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.
1522
+ `;
1523
+ var REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
1524
+ var REVIEW_SKILL_NAME = "review";
1525
+ var REVIEW_SKILL_FRONTMATTER = `---
1526
+ name: review
1527
+ description: >-
1528
+ Merge-readiness review for this repository. Use when reviewing a PR, worktree
1529
+ diff, or Sideboard Review (Approve / Approve with nits / Request changes).
1530
+ ---
1531
+
1532
+ `;
1533
+ function wrapReviewSkillMarkdown(body) {
1534
+ const trimmed = body.replace(/^\uFEFF/, "").trim();
1535
+ const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
1536
+ if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
1537
+ return source.endsWith("\n") ? source : `${source}
1538
+ `;
1611
1539
  }
1612
- return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
1540
+ return `${REVIEW_SKILL_FRONTMATTER}${source}
1541
+ `;
1613
1542
  }
1614
- async function cleanupOrphanWorktrees(opts) {
1615
- const settings = loadAppSettings();
1616
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
1617
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
1618
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
1619
- const budget = Math.max(0, maxCount - activeCount);
1620
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
1621
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
1622
- const removed = [];
1623
- const kept = [];
1624
- for (const orphan of orphans) {
1625
- if (keep.has(orphan.path)) {
1626
- kept.push(orphan.path);
1627
- continue;
1628
- }
1629
- if (opts?.dryRun) {
1630
- removed.push(orphan.path);
1631
- continue;
1632
- }
1633
- try {
1634
- await removeWorktree(orphan.repoPath, orphan.path);
1635
- removed.push(orphan.path);
1636
- } catch {
1637
- kept.push(orphan.path);
1638
- }
1543
+
1544
+ // src/review/request-review.ts
1545
+ var REPO_REVIEW_PATH = ".sideboard/review.md";
1546
+ var REPO_REVIEW_NAME = "review.md";
1547
+ var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
1548
+ var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
1549
+ var REVIEW_REQUEST_NAME = "Review request.md";
1550
+ var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
1551
+ var LEGACY_REVIEW_TEMPLATE_MARKERS = [
1552
+ "You are acting as a reviewer for a proposed code change made by another engineer.",
1553
+ "HOW MANY FINDINGS TO RETURN:"
1554
+ ];
1555
+ function shouldRefreshReviewRequestTemplate(content) {
1556
+ const trimmed = content.trim();
1557
+ if (!trimmed) return true;
1558
+ if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
1559
+ return false;
1639
1560
  }
1640
- if (!opts?.dryRun) {
1641
- updateAdvancedSettings({
1642
- worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
1643
- });
1561
+ return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
1562
+ }
1563
+ function readTextIfPresent(abs) {
1564
+ if (!existsSync3(abs)) return null;
1565
+ try {
1566
+ const content = readFileSync3(abs, "utf8");
1567
+ return content.trim() ? content : null;
1568
+ } catch {
1569
+ return null;
1644
1570
  }
1645
- return { removed, kept, orphans };
1646
1571
  }
1647
- function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
1648
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
1649
- const last = settings.advanced.worktreeLastCleanupAt;
1650
- if (!last) return true;
1651
- const elapsed = Date.now() - Date.parse(last);
1652
- return elapsed >= intervalHours * 36e5;
1572
+ function readLocalGuidelines(worktreePath) {
1573
+ const localAbs = join4(worktreePath, REVIEW_REQUEST_PATH);
1574
+ const localContent = readTextIfPresent(localAbs);
1575
+ if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
1576
+ return { path: REVIEW_REQUEST_PATH, content: localContent };
1577
+ }
1578
+ const legacyAbs = join4(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
1579
+ const legacyContent = readTextIfPresent(legacyAbs);
1580
+ if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
1581
+ return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
1582
+ }
1583
+ return null;
1653
1584
  }
1654
- function worktreeCleanupSettings() {
1655
- const a = loadAppSettings().advanced;
1585
+ function skillGuidelines(content, source) {
1656
1586
  return {
1657
- worktreeMaxCount: a.worktreeMaxCount,
1658
- worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
1659
- worktreeLastCleanupAt: a.worktreeLastCleanupAt,
1660
- autoCleanupOrphans: a.autoCleanupOrphans
1587
+ path: REVIEW_SKILL_PATH,
1588
+ name: REVIEW_SKILL_NAME,
1589
+ content,
1590
+ source
1661
1591
  };
1662
1592
  }
1663
-
1664
- // src/git/apply-into-main.ts
1665
- import { execa as execa3 } from "execa";
1666
- async function applyThreadIntoMain(thread, opts) {
1667
- const method = opts?.method ?? "merge";
1668
- const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
1669
- const status = await execa3("git", ["status", "--porcelain"], {
1670
- cwd: thread.repoPath,
1671
- reject: false
1672
- });
1673
- if (status.stdout.trim()) {
1674
- throw new Error(
1675
- `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
1676
- );
1593
+ function ensureReviewSkillFile(worktreePath) {
1594
+ const abs = join4(worktreePath, REVIEW_SKILL_PATH);
1595
+ const existing = readTextIfPresent(abs);
1596
+ if (existing) {
1597
+ return { path: REVIEW_SKILL_PATH, content: existing, wrote: false };
1677
1598
  }
1678
- const checkout = await execa3("git", ["checkout", target], {
1679
- cwd: thread.repoPath,
1680
- reject: false
1681
- });
1682
- if (checkout.exitCode !== 0) {
1599
+ const fromRepo = readTextIfPresent(join4(worktreePath, REPO_REVIEW_PATH));
1600
+ const fromLocal = readLocalGuidelines(worktreePath)?.content ?? null;
1601
+ const content = wrapReviewSkillMarkdown(fromRepo ?? fromLocal ?? REVIEW_REQUEST_TEMPLATE);
1602
+ mkdirSync(dirname(abs), { recursive: true });
1603
+ writeFileSync(abs, content, "utf8");
1604
+ return { path: REVIEW_SKILL_PATH, content, wrote: true };
1605
+ }
1606
+ function resolveReviewGuidelines(worktreePath) {
1607
+ const skillContent = readTextIfPresent(join4(worktreePath, REVIEW_SKILL_PATH));
1608
+ if (skillContent) return skillGuidelines(skillContent, "skill");
1609
+ const local = readLocalGuidelines(worktreePath);
1610
+ if (local) {
1611
+ return {
1612
+ path: local.path,
1613
+ name: REVIEW_REQUEST_NAME,
1614
+ content: local.content,
1615
+ source: "local"
1616
+ };
1617
+ }
1618
+ const seeded = ensureReviewSkillFile(worktreePath);
1619
+ return skillGuidelines(seeded.content, seeded.wrote ? "stock" : "skill");
1620
+ }
1621
+ function ensureReviewRequestFile(worktreePath) {
1622
+ const seeded = ensureReviewSkillFile(worktreePath);
1623
+ return skillGuidelines(seeded.content, "skill");
1624
+ }
1625
+ function buildReviewRequestAttachment(content, opts) {
1626
+ const path = opts?.path ?? REVIEW_SKILL_PATH;
1627
+ const name = opts?.name ?? (path === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
1628
+ return {
1629
+ id: randomUUID2(),
1630
+ name,
1631
+ kind: "file",
1632
+ path,
1633
+ content
1634
+ };
1635
+ }
1636
+ function readExistingReviewRequestFile(worktreePath) {
1637
+ return readTextIfPresent(join4(worktreePath, REVIEW_SKILL_PATH)) ?? readTextIfPresent(join4(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent(join4(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent(join4(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
1638
+ }
1639
+ async function requestReview(threadRef, send) {
1640
+ const from = findThreadByRef(threadRef);
1641
+ if (!from) throw new Error(`Thread not found: ${threadRef}`);
1642
+ if (isOrchestratorThread(from)) {
1683
1643
  throw new Error(
1684
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
1644
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
1685
1645
  );
1686
1646
  }
1687
- if (method === "cherry-pick") {
1688
- const log = await execa3(
1689
- "git",
1690
- ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
1691
- { cwd: thread.repoPath, reject: false }
1692
- );
1693
- const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
1694
- if (!shas.length) {
1695
- return {
1696
- applied: false,
1697
- method,
1698
- targetBranch: target,
1699
- message: `No commits on ${thread.branchName} not already in ${target}`
1700
- };
1701
- }
1702
- for (const sha of shas) {
1703
- const cp = await execa3("git", ["cherry-pick", sha], {
1704
- cwd: thread.repoPath,
1705
- reject: false
1706
- });
1707
- if (cp.exitCode !== 0) {
1708
- await execa3("git", ["cherry-pick", "--abort"], {
1709
- cwd: thread.repoPath,
1710
- reject: false
1711
- });
1712
- throw new Error(
1713
- `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
1714
- );
1715
- }
1716
- }
1717
- return {
1718
- applied: true,
1719
- method,
1720
- targetBranch: target,
1721
- message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
1722
- };
1723
- }
1724
- const merge = await execa3(
1725
- "git",
1726
- ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
1727
- { cwd: thread.repoPath, reject: false }
1728
- );
1729
- if (merge.exitCode !== 0) {
1730
- await execa3("git", ["merge", "--abort"], {
1731
- cwd: thread.repoPath,
1732
- reject: false
1733
- });
1734
- throw new Error(
1735
- `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
1736
- );
1737
- }
1738
- return {
1739
- applied: true,
1740
- method: "merge",
1741
- targetBranch: target,
1742
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
1743
- };
1744
- }
1745
-
1746
- // src/git/clone-repo.ts
1747
- import { existsSync as existsSync5 } from "fs";
1748
- import { basename as basename2, join as join6 } from "path";
1749
- import { execa as execa4 } from "execa";
1750
- async function cloneRepoIntoSideboard(opts) {
1751
- const url = opts.url.trim();
1752
- if (!url) throw new Error("Clone URL is required");
1753
- let name = opts.name?.trim();
1754
- if (!name) {
1755
- const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
1756
- name = leaf || "repo";
1757
- }
1758
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
1759
- const dest = join6(sideboardReposDir(), name);
1760
- if (existsSync5(dest)) {
1761
- const repoPath2 = await resolveRepoRoot(dest);
1762
- const workspace2 = await ensureWorkspace(repoPath2);
1763
- return { repoPath: repoPath2, workspace: workspace2 };
1764
- }
1765
- const result = await execa4("git", ["clone", url, dest], { reject: false });
1766
- if (result.exitCode !== 0) {
1767
- throw new Error(
1768
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
1769
- );
1647
+ if (from.status === "archived") {
1648
+ throw new Error(`Thread is archived: ${from.id}`);
1770
1649
  }
1771
- const repoPath = await resolveRepoRoot(dest);
1772
- const workspace = await ensureWorkspace(repoPath);
1773
- return { repoPath, workspace };
1650
+ const guidelines = resolveReviewGuidelines(from.worktreePath);
1651
+ const tab = createChatTab({
1652
+ fromThreadId: from.id,
1653
+ title: "Review",
1654
+ attachments: [
1655
+ buildReviewRequestAttachment(guidelines.content, {
1656
+ path: guidelines.path,
1657
+ name: guidelines.name
1658
+ })
1659
+ ]
1660
+ });
1661
+ const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
1662
+ return { tab: started, from };
1774
1663
  }
1775
1664
 
1776
- // src/store/desktop-host.ts
1777
- import { readFileSync as readFileSync4, unlinkSync, writeFileSync } from "fs";
1778
- import { join as join7 } from "path";
1779
- function desktopHostPidPath() {
1780
- return join7(appDataDir(), "desktop-host.pid");
1665
+ // src/hook/conductor.ts
1666
+ var PORT_RANGE_SIZE = 10;
1667
+ function matchSimpleGlob(pattern, name) {
1668
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1669
+ return new RegExp(`^${escaped}$`).test(name);
1781
1670
  }
1782
- function claimDesktopHost(pid = process.pid) {
1783
- writeFileSync(desktopHostPidPath(), `${pid}
1784
- `, "utf8");
1671
+ function readWorktreeInclude(repoPath) {
1672
+ const path = join5(repoPath, ".worktreeinclude");
1673
+ if (!existsSync4(path)) return [];
1674
+ return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
1785
1675
  }
1786
- function releaseDesktopHost(pid = process.pid) {
1787
- if (readDesktopHostPid() !== pid) return;
1676
+ function resolveFilesToCopy(repoPath) {
1677
+ const fromInclude = readWorktreeInclude(repoPath);
1678
+ if (fromInclude.length) return fromInclude;
1679
+ const settings = loadRepoSettings(repoPath);
1680
+ if (settings?.filesToCopy?.length) return settings.filesToCopy;
1681
+ if (settings?.fileIncludeGlobs?.length) {
1682
+ const matched = [];
1683
+ try {
1684
+ for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1685
+ if (!entry.isFile()) continue;
1686
+ for (const glob of settings.fileIncludeGlobs) {
1687
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
1688
+ matched.push(entry.name);
1689
+ break;
1690
+ }
1691
+ }
1692
+ }
1693
+ } catch {
1694
+ }
1695
+ if (matched.length) return [...new Set(matched)];
1696
+ }
1697
+ const defaults = [];
1788
1698
  try {
1789
- unlinkSync(desktopHostPidPath());
1699
+ for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1700
+ if (entry.isFile() && entry.name.startsWith(".env")) {
1701
+ defaults.push(entry.name);
1702
+ }
1703
+ }
1790
1704
  } catch {
1791
1705
  }
1706
+ if (defaults.length) return defaults;
1707
+ return [".env.local", ".env"];
1792
1708
  }
1793
- function readDesktopHostPid() {
1794
- try {
1795
- const pid = Number.parseInt(readFileSync4(desktopHostPidPath(), "utf8").trim(), 10);
1796
- if (!Number.isFinite(pid) || pid <= 0) return null;
1797
- return pid;
1798
- } catch {
1799
- return null;
1709
+ function copyConfiguredFiles(repoPath, worktreePath) {
1710
+ const patterns = resolveFilesToCopy(repoPath);
1711
+ const copied = [];
1712
+ for (const rel of patterns) {
1713
+ const src = join5(repoPath, rel);
1714
+ if (!existsSync4(src)) continue;
1715
+ const dest = join5(worktreePath, rel);
1716
+ mkdirSync2(dirname2(dest), { recursive: true });
1717
+ copyFileSync(src, dest);
1718
+ copied.push(rel);
1800
1719
  }
1720
+ return copied;
1801
1721
  }
1802
- function pidAlive(pid) {
1722
+ var cachedLoginEnv = null;
1723
+ async function captureLoginEnv() {
1724
+ if (cachedLoginEnv) return { ...cachedLoginEnv };
1725
+ const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
1803
1726
  try {
1804
- process.kill(pid, 0);
1805
- return true;
1727
+ const result = await execa2(shell, ["-l", "-c", "env -0"], {
1728
+ reject: false,
1729
+ timeout: 5e3
1730
+ });
1731
+ if (result.exitCode === 0 && result.stdout) {
1732
+ const env = { ...process.env };
1733
+ for (const entry of result.stdout.split("\0")) {
1734
+ if (!entry) continue;
1735
+ const eq = entry.indexOf("=");
1736
+ if (eq <= 0) continue;
1737
+ const key = entry.slice(0, eq);
1738
+ const value = entry.slice(eq + 1);
1739
+ if (key) env[key] = value;
1740
+ }
1741
+ cachedLoginEnv = env;
1742
+ return { ...env };
1743
+ }
1806
1744
  } catch {
1807
- return false;
1808
1745
  }
1746
+ cachedLoginEnv = { ...process.env };
1747
+ return { ...cachedLoginEnv };
1809
1748
  }
1810
- function isDesktopHostAlive() {
1811
- const pid = readDesktopHostPid();
1812
- return pid != null && pidAlive(pid);
1749
+ function buildWorkspaceScriptEnv(opts, baseEnv) {
1750
+ const env = stripNestedElectronEnv({
1751
+ ...baseEnv ?? process.env
1752
+ });
1753
+ const name = opts.workspaceName ?? basename(opts.worktreePath);
1754
+ const ports = opts.ports ?? [];
1755
+ const primary = ports[0];
1756
+ env.SIDEBOARD_WORKSPACE_NAME = name;
1757
+ env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
1758
+ env.SIDEBOARD_ROOT_PATH = opts.repoPath;
1759
+ env.SIDEBOARD_IS_LOCAL = "1";
1760
+ if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
1761
+ env.CONDUCTOR_WORKSPACE_NAME = name;
1762
+ env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
1763
+ env.CONDUCTOR_ROOT_PATH = opts.repoPath;
1764
+ env.CONDUCTOR_IS_LOCAL = "1";
1765
+ if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
1766
+ if (primary != null) {
1767
+ env.SIDEBOARD_PORT = String(primary);
1768
+ env.CONDUCTOR_PORT = String(primary);
1769
+ env.PORT = String(primary);
1770
+ for (let i = 1; i < ports.length; i++) {
1771
+ const p = ports[i];
1772
+ env[`SIDEBOARD_PORT_${i}`] = String(p);
1773
+ env[`CONDUCTOR_PORT_${i}`] = String(p);
1774
+ }
1775
+ }
1776
+ return env;
1813
1777
  }
1814
- function isThisProcessDesktopHost() {
1815
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
1778
+ function pipeLines(stream, onLine) {
1779
+ if (!stream || !onLine) return;
1780
+ const rl = createInterface2({ input: stream });
1781
+ rl.on("line", onLine);
1816
1782
  }
1817
- function thisProcessShouldDrainAgentQueues() {
1818
- if (isThisProcessDesktopHost()) return true;
1819
- return !isDesktopHostAlive();
1783
+ function killScriptTree(child, ports = []) {
1784
+ const pid = child.pid;
1785
+ if (pid) {
1786
+ try {
1787
+ if (process.platform === "win32") {
1788
+ void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
1789
+ } else {
1790
+ process.kill(-pid, "SIGTERM");
1791
+ setTimeout(() => {
1792
+ try {
1793
+ process.kill(-pid, "SIGKILL");
1794
+ } catch {
1795
+ }
1796
+ }, 2500).unref?.();
1797
+ }
1798
+ } catch {
1799
+ try {
1800
+ child.kill("SIGTERM");
1801
+ } catch {
1802
+ }
1803
+ }
1804
+ }
1805
+ for (const port of ports) {
1806
+ if (!Number.isFinite(port) || port <= 0) continue;
1807
+ if (process.platform === "win32") {
1808
+ void execa2(
1809
+ "powershell",
1810
+ [
1811
+ "-NoProfile",
1812
+ "-Command",
1813
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
1814
+ ],
1815
+ { reject: false }
1816
+ );
1817
+ } else {
1818
+ void execa2(
1819
+ "zsh",
1820
+ ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
1821
+ { reject: false }
1822
+ );
1823
+ }
1824
+ }
1820
1825
  }
1821
-
1822
- // src/threads/create.ts
1823
- import { existsSync as existsSync6 } from "fs";
1824
-
1825
- // src/detect/detect.ts
1826
- async function detectAgents() {
1827
- ensureAgentPath();
1828
- enrichPathWithNpmGlobalBin();
1829
- return Promise.all(allAdapters().map((a) => a.detect()));
1830
- }
1831
- var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
1832
- async function requireAgent(agent, opts) {
1833
- ensureAgentPath();
1834
- enrichPathWithNpmGlobalBin();
1835
- const status = await Promise.race([
1836
- getAdapter(agent).detect(),
1837
- new Promise((_, reject) => {
1838
- setTimeout(() => {
1839
- reject(
1840
- new Error(
1841
- `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
1842
- )
1843
- );
1844
- }, REQUIRE_AGENT_TIMEOUT_MS);
1845
- })
1846
- ]);
1847
- if (!status.installed || !status.authenticated) {
1848
- throw new Error(status.reason ?? `${agent} is not available`);
1849
- }
1850
- if (opts?.requireLinear && !status.linearMcp) {
1851
- throw new Error(
1852
- `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
1826
+ async function spawnWorkspaceScript(command, opts) {
1827
+ const loginEnv = await captureLoginEnv();
1828
+ const env = buildWorkspaceScriptEnv(
1829
+ {
1830
+ worktreePath: opts.worktreePath,
1831
+ repoPath: opts.repoPath,
1832
+ workspaceName: opts.workspaceName,
1833
+ defaultBranch: opts.defaultBranch,
1834
+ ports: opts.ports
1835
+ },
1836
+ loginEnv
1837
+ );
1838
+ try {
1839
+ mergeAgentGitAuthEnv(
1840
+ env,
1841
+ await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
1853
1842
  );
1843
+ } catch {
1854
1844
  }
1855
- return status;
1856
- }
1857
-
1858
- // src/threads/create.ts
1859
- async function createThread(input, _onSetupLine) {
1860
- const resolved = resolveNewThreadOptions({
1861
- agent: input.agent,
1862
- model: input.model,
1863
- effort: input.effort,
1864
- fast: input.fast
1845
+ const shell = process.platform === "darwin" ? "zsh" : "bash";
1846
+ const child = execa2(shell, ["-lc", command], {
1847
+ cwd: opts.worktreePath,
1848
+ reject: false,
1849
+ env,
1850
+ // Own process group so Stop can tear down the whole tree (not just the shell).
1851
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
1852
+ ...process.platform === "win32" ? {} : { detached: true }
1865
1853
  });
1866
- await requireAgent(resolved.agent);
1867
- const repoPath = await resolveRepoRoot(input.repoPath);
1868
- if (!existsSync6(repoPath)) {
1869
- throw new Error(`Repo not found: ${repoPath}`);
1870
- }
1871
- let sourceRef = input.sourceRef;
1872
- let sourceIsFork = false;
1873
- let prUrl = null;
1874
- let prTitle = null;
1875
- if (input.sourceType === "pr") {
1876
- const num = Number(input.sourceRef.replace(/^#/, ""));
1877
- if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
1878
- const pr = await getPr(repoPath, num);
1879
- if (!pr) throw new Error(`PR #${num} not found`);
1880
- sourceIsFork = pr.isCrossRepository;
1881
- prUrl = pr.url;
1882
- const localFetchBranch = `sideboard-pr-${pr.number}`;
1883
- await fetchPrHead(repoPath, pr.number, localFetchBranch);
1884
- sourceRef = localFetchBranch;
1885
- } else if (input.sourceType === "ticket") {
1886
- sourceRef = await resolveDefaultBranch(repoPath);
1887
- } else if (input.sourceType === "branch") {
1888
- if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
1889
- sourceRef = await resolveDefaultBranch(repoPath);
1890
- } else {
1891
- const existing = await getPrForHeadBranch(repoPath, sourceRef);
1892
- if (existing?.url) {
1893
- prUrl = existing.url;
1894
- prTitle = existing.title;
1895
- }
1896
- }
1897
- } else if (input.sourceType === "adopt") {
1898
- throw new Error("Use adoptThread() for adopt sources");
1854
+ pipeLines(child.stdout, opts.onLine);
1855
+ pipeLines(child.stderr, opts.onLine);
1856
+ const ports = opts.ports ?? [];
1857
+ return {
1858
+ pid: child.pid,
1859
+ kill: () => killScriptTree(child, ports),
1860
+ done: child.then((r) => r.exitCode ?? null),
1861
+ child
1862
+ };
1863
+ }
1864
+ async function attachAbort(handle, signal) {
1865
+ if (signal) {
1866
+ const onAbort = () => handle.kill();
1867
+ if (signal.aborted) onAbort();
1868
+ else signal.addEventListener("abort", onAbort, { once: true });
1899
1869
  }
1900
- const team = allocateTeamSlug(repoPath);
1901
- const { branchName, worktreePath } = await createThreadWorktree({
1870
+ return handle.done;
1871
+ }
1872
+ async function runSetupScript(repoPath, worktreePath, onLine, opts) {
1873
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
1874
+ if (!settings?.setup) return { ran: false, exitCode: null, source: null };
1875
+ const handle = await spawnWorkspaceScript(settings.setup, {
1876
+ worktreePath,
1902
1877
  repoPath,
1903
- sourceRef,
1904
- slug: team.slug
1878
+ defaultBranch: opts?.defaultBranch,
1879
+ onLine
1905
1880
  });
1906
- copyConfiguredFiles(repoPath, worktreePath);
1907
- const explicitTitle = input.title?.trim();
1908
- const thread = createEmptyThread({
1909
- title: explicitTitle || team.name,
1910
- userSetTitle: Boolean(explicitTitle),
1911
- sourceType: input.sourceType,
1912
- sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
1913
- branchName,
1881
+ const exitCode = await attachAbort(handle, opts?.signal);
1882
+ return {
1883
+ ran: true,
1884
+ exitCode,
1885
+ source: workspaceSettingsSourceLabel(worktreePath, repoPath),
1886
+ kill: handle.kill
1887
+ };
1888
+ }
1889
+ async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
1890
+ const found = findConventionSetup(worktreePath, repoPath);
1891
+ if (!found) return { ran: false, exitCode: null, source: null };
1892
+ onLine?.(`[setup] ${found.source}`);
1893
+ const handle = await spawnWorkspaceScript(found.command, {
1914
1894
  worktreePath,
1915
1895
  repoPath,
1916
- agent: resolved.agent,
1917
- autonomy: input.autonomy ?? "default",
1918
- model: resolved.model,
1919
- effort: resolved.effort,
1920
- fast: resolved.fast,
1921
- planMode: Boolean(input.planMode),
1922
- attachments: input.attachments ?? [],
1923
- sourceIsFork,
1924
- parentThreadId: input.parentThreadId ?? null,
1925
- status: "idle",
1926
- prUrl,
1927
- prTitle
1896
+ defaultBranch: opts?.defaultBranch,
1897
+ onLine
1928
1898
  });
1929
- writeThread(thread);
1930
- await ensureWorkspace(repoPath);
1931
- return readThread(thread.id) ?? thread;
1899
+ const exitCode = await attachAbort(handle, opts?.signal);
1900
+ return {
1901
+ ran: true,
1902
+ exitCode,
1903
+ source: found.source,
1904
+ kill: handle.kill
1905
+ };
1932
1906
  }
1933
- async function listLinearIssues(agent, repoPath) {
1934
- const { getAdapter: getAdapter2 } = await import("./agents-4XQANGFO.js");
1935
- await requireAgent(agent, { requireLinear: true });
1936
- const adapter = getAdapter2(agent);
1937
- if (!adapter.listLinearIssues) {
1938
- throw new Error(`${agent} does not support Linear issue listing`);
1907
+ async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
1908
+ const reviewSkill = ensureReviewSkillFile(worktreePath);
1909
+ if (reviewSkill.wrote) {
1910
+ onLine?.(
1911
+ `[setup] wrote ${REVIEW_SKILL_PATH} \u2014 commit it so later worktrees inherit Review guidelines`
1912
+ );
1939
1913
  }
1940
- return adapter.listLinearIssues(repoPath);
1941
- }
1942
-
1943
- // src/threads/chat-tabs.ts
1944
- import { randomUUID } from "crypto";
1945
-
1946
- // src/composer/summarize.ts
1947
- var MAX_TRANSCRIPT_CHARS = 12e4;
1948
- var MAX_SUMMARY_CHARS = 6e3;
1949
- async function summarizeConversation(transcript, opts) {
1950
- const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
1951
-
1952
- [\u2026truncated\u2026]` : transcript;
1953
- const claudeSummary = await tryClaudeSummary(clipped, opts);
1954
- if (claudeSummary) {
1955
- return { summary: clipSummary(claudeSummary), method: "claude" };
1914
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
1915
+ if (!setup.ran) {
1916
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
1956
1917
  }
1957
- return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
1958
- }
1959
- async function tryClaudeSummary(transcript, opts) {
1960
- ensureAgentPath();
1961
- const prompt = [
1962
- "Summarize this coding-agent conversation for continuity in a fresh session.",
1963
- "Use concise markdown bullets covering:",
1964
- "- Goal / request",
1965
- "- Key decisions and constraints",
1966
- "- Files / areas touched",
1967
- "- Current state and open tasks",
1968
- "- Anything the next turn must remember",
1969
- "Do not invent details. No preamble.",
1970
- "",
1971
- "Transcript:",
1972
- transcript
1973
- ].join("\n");
1974
- try {
1975
- const { stdout, exitCode } = await run(
1976
- "claude",
1977
- [
1978
- "-p",
1979
- prompt,
1980
- "--output-format",
1981
- "text",
1982
- "--permission-mode",
1983
- "bypassPermissions",
1984
- "--model",
1985
- "haiku"
1986
- ],
1987
- { cwd: opts?.cwd, reject: false }
1988
- );
1989
- if (exitCode !== 0) return null;
1990
- const text = stdout.trim();
1991
- if (text.length < 40) return null;
1992
- return text;
1993
- } catch {
1994
- return null;
1918
+ if (!setup.ran) {
1919
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
1995
1920
  }
1921
+ return setup;
1996
1922
  }
1997
- function extractiveSummary(transcript) {
1998
- const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
1999
- const users = [];
2000
- const tools = [];
2001
- const agents = [];
2002
- let mode = "none";
2003
- for (const line of lines) {
2004
- if (line.startsWith("### User")) {
2005
- mode = "user";
2006
- continue;
2007
- }
2008
- if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
2009
- mode = "agent";
2010
- continue;
2011
- }
2012
- if (line.startsWith("- ") && line.includes(":")) {
2013
- tools.push(line.replace(/^- /, ""));
2014
- continue;
2015
- }
2016
- if (mode === "user" && users.length < 8) {
2017
- users.push(line.slice(0, 240));
2018
- } else if (mode === "agent" && agents.length < 6) {
2019
- agents.push(line.slice(0, 240));
2020
- }
2021
- }
2022
- const uniqueTools = [...new Set(tools)].slice(0, 20);
2023
- const parts = [
2024
- "## Compacted context (extractive)",
2025
- "",
2026
- "### User goals",
2027
- ...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
2028
- "",
2029
- "### Recent agent notes",
2030
- ...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
2031
- ];
2032
- if (uniqueTools.length) {
2033
- parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
2034
- }
2035
- parts.push(
2036
- "",
2037
- "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
2038
- );
2039
- return parts.join("\n");
1923
+ async function runArchiveScript(repoPath, worktreePath, onLine) {
1924
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
1925
+ if (!settings?.archive) return { ran: false, exitCode: null };
1926
+ const handle = await spawnWorkspaceScript(settings.archive, {
1927
+ worktreePath,
1928
+ repoPath,
1929
+ onLine
1930
+ });
1931
+ const exitCode = await handle.done;
1932
+ return { ran: true, exitCode };
2040
1933
  }
2041
- function clipSummary(text) {
2042
- if (text.length <= MAX_SUMMARY_CHARS) return text;
2043
- return `${text.slice(0, MAX_SUMMARY_CHARS)}
2044
-
2045
- [\u2026summary truncated\u2026]`;
1934
+ function listRunScripts(worktreePath, repoPath) {
1935
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
1936
+ if (!settings?.runScripts.length) return [];
1937
+ return settings.runScripts.filter((s) => {
1938
+ if (!s.availableIn?.length) return true;
1939
+ return s.availableIn.includes("local");
1940
+ });
2046
1941
  }
2047
-
2048
- // src/composer/context-compact.ts
2049
- var CONTEXT_COMPACT_CHARS = 1e5;
2050
- var CONTEXT_KEEP_RECENT_CHARS = 24e3;
2051
- var CONTEXT_KEEP_RECENT_MESSAGES = 12;
2052
- var CONTEXT_MIN_MESSAGES = 10;
2053
- function estimateMessageChars(message) {
2054
- let n = message.text.length + 16;
2055
- for (const part of message.parts ?? []) {
2056
- n += estimatePartChars(part);
2057
- }
2058
- return n;
1942
+ function getDefaultRunScript(worktreePath, repoPath) {
1943
+ const scripts = listRunScripts(worktreePath, repoPath);
1944
+ if (!scripts.length) return null;
1945
+ return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
2059
1946
  }
2060
- function estimatePartChars(part) {
2061
- switch (part.type) {
2062
- case "text":
2063
- case "thinking":
2064
- return part.text.length;
2065
- case "tool": {
2066
- const input = part.input ? JSON.stringify(part.input) : "";
2067
- return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
2068
- }
2069
- default:
2070
- return 0;
1947
+ function getRunScript(worktreePath, repoPath, name) {
1948
+ const scripts = listRunScripts(worktreePath, repoPath);
1949
+ if (!scripts.length) return null;
1950
+ if (name) {
1951
+ return scripts.find((s) => s.name === name) ?? null;
2071
1952
  }
1953
+ return getDefaultRunScript(worktreePath, repoPath);
2072
1954
  }
2073
- function estimateThreadChars(messages) {
2074
- return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
1955
+ function getRunMode(worktreePath, repoPath) {
1956
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
2075
1957
  }
2076
- function shouldCompactContext(messages, thresholds = {}) {
2077
- const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
2078
- const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
2079
- if (messages.length < minMessages) return false;
2080
- return estimateThreadChars(messages) >= maxChars;
1958
+ async function allocatePort() {
1959
+ return new Promise((resolve, reject) => {
1960
+ const server = createServer();
1961
+ server.listen(0, "127.0.0.1", () => {
1962
+ const addr = server.address();
1963
+ if (!addr || typeof addr === "string") {
1964
+ server.close();
1965
+ reject(new Error("Failed to allocate port"));
1966
+ return;
1967
+ }
1968
+ const port = addr.port;
1969
+ server.close((err) => err ? reject(err) : resolve(port));
1970
+ });
1971
+ server.on("error", reject);
1972
+ });
2081
1973
  }
2082
- function splitForCompaction(messages, thresholds = {}) {
2083
- const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
2084
- const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
2085
- if (messages.length === 0) return { older: [], recent: [] };
2086
- let recentChars = 0;
2087
- let cut = messages.length;
2088
- for (let i = messages.length - 1; i >= 0; i--) {
2089
- const next = recentChars + estimateMessageChars(messages[i]);
2090
- const kept = messages.length - i;
2091
- if (kept >= keepCount && next > keepChars) break;
2092
- recentChars = next;
2093
- cut = i;
1974
+ async function allocatePortRange(size = PORT_RANGE_SIZE) {
1975
+ const base = await allocatePort();
1976
+ const ports = [base];
1977
+ for (let i = 1; i < size; i++) {
1978
+ const candidate = base + i;
1979
+ const free = await new Promise((resolve) => {
1980
+ const server = createServer();
1981
+ server.once("error", () => resolve(false));
1982
+ server.listen(candidate, "127.0.0.1", () => {
1983
+ server.close(() => resolve(true));
1984
+ });
1985
+ });
1986
+ if (free) {
1987
+ ports.push(candidate);
1988
+ } else {
1989
+ ports.push(await allocatePort());
1990
+ }
2094
1991
  }
2095
- const minRecent = Math.min(keepCount, messages.length);
2096
- cut = Math.min(cut, messages.length - minRecent);
2097
- if (cut <= 0) return { older: [], recent: messages };
1992
+ return ports;
1993
+ }
1994
+ async function startDevServer(repoPath, worktreePath, onLine, opts) {
1995
+ const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
1996
+ if (!script) return null;
1997
+ const ports = await allocatePortRange(PORT_RANGE_SIZE);
1998
+ const handle = await spawnWorkspaceScript(script.command, {
1999
+ worktreePath,
2000
+ repoPath,
2001
+ ports,
2002
+ defaultBranch: opts?.defaultBranch,
2003
+ onLine
2004
+ });
2098
2005
  return {
2099
- older: messages.slice(0, cut),
2100
- recent: messages.slice(cut)
2006
+ pid: handle.pid,
2007
+ port: ports[0],
2008
+ ports,
2009
+ scriptName: script.name,
2010
+ kill: handle.kill,
2011
+ done: handle.done
2101
2012
  };
2102
2013
  }
2103
- function formatToolPart(t, detail) {
2104
- if (detail === "summary") {
2105
- const label = t.description || t.detail || t.name;
2106
- const path = t.filePath ? ` (${t.filePath})` : "";
2107
- return `- ${t.name}: ${label}${path}`;
2108
- }
2109
- const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
2110
- if (t.description) lines.push(`Description: ${t.description}`);
2111
- if (t.filePath) lines.push(`Path: ${t.filePath}`);
2112
- if (t.input && Object.keys(t.input).length > 0) {
2113
- lines.push("Input:");
2114
- lines.push("```json");
2115
- lines.push(JSON.stringify(t.input, null, 2));
2116
- lines.push("```");
2117
- } else if (t.detail) {
2118
- lines.push(`Detail: ${t.detail}`);
2119
- }
2120
- if (t.result != null && t.result !== "") {
2121
- lines.push("Result:");
2122
- lines.push("```");
2123
- lines.push(t.result);
2124
- lines.push("```");
2125
- }
2126
- return lines.join("\n");
2014
+
2015
+ // src/git/orphan-cleanup.ts
2016
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync } from "fs";
2017
+ import { join as join6 } from "path";
2018
+ function isSideboardWorktreePath(path) {
2019
+ return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
2127
2020
  }
2128
- function formatMessagesAsTranscript(messages, opts) {
2129
- const tools = opts?.tools ?? "full";
2130
- const blocks = [];
2131
- for (const m of messages) {
2132
- if (m.role === "summary") {
2133
- blocks.push(`## Prior summary
2134
- ${m.text}`);
2135
- continue;
2136
- }
2137
- if (m.role === "user") {
2138
- blocks.push(`### User
2139
- ${m.text}`);
2140
- continue;
2141
- }
2142
- const bits = [];
2143
- if (m.text?.trim()) {
2144
- bits.push(`### Agent
2145
- ${m.text}`);
2146
- } else {
2147
- bits.push("### Agent");
2148
- }
2149
- const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
2150
- for (const th of thinking) {
2151
- if (th.type !== "thinking" || !th.text.trim()) continue;
2152
- bits.push("Thinking:");
2153
- bits.push(th.text);
2154
- }
2155
- const toolParts = (m.parts ?? []).filter(
2156
- (p) => p.type === "tool"
2157
- );
2158
- if (tools !== "none" && toolParts.length > 0) {
2159
- if (tools === "summary") bits.push("Tools:");
2160
- for (const t of toolParts) {
2161
- bits.push(formatToolPart(t, tools));
2021
+ async function findOrphanWorktrees(repoPaths) {
2022
+ const threads = listThreads({ includeArchived: true });
2023
+ const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
2024
+ const repos = new Set(
2025
+ repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
2026
+ );
2027
+ const homeRoot = sideboardWorkspacesDir();
2028
+ if (existsSync5(homeRoot)) {
2029
+ try {
2030
+ for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
2031
+ if (!entry.isDirectory()) continue;
2032
+ void entry;
2162
2033
  }
2034
+ } catch {
2163
2035
  }
2164
- blocks.push(bits.join("\n"));
2165
2036
  }
2166
- return blocks.join("\n\n");
2167
- }
2168
- function buildSessionSeed(messages, opts) {
2169
- if (messages.length === 0) return null;
2170
- const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
2171
- if (!body.trim()) return null;
2172
- return [
2173
- "Sideboard conversation context (restored after compaction or a new session):",
2174
- "",
2175
- body,
2176
- "",
2177
- "Continue from this context. Do not repeat the summary unless asked."
2178
- ].join("\n");
2179
- }
2180
- function applyCompaction(messages, summaryText, thresholds = {}) {
2181
- const { older, recent } = splitForCompaction(messages, thresholds);
2182
- if (older.length === 0) return messages;
2183
- const summary = {
2184
- role: "summary",
2185
- text: summaryText.trim(),
2186
- ts: (/* @__PURE__ */ new Date()).toISOString()
2187
- };
2188
- return [summary, ...recent];
2189
- }
2190
- async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
2191
- if (!shouldCompactContext(thread.messages, thresholds)) {
2192
- return { didCompact: false, thread };
2037
+ const orphans = [];
2038
+ const seen = /* @__PURE__ */ new Set();
2039
+ for (const repoPath of repos) {
2040
+ if (!repoPath || !existsSync5(repoPath)) continue;
2041
+ try {
2042
+ const wts = await listWorktrees(repoPath);
2043
+ for (const wt of wts) {
2044
+ const path = wt.path.replace(/\/$/, "");
2045
+ if (!isSideboardWorktreePath(path)) continue;
2046
+ if (known.has(path)) continue;
2047
+ if (seen.has(path)) continue;
2048
+ seen.add(path);
2049
+ let mtimeMs = 0;
2050
+ try {
2051
+ mtimeMs = statSync(path).mtimeMs;
2052
+ } catch {
2053
+ mtimeMs = 0;
2054
+ }
2055
+ orphans.push({ path, repoPath, mtimeMs });
2056
+ }
2057
+ } catch {
2058
+ }
2059
+ try {
2060
+ const root = worktreesRoot(repoPath);
2061
+ if (existsSync5(root)) {
2062
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
2063
+ if (!entry.isDirectory()) continue;
2064
+ const path = join6(root, entry.name).replace(/\/$/, "");
2065
+ if (known.has(path) || seen.has(path)) continue;
2066
+ if (!existsSync5(join6(path, ".git"))) continue;
2067
+ seen.add(path);
2068
+ let mtimeMs = 0;
2069
+ try {
2070
+ mtimeMs = statSync(path).mtimeMs;
2071
+ } catch {
2072
+ mtimeMs = Date.now();
2073
+ }
2074
+ orphans.push({ path, repoPath, mtimeMs });
2075
+ }
2076
+ }
2077
+ } catch {
2078
+ }
2193
2079
  }
2194
- const { older } = splitForCompaction(thread.messages, thresholds);
2195
- if (older.length === 0) {
2196
- return { didCompact: false, thread };
2080
+ return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
2081
+ }
2082
+ async function cleanupOrphanWorktrees(opts) {
2083
+ const settings = loadAppSettings();
2084
+ const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
2085
+ const orphans = await findOrphanWorktrees(opts?.repoPaths);
2086
+ const activeCount = listThreads().filter((t) => t.status !== "archived").length;
2087
+ const budget = Math.max(0, maxCount - activeCount);
2088
+ const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
2089
+ const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
2090
+ const removed = [];
2091
+ const kept = [];
2092
+ for (const orphan of orphans) {
2093
+ if (keep.has(orphan.path)) {
2094
+ kept.push(orphan.path);
2095
+ continue;
2096
+ }
2097
+ if (opts?.dryRun) {
2098
+ removed.push(orphan.path);
2099
+ continue;
2100
+ }
2101
+ try {
2102
+ await removeWorktree(orphan.repoPath, orphan.path);
2103
+ removed.push(orphan.path);
2104
+ } catch {
2105
+ kept.push(orphan.path);
2106
+ }
2197
2107
  }
2198
- const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
2199
- const { summary, method } = await summarize(transcript, {
2200
- cwd: thread.worktreePath
2201
- });
2202
- const messages = applyCompaction(thread.messages, summary, thresholds);
2203
- const next = {
2204
- ...thread,
2205
- messages,
2206
- sessionId: null,
2207
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2208
- };
2108
+ if (!opts?.dryRun) {
2109
+ updateAdvancedSettings({
2110
+ worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
2111
+ });
2112
+ }
2113
+ return { removed, kept, orphans };
2114
+ }
2115
+ function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
2116
+ const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
2117
+ const last = settings.advanced.worktreeLastCleanupAt;
2118
+ if (!last) return true;
2119
+ const elapsed = Date.now() - Date.parse(last);
2120
+ return elapsed >= intervalHours * 36e5;
2121
+ }
2122
+ function worktreeCleanupSettings() {
2123
+ const a = loadAppSettings().advanced;
2209
2124
  return {
2210
- didCompact: true,
2211
- thread: next,
2212
- summary,
2213
- method,
2214
- olderCount: older.length
2125
+ worktreeMaxCount: a.worktreeMaxCount,
2126
+ worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
2127
+ worktreeLastCleanupAt: a.worktreeLastCleanupAt,
2128
+ autoCleanupOrphans: a.autoCleanupOrphans
2215
2129
  };
2216
2130
  }
2217
2131
 
2218
- // src/threads/chat-tabs.ts
2219
- function sameWorktreePath(a, b) {
2220
- return normalizeWorktreePath(a) === normalizeWorktreePath(b);
2221
- }
2222
- function takenTeamSlugsForChatTab(worktreePath) {
2223
- const key = normalizeWorktreePath(worktreePath);
2224
- const taken = /* @__PURE__ */ new Set();
2225
- const dir = worktreeNameFromPath(key);
2226
- if (dir) taken.add(dir.toLowerCase());
2227
- for (const sibling of threadsSharingWorktree(key)) {
2228
- for (const slug of takenSlugsFromThread(sibling)) {
2229
- taken.add(slug);
2132
+ // src/git/apply-into-main.ts
2133
+ import { execa as execa3 } from "execa";
2134
+ async function applyThreadIntoMain(thread, opts) {
2135
+ const method = opts?.method ?? "merge";
2136
+ const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
2137
+ const status = await execa3("git", ["status", "--porcelain"], {
2138
+ cwd: thread.repoPath,
2139
+ reject: false
2140
+ });
2141
+ if (status.stdout.trim()) {
2142
+ throw new Error(
2143
+ `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
2144
+ );
2145
+ }
2146
+ const checkout = await execa3("git", ["checkout", target], {
2147
+ cwd: thread.repoPath,
2148
+ reject: false
2149
+ });
2150
+ if (checkout.exitCode !== 0) {
2151
+ throw new Error(
2152
+ `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
2153
+ );
2154
+ }
2155
+ if (method === "cherry-pick") {
2156
+ const log = await execa3(
2157
+ "git",
2158
+ ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
2159
+ { cwd: thread.repoPath, reject: false }
2160
+ );
2161
+ const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
2162
+ if (!shas.length) {
2163
+ return {
2164
+ applied: false,
2165
+ method,
2166
+ targetBranch: target,
2167
+ message: `No commits on ${thread.branchName} not already in ${target}`
2168
+ };
2169
+ }
2170
+ for (const sha of shas) {
2171
+ const cp = await execa3("git", ["cherry-pick", sha], {
2172
+ cwd: thread.repoPath,
2173
+ reject: false
2174
+ });
2175
+ if (cp.exitCode !== 0) {
2176
+ await execa3("git", ["cherry-pick", "--abort"], {
2177
+ cwd: thread.repoPath,
2178
+ reject: false
2179
+ });
2180
+ throw new Error(
2181
+ `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
2182
+ );
2183
+ }
2230
2184
  }
2185
+ return {
2186
+ applied: true,
2187
+ method,
2188
+ targetBranch: target,
2189
+ message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
2190
+ };
2191
+ }
2192
+ const merge = await execa3(
2193
+ "git",
2194
+ ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
2195
+ { cwd: thread.repoPath, reject: false }
2196
+ );
2197
+ if (merge.exitCode !== 0) {
2198
+ await execa3("git", ["merge", "--abort"], {
2199
+ cwd: thread.repoPath,
2200
+ reject: false
2201
+ });
2202
+ throw new Error(
2203
+ `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
2204
+ );
2231
2205
  }
2232
- return [...taken];
2233
- }
2234
- function requireThread(idOrRef) {
2235
- const thread = findThreadByRef(idOrRef) ?? null;
2236
- if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
2237
- return thread;
2238
- }
2239
- function worktreeBindingFrom(from) {
2240
2206
  return {
2241
- worktreePath: normalizeWorktreePath(from.worktreePath),
2242
- repoPath: from.repoPath,
2243
- branchName: from.branchName,
2244
- sourceRef: from.sourceRef,
2245
- // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
2246
- // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
2247
- sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
2248
- sourceIsFork: from.sourceIsFork,
2249
- parentThreadId: from.parentThreadId,
2250
- prUrl: from.prUrl,
2251
- prTitle: from.prTitle,
2252
- prState: from.prState,
2253
- skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
2254
- stackId: from.stackId,
2255
- stackLayer: from.stackLayer
2207
+ applied: true,
2208
+ method: "merge",
2209
+ targetBranch: target,
2210
+ message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
2256
2211
  };
2257
2212
  }
2258
- function threadsSharingWorktree(worktreePath) {
2259
- if (!worktreePath) return [];
2260
- const key = normalizeWorktreePath(worktreePath);
2261
- return listThreads({ includeArchived: true }).filter(
2262
- (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
2263
- ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2213
+
2214
+ // src/git/clone-repo.ts
2215
+ import { existsSync as existsSync6 } from "fs";
2216
+ import { basename as basename2, join as join7 } from "path";
2217
+ import { execa as execa4 } from "execa";
2218
+ async function cloneRepoIntoSideboard(opts) {
2219
+ const url = opts.url.trim();
2220
+ if (!url) throw new Error("Clone URL is required");
2221
+ let name = opts.name?.trim();
2222
+ if (!name) {
2223
+ const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
2224
+ name = leaf || "repo";
2225
+ }
2226
+ name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
2227
+ const dest = join7(sideboardReposDir(), name);
2228
+ if (existsSync6(dest)) {
2229
+ const repoPath2 = await resolveRepoRoot(dest);
2230
+ const workspace2 = await ensureWorkspace(repoPath2);
2231
+ return { repoPath: repoPath2, workspace: workspace2 };
2232
+ }
2233
+ const result = await execa4("git", ["clone", url, dest], { reject: false });
2234
+ if (result.exitCode !== 0) {
2235
+ throw new Error(
2236
+ `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
2237
+ );
2238
+ }
2239
+ const repoPath = await resolveRepoRoot(dest);
2240
+ const workspace = await ensureWorkspace(repoPath);
2241
+ return { repoPath, workspace };
2264
2242
  }
2265
- function formatTranscriptMarkdown(title, messages) {
2266
- const body = formatMessagesAsTranscript(messages, { tools: "full" });
2267
- return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
2243
+
2244
+ // src/store/desktop-host.ts
2245
+ import { readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2246
+ import { join as join8 } from "path";
2247
+ function desktopHostPidPath() {
2248
+ return join8(appDataDir(), "desktop-host.pid");
2268
2249
  }
2269
- function forkMessageSlice(from, throughIndex) {
2270
- const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
2271
- return through >= 0 ? from.messages.slice(0, through + 1) : [];
2272
- }
2273
- function buildForkTranscriptAttachment(baseTitle, messages) {
2274
- const title = baseTitle || "Chat";
2275
- return {
2276
- id: randomUUID(),
2277
- name: `Transcript of ${title}.md`,
2278
- kind: "transcript",
2279
- content: formatTranscriptMarkdown(title, messages)
2280
- };
2281
- }
2282
- function createChatTab(input) {
2283
- const from = requireThread(input.fromThreadId);
2284
- const binding = worktreeBindingFrom(from);
2285
- const explicitTitle = input.title?.trim();
2286
- const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
2287
- const nextAgent = input.agent ?? from.agent;
2288
- if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
2289
- assertOrchestratorCapableAgent(nextAgent);
2290
- }
2291
- const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
2292
- const thread = createEmptyThread({
2293
- title,
2294
- // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
2295
- // syncThreadBranchFromGit would otherwise rewrite every sibling to the
2296
- // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
2297
- userSetTitle: true,
2298
- ...binding,
2299
- // Slack / Brightsy cloud identity stays on the original chat. A + tab
2300
- // that copies `slack:team:user` would steal inbound DMs after you close
2301
- // the connected chat.
2302
- sourceRef: singletonInbox ? title : binding.sourceRef,
2303
- agent: nextAgent,
2304
- model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
2305
- effort: input.effort !== void 0 ? input.effort : from.effort,
2306
- fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
2307
- planMode: from.planMode,
2308
- autonomy: input.autonomy ?? from.autonomy,
2309
- attachments: input.attachments ?? [],
2310
- status: "idle"
2311
- });
2312
- writeThread(thread);
2313
- return thread;
2314
- }
2315
- function forkChatTab(input) {
2316
- const from = requireThread(input.threadId);
2317
- const slice = forkMessageSlice(from, input.throughIndex);
2318
- const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
2319
- const tab = createChatTab({
2320
- fromThreadId: input.threadId,
2321
- agent: input.agent ?? from.agent,
2322
- model: input.model,
2323
- title: input.title?.trim() || void 0,
2324
- attachments: [attachment]
2325
- });
2326
- if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
2327
- const next = { ...tab, parentThreadId: from.id };
2328
- writeThread(next);
2329
- return next;
2330
- }
2331
- return tab;
2332
- }
2333
-
2334
- // src/store/turn-live.ts
2335
- import {
2336
- existsSync as existsSync7,
2337
- renameSync,
2338
- unlinkSync as unlinkSync2,
2339
- writeFileSync as writeFileSync2,
2340
- readFileSync as readFileSync5
2341
- } from "fs";
2342
- var buffers = /* @__PURE__ */ new Map();
2343
- var FLUSH_MS = 800;
2344
- var MAX_PARTS = 80;
2345
- function trimParts(parts) {
2346
- if (parts.length <= MAX_PARTS) return parts;
2347
- return parts.slice(-Math.floor(MAX_PARTS / 2));
2348
- }
2349
- function lastText(parts, type) {
2350
- for (let i = parts.length - 1; i >= 0; i--) {
2351
- const p = parts[i];
2352
- if (p.type === type && p.text.trim()) return p.text.trim();
2353
- }
2354
- return "";
2355
- }
2356
- function summarizeTurnLive(parts) {
2357
- const tools = parts.filter(
2358
- (p) => p.type === "tool"
2359
- );
2360
- const lastTool = tools[tools.length - 1];
2361
- const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
2362
- const running = lastTool?.status === "running";
2363
- const thinking = lastText(parts, "thinking");
2364
- const text = lastText(parts, "text");
2365
- const excerptRaw = thinking || text;
2366
- const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
2367
- let summary = "Working\u2026";
2368
- if (lastToolLabel) {
2369
- const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
2370
- summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
2371
- } else if (thinking) {
2372
- summary = "Thinking\u2026";
2373
- } else if (text) {
2374
- summary = "Writing reply\u2026";
2375
- }
2376
- return {
2377
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2378
- summary,
2379
- lastTool: lastToolLabel,
2380
- toolCount: tools.length,
2381
- excerpt
2382
- };
2383
- }
2384
- function flush(threadId) {
2385
- const buf = buffers.get(threadId);
2386
- if (!buf) return;
2387
- if (buf.timer) {
2388
- clearTimeout(buf.timer);
2389
- buf.timer = null;
2390
- }
2391
- writeTurnLive(threadId, summarizeTurnLive(buf.parts));
2392
- }
2393
- function scheduleFlush(threadId) {
2394
- const buf = buffers.get(threadId);
2395
- if (!buf) return;
2396
- if (buf.timer) return;
2397
- buf.timer = setTimeout(() => {
2398
- const live = buffers.get(threadId);
2399
- if (live) live.timer = null;
2400
- flush(threadId);
2401
- }, FLUSH_MS);
2402
- }
2403
- function noteTurnLiveEvent(threadId, event) {
2404
- if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
2405
- return;
2406
- }
2407
- const prev = buffers.get(threadId) ?? { parts: [], timer: null };
2408
- prev.parts = trimParts(applyAgentEvent(prev.parts, event));
2409
- buffers.set(threadId, prev);
2410
- if (event.type === "exit") {
2411
- flush(threadId);
2412
- const cur = buffers.get(threadId);
2413
- if (cur?.timer) clearTimeout(cur.timer);
2414
- buffers.delete(threadId);
2415
- return;
2416
- }
2417
- const immediate = event.type === "tool_use" || event.type === "tool_result";
2418
- if (immediate) flush(threadId);
2419
- else scheduleFlush(threadId);
2250
+ function claimDesktopHost(pid = process.pid) {
2251
+ writeFileSync2(desktopHostPidPath(), `${pid}
2252
+ `, "utf8");
2420
2253
  }
2421
- function writeTurnLive(threadId, progress) {
2422
- const path = threadLivePath(threadId);
2423
- const tmp = `${path}.${process.pid}.tmp`;
2254
+ function releaseDesktopHost(pid = process.pid) {
2255
+ if (readDesktopHostPid() !== pid) return;
2424
2256
  try {
2425
- writeFileSync2(tmp, JSON.stringify(progress), "utf8");
2426
- renameSync(tmp, path);
2257
+ unlinkSync(desktopHostPidPath());
2427
2258
  } catch {
2428
- try {
2429
- unlinkSync2(tmp);
2430
- } catch {
2431
- }
2432
2259
  }
2433
2260
  }
2434
- function readTurnLive(threadId) {
2435
- const path = threadLivePath(threadId);
2436
- if (!existsSync7(path)) return null;
2261
+ function readDesktopHostPid() {
2437
2262
  try {
2438
- const raw = JSON.parse(readFileSync5(path, "utf8"));
2439
- if (!raw || typeof raw.summary !== "string") return null;
2440
- return raw;
2263
+ const pid = Number.parseInt(readFileSync5(desktopHostPidPath(), "utf8").trim(), 10);
2264
+ if (!Number.isFinite(pid) || pid <= 0) return null;
2265
+ return pid;
2441
2266
  } catch {
2442
2267
  return null;
2443
2268
  }
2444
2269
  }
2445
- function clearTurnLive(threadId) {
2446
- const buf = buffers.get(threadId);
2447
- if (buf?.timer) clearTimeout(buf.timer);
2448
- buffers.delete(threadId);
2449
- const path = threadLivePath(threadId);
2450
- if (!existsSync7(path)) return;
2270
+ function pidAlive(pid) {
2451
2271
  try {
2452
- unlinkSync2(path);
2272
+ process.kill(pid, 0);
2273
+ return true;
2453
2274
  } catch {
2275
+ return false;
2454
2276
  }
2455
2277
  }
2278
+ function isDesktopHostAlive() {
2279
+ const pid = readDesktopHostPid();
2280
+ return pid != null && pidAlive(pid);
2281
+ }
2282
+ function isThisProcessDesktopHost() {
2283
+ return readDesktopHostPid() === process.pid && pidAlive(process.pid);
2284
+ }
2285
+ function thisProcessShouldDrainAgentQueues() {
2286
+ if (isThisProcessDesktopHost()) return true;
2287
+ return !isDesktopHostAlive();
2288
+ }
2456
2289
 
2457
- // src/review/request-review.ts
2458
- import { randomUUID as randomUUID2 } from "crypto";
2459
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2460
- import { dirname as dirname2, join as join8 } from "path";
2461
-
2462
- // src/review/review-request-template.ts
2463
- var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
2464
-
2465
- 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.
2466
-
2467
- ## Required outcome
2468
-
2469
- Start your reply with a **Recommendation** section using exactly one of:
2470
-
2471
- - **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
2472
- - **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
2473
- - **Request changes** \u2014 not ready; blocking issues must be fixed first.
2474
- - **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
2475
-
2476
- 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.
2477
-
2478
- People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
2479
-
2480
- ## Findings
2481
-
2482
- Below are guidelines for determining whether an issue is worth flagging to the original author.
2483
-
2484
- These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
2485
-
2486
- Flag something as a bug / blocking finding only when:
2487
-
2488
- 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
2489
- 2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
2490
- 3. Fixing it does not demand rigor absent from the rest of the codebase.
2491
- 4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
2492
- 5. The author would likely fix it if made aware.
2493
- 6. It does not rely on unstated assumptions about the codebase or author intent.
2494
- 7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
2495
- 8. It is clearly not just an intentional change by the author.
2496
-
2497
- When flagging an issue, include a short accompanying comment:
2498
-
2499
- 1. Clear about why it is a problem.
2500
- 2. Severity must match reality \u2014 do not inflate.
2501
- 3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
2502
- 4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
2503
- 5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
2504
- 6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
2505
- 7. Skimmable on first read.
2506
- 8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
2507
-
2508
- HOW MANY FINDINGS TO RETURN:
2509
-
2510
- 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.
2511
-
2512
- GUIDELINES:
2513
-
2514
- - Ignore trivial style unless it obscures meaning or violates documented standards.
2515
- - One comment per distinct issue (or a short multi-line range if needed).
2516
- - Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
2517
- - In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
2518
- - Do NOT introduce or remove outer indentation levels unless that is the actual fix.
2519
- - Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
2520
-
2521
- 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).
2522
-
2523
- ## Getting the diff
2524
-
2525
- 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.
2526
-
2527
- 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.
2528
-
2529
- ## Fallback: if you don't have access to the Sideboard diff tool
2530
-
2531
- If you don't have access to \`get_diff\`, use the following git commands to get the diff:
2532
-
2533
- \`\`\`bash
2534
- # Get the merge base between this branch and the target
2535
- MERGE_BASE=$(git merge-base origin/main HEAD)
2536
-
2537
- # Get the committed diff against the merge base
2538
- git diff $MERGE_BASE HEAD
2539
-
2540
- # Get any uncommitted changes (staged and unstaged)
2541
- git diff HEAD
2542
- \`\`\`
2543
-
2544
- 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.
2545
-
2546
- No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
2547
-
2548
- ## Output format
2549
-
2550
- **1. Recommendation first** (required), then **2. Findings** (may be empty).
2551
-
2552
- Only report ONE finding per unique issue.
2553
-
2554
- <example>
2555
- ## Recommendation
2556
-
2557
- **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.
2558
-
2559
- ## Findings
2560
-
2561
- ### **#1 Empty input causes crash** (blocking)
2562
-
2563
- If the input field is empty when the page loads, the app will crash.
2564
-
2565
- File: src/client/frontends/desktop/ui/Input.tsx
2566
-
2567
- ### **#2 Dead code** (nit)
2568
-
2569
- The getUserData function is now unused. It should be deleted.
2570
-
2571
- File: src/client/frontends/desktop/core/UserData.ts
2572
- </example>
2573
-
2574
- <example>
2575
- ## Recommendation
2576
-
2577
- **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
2578
- </example>
2579
-
2580
- ## Growing the rules
2290
+ // src/threads/create.ts
2291
+ import { existsSync as existsSync7 } from "fs";
2581
2292
 
2582
- 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\`.
2583
- `;
2293
+ // src/detect/detect.ts
2294
+ async function detectAgents() {
2295
+ ensureAgentPath();
2296
+ enrichPathWithNpmGlobalBin();
2297
+ return Promise.all(allAdapters().map((a) => a.detect()));
2298
+ }
2299
+ var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
2300
+ async function requireAgent(agent, opts) {
2301
+ ensureAgentPath();
2302
+ enrichPathWithNpmGlobalBin();
2303
+ const status = await Promise.race([
2304
+ getAdapter(agent).detect(),
2305
+ new Promise((_, reject) => {
2306
+ setTimeout(() => {
2307
+ reject(
2308
+ new Error(
2309
+ `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
2310
+ )
2311
+ );
2312
+ }, REQUIRE_AGENT_TIMEOUT_MS);
2313
+ })
2314
+ ]);
2315
+ if (!status.installed || !status.authenticated) {
2316
+ throw new Error(status.reason ?? `${agent} is not available`);
2317
+ }
2318
+ if (opts?.requireLinear && !status.linearMcp) {
2319
+ throw new Error(
2320
+ `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
2321
+ );
2322
+ }
2323
+ return status;
2324
+ }
2584
2325
 
2585
- // src/review/request-review.ts
2586
- var REPO_REVIEW_PATH = ".sideboard/review.md";
2587
- var REPO_REVIEW_NAME = "review.md";
2588
- var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
2589
- var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
2590
- var REVIEW_REQUEST_NAME = "Review request.md";
2591
- var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
2592
- var LEGACY_REVIEW_TEMPLATE_MARKERS = [
2593
- "You are acting as a reviewer for a proposed code change made by another engineer.",
2594
- "HOW MANY FINDINGS TO RETURN:"
2595
- ];
2596
- function shouldRefreshReviewRequestTemplate(content) {
2597
- const trimmed = content.trim();
2598
- if (!trimmed) return true;
2599
- if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
2600
- return false;
2326
+ // src/threads/create.ts
2327
+ async function createThread(input, _onSetupLine) {
2328
+ const resolved = resolveNewThreadOptions({
2329
+ agent: input.agent,
2330
+ model: input.model,
2331
+ effort: input.effort,
2332
+ fast: input.fast
2333
+ });
2334
+ await requireAgent(resolved.agent);
2335
+ const repoPath = await resolveRepoRoot(input.repoPath);
2336
+ if (!existsSync7(repoPath)) {
2337
+ throw new Error(`Repo not found: ${repoPath}`);
2601
2338
  }
2602
- return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
2339
+ if (input.cowboy) {
2340
+ const { cowboyModeEnabled } = await import("./app-settings-J4LUWIRN.js");
2341
+ if (!cowboyModeEnabled()) {
2342
+ throw new Error(
2343
+ "Cowboy mode is off. Enable it in Settings \u2192 Advanced."
2344
+ );
2345
+ }
2346
+ if (input.sourceType === "pr" || input.sourceType === "ticket" || input.sourceType === "adopt") {
2347
+ throw new Error(
2348
+ "Cowboy mode works on the project folder (default branch), not a PR, ticket, or adopt."
2349
+ );
2350
+ }
2351
+ const defaultBranch = await resolveDefaultBranch(repoPath, { network: false });
2352
+ const head = await currentBranch(repoPath);
2353
+ if (head === "HEAD") {
2354
+ throw new Error(
2355
+ "Cowboy mode needs a named branch in the project folder (not a detached HEAD)."
2356
+ );
2357
+ }
2358
+ if (head !== defaultBranch) {
2359
+ throw new Error(
2360
+ `Cowboy mode uses ${defaultBranch} in the project folder. Switch that checkout to ${defaultBranch} first (currently ${head}).`
2361
+ );
2362
+ }
2363
+ const explicitTitle2 = input.title?.trim();
2364
+ const thread2 = createEmptyThread({
2365
+ title: explicitTitle2 || `Cowboy \xB7 ${head}`,
2366
+ userSetTitle: Boolean(explicitTitle2),
2367
+ sourceType: "branch",
2368
+ sourceRef: head,
2369
+ branchName: head,
2370
+ worktreePath: repoPath,
2371
+ repoPath,
2372
+ agent: resolved.agent,
2373
+ autonomy: input.autonomy ?? "default",
2374
+ model: resolved.model,
2375
+ effort: resolved.effort,
2376
+ fast: resolved.fast,
2377
+ planMode: Boolean(input.planMode),
2378
+ attachments: input.attachments ?? [],
2379
+ sourceIsFork: false,
2380
+ parentThreadId: input.parentThreadId ?? null,
2381
+ status: "idle",
2382
+ cowboy: true
2383
+ });
2384
+ writeThread(thread2);
2385
+ await ensureWorkspace(repoPath);
2386
+ return readThread(thread2.id) ?? thread2;
2387
+ }
2388
+ let sourceRef = input.sourceRef;
2389
+ let sourceIsFork = false;
2390
+ let prUrl = null;
2391
+ let prTitle = null;
2392
+ if (input.sourceType === "pr") {
2393
+ const num = Number(input.sourceRef.replace(/^#/, ""));
2394
+ if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
2395
+ const pr = await getPr(repoPath, num);
2396
+ if (!pr) throw new Error(`PR #${num} not found`);
2397
+ sourceIsFork = pr.isCrossRepository;
2398
+ prUrl = pr.url;
2399
+ const localFetchBranch = `sideboard-pr-${pr.number}`;
2400
+ await fetchPrHead(repoPath, pr.number, localFetchBranch);
2401
+ sourceRef = localFetchBranch;
2402
+ } else if (input.sourceType === "ticket") {
2403
+ sourceRef = await resolveDefaultBranch(repoPath);
2404
+ } else if (input.sourceType === "branch") {
2405
+ if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
2406
+ sourceRef = await resolveDefaultBranch(repoPath);
2407
+ } else {
2408
+ const existing = await getPrForHeadBranch(repoPath, sourceRef);
2409
+ if (existing?.url) {
2410
+ prUrl = existing.url;
2411
+ prTitle = existing.title;
2412
+ }
2413
+ }
2414
+ } else if (input.sourceType === "adopt") {
2415
+ throw new Error("Use adoptThread() for adopt sources");
2416
+ }
2417
+ const team = allocateTeamSlug(repoPath);
2418
+ const { branchName, worktreePath } = await createThreadWorktree({
2419
+ repoPath,
2420
+ sourceRef,
2421
+ slug: team.slug
2422
+ });
2423
+ copyConfiguredFiles(repoPath, worktreePath);
2424
+ const explicitTitle = input.title?.trim();
2425
+ const thread = createEmptyThread({
2426
+ title: explicitTitle || team.name,
2427
+ userSetTitle: Boolean(explicitTitle),
2428
+ sourceType: input.sourceType,
2429
+ sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
2430
+ branchName,
2431
+ worktreePath,
2432
+ repoPath,
2433
+ agent: resolved.agent,
2434
+ autonomy: input.autonomy ?? "default",
2435
+ model: resolved.model,
2436
+ effort: resolved.effort,
2437
+ fast: resolved.fast,
2438
+ planMode: Boolean(input.planMode),
2439
+ attachments: input.attachments ?? [],
2440
+ sourceIsFork,
2441
+ parentThreadId: input.parentThreadId ?? null,
2442
+ status: "idle",
2443
+ prUrl,
2444
+ prTitle
2445
+ });
2446
+ writeThread(thread);
2447
+ await ensureWorkspace(repoPath);
2448
+ return readThread(thread.id) ?? thread;
2603
2449
  }
2604
- function readTextIfPresent(abs) {
2605
- if (!existsSync8(abs)) return null;
2606
- try {
2607
- const content = readFileSync6(abs, "utf8");
2608
- return content.trim() ? content : null;
2609
- } catch {
2610
- return null;
2450
+ async function listLinearIssues(agent, repoPath) {
2451
+ const { getAdapter: getAdapter2 } = await import("./agents-WK54VP7J.js");
2452
+ await requireAgent(agent, { requireLinear: true });
2453
+ const adapter = getAdapter2(agent);
2454
+ if (!adapter.listLinearIssues) {
2455
+ throw new Error(`${agent} does not support Linear issue listing`);
2611
2456
  }
2457
+ return adapter.listLinearIssues(repoPath);
2612
2458
  }
2613
- function ensureAttachmentsGitignore(worktreePath) {
2614
- const gitignoreAbs = join8(worktreePath, ATTACHMENTS_DIR, ".gitignore");
2615
- if (existsSync8(gitignoreAbs)) return;
2616
- mkdirSync2(dirname2(gitignoreAbs), { recursive: true });
2617
- writeFileSync3(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
2459
+
2460
+ // src/threads/cowboy.ts
2461
+ function isCowboyThread(thread) {
2462
+ return Boolean(thread?.cowboy);
2618
2463
  }
2619
- function resolveReviewGuidelines(worktreePath) {
2620
- const repoAbs = join8(worktreePath, REPO_REVIEW_PATH);
2621
- const repoContent = readTextIfPresent(repoAbs);
2622
- if (repoContent) {
2623
- return {
2624
- path: REPO_REVIEW_PATH,
2625
- name: REPO_REVIEW_NAME,
2626
- content: repoContent,
2627
- source: "repo"
2628
- };
2629
- }
2630
- const localAbs = join8(worktreePath, REVIEW_REQUEST_PATH);
2631
- const localContent = readTextIfPresent(localAbs);
2632
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
2633
- return {
2634
- path: REVIEW_REQUEST_PATH,
2635
- name: REVIEW_REQUEST_NAME,
2636
- content: localContent,
2637
- source: "local"
2638
- };
2464
+ function isPrimaryCheckoutThread(thread) {
2465
+ if (!thread?.worktreePath || !thread.repoPath) return false;
2466
+ return normalizeWorktreePath(thread.worktreePath) === normalizeWorktreePath(thread.repoPath);
2467
+ }
2468
+ function shouldRemoveWorktreeOnTeardown(thread) {
2469
+ return !isCowboyThread(thread) && !isPrimaryCheckoutThread(thread);
2470
+ }
2471
+
2472
+ // src/store/turn-live.ts
2473
+ import {
2474
+ existsSync as existsSync8,
2475
+ renameSync,
2476
+ unlinkSync as unlinkSync2,
2477
+ writeFileSync as writeFileSync3,
2478
+ readFileSync as readFileSync6
2479
+ } from "fs";
2480
+ var buffers = /* @__PURE__ */ new Map();
2481
+ var FLUSH_MS = 800;
2482
+ var MAX_PARTS = 80;
2483
+ function trimParts(parts) {
2484
+ if (parts.length <= MAX_PARTS) return parts;
2485
+ return parts.slice(-Math.floor(MAX_PARTS / 2));
2486
+ }
2487
+ function lastText(parts, type) {
2488
+ for (let i = parts.length - 1; i >= 0; i--) {
2489
+ const p = parts[i];
2490
+ if (p.type === type && p.text.trim()) return p.text.trim();
2639
2491
  }
2640
- const legacyAbs = join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
2641
- const legacyContent = readTextIfPresent(legacyAbs);
2642
- if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
2643
- return {
2644
- path: LEGACY_REVIEW_REQUEST_PATH,
2645
- name: REVIEW_REQUEST_NAME,
2646
- content: legacyContent,
2647
- source: "local"
2648
- };
2492
+ return "";
2493
+ }
2494
+ function summarizeTurnLive(parts) {
2495
+ const tools = parts.filter(
2496
+ (p) => p.type === "tool"
2497
+ );
2498
+ const lastTool = tools[tools.length - 1];
2499
+ const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
2500
+ const running = lastTool?.status === "running";
2501
+ const thinking = lastText(parts, "thinking");
2502
+ const text = lastText(parts, "text");
2503
+ const excerptRaw = thinking || text;
2504
+ const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
2505
+ let summary = "Working\u2026";
2506
+ if (lastToolLabel) {
2507
+ const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
2508
+ summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
2509
+ } else if (thinking) {
2510
+ summary = "Thinking\u2026";
2511
+ } else if (text) {
2512
+ summary = "Writing reply\u2026";
2649
2513
  }
2650
- ensureAttachmentsGitignore(worktreePath);
2651
- mkdirSync2(dirname2(localAbs), { recursive: true });
2652
- writeFileSync3(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
2653
2514
  return {
2654
- path: REVIEW_REQUEST_PATH,
2655
- name: REVIEW_REQUEST_NAME,
2656
- content: REVIEW_REQUEST_TEMPLATE,
2657
- source: "stock"
2515
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2516
+ summary,
2517
+ lastTool: lastToolLabel,
2518
+ toolCount: tools.length,
2519
+ excerpt
2658
2520
  };
2659
2521
  }
2660
- function ensureReviewRequestFile(worktreePath) {
2661
- const repoAbs = join8(worktreePath, REPO_REVIEW_PATH);
2662
- const repoContent = readTextIfPresent(repoAbs);
2663
- if (repoContent) {
2664
- return {
2665
- path: REPO_REVIEW_PATH,
2666
- name: REPO_REVIEW_NAME,
2667
- content: repoContent,
2668
- source: "repo"
2669
- };
2670
- }
2671
- const localAbs = join8(worktreePath, REVIEW_REQUEST_PATH);
2672
- const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent(join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
2673
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
2674
- const path = existsSync8(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
2675
- return {
2676
- path,
2677
- name: REVIEW_REQUEST_NAME,
2678
- content: localContent,
2679
- source: "local"
2680
- };
2522
+ function flush(threadId) {
2523
+ const buf = buffers.get(threadId);
2524
+ if (!buf) return;
2525
+ if (buf.timer) {
2526
+ clearTimeout(buf.timer);
2527
+ buf.timer = null;
2681
2528
  }
2682
- mkdirSync2(dirname2(repoAbs), { recursive: true });
2683
- writeFileSync3(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
2684
- return {
2685
- path: REPO_REVIEW_PATH,
2686
- name: REPO_REVIEW_NAME,
2687
- content: REVIEW_REQUEST_TEMPLATE,
2688
- source: "repo"
2689
- };
2529
+ writeTurnLive(threadId, summarizeTurnLive(buf.parts));
2690
2530
  }
2691
- function buildReviewRequestAttachment(content, opts) {
2692
- const path = opts?.path ?? REVIEW_REQUEST_PATH;
2693
- const name = opts?.name ?? (path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
2694
- return {
2695
- id: randomUUID2(),
2696
- name,
2697
- kind: "file",
2698
- path,
2699
- content
2700
- };
2531
+ function scheduleFlush(threadId) {
2532
+ const buf = buffers.get(threadId);
2533
+ if (!buf) return;
2534
+ if (buf.timer) return;
2535
+ buf.timer = setTimeout(() => {
2536
+ const live = buffers.get(threadId);
2537
+ if (live) live.timer = null;
2538
+ flush(threadId);
2539
+ }, FLUSH_MS);
2701
2540
  }
2702
- function readExistingReviewRequestFile(worktreePath) {
2703
- return readTextIfPresent(join8(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent(join8(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent(join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
2541
+ function noteTurnLiveEvent(threadId, event) {
2542
+ if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
2543
+ return;
2544
+ }
2545
+ const prev = buffers.get(threadId) ?? { parts: [], timer: null };
2546
+ prev.parts = trimParts(applyAgentEvent(prev.parts, event));
2547
+ buffers.set(threadId, prev);
2548
+ if (event.type === "exit") {
2549
+ flush(threadId);
2550
+ const cur = buffers.get(threadId);
2551
+ if (cur?.timer) clearTimeout(cur.timer);
2552
+ buffers.delete(threadId);
2553
+ return;
2554
+ }
2555
+ const immediate = event.type === "tool_use" || event.type === "tool_result";
2556
+ if (immediate) flush(threadId);
2557
+ else scheduleFlush(threadId);
2704
2558
  }
2705
- async function requestReview(threadRef, send) {
2706
- const from = findThreadByRef(threadRef);
2707
- if (!from) throw new Error(`Thread not found: ${threadRef}`);
2708
- if (isOrchestratorThread(from)) {
2709
- throw new Error(
2710
- "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
2711
- );
2559
+ function writeTurnLive(threadId, progress) {
2560
+ const path = threadLivePath(threadId);
2561
+ const tmp = `${path}.${process.pid}.tmp`;
2562
+ try {
2563
+ writeFileSync3(tmp, JSON.stringify(progress), "utf8");
2564
+ renameSync(tmp, path);
2565
+ } catch {
2566
+ try {
2567
+ unlinkSync2(tmp);
2568
+ } catch {
2569
+ }
2712
2570
  }
2713
- if (from.status === "archived") {
2714
- throw new Error(`Thread is archived: ${from.id}`);
2571
+ }
2572
+ function readTurnLive(threadId) {
2573
+ const path = threadLivePath(threadId);
2574
+ if (!existsSync8(path)) return null;
2575
+ try {
2576
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
2577
+ if (!raw || typeof raw.summary !== "string") return null;
2578
+ return raw;
2579
+ } catch {
2580
+ return null;
2581
+ }
2582
+ }
2583
+ function clearTurnLive(threadId) {
2584
+ const buf = buffers.get(threadId);
2585
+ if (buf?.timer) clearTimeout(buf.timer);
2586
+ buffers.delete(threadId);
2587
+ const path = threadLivePath(threadId);
2588
+ if (!existsSync8(path)) return;
2589
+ try {
2590
+ unlinkSync2(path);
2591
+ } catch {
2715
2592
  }
2716
- const guidelines = resolveReviewGuidelines(from.worktreePath);
2717
- const tab = createChatTab({
2718
- fromThreadId: from.id,
2719
- title: "Review",
2720
- attachments: [
2721
- buildReviewRequestAttachment(guidelines.content, {
2722
- path: guidelines.path,
2723
- name: guidelines.name
2724
- })
2725
- ]
2726
- });
2727
- const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
2728
- return { tab: started, from };
2729
2593
  }
2730
2594
 
2731
2595
  // src/threads/fork-worktree.ts
@@ -2969,7 +2833,7 @@ async function adoptThread(input) {
2969
2833
  messages: input.messages ?? []
2970
2834
  });
2971
2835
  writeThread(thread);
2972
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-UPEVANU4.js");
2836
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-HV3J4TTW.js");
2973
2837
  await ensureWorkspace2(repoPath);
2974
2838
  return thread;
2975
2839
  }
@@ -4215,13 +4079,22 @@ async function previewLand(thread) {
4215
4079
  isFork: true
4216
4080
  };
4217
4081
  }
4218
- const target = await resolveDefaultBranch(thread.repoPath);
4219
- if (thread.branchName === target || thread.branchName === "main" || thread.branchName === "master") {
4082
+ if (isCowboyThread(thread)) {
4083
+ const current2 = await currentBranch(thread.worktreePath);
4084
+ const dirty = await isDirty(thread.worktreePath);
4085
+ const diff2 = await getDiff(thread.worktreePath, thread.repoPath, { base: current2 });
4086
+ return {
4087
+ branch: current2,
4088
+ target: current2,
4089
+ diffStat: diff2.stat,
4090
+ dirty,
4091
+ blocked: false,
4092
+ isFork: false,
4093
+ cowboy: true
4094
+ };
4220
4095
  }
4221
- const { stdout: head } = await import("./run-CFKZPY7F.js").then(
4222
- ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], thread.worktreePath)
4223
- );
4224
- const current = head.trim();
4096
+ const target = await resolveDefaultBranch(thread.repoPath);
4097
+ const current = await currentBranch(thread.worktreePath);
4225
4098
  if (current === target) {
4226
4099
  return {
4227
4100
  branch: current,
@@ -4248,6 +4121,19 @@ async function confirmLand(thread, opts) {
4248
4121
  if (preview.blocked) {
4249
4122
  throw new Error(preview.blockReason ?? "Land blocked");
4250
4123
  }
4124
+ if (preview.cowboy) {
4125
+ const meta2 = await suggestPrMetadata(thread.worktreePath, {
4126
+ base: preview.branch,
4127
+ fallbackTitle: thread.title,
4128
+ sourceLabel: `${thread.sourceType}:${thread.sourceRef}`
4129
+ });
4130
+ let committed2 = false;
4131
+ if (preview.dirty) {
4132
+ committed2 = await commitAll(thread.worktreePath, meta2.commitMessage);
4133
+ }
4134
+ await pushBranch(thread.worktreePath, preview.branch);
4135
+ return { prUrl: null, pushed: true, committed: committed2 };
4136
+ }
4251
4137
  const meta = await suggestPrMetadata(thread.worktreePath, {
4252
4138
  base: preview.target,
4253
4139
  fallbackTitle: thread.title,
@@ -4845,8 +4731,9 @@ function formatProcessGuideDirective() {
4845
4731
  "Process guides (recurring work only):",
4846
4732
  "- 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.",
4847
4733
  "- 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.",
4848
- "- 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.",
4849
- "- 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."
4734
+ "- 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).",
4735
+ "- 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.",
4736
+ "- 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."
4850
4737
  ].join("\n");
4851
4738
  }
4852
4739
  function formatArtifactDirective() {
@@ -4873,15 +4760,7 @@ function formatArtifactDirective() {
4873
4760
  ].join("\n");
4874
4761
  }
4875
4762
  function formatUiReminder() {
4876
- return [
4877
- "Sideboard side column (important):",
4878
- "There is no claude.ai Artifact tool here \u2014 that is normal.",
4879
- "HTML/SVG/markdown: ```html fence or MCP present_artifact.",
4880
- "CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
4881
- "Files column: MCP present_files (brightsy account storage or memory).",
4882
- "Do not say artifacts/CMS UI are unavailable.",
4883
- "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."
4884
- ].join(" ");
4763
+ 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.";
4885
4764
  }
4886
4765
  var FILES_BY_AGENT = {
4887
4766
  claude: [
@@ -5226,7 +5105,7 @@ function formatScheduledPrompt(name, prompt) {
5226
5105
  ${prompt}`;
5227
5106
  }
5228
5107
  async function defaultDeps() {
5229
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-DEHSKERK.js");
5108
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-FCZVCKBK.js");
5230
5109
  const orch = getOrchestrator2();
5231
5110
  return {
5232
5111
  findThread: (id) => findThreadByRef(id),
@@ -5444,7 +5323,7 @@ var Orchestrator = class {
5444
5323
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
5445
5324
  });
5446
5325
  }
5447
- const { autoCleanupOrphansEnabled } = await import("./app-settings-6AXBWM3K.js");
5326
+ const { autoCleanupOrphansEnabled } = await import("./app-settings-J4LUWIRN.js");
5448
5327
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
5449
5328
  await cleanupOrphanWorktrees({ repoPaths });
5450
5329
  }
@@ -5601,7 +5480,9 @@ var Orchestrator = class {
5601
5480
  return thread;
5602
5481
  }
5603
5482
  async finishCreateThread(threadId, prompt) {
5604
- const setup = this.runSetupAfterCreate(threadId);
5483
+ const created = this.getThread(threadId);
5484
+ const skipSetup = isCowboyThread(created);
5485
+ const setup = skipSetup ? Promise.resolve() : this.runSetupAfterCreate(threadId);
5605
5486
  if (prompt) {
5606
5487
  try {
5607
5488
  await this.send(threadId, prompt);
@@ -5613,7 +5494,8 @@ var Orchestrator = class {
5613
5494
  }
5614
5495
  }
5615
5496
  await setup;
5616
- const { autoRunAfterSetupEnabled } = await import("./app-settings-6AXBWM3K.js");
5497
+ if (skipSetup) return;
5498
+ const { autoRunAfterSetupEnabled } = await import("./app-settings-J4LUWIRN.js");
5617
5499
  if (autoRunAfterSetupEnabled()) {
5618
5500
  try {
5619
5501
  await this.startDev(threadId);
@@ -5827,7 +5709,7 @@ var Orchestrator = class {
5827
5709
  if (compact.didCompact) {
5828
5710
  thread = updateThread(threadId, {
5829
5711
  messages: compact.thread.messages,
5830
- sessionId: null
5712
+ sessionId: compact.thread.sessionId
5831
5713
  });
5832
5714
  this.emit({
5833
5715
  type: "context_compacted",
@@ -5898,7 +5780,7 @@ var Orchestrator = class {
5898
5780
  }
5899
5781
  const isBrightsy = fresh.agent === "brightsy";
5900
5782
  const isOrchestration = isOrchestratorThread(fresh);
5901
- const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-6AXBWM3K.js");
5783
+ const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-J4LUWIRN.js");
5902
5784
  const gitAuthMode = getGithubGitAuthMode();
5903
5785
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
5904
5786
  githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
@@ -6657,7 +6539,7 @@ var Orchestrator = class {
6657
6539
  updateThread(thread.id, { title: meta.title });
6658
6540
  }
6659
6541
  }
6660
- const { autoArchiveOnMergeEnabled } = await import("./app-settings-6AXBWM3K.js");
6542
+ const { autoArchiveOnMergeEnabled } = await import("./app-settings-J4LUWIRN.js");
6661
6543
  const latest = this.requireThread(thread.id);
6662
6544
  if (!shouldAutoArchiveOnPrMerge({
6663
6545
  previousPrState: prevState || null,
@@ -6944,23 +6826,25 @@ var Orchestrator = class {
6944
6826
  const siblings = threadsSharingWorktree(thread.worktreePath).filter((t) => t.id !== thread.id);
6945
6827
  if (siblings.length === 0) {
6946
6828
  this.stopDev(thread.id);
6947
- try {
6948
- await runArchiveScript(thread.repoPath, thread.worktreePath, (line) => {
6949
- this.emit({
6950
- type: "turn_output",
6951
- threadId: thread.id,
6952
- event: { type: "stdout", data: `[archive] ${line}` }
6829
+ if (shouldRemoveWorktreeOnTeardown(thread)) {
6830
+ try {
6831
+ await runArchiveScript(thread.repoPath, thread.worktreePath, (line) => {
6832
+ this.emit({
6833
+ type: "turn_output",
6834
+ threadId: thread.id,
6835
+ event: { type: "stdout", data: `[archive] ${line}` }
6836
+ });
6953
6837
  });
6954
- });
6955
- } catch {
6838
+ } catch {
6839
+ }
6840
+ await removeWorktree(thread.repoPath, thread.worktreePath);
6956
6841
  }
6957
- await removeWorktree(thread.repoPath, thread.worktreePath);
6958
6842
  }
6959
6843
  const archived = setStatus(thread.id, "archived");
6960
6844
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
6961
6845
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
6962
6846
  try {
6963
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-UPEVANU4.js");
6847
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-HV3J4TTW.js");
6964
6848
  await ensureWorkspace2(thread.repoPath);
6965
6849
  } catch {
6966
6850
  }
@@ -6982,15 +6866,17 @@ var Orchestrator = class {
6982
6866
  const siblings = threadsSharingWorktree(thread.worktreePath).filter((t) => t.id !== thread.id);
6983
6867
  if (siblings.length === 0) {
6984
6868
  this.stopDev(thread.id);
6985
- try {
6986
- await runArchiveScript(thread.repoPath, thread.worktreePath);
6987
- } catch {
6869
+ if (shouldRemoveWorktreeOnTeardown(thread)) {
6870
+ try {
6871
+ await runArchiveScript(thread.repoPath, thread.worktreePath);
6872
+ } catch {
6873
+ }
6874
+ const { deleteBranchOnPurgeEnabled } = await import("./app-settings-J4LUWIRN.js");
6875
+ const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
6876
+ await removeWorktree(thread.repoPath, thread.worktreePath, {
6877
+ deleteBranch: deleteBranch ? thread.branchName : void 0
6878
+ });
6988
6879
  }
6989
- const { deleteBranchOnPurgeEnabled } = await import("./app-settings-6AXBWM3K.js");
6990
- const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
6991
- await removeWorktree(thread.repoPath, thread.worktreePath, {
6992
- deleteBranch: deleteBranch ? thread.branchName : void 0
6993
- });
6994
6880
  }
6995
6881
  deleteThreadRecord(thread.id);
6996
6882
  }
@@ -7007,7 +6893,12 @@ var Orchestrator = class {
7007
6893
  return restored2;
7008
6894
  }
7009
6895
  if (!existsSync16(thread.worktreePath)) {
7010
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-NR5YJG2E.js");
6896
+ if (isCowboyThread(thread) || isPrimaryCheckoutThread(thread)) {
6897
+ throw new Error(
6898
+ `Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
6899
+ );
6900
+ }
6901
+ const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-XFJED3VU.js");
7011
6902
  const slug = thread.worktreePath.split("/").pop();
7012
6903
  const dest = thread.worktreePath;
7013
6904
  await withRepoGitLock(thread.repoPath, async () => {
@@ -7115,7 +7006,7 @@ async function startOrchestration(opts) {
7115
7006
  sourceRef: "default",
7116
7007
  ...createOpts
7117
7008
  }).catch(async () => {
7118
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-NR5YJG2E.js");
7009
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-XFJED3VU.js");
7119
7010
  const repo = await resolveRepoRoot2(repoPath);
7120
7011
  const def = await resolveDefaultBranch2(repo);
7121
7012
  return orch.createThread({
@@ -7125,7 +7016,7 @@ async function startOrchestration(opts) {
7125
7016
  repoPath: repo
7126
7017
  });
7127
7018
  });
7128
- const { updateThread: upd } = await import("./thread-store-4OUEQ2DB.js");
7019
+ const { updateThread: upd } = await import("./thread-store-OEALWU7Y.js");
7129
7020
  const updated = upd(thread.id, {
7130
7021
  sourceType: "orchestration",
7131
7022
  sourceRef: goal
@@ -7179,50 +7070,17 @@ export {
7179
7070
  dismissSlackReplyBadge,
7180
7071
  permalinkForSlackReplyBadge,
7181
7072
  refreshSlackReplyBadges,
7182
- toolDetail,
7183
- toolDescription,
7184
- toolFilePath,
7185
- applyAgentEvent,
7186
- partsToAssistantText,
7187
- stripBrightsyNdjsonNoise,
7188
- isBrightsyNdjsonLine,
7189
- finalizeParts,
7190
- normalizeParseResult,
7073
+ applyPromptCacheTtlEnv,
7191
7074
  spawnAgentTurn,
7192
7075
  AGENT_GIT_ACTIONS,
7193
7076
  agentGitPrompt,
7194
- readWorktreeInclude,
7195
- resolveFilesToCopy,
7196
- copyConfiguredFiles,
7197
- captureLoginEnv,
7198
- buildWorkspaceScriptEnv,
7199
- runSetupScript,
7200
- runConventionSetup,
7201
- runWorkspaceSetup,
7202
- runArchiveScript,
7203
- listRunScripts,
7204
- getDefaultRunScript,
7205
- getRunScript,
7206
- getRunMode,
7207
- allocatePort,
7208
- allocatePortRange,
7209
- startDevServer,
7210
- findOrphanWorktrees,
7211
- cleanupOrphanWorktrees,
7212
- shouldRunWorktreeCleanup,
7213
- worktreeCleanupSettings,
7214
- applyThreadIntoMain,
7215
- cloneRepoIntoSideboard,
7216
- detectAgents,
7217
- requireAgent,
7218
- createThread,
7219
- listLinearIssues,
7220
7077
  summarizeConversation,
7221
7078
  extractiveSummary,
7222
7079
  CONTEXT_COMPACT_CHARS,
7223
7080
  CONTEXT_KEEP_RECENT_CHARS,
7224
7081
  CONTEXT_KEEP_RECENT_MESSAGES,
7225
7082
  CONTEXT_MIN_MESSAGES,
7083
+ SESSION_RESET_OCCUPANCY_TOKENS,
7226
7084
  estimateMessageChars,
7227
7085
  estimateThreadChars,
7228
7086
  shouldCompactContext,
@@ -7230,6 +7088,8 @@ export {
7230
7088
  formatMessagesAsTranscript,
7231
7089
  buildSessionSeed,
7232
7090
  applyCompaction,
7091
+ lastRequestOccupancy,
7092
+ shouldResetSessionForOccupancy,
7233
7093
  maybeCompactContext,
7234
7094
  sameWorktreePath,
7235
7095
  takenTeamSlugsForChatTab,
@@ -7239,8 +7099,10 @@ export {
7239
7099
  buildForkTranscriptAttachment,
7240
7100
  createChatTab,
7241
7101
  forkChatTab,
7242
- readTurnLive,
7243
7102
  REVIEW_REQUEST_TEMPLATE,
7103
+ REVIEW_SKILL_PATH,
7104
+ REVIEW_SKILL_NAME,
7105
+ wrapReviewSkillMarkdown,
7244
7106
  REPO_REVIEW_PATH,
7245
7107
  REPO_REVIEW_NAME,
7246
7108
  REVIEW_REQUEST_PATH,
@@ -7248,11 +7110,42 @@ export {
7248
7110
  REVIEW_REQUEST_NAME,
7249
7111
  REVIEW_REQUEST_PREFILL,
7250
7112
  shouldRefreshReviewRequestTemplate,
7113
+ ensureReviewSkillFile,
7251
7114
  resolveReviewGuidelines,
7252
7115
  ensureReviewRequestFile,
7253
7116
  buildReviewRequestAttachment,
7254
7117
  readExistingReviewRequestFile,
7255
7118
  requestReview,
7119
+ readWorktreeInclude,
7120
+ resolveFilesToCopy,
7121
+ copyConfiguredFiles,
7122
+ captureLoginEnv,
7123
+ buildWorkspaceScriptEnv,
7124
+ runSetupScript,
7125
+ runConventionSetup,
7126
+ runWorkspaceSetup,
7127
+ runArchiveScript,
7128
+ listRunScripts,
7129
+ getDefaultRunScript,
7130
+ getRunScript,
7131
+ getRunMode,
7132
+ allocatePort,
7133
+ allocatePortRange,
7134
+ startDevServer,
7135
+ findOrphanWorktrees,
7136
+ cleanupOrphanWorktrees,
7137
+ shouldRunWorktreeCleanup,
7138
+ worktreeCleanupSettings,
7139
+ applyThreadIntoMain,
7140
+ cloneRepoIntoSideboard,
7141
+ detectAgents,
7142
+ requireAgent,
7143
+ createThread,
7144
+ listLinearIssues,
7145
+ isCowboyThread,
7146
+ isPrimaryCheckoutThread,
7147
+ shouldRemoveWorktreeOnTeardown,
7148
+ readTurnLive,
7256
7149
  forkThreadWorktree,
7257
7150
  resolveConductorCursorAgentId,
7258
7151
  adoptThread,