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