@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.
- package/dist/agents/cursor-runner.cjs +87 -12
- package/dist/agents/cursor-runner.js +38 -12
- package/dist/{agents-T4RHL5UX.js → agents-MQDW3YXL.js} +6 -6
- package/dist/{agents-4XQANGFO.js → agents-WK54VP7J.js} +8 -8
- package/dist/{app-settings-2WH6DVOW.js → app-settings-5XAGNDX7.js} +3 -1
- package/dist/{app-settings-6AXBWM3K.js → app-settings-J4LUWIRN.js} +4 -2
- package/dist/{chunk-HUKCGRAT.js → chunk-3UOKB6VQ.js} +47 -2
- package/dist/{chunk-SMWVSSE5.js → chunk-4EWPKYOU.js} +4 -4
- package/dist/{chunk-7ZTQHC2Q.js → chunk-4NR5FL46.js} +11 -1
- package/dist/{chunk-TLPJHLLM.js → chunk-4TR3HZFT.js} +1 -1
- package/dist/{chunk-VTTTV2LM.js → chunk-7KWYXGIU.js} +2 -2
- package/dist/{chunk-KGSVLZV6.js → chunk-A4VZXJEJ.js} +2 -0
- package/dist/{chunk-5XUKY2JY.js → chunk-AROMOP3C.js} +8 -16
- package/dist/{chunk-PZVA2VDF.js → chunk-C4KHDW3U.js} +2 -2
- package/dist/{chunk-3SIFRHJ3.js → chunk-EAAB4EK5.js} +1597 -1704
- package/dist/{chunk-GYFNFU62.js → chunk-KQBI5HNT.js} +8 -16
- package/dist/{chunk-P3BQ5DOL.js → chunk-KYOTBZ6S.js} +2 -0
- package/dist/{chunk-6HRR4T4P.js → chunk-RSIWPLRG.js} +483 -46
- package/dist/{chunk-EX4NZN3O.js → chunk-SGEVS5SY.js} +11 -1
- package/dist/{chunk-THXSFETX.js → chunk-VOD3HFLP.js} +4 -4
- package/dist/{chunk-XOYH3OMI.js → chunk-VOJSO3RM.js} +484 -55
- package/dist/{chunk-MMI4RJ5I.js → chunk-WIHKHR5R.js} +14 -3
- package/dist/{chunk-XEFHG6VG.js → chunk-XOZDAVOP.js} +1449 -1518
- package/dist/{chunk-LRO7YVLJ.js → chunk-YFAXVUY2.js} +2 -2
- package/dist/{chunk-6JJTMI7G.js → chunk-YXDR43ZC.js} +2 -2
- package/dist/{coordinator-prompt-4TUQUEHY.js → coordinator-prompt-AR66L3N4.js} +5 -5
- package/dist/{coordinator-prompt-YQWRXCPX.js → coordinator-prompt-BHMLWL64.js} +4 -4
- package/dist/{global-workspace-H37LFFXI.js → global-workspace-SKFODUQQ.js} +5 -5
- package/dist/{global-workspace-V6TMECDU.js → global-workspace-XSUFEIAQ.js} +6 -6
- package/dist/index.cjs +2388 -2006
- package/dist/index.d.cts +115 -26
- package/dist/index.d.ts +115 -26
- package/dist/index.js +141 -98
- package/dist/mcp/run-stdio.cjs +2354 -1988
- package/dist/mcp/run-stdio.js +90 -82
- package/dist/{orchestrator-DEHSKERK.js → orchestrator-FCZVCKBK.js} +11 -11
- package/dist/{orchestrator-2P7DX44Q.js → orchestrator-SCATSB3O.js} +8 -8
- package/dist/{thread-store-DQJGDKIJ.js → thread-store-LPTBVW5C.js} +1 -1
- package/dist/{thread-store-4OUEQ2DB.js → thread-store-OEALWU7Y.js} +1 -1
- package/dist/{workspaces-UPEVANU4.js → workspaces-HV3J4TTW.js} +7 -7
- package/dist/{workspaces-JU3P6FHB.js → workspaces-XAQVKTLO.js} +6 -6
- package/dist/{worktree-6MFKE465.js → worktree-N2EV24EE.js} +3 -3
- package/dist/{worktree-NR5YJG2E.js → worktree-XFJED3VU.js} +4 -4
- 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
|
-
|
|
10
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
126
|
+
} from "./chunk-4NR5FL46.js";
|
|
119
127
|
import {
|
|
120
128
|
stripNestedElectronEnv
|
|
121
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
731
|
-
|
|
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 (
|
|
900
|
-
|
|
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-
|
|
752
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-XSUFEIAQ.js");
|
|
967
753
|
if (isGlobalThread2(thread)) {
|
|
968
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
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
|
|
1196
|
-
mkdirSync,
|
|
982
|
+
existsSync as existsSync4,
|
|
983
|
+
mkdirSync as mkdirSync2,
|
|
1197
984
|
readdirSync,
|
|
1198
|
-
readFileSync as
|
|
985
|
+
readFileSync as readFileSync4
|
|
1199
986
|
} from "fs";
|
|
1200
987
|
import { createServer } from "net";
|
|
1201
|
-
import { basename, dirname, join as
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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 (
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
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
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
|
1113
|
+
return n;
|
|
1259
1114
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1124
|
+
default:
|
|
1125
|
+
return 0;
|
|
1283
1126
|
}
|
|
1284
|
-
cachedLoginEnv = { ...process.env };
|
|
1285
|
-
return { ...cachedLoginEnv };
|
|
1286
1127
|
}
|
|
1287
|
-
function
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
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
|
|
1317
|
-
if (
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
const
|
|
1405
|
-
if (
|
|
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
|
|
1250
|
+
return 0;
|
|
1409
1251
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1276
|
+
didCompact: true,
|
|
1277
|
+
thread: next,
|
|
1278
|
+
summary,
|
|
1279
|
+
method,
|
|
1280
|
+
olderCount: older.length
|
|
1425
1281
|
};
|
|
1426
1282
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
|
1298
|
+
return [...taken];
|
|
1454
1299
|
}
|
|
1455
|
-
|
|
1456
|
-
const
|
|
1457
|
-
if (!
|
|
1458
|
-
|
|
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
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
return
|
|
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
|
|
1480
|
-
const
|
|
1481
|
-
|
|
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
|
|
1488
|
-
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
1507
|
-
const
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
const
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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/
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
|
1540
|
+
return `${REVIEW_SKILL_FRONTMATTER}${source}
|
|
1541
|
+
`;
|
|
1613
1542
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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
|
|
1648
|
-
const
|
|
1649
|
-
const
|
|
1650
|
-
if (!
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
|
1655
|
-
const a = loadAppSettings().advanced;
|
|
1585
|
+
function skillGuidelines(content, source) {
|
|
1656
1586
|
return {
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1587
|
+
path: REVIEW_SKILL_PATH,
|
|
1588
|
+
name: REVIEW_SKILL_NAME,
|
|
1589
|
+
content,
|
|
1590
|
+
source
|
|
1661
1591
|
};
|
|
1662
1592
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
});
|
|
1682
|
-
|
|
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
|
-
|
|
1644
|
+
"request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
1685
1645
|
);
|
|
1686
1646
|
}
|
|
1687
|
-
if (
|
|
1688
|
-
|
|
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
|
|
1772
|
-
const
|
|
1773
|
-
|
|
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/
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
return
|
|
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
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
|
1787
|
-
|
|
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
|
-
|
|
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
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1805
|
-
|
|
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
|
|
1811
|
-
const
|
|
1812
|
-
|
|
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
|
|
1815
|
-
|
|
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
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
const
|
|
1879
|
-
if (
|
|
1880
|
-
|
|
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
|
-
|
|
1901
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1878
|
+
defaultBranch: opts?.defaultBranch,
|
|
1879
|
+
onLine
|
|
1905
1880
|
});
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
-
|
|
1917
|
-
|
|
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
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1998
|
-
const
|
|
1999
|
-
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
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
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
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
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
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
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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
|
|
2074
|
-
return
|
|
1955
|
+
function getRunMode(worktreePath, repoPath) {
|
|
1956
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2075
1957
|
}
|
|
2076
|
-
function
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
-
|
|
2100
|
-
|
|
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
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
|
2129
|
-
const
|
|
2130
|
-
const
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
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
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
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
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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/
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
const
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
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
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
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
|
|
2270
|
-
|
|
2271
|
-
|
|
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
|
|
2422
|
-
|
|
2423
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
2254
|
+
function releaseDesktopHost(pid = process.pid) {
|
|
2255
|
+
if (readDesktopHostPid() !== pid) return;
|
|
2424
2256
|
try {
|
|
2425
|
-
|
|
2426
|
-
renameSync(tmp, path);
|
|
2257
|
+
unlinkSync(desktopHostPidPath());
|
|
2427
2258
|
} catch {
|
|
2428
|
-
try {
|
|
2429
|
-
unlinkSync2(tmp);
|
|
2430
|
-
} catch {
|
|
2431
|
-
}
|
|
2432
2259
|
}
|
|
2433
2260
|
}
|
|
2434
|
-
function
|
|
2435
|
-
const path = threadLivePath(threadId);
|
|
2436
|
-
if (!existsSync7(path)) return null;
|
|
2261
|
+
function readDesktopHostPid() {
|
|
2437
2262
|
try {
|
|
2438
|
-
const
|
|
2439
|
-
if (!
|
|
2440
|
-
return
|
|
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
|
|
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
|
-
|
|
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/
|
|
2458
|
-
import {
|
|
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
|
-
|
|
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/
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
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
|
-
|
|
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
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
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
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
writeFileSync3(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
|
|
2459
|
+
|
|
2460
|
+
// src/threads/cowboy.ts
|
|
2461
|
+
function isCowboyThread(thread) {
|
|
2462
|
+
return Boolean(thread?.cowboy);
|
|
2618
2463
|
}
|
|
2619
|
-
function
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
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
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
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
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2515
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2516
|
+
summary,
|
|
2517
|
+
lastTool: lastToolLabel,
|
|
2518
|
+
toolCount: tools.length,
|
|
2519
|
+
excerpt
|
|
2658
2520
|
};
|
|
2659
2521
|
}
|
|
2660
|
-
function
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
if (
|
|
2664
|
-
|
|
2665
|
-
|
|
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
|
-
|
|
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
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2694
|
-
return
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
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
|
|
2703
|
-
|
|
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
|
-
|
|
2706
|
-
const
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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
|
-
|
|
2714
|
-
|
|
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-
|
|
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
|
-
|
|
4219
|
-
|
|
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
|
|
4222
|
-
|
|
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
|
-
"-
|
|
4849
|
-
"-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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-
|
|
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-
|
|
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
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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,
|