@sideboard-ai/core 0.1.107 → 0.1.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/cursor-runner.js +2 -2
- package/dist/{agents-T4RHL5UX.js → agents-XB332VUJ.js} +5 -5
- package/dist/{agents-4XQANGFO.js → agents-YIMHDI46.js} +7 -7
- package/dist/{app-settings-2WH6DVOW.js → app-settings-EBCVMFF6.js} +1 -1
- package/dist/{app-settings-6AXBWM3K.js → app-settings-IYSSVZHH.js} +2 -2
- package/dist/{chunk-7ZTQHC2Q.js → chunk-4LKD3PGO.js} +1 -1
- package/dist/{chunk-TLPJHLLM.js → chunk-4TR3HZFT.js} +1 -1
- package/dist/{chunk-SMWVSSE5.js → chunk-756EO4GK.js} +3 -3
- package/dist/{chunk-XOYH3OMI.js → chunk-AY2EQI2P.js} +16 -13
- package/dist/{chunk-PZVA2VDF.js → chunk-EW7COXYE.js} +1 -1
- package/dist/{chunk-VTTTV2LM.js → chunk-FBV6NK62.js} +2 -2
- package/dist/{chunk-GYFNFU62.js → chunk-I2OJ2YSP.js} +8 -16
- package/dist/{chunk-6JJTMI7G.js → chunk-JVIW55KT.js} +1 -1
- package/dist/{chunk-6HRR4T4P.js → chunk-KKHDPV45.js} +15 -14
- package/dist/{chunk-XEFHG6VG.js → chunk-LTPX5N6K.js} +1310 -1265
- package/dist/{chunk-EX4NZN3O.js → chunk-NE4TOOKS.js} +1 -1
- package/dist/{chunk-LRO7YVLJ.js → chunk-OQDIYVDD.js} +2 -2
- package/dist/{chunk-3SIFRHJ3.js → chunk-Q6B3NRCT.js} +1457 -1431
- package/dist/{chunk-THXSFETX.js → chunk-TROJ3PVH.js} +3 -3
- package/dist/{chunk-5XUKY2JY.js → chunk-VRTKGKUW.js} +8 -16
- package/dist/{chunk-HUKCGRAT.js → chunk-WD2TARQS.js} +7 -2
- package/dist/{coordinator-prompt-4TUQUEHY.js → coordinator-prompt-JKOU6BWV.js} +4 -4
- package/dist/{coordinator-prompt-YQWRXCPX.js → coordinator-prompt-V66BYTUV.js} +3 -3
- package/dist/{global-workspace-V6TMECDU.js → global-workspace-NCTBE7G7.js} +5 -5
- package/dist/{global-workspace-H37LFFXI.js → global-workspace-WBQWQQQY.js} +4 -4
- package/dist/index.cjs +1697 -1661
- package/dist/index.d.cts +65 -24
- package/dist/index.d.ts +65 -24
- package/dist/index.js +107 -85
- package/dist/mcp/run-stdio.cjs +1660 -1619
- package/dist/mcp/run-stdio.js +82 -79
- package/dist/{orchestrator-DEHSKERK.js → orchestrator-6W7EKSPO.js} +9 -9
- package/dist/{orchestrator-2P7DX44Q.js → orchestrator-JVGVPKLI.js} +7 -7
- package/dist/{workspaces-JU3P6FHB.js → workspaces-I3554R3F.js} +5 -5
- package/dist/{workspaces-UPEVANU4.js → workspaces-SUBEDSGB.js} +6 -6
- package/dist/{worktree-NR5YJG2E.js → worktree-2UWO7VEK.js} +3 -3
- package/dist/{worktree-6MFKE465.js → worktree-7H6HB6GW.js} +2 -2
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
PLAN_MODE_INSTRUCTION,
|
|
5
5
|
applyTurnUsage,
|
|
6
|
+
contextTokens,
|
|
6
7
|
fallbackTurnFailDetail,
|
|
7
8
|
formatTurnExitError,
|
|
8
9
|
getAdapter,
|
|
@@ -16,13 +17,13 @@ import {
|
|
|
16
17
|
shouldRetryFailedAgentTurn,
|
|
17
18
|
summarizeTurnStderr,
|
|
18
19
|
turnFailChatText
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-AY2EQI2P.js";
|
|
20
21
|
import {
|
|
21
22
|
addWorkspace,
|
|
22
23
|
ensureWorkspace,
|
|
23
24
|
removeWorkspace,
|
|
24
25
|
syncWorkspacesFromThreads
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-OQDIYVDD.js";
|
|
26
27
|
import {
|
|
27
28
|
extractPresentedPlan,
|
|
28
29
|
readPlanFile,
|
|
@@ -43,14 +44,14 @@ import {
|
|
|
43
44
|
isOrchestratorThread,
|
|
44
45
|
isSlackCoordinatorThread,
|
|
45
46
|
orchestratorSessionPoisonedByBuiltins
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-TROJ3PVH.js";
|
|
47
48
|
import {
|
|
48
49
|
SLACK_REPLY_FORMATTING,
|
|
49
50
|
coordinatorSystemPrompt,
|
|
50
51
|
coordinatorTurnReminder,
|
|
51
52
|
enrichWorkspacesWithGithub,
|
|
52
53
|
ensureGlobalCoordinatorCwd
|
|
53
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-VRTKGKUW.js";
|
|
54
55
|
import {
|
|
55
56
|
addPrStackLayer,
|
|
56
57
|
allocateTeamName,
|
|
@@ -93,7 +94,7 @@ import {
|
|
|
93
94
|
threadDisplayLabel,
|
|
94
95
|
withRepoGitLock,
|
|
95
96
|
worktreeNameFromPath
|
|
96
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-JVIW55KT.js";
|
|
97
98
|
import {
|
|
98
99
|
ATTACHMENTS_DIR,
|
|
99
100
|
LEGACY_ATTACHMENTS_DIR,
|
|
@@ -124,7 +125,7 @@ import {
|
|
|
124
125
|
stripNestedElectronEnv,
|
|
125
126
|
updateAdvancedSettings,
|
|
126
127
|
writeSecureJson
|
|
127
|
-
} from "./chunk-
|
|
128
|
+
} from "./chunk-NE4TOOKS.js";
|
|
128
129
|
import {
|
|
129
130
|
writePrivateFile
|
|
130
131
|
} from "./chunk-NSTQ6QKD.js";
|
|
@@ -925,6 +926,14 @@ function createAgentStreamCoalescer(emit, opts) {
|
|
|
925
926
|
}
|
|
926
927
|
|
|
927
928
|
// src/agents/spawn.ts
|
|
929
|
+
function applyPromptCacheTtlEnv(agent, env) {
|
|
930
|
+
if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
|
|
931
|
+
env.ENABLE_PROMPT_CACHING_1H ??= "1";
|
|
932
|
+
}
|
|
933
|
+
if (agent === "opencode" && env.OPENCODE_ANTHROPIC_FORCE_PROMPT_CACHING_5M !== "1") {
|
|
934
|
+
env.OPENCODE_ANTHROPIC_PROMPT_CACHING_1H ??= "1";
|
|
935
|
+
}
|
|
936
|
+
}
|
|
928
937
|
async function spawnAgentTurn(thread, input, onEvent) {
|
|
929
938
|
ensureAgentPath();
|
|
930
939
|
if (!thread.worktreePath?.trim()) {
|
|
@@ -932,9 +941,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
932
941
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
933
942
|
);
|
|
934
943
|
}
|
|
935
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
944
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-WBQWQQQY.js");
|
|
936
945
|
if (isGlobalThread2(thread)) {
|
|
937
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
946
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-V66BYTUV.js");
|
|
938
947
|
ensureGlobalCoordinatorCwd2(
|
|
939
948
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
940
949
|
);
|
|
@@ -950,6 +959,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
950
959
|
);
|
|
951
960
|
}
|
|
952
961
|
const env = childEnvWithAppSettings(cmd.env);
|
|
962
|
+
applyPromptCacheTtlEnv(thread.agent, env);
|
|
953
963
|
try {
|
|
954
964
|
if (isOrchestratorThread(thread)) {
|
|
955
965
|
mergeAgentGitAuthEnv(env, await resolveAgentGitAuthEnv(env));
|
|
@@ -1161,1127 +1171,1409 @@ function shouldAutoArchiveOnPrMerge(opts) {
|
|
|
1161
1171
|
// src/hook/conductor.ts
|
|
1162
1172
|
import {
|
|
1163
1173
|
copyFileSync,
|
|
1164
|
-
existsSync as
|
|
1165
|
-
mkdirSync,
|
|
1174
|
+
existsSync as existsSync4,
|
|
1175
|
+
mkdirSync as mkdirSync2,
|
|
1166
1176
|
readdirSync,
|
|
1167
|
-
readFileSync as
|
|
1177
|
+
readFileSync as readFileSync4
|
|
1168
1178
|
} from "fs";
|
|
1169
1179
|
import { createServer } from "net";
|
|
1170
|
-
import { basename, dirname, join as
|
|
1180
|
+
import { basename, dirname as dirname2, join as join5 } from "path";
|
|
1171
1181
|
import { execa as execa2 } from "execa";
|
|
1172
1182
|
import { createInterface as createInterface2 } from "readline";
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (!entry.isFile()) continue;
|
|
1193
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
1194
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
1195
|
-
matched.push(entry.name);
|
|
1196
|
-
break;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
} catch {
|
|
1201
|
-
}
|
|
1202
|
-
if (matched.length) return [...new Set(matched)];
|
|
1183
|
+
|
|
1184
|
+
// src/review/request-review.ts
|
|
1185
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1186
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1187
|
+
import { dirname, join as join4 } from "path";
|
|
1188
|
+
|
|
1189
|
+
// src/threads/chat-tabs.ts
|
|
1190
|
+
import { randomUUID } from "crypto";
|
|
1191
|
+
|
|
1192
|
+
// src/composer/summarize.ts
|
|
1193
|
+
var MAX_TRANSCRIPT_CHARS = 12e4;
|
|
1194
|
+
var MAX_SUMMARY_CHARS = 6e3;
|
|
1195
|
+
async function summarizeConversation(transcript, opts) {
|
|
1196
|
+
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
1197
|
+
|
|
1198
|
+
[\u2026truncated\u2026]` : transcript;
|
|
1199
|
+
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
1200
|
+
if (claudeSummary) {
|
|
1201
|
+
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
1203
1202
|
}
|
|
1204
|
-
|
|
1203
|
+
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
1204
|
+
}
|
|
1205
|
+
async function tryClaudeSummary(transcript, opts) {
|
|
1206
|
+
ensureAgentPath();
|
|
1207
|
+
const prompt = [
|
|
1208
|
+
"Summarize this coding-agent conversation for continuity in a fresh session.",
|
|
1209
|
+
"Use concise markdown bullets covering:",
|
|
1210
|
+
"- Goal / request",
|
|
1211
|
+
"- Key decisions and constraints",
|
|
1212
|
+
"- Files / areas touched",
|
|
1213
|
+
"- Current state and open tasks",
|
|
1214
|
+
"- Anything the next turn must remember",
|
|
1215
|
+
"Do not invent details. No preamble.",
|
|
1216
|
+
"",
|
|
1217
|
+
"Transcript:",
|
|
1218
|
+
transcript
|
|
1219
|
+
].join("\n");
|
|
1205
1220
|
try {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1221
|
+
const { stdout, exitCode } = await run(
|
|
1222
|
+
"claude",
|
|
1223
|
+
[
|
|
1224
|
+
"-p",
|
|
1225
|
+
prompt,
|
|
1226
|
+
"--output-format",
|
|
1227
|
+
"text",
|
|
1228
|
+
"--permission-mode",
|
|
1229
|
+
"bypassPermissions",
|
|
1230
|
+
"--model",
|
|
1231
|
+
"haiku"
|
|
1232
|
+
],
|
|
1233
|
+
{ cwd: opts?.cwd, reject: false }
|
|
1234
|
+
);
|
|
1235
|
+
if (exitCode !== 0) return null;
|
|
1236
|
+
const text = stdout.trim();
|
|
1237
|
+
if (text.length < 40) return null;
|
|
1238
|
+
return text;
|
|
1211
1239
|
} catch {
|
|
1240
|
+
return null;
|
|
1212
1241
|
}
|
|
1213
|
-
if (defaults.length) return defaults;
|
|
1214
|
-
return [".env.local", ".env"];
|
|
1215
1242
|
}
|
|
1216
|
-
function
|
|
1217
|
-
const
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1243
|
+
function extractiveSummary(transcript) {
|
|
1244
|
+
const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1245
|
+
const users = [];
|
|
1246
|
+
const tools = [];
|
|
1247
|
+
const agents = [];
|
|
1248
|
+
let mode = "none";
|
|
1249
|
+
for (const line of lines) {
|
|
1250
|
+
if (line.startsWith("### User")) {
|
|
1251
|
+
mode = "user";
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
|
|
1255
|
+
mode = "agent";
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (line.startsWith("- ") && line.includes(":")) {
|
|
1259
|
+
tools.push(line.replace(/^- /, ""));
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (mode === "user" && users.length < 8) {
|
|
1263
|
+
users.push(line.slice(0, 240));
|
|
1264
|
+
} else if (mode === "agent" && agents.length < 6) {
|
|
1265
|
+
agents.push(line.slice(0, 240));
|
|
1266
|
+
}
|
|
1226
1267
|
}
|
|
1227
|
-
|
|
1268
|
+
const uniqueTools = [...new Set(tools)].slice(0, 20);
|
|
1269
|
+
const parts = [
|
|
1270
|
+
"## Compacted context (extractive)",
|
|
1271
|
+
"",
|
|
1272
|
+
"### User goals",
|
|
1273
|
+
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
1274
|
+
"",
|
|
1275
|
+
"### Recent agent notes",
|
|
1276
|
+
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
1277
|
+
];
|
|
1278
|
+
if (uniqueTools.length) {
|
|
1279
|
+
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
1280
|
+
}
|
|
1281
|
+
parts.push(
|
|
1282
|
+
"",
|
|
1283
|
+
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
1284
|
+
);
|
|
1285
|
+
return parts.join("\n");
|
|
1228
1286
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (key) env[key] = value;
|
|
1247
|
-
}
|
|
1248
|
-
cachedLoginEnv = env;
|
|
1249
|
-
return { ...env };
|
|
1250
|
-
}
|
|
1251
|
-
} catch {
|
|
1287
|
+
function clipSummary(text) {
|
|
1288
|
+
if (text.length <= MAX_SUMMARY_CHARS) return text;
|
|
1289
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}
|
|
1290
|
+
|
|
1291
|
+
[\u2026summary truncated\u2026]`;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/composer/context-compact.ts
|
|
1295
|
+
var CONTEXT_COMPACT_CHARS = 4e5;
|
|
1296
|
+
var CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
1297
|
+
var CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
1298
|
+
var CONTEXT_MIN_MESSAGES = 10;
|
|
1299
|
+
var SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
|
|
1300
|
+
function estimateMessageChars(message) {
|
|
1301
|
+
let n = message.text.length + 16;
|
|
1302
|
+
for (const part of message.parts ?? []) {
|
|
1303
|
+
n += estimatePartChars(part);
|
|
1252
1304
|
}
|
|
1253
|
-
|
|
1254
|
-
return { ...cachedLoginEnv };
|
|
1305
|
+
return n;
|
|
1255
1306
|
}
|
|
1256
|
-
function
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
1265
|
-
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
1266
|
-
env.SIDEBOARD_IS_LOCAL = "1";
|
|
1267
|
-
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1268
|
-
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
1269
|
-
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
1270
|
-
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
1271
|
-
env.CONDUCTOR_IS_LOCAL = "1";
|
|
1272
|
-
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1273
|
-
if (primary != null) {
|
|
1274
|
-
env.SIDEBOARD_PORT = String(primary);
|
|
1275
|
-
env.CONDUCTOR_PORT = String(primary);
|
|
1276
|
-
env.PORT = String(primary);
|
|
1277
|
-
for (let i = 1; i < ports.length; i++) {
|
|
1278
|
-
const p = ports[i];
|
|
1279
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
1280
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
1307
|
+
function estimatePartChars(part) {
|
|
1308
|
+
switch (part.type) {
|
|
1309
|
+
case "text":
|
|
1310
|
+
case "thinking":
|
|
1311
|
+
return part.text.length;
|
|
1312
|
+
case "tool": {
|
|
1313
|
+
const input = part.input ? JSON.stringify(part.input) : "";
|
|
1314
|
+
return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
|
|
1281
1315
|
}
|
|
1316
|
+
default:
|
|
1317
|
+
return 0;
|
|
1282
1318
|
}
|
|
1283
|
-
return env;
|
|
1284
1319
|
}
|
|
1285
|
-
function
|
|
1286
|
-
|
|
1287
|
-
const rl = createInterface2({ input: stream });
|
|
1288
|
-
rl.on("line", onLine);
|
|
1320
|
+
function estimateThreadChars(messages) {
|
|
1321
|
+
return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
|
|
1289
1322
|
}
|
|
1290
|
-
function
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
1296
|
-
} else {
|
|
1297
|
-
process.kill(-pid, "SIGTERM");
|
|
1298
|
-
setTimeout(() => {
|
|
1299
|
-
try {
|
|
1300
|
-
process.kill(-pid, "SIGKILL");
|
|
1301
|
-
} catch {
|
|
1302
|
-
}
|
|
1303
|
-
}, 2500).unref?.();
|
|
1304
|
-
}
|
|
1305
|
-
} catch {
|
|
1306
|
-
try {
|
|
1307
|
-
child.kill("SIGTERM");
|
|
1308
|
-
} catch {
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
for (const port of ports) {
|
|
1313
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
1314
|
-
if (process.platform === "win32") {
|
|
1315
|
-
void execa2(
|
|
1316
|
-
"powershell",
|
|
1317
|
-
[
|
|
1318
|
-
"-NoProfile",
|
|
1319
|
-
"-Command",
|
|
1320
|
-
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
1321
|
-
],
|
|
1322
|
-
{ reject: false }
|
|
1323
|
-
);
|
|
1324
|
-
} else {
|
|
1325
|
-
void execa2(
|
|
1326
|
-
"zsh",
|
|
1327
|
-
["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
|
|
1328
|
-
{ reject: false }
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1323
|
+
function shouldCompactContext(messages, thresholds = {}) {
|
|
1324
|
+
const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
|
|
1325
|
+
const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
|
|
1326
|
+
if (messages.length < minMessages) return false;
|
|
1327
|
+
return estimateThreadChars(messages) >= maxChars;
|
|
1332
1328
|
}
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
try {
|
|
1346
|
-
mergeAgentGitAuthEnv(
|
|
1347
|
-
env,
|
|
1348
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
1349
|
-
);
|
|
1350
|
-
} catch {
|
|
1329
|
+
function splitForCompaction(messages, thresholds = {}) {
|
|
1330
|
+
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
1331
|
+
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
1332
|
+
if (messages.length === 0) return { older: [], recent: [] };
|
|
1333
|
+
let recentChars = 0;
|
|
1334
|
+
let cut = messages.length;
|
|
1335
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1336
|
+
const next = recentChars + estimateMessageChars(messages[i]);
|
|
1337
|
+
const kept = messages.length - i;
|
|
1338
|
+
if (kept >= keepCount && next > keepChars) break;
|
|
1339
|
+
recentChars = next;
|
|
1340
|
+
cut = i;
|
|
1351
1341
|
}
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
reject: false,
|
|
1356
|
-
env,
|
|
1357
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
1358
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
1359
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
1360
|
-
});
|
|
1361
|
-
pipeLines(child.stdout, opts.onLine);
|
|
1362
|
-
pipeLines(child.stderr, opts.onLine);
|
|
1363
|
-
const ports = opts.ports ?? [];
|
|
1342
|
+
const minRecent = Math.min(keepCount, messages.length);
|
|
1343
|
+
cut = Math.min(cut, messages.length - minRecent);
|
|
1344
|
+
if (cut <= 0) return { older: [], recent: messages };
|
|
1364
1345
|
return {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
1368
|
-
child
|
|
1346
|
+
older: messages.slice(0, cut),
|
|
1347
|
+
recent: messages.slice(cut)
|
|
1369
1348
|
};
|
|
1370
1349
|
}
|
|
1371
|
-
|
|
1372
|
-
if (
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1350
|
+
function formatToolPart(t, detail) {
|
|
1351
|
+
if (detail === "summary") {
|
|
1352
|
+
const label = t.description || t.detail || t.name;
|
|
1353
|
+
const path = t.filePath ? ` (${t.filePath})` : "";
|
|
1354
|
+
return `- ${t.name}: ${label}${path}`;
|
|
1376
1355
|
}
|
|
1377
|
-
|
|
1356
|
+
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
1357
|
+
if (t.description) lines.push(`Description: ${t.description}`);
|
|
1358
|
+
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
1359
|
+
if (t.input && Object.keys(t.input).length > 0) {
|
|
1360
|
+
lines.push("Input:");
|
|
1361
|
+
lines.push("```json");
|
|
1362
|
+
lines.push(JSON.stringify(t.input, null, 2));
|
|
1363
|
+
lines.push("```");
|
|
1364
|
+
} else if (t.detail) {
|
|
1365
|
+
lines.push(`Detail: ${t.detail}`);
|
|
1366
|
+
}
|
|
1367
|
+
if (t.result != null && t.result !== "") {
|
|
1368
|
+
lines.push("Result:");
|
|
1369
|
+
lines.push("```");
|
|
1370
|
+
lines.push(t.result);
|
|
1371
|
+
lines.push("```");
|
|
1372
|
+
}
|
|
1373
|
+
return lines.join("\n");
|
|
1378
1374
|
}
|
|
1379
|
-
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1375
|
+
function formatMessagesAsTranscript(messages, opts) {
|
|
1376
|
+
const tools = opts?.tools ?? "full";
|
|
1377
|
+
const blocks = [];
|
|
1378
|
+
for (const m of messages) {
|
|
1379
|
+
if (m.role === "summary") {
|
|
1380
|
+
blocks.push(`## Prior summary
|
|
1381
|
+
${m.text}`);
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (m.role === "user") {
|
|
1385
|
+
blocks.push(`### User
|
|
1386
|
+
${m.text}`);
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
const bits = [];
|
|
1390
|
+
if (m.text?.trim()) {
|
|
1391
|
+
bits.push(`### Agent
|
|
1392
|
+
${m.text}`);
|
|
1393
|
+
} else {
|
|
1394
|
+
bits.push("### Agent");
|
|
1395
|
+
}
|
|
1396
|
+
const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
|
|
1397
|
+
for (const th of thinking) {
|
|
1398
|
+
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
1399
|
+
bits.push("Thinking:");
|
|
1400
|
+
bits.push(th.text);
|
|
1401
|
+
}
|
|
1402
|
+
const toolParts = (m.parts ?? []).filter(
|
|
1403
|
+
(p) => p.type === "tool"
|
|
1404
|
+
);
|
|
1405
|
+
if (tools !== "none" && toolParts.length > 0) {
|
|
1406
|
+
if (tools === "summary") bits.push("Tools:");
|
|
1407
|
+
for (const t of toolParts) {
|
|
1408
|
+
bits.push(formatToolPart(t, tools));
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
blocks.push(bits.join("\n"));
|
|
1412
|
+
}
|
|
1413
|
+
return blocks.join("\n\n");
|
|
1395
1414
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1415
|
+
function buildSessionSeed(messages, opts) {
|
|
1416
|
+
if (messages.length === 0) return null;
|
|
1417
|
+
const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
|
|
1418
|
+
if (!body.trim()) return null;
|
|
1419
|
+
return [
|
|
1420
|
+
"Sideboard conversation context (restored after compaction or a new session):",
|
|
1421
|
+
"",
|
|
1422
|
+
body,
|
|
1423
|
+
"",
|
|
1424
|
+
"Continue from this context. Do not repeat the summary unless asked."
|
|
1425
|
+
].join("\n");
|
|
1426
|
+
}
|
|
1427
|
+
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
1428
|
+
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
1429
|
+
if (older.length === 0) return messages;
|
|
1430
|
+
const summary = {
|
|
1431
|
+
role: "summary",
|
|
1432
|
+
text: summaryText.trim(),
|
|
1433
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1412
1434
|
};
|
|
1435
|
+
return [summary, ...recent];
|
|
1413
1436
|
}
|
|
1414
|
-
|
|
1415
|
-
let
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
}
|
|
1419
|
-
if (!setup.ran) {
|
|
1420
|
-
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
1437
|
+
function lastRequestOccupancy(thread) {
|
|
1438
|
+
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
1439
|
+
const usage = thread.messages[i]?.usage;
|
|
1440
|
+
if (usage) return contextTokens(usage);
|
|
1421
1441
|
}
|
|
1422
|
-
return
|
|
1442
|
+
return 0;
|
|
1423
1443
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
1427
|
-
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
1428
|
-
worktreePath,
|
|
1429
|
-
repoPath,
|
|
1430
|
-
onLine
|
|
1431
|
-
});
|
|
1432
|
-
const exitCode = await handle.done;
|
|
1433
|
-
return { ran: true, exitCode };
|
|
1444
|
+
function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
|
|
1445
|
+
return lastRequestOccupancy(thread) >= occupancyTokens;
|
|
1434
1446
|
}
|
|
1435
|
-
function
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1447
|
+
async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
|
|
1448
|
+
if (!shouldCompactContext(thread.messages, thresholds)) {
|
|
1449
|
+
return { didCompact: false, thread };
|
|
1450
|
+
}
|
|
1451
|
+
const { older } = splitForCompaction(thread.messages, thresholds);
|
|
1452
|
+
if (older.length === 0) {
|
|
1453
|
+
return { didCompact: false, thread };
|
|
1454
|
+
}
|
|
1455
|
+
const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
|
|
1456
|
+
const { summary, method } = await summarize(transcript, {
|
|
1457
|
+
cwd: thread.worktreePath
|
|
1441
1458
|
});
|
|
1459
|
+
const messages = applyCompaction(thread.messages, summary, thresholds);
|
|
1460
|
+
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
1461
|
+
const next = {
|
|
1462
|
+
...thread,
|
|
1463
|
+
messages,
|
|
1464
|
+
sessionId: resetSession ? null : thread.sessionId,
|
|
1465
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1466
|
+
};
|
|
1467
|
+
return {
|
|
1468
|
+
didCompact: true,
|
|
1469
|
+
thread: next,
|
|
1470
|
+
summary,
|
|
1471
|
+
method,
|
|
1472
|
+
olderCount: older.length
|
|
1473
|
+
};
|
|
1442
1474
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
return
|
|
1475
|
+
|
|
1476
|
+
// src/threads/chat-tabs.ts
|
|
1477
|
+
function sameWorktreePath(a, b) {
|
|
1478
|
+
return normalizeWorktreePath(a) === normalizeWorktreePath(b);
|
|
1447
1479
|
}
|
|
1448
|
-
function
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1480
|
+
function takenTeamSlugsForChatTab(worktreePath) {
|
|
1481
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
1482
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1483
|
+
const dir = worktreeNameFromPath(key);
|
|
1484
|
+
if (dir) taken.add(dir.toLowerCase());
|
|
1485
|
+
for (const sibling of threadsSharingWorktree(key)) {
|
|
1486
|
+
for (const slug of takenSlugsFromThread(sibling)) {
|
|
1487
|
+
taken.add(slug);
|
|
1488
|
+
}
|
|
1453
1489
|
}
|
|
1454
|
-
return
|
|
1490
|
+
return [...taken];
|
|
1455
1491
|
}
|
|
1456
|
-
function
|
|
1457
|
-
|
|
1492
|
+
function requireThread(idOrRef) {
|
|
1493
|
+
const thread = findThreadByRef(idOrRef) ?? null;
|
|
1494
|
+
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
1495
|
+
return thread;
|
|
1458
1496
|
}
|
|
1459
|
-
|
|
1460
|
-
return
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1497
|
+
function worktreeBindingFrom(from) {
|
|
1498
|
+
return {
|
|
1499
|
+
worktreePath: normalizeWorktreePath(from.worktreePath),
|
|
1500
|
+
repoPath: from.repoPath,
|
|
1501
|
+
branchName: from.branchName,
|
|
1502
|
+
sourceRef: from.sourceRef,
|
|
1503
|
+
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
1504
|
+
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
1505
|
+
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
1506
|
+
sourceIsFork: from.sourceIsFork,
|
|
1507
|
+
parentThreadId: from.parentThreadId,
|
|
1508
|
+
prUrl: from.prUrl,
|
|
1509
|
+
prTitle: from.prTitle,
|
|
1510
|
+
prState: from.prState,
|
|
1511
|
+
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
1512
|
+
stackId: from.stackId,
|
|
1513
|
+
stackLayer: from.stackLayer
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function threadsSharingWorktree(worktreePath) {
|
|
1517
|
+
if (!worktreePath) return [];
|
|
1518
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
1519
|
+
return listThreads({ includeArchived: true }).filter(
|
|
1520
|
+
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
1521
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1522
|
+
}
|
|
1523
|
+
function formatTranscriptMarkdown(title, messages) {
|
|
1524
|
+
const body = formatMessagesAsTranscript(messages, { tools: "full" });
|
|
1525
|
+
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
1526
|
+
}
|
|
1527
|
+
function forkMessageSlice(from, throughIndex) {
|
|
1528
|
+
const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
|
|
1529
|
+
return through >= 0 ? from.messages.slice(0, through + 1) : [];
|
|
1530
|
+
}
|
|
1531
|
+
function buildForkTranscriptAttachment(baseTitle, messages) {
|
|
1532
|
+
const title = baseTitle || "Chat";
|
|
1533
|
+
return {
|
|
1534
|
+
id: randomUUID(),
|
|
1535
|
+
name: `Transcript of ${title}.md`,
|
|
1536
|
+
kind: "transcript",
|
|
1537
|
+
content: formatTranscriptMarkdown(title, messages)
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function createChatTab(input) {
|
|
1541
|
+
const from = requireThread(input.fromThreadId);
|
|
1542
|
+
const binding = worktreeBindingFrom(from);
|
|
1543
|
+
const explicitTitle = input.title?.trim();
|
|
1544
|
+
const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
|
|
1545
|
+
const nextAgent = input.agent ?? from.agent;
|
|
1546
|
+
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
1547
|
+
assertOrchestratorCapableAgent(nextAgent);
|
|
1548
|
+
}
|
|
1549
|
+
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
1550
|
+
const thread = createEmptyThread({
|
|
1551
|
+
title,
|
|
1552
|
+
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
1553
|
+
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
1554
|
+
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
1555
|
+
userSetTitle: true,
|
|
1556
|
+
...binding,
|
|
1557
|
+
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
1558
|
+
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
1559
|
+
// the connected chat.
|
|
1560
|
+
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
1561
|
+
agent: nextAgent,
|
|
1562
|
+
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
1563
|
+
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
1564
|
+
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
1565
|
+
planMode: from.planMode,
|
|
1566
|
+
autonomy: input.autonomy ?? from.autonomy,
|
|
1567
|
+
attachments: input.attachments ?? [],
|
|
1568
|
+
status: "idle"
|
|
1473
1569
|
});
|
|
1570
|
+
writeThread(thread);
|
|
1571
|
+
return thread;
|
|
1474
1572
|
}
|
|
1475
|
-
|
|
1476
|
-
const
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1573
|
+
function forkChatTab(input) {
|
|
1574
|
+
const from = requireThread(input.threadId);
|
|
1575
|
+
const slice = forkMessageSlice(from, input.throughIndex);
|
|
1576
|
+
const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
|
|
1577
|
+
const tab = createChatTab({
|
|
1578
|
+
fromThreadId: input.threadId,
|
|
1579
|
+
agent: input.agent ?? from.agent,
|
|
1580
|
+
model: input.model,
|
|
1581
|
+
title: input.title?.trim() || void 0,
|
|
1582
|
+
attachments: [attachment]
|
|
1583
|
+
});
|
|
1584
|
+
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
1585
|
+
const next = { ...tab, parentThreadId: from.id };
|
|
1586
|
+
writeThread(next);
|
|
1587
|
+
return next;
|
|
1588
|
+
}
|
|
1589
|
+
return tab;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/review/review-request-template.ts
|
|
1593
|
+
var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
|
|
1594
|
+
|
|
1595
|
+
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.
|
|
1596
|
+
|
|
1597
|
+
## Required outcome
|
|
1598
|
+
|
|
1599
|
+
Start your reply with a **Recommendation** section using exactly one of:
|
|
1600
|
+
|
|
1601
|
+
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
1602
|
+
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
1603
|
+
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
1604
|
+
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
1605
|
+
|
|
1606
|
+
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.
|
|
1607
|
+
|
|
1608
|
+
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
1609
|
+
|
|
1610
|
+
## Findings
|
|
1611
|
+
|
|
1612
|
+
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
1613
|
+
|
|
1614
|
+
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
1615
|
+
|
|
1616
|
+
Flag something as a bug / blocking finding only when:
|
|
1617
|
+
|
|
1618
|
+
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
1619
|
+
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
1620
|
+
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
1621
|
+
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
1622
|
+
5. The author would likely fix it if made aware.
|
|
1623
|
+
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
1624
|
+
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
1625
|
+
8. It is clearly not just an intentional change by the author.
|
|
1626
|
+
|
|
1627
|
+
When flagging an issue, include a short accompanying comment:
|
|
1628
|
+
|
|
1629
|
+
1. Clear about why it is a problem.
|
|
1630
|
+
2. Severity must match reality \u2014 do not inflate.
|
|
1631
|
+
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
1632
|
+
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
1633
|
+
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
1634
|
+
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
1635
|
+
7. Skimmable on first read.
|
|
1636
|
+
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
1637
|
+
|
|
1638
|
+
HOW MANY FINDINGS TO RETURN:
|
|
1639
|
+
|
|
1640
|
+
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.
|
|
1641
|
+
|
|
1642
|
+
GUIDELINES:
|
|
1643
|
+
|
|
1644
|
+
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
1645
|
+
- One comment per distinct issue (or a short multi-line range if needed).
|
|
1646
|
+
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
1647
|
+
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
1648
|
+
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
1649
|
+
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
1650
|
+
|
|
1651
|
+
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).
|
|
1652
|
+
|
|
1653
|
+
## Getting the diff
|
|
1654
|
+
|
|
1655
|
+
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.
|
|
1656
|
+
|
|
1657
|
+
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.
|
|
1658
|
+
|
|
1659
|
+
## Fallback: if you don't have access to the Sideboard diff tool
|
|
1660
|
+
|
|
1661
|
+
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
1662
|
+
|
|
1663
|
+
\`\`\`bash
|
|
1664
|
+
# Get the merge base between this branch and the target
|
|
1665
|
+
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
1666
|
+
|
|
1667
|
+
# Get the committed diff against the merge base
|
|
1668
|
+
git diff $MERGE_BASE HEAD
|
|
1669
|
+
|
|
1670
|
+
# Get any uncommitted changes (staged and unstaged)
|
|
1671
|
+
git diff HEAD
|
|
1672
|
+
\`\`\`
|
|
1673
|
+
|
|
1674
|
+
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.
|
|
1675
|
+
|
|
1676
|
+
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
1677
|
+
|
|
1678
|
+
## Output format
|
|
1679
|
+
|
|
1680
|
+
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
1681
|
+
|
|
1682
|
+
Only report ONE finding per unique issue.
|
|
1683
|
+
|
|
1684
|
+
<example>
|
|
1685
|
+
## Recommendation
|
|
1686
|
+
|
|
1687
|
+
**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.
|
|
1688
|
+
|
|
1689
|
+
## Findings
|
|
1690
|
+
|
|
1691
|
+
### **#1 Empty input causes crash** (blocking)
|
|
1692
|
+
|
|
1693
|
+
If the input field is empty when the page loads, the app will crash.
|
|
1694
|
+
|
|
1695
|
+
File: src/client/frontends/desktop/ui/Input.tsx
|
|
1696
|
+
|
|
1697
|
+
### **#2 Dead code** (nit)
|
|
1698
|
+
|
|
1699
|
+
The getUserData function is now unused. It should be deleted.
|
|
1700
|
+
|
|
1701
|
+
File: src/client/frontends/desktop/core/UserData.ts
|
|
1702
|
+
</example>
|
|
1703
|
+
|
|
1704
|
+
<example>
|
|
1705
|
+
## Recommendation
|
|
1706
|
+
|
|
1707
|
+
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
1708
|
+
</example>
|
|
1709
|
+
|
|
1710
|
+
## Growing the rules
|
|
1711
|
+
|
|
1712
|
+
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.
|
|
1713
|
+
`;
|
|
1714
|
+
var REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
|
|
1715
|
+
var REVIEW_SKILL_NAME = "review";
|
|
1716
|
+
var REVIEW_SKILL_FRONTMATTER = `---
|
|
1717
|
+
name: review
|
|
1718
|
+
description: >-
|
|
1719
|
+
Merge-readiness review for this repository. Use when reviewing a PR, worktree
|
|
1720
|
+
diff, or Sideboard Review (Approve / Approve with nits / Request changes).
|
|
1721
|
+
---
|
|
1722
|
+
|
|
1723
|
+
`;
|
|
1724
|
+
function wrapReviewSkillMarkdown(body) {
|
|
1725
|
+
const trimmed = body.replace(/^\uFEFF/, "").trim();
|
|
1726
|
+
const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
|
|
1727
|
+
if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
|
|
1728
|
+
return source.endsWith("\n") ? source : `${source}
|
|
1729
|
+
`;
|
|
1730
|
+
}
|
|
1731
|
+
return `${REVIEW_SKILL_FRONTMATTER}${source}
|
|
1732
|
+
`;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/review/request-review.ts
|
|
1736
|
+
var REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
1737
|
+
var REPO_REVIEW_NAME = "review.md";
|
|
1738
|
+
var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
1739
|
+
var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
1740
|
+
var REVIEW_REQUEST_NAME = "Review request.md";
|
|
1741
|
+
var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
1742
|
+
var LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
1743
|
+
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
1744
|
+
"HOW MANY FINDINGS TO RETURN:"
|
|
1745
|
+
];
|
|
1746
|
+
function shouldRefreshReviewRequestTemplate(content) {
|
|
1747
|
+
const trimmed = content.trim();
|
|
1748
|
+
if (!trimmed) return true;
|
|
1749
|
+
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
1753
|
+
}
|
|
1754
|
+
function readTextIfPresent(abs) {
|
|
1755
|
+
if (!existsSync3(abs)) return null;
|
|
1756
|
+
try {
|
|
1757
|
+
const content = readFileSync3(abs, "utf8");
|
|
1758
|
+
return content.trim() ? content : null;
|
|
1759
|
+
} catch {
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function readLocalGuidelines(worktreePath) {
|
|
1764
|
+
const localAbs = join4(worktreePath, REVIEW_REQUEST_PATH);
|
|
1765
|
+
const localContent = readTextIfPresent(localAbs);
|
|
1766
|
+
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
1767
|
+
return { path: REVIEW_REQUEST_PATH, content: localContent };
|
|
1768
|
+
}
|
|
1769
|
+
const legacyAbs = join4(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
1770
|
+
const legacyContent = readTextIfPresent(legacyAbs);
|
|
1771
|
+
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
1772
|
+
return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
|
|
1773
|
+
}
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
function skillGuidelines(content, source) {
|
|
1777
|
+
return {
|
|
1778
|
+
path: REVIEW_SKILL_PATH,
|
|
1779
|
+
name: REVIEW_SKILL_NAME,
|
|
1780
|
+
content,
|
|
1781
|
+
source
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function ensureReviewSkillFile(worktreePath) {
|
|
1785
|
+
const abs = join4(worktreePath, REVIEW_SKILL_PATH);
|
|
1786
|
+
const existing = readTextIfPresent(abs);
|
|
1787
|
+
if (existing) {
|
|
1788
|
+
return { path: REVIEW_SKILL_PATH, content: existing, wrote: false };
|
|
1789
|
+
}
|
|
1790
|
+
const fromRepo = readTextIfPresent(join4(worktreePath, REPO_REVIEW_PATH));
|
|
1791
|
+
const fromLocal = readLocalGuidelines(worktreePath)?.content ?? null;
|
|
1792
|
+
const content = wrapReviewSkillMarkdown(fromRepo ?? fromLocal ?? REVIEW_REQUEST_TEMPLATE);
|
|
1793
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
1794
|
+
writeFileSync(abs, content, "utf8");
|
|
1795
|
+
return { path: REVIEW_SKILL_PATH, content, wrote: true };
|
|
1796
|
+
}
|
|
1797
|
+
function resolveReviewGuidelines(worktreePath) {
|
|
1798
|
+
const skillContent = readTextIfPresent(join4(worktreePath, REVIEW_SKILL_PATH));
|
|
1799
|
+
if (skillContent) return skillGuidelines(skillContent, "skill");
|
|
1800
|
+
const local = readLocalGuidelines(worktreePath);
|
|
1801
|
+
if (local) {
|
|
1802
|
+
return {
|
|
1803
|
+
path: local.path,
|
|
1804
|
+
name: REVIEW_REQUEST_NAME,
|
|
1805
|
+
content: local.content,
|
|
1806
|
+
source: "local"
|
|
1807
|
+
};
|
|
1492
1808
|
}
|
|
1493
|
-
|
|
1809
|
+
const seeded = ensureReviewSkillFile(worktreePath);
|
|
1810
|
+
return skillGuidelines(seeded.content, seeded.wrote ? "stock" : "skill");
|
|
1494
1811
|
}
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
1499
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
1500
|
-
worktreePath,
|
|
1501
|
-
repoPath,
|
|
1502
|
-
ports,
|
|
1503
|
-
defaultBranch: opts?.defaultBranch,
|
|
1504
|
-
onLine
|
|
1505
|
-
});
|
|
1812
|
+
function buildReviewRequestAttachment(content, opts) {
|
|
1813
|
+
const path = opts?.path ?? REVIEW_SKILL_PATH;
|
|
1814
|
+
const name = opts?.name ?? (path === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
|
|
1506
1815
|
return {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
done: handle.done
|
|
1816
|
+
id: randomUUID2(),
|
|
1817
|
+
name,
|
|
1818
|
+
kind: "file",
|
|
1819
|
+
path,
|
|
1820
|
+
content
|
|
1513
1821
|
};
|
|
1514
1822
|
}
|
|
1823
|
+
async function requestReview(threadRef, send) {
|
|
1824
|
+
const from = findThreadByRef(threadRef);
|
|
1825
|
+
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
1826
|
+
if (isOrchestratorThread(from)) {
|
|
1827
|
+
throw new Error(
|
|
1828
|
+
"request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
if (from.status === "archived") {
|
|
1832
|
+
throw new Error(`Thread is archived: ${from.id}`);
|
|
1833
|
+
}
|
|
1834
|
+
const guidelines = resolveReviewGuidelines(from.worktreePath);
|
|
1835
|
+
const tab = createChatTab({
|
|
1836
|
+
fromThreadId: from.id,
|
|
1837
|
+
title: "Review",
|
|
1838
|
+
attachments: [
|
|
1839
|
+
buildReviewRequestAttachment(guidelines.content, {
|
|
1840
|
+
path: guidelines.path,
|
|
1841
|
+
name: guidelines.name
|
|
1842
|
+
})
|
|
1843
|
+
]
|
|
1844
|
+
});
|
|
1845
|
+
const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
|
|
1846
|
+
return { tab: started, from };
|
|
1847
|
+
}
|
|
1515
1848
|
|
|
1516
|
-
// src/
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
return
|
|
1849
|
+
// src/hook/conductor.ts
|
|
1850
|
+
var PORT_RANGE_SIZE = 10;
|
|
1851
|
+
function matchSimpleGlob(pattern, name) {
|
|
1852
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1853
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
1521
1854
|
}
|
|
1522
|
-
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const
|
|
1529
|
-
if (
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
1535
|
-
} catch {
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
const orphans = [];
|
|
1539
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1540
|
-
for (const repoPath of repos) {
|
|
1541
|
-
if (!repoPath || !existsSync4(repoPath)) continue;
|
|
1542
|
-
try {
|
|
1543
|
-
const wts = await listWorktrees(repoPath);
|
|
1544
|
-
for (const wt of wts) {
|
|
1545
|
-
const path = wt.path.replace(/\/$/, "");
|
|
1546
|
-
if (!isSideboardWorktreePath(path)) continue;
|
|
1547
|
-
if (known.has(path)) continue;
|
|
1548
|
-
if (seen.has(path)) continue;
|
|
1549
|
-
seen.add(path);
|
|
1550
|
-
let mtimeMs = 0;
|
|
1551
|
-
try {
|
|
1552
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
1553
|
-
} catch {
|
|
1554
|
-
mtimeMs = 0;
|
|
1555
|
-
}
|
|
1556
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
1557
|
-
}
|
|
1558
|
-
} catch {
|
|
1559
|
-
}
|
|
1855
|
+
function readWorktreeInclude(repoPath) {
|
|
1856
|
+
const path = join5(repoPath, ".worktreeinclude");
|
|
1857
|
+
if (!existsSync4(path)) return [];
|
|
1858
|
+
return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
1859
|
+
}
|
|
1860
|
+
function resolveFilesToCopy(repoPath) {
|
|
1861
|
+
const fromInclude = readWorktreeInclude(repoPath);
|
|
1862
|
+
if (fromInclude.length) return fromInclude;
|
|
1863
|
+
const settings = loadRepoSettings(repoPath);
|
|
1864
|
+
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
1865
|
+
if (settings?.fileIncludeGlobs?.length) {
|
|
1866
|
+
const matched = [];
|
|
1560
1867
|
try {
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
for (const
|
|
1564
|
-
if (
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
if (!existsSync4(join5(path, ".git"))) continue;
|
|
1568
|
-
seen.add(path);
|
|
1569
|
-
let mtimeMs = 0;
|
|
1570
|
-
try {
|
|
1571
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
1572
|
-
} catch {
|
|
1573
|
-
mtimeMs = Date.now();
|
|
1868
|
+
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1869
|
+
if (!entry.isFile()) continue;
|
|
1870
|
+
for (const glob of settings.fileIncludeGlobs) {
|
|
1871
|
+
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
1872
|
+
matched.push(entry.name);
|
|
1873
|
+
break;
|
|
1574
1874
|
}
|
|
1575
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
1576
1875
|
}
|
|
1577
1876
|
}
|
|
1578
1877
|
} catch {
|
|
1579
1878
|
}
|
|
1879
|
+
if (matched.length) return [...new Set(matched)];
|
|
1580
1880
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
1588
|
-
const budget = Math.max(0, maxCount - activeCount);
|
|
1589
|
-
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1590
|
-
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
1591
|
-
const removed = [];
|
|
1592
|
-
const kept = [];
|
|
1593
|
-
for (const orphan of orphans) {
|
|
1594
|
-
if (keep.has(orphan.path)) {
|
|
1595
|
-
kept.push(orphan.path);
|
|
1596
|
-
continue;
|
|
1597
|
-
}
|
|
1598
|
-
if (opts?.dryRun) {
|
|
1599
|
-
removed.push(orphan.path);
|
|
1600
|
-
continue;
|
|
1601
|
-
}
|
|
1602
|
-
try {
|
|
1603
|
-
await removeWorktree(orphan.repoPath, orphan.path);
|
|
1604
|
-
removed.push(orphan.path);
|
|
1605
|
-
} catch {
|
|
1606
|
-
kept.push(orphan.path);
|
|
1881
|
+
const defaults = [];
|
|
1882
|
+
try {
|
|
1883
|
+
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1884
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
1885
|
+
defaults.push(entry.name);
|
|
1886
|
+
}
|
|
1607
1887
|
}
|
|
1888
|
+
} catch {
|
|
1608
1889
|
}
|
|
1609
|
-
if (
|
|
1610
|
-
|
|
1611
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1612
|
-
});
|
|
1613
|
-
}
|
|
1614
|
-
return { removed, kept, orphans };
|
|
1615
|
-
}
|
|
1616
|
-
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
1617
|
-
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
1618
|
-
const last = settings.advanced.worktreeLastCleanupAt;
|
|
1619
|
-
if (!last) return true;
|
|
1620
|
-
const elapsed = Date.now() - Date.parse(last);
|
|
1621
|
-
return elapsed >= intervalHours * 36e5;
|
|
1890
|
+
if (defaults.length) return defaults;
|
|
1891
|
+
return [".env.local", ".env"];
|
|
1622
1892
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
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
|
-
);
|
|
1637
|
-
}
|
|
1638
|
-
const checkout = await execa3("git", ["checkout", target], {
|
|
1639
|
-
cwd: thread.repoPath,
|
|
1640
|
-
reject: false
|
|
1641
|
-
});
|
|
1642
|
-
if (checkout.exitCode !== 0) {
|
|
1643
|
-
throw new Error(
|
|
1644
|
-
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
|
-
if (method === "cherry-pick") {
|
|
1648
|
-
const log = await execa3(
|
|
1649
|
-
"git",
|
|
1650
|
-
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
1651
|
-
{ cwd: thread.repoPath, reject: false }
|
|
1652
|
-
);
|
|
1653
|
-
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1654
|
-
if (!shas.length) {
|
|
1655
|
-
return {
|
|
1656
|
-
applied: false,
|
|
1657
|
-
method,
|
|
1658
|
-
targetBranch: target,
|
|
1659
|
-
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
|
-
for (const sha of shas) {
|
|
1663
|
-
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
1664
|
-
cwd: thread.repoPath,
|
|
1665
|
-
reject: false
|
|
1666
|
-
});
|
|
1667
|
-
if (cp.exitCode !== 0) {
|
|
1668
|
-
await execa3("git", ["cherry-pick", "--abort"], {
|
|
1669
|
-
cwd: thread.repoPath,
|
|
1670
|
-
reject: false
|
|
1671
|
-
});
|
|
1672
|
-
throw new Error(
|
|
1673
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
1674
|
-
);
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
return {
|
|
1678
|
-
applied: true,
|
|
1679
|
-
method,
|
|
1680
|
-
targetBranch: target,
|
|
1681
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
1682
|
-
};
|
|
1893
|
+
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
1894
|
+
const patterns = resolveFilesToCopy(repoPath);
|
|
1895
|
+
const copied = [];
|
|
1896
|
+
for (const rel of patterns) {
|
|
1897
|
+
const src = join5(repoPath, rel);
|
|
1898
|
+
if (!existsSync4(src)) continue;
|
|
1899
|
+
const dest = join5(worktreePath, rel);
|
|
1900
|
+
mkdirSync2(dirname2(dest), { recursive: true });
|
|
1901
|
+
copyFileSync(src, dest);
|
|
1902
|
+
copied.push(rel);
|
|
1683
1903
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
);
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
reject: false
|
|
1904
|
+
return copied;
|
|
1905
|
+
}
|
|
1906
|
+
var cachedLoginEnv = null;
|
|
1907
|
+
async function captureLoginEnv() {
|
|
1908
|
+
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
1909
|
+
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
1910
|
+
try {
|
|
1911
|
+
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
1912
|
+
reject: false,
|
|
1913
|
+
timeout: 5e3
|
|
1693
1914
|
});
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1915
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
1916
|
+
const env = { ...process.env };
|
|
1917
|
+
for (const entry of result.stdout.split("\0")) {
|
|
1918
|
+
if (!entry) continue;
|
|
1919
|
+
const eq = entry.indexOf("=");
|
|
1920
|
+
if (eq <= 0) continue;
|
|
1921
|
+
const key = entry.slice(0, eq);
|
|
1922
|
+
const value = entry.slice(eq + 1);
|
|
1923
|
+
if (key) env[key] = value;
|
|
1924
|
+
}
|
|
1925
|
+
cachedLoginEnv = env;
|
|
1926
|
+
return { ...env };
|
|
1927
|
+
}
|
|
1928
|
+
} catch {
|
|
1697
1929
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
method: "merge",
|
|
1701
|
-
targetBranch: target,
|
|
1702
|
-
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
1703
|
-
};
|
|
1930
|
+
cachedLoginEnv = { ...process.env };
|
|
1931
|
+
return { ...cachedLoginEnv };
|
|
1704
1932
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
`
|
|
1729
|
-
|
|
1933
|
+
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
1934
|
+
const env = stripNestedElectronEnv({
|
|
1935
|
+
...baseEnv ?? process.env
|
|
1936
|
+
});
|
|
1937
|
+
const name = opts.workspaceName ?? basename(opts.worktreePath);
|
|
1938
|
+
const ports = opts.ports ?? [];
|
|
1939
|
+
const primary = ports[0];
|
|
1940
|
+
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
1941
|
+
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
1942
|
+
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
1943
|
+
env.SIDEBOARD_IS_LOCAL = "1";
|
|
1944
|
+
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1945
|
+
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
1946
|
+
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
1947
|
+
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
1948
|
+
env.CONDUCTOR_IS_LOCAL = "1";
|
|
1949
|
+
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1950
|
+
if (primary != null) {
|
|
1951
|
+
env.SIDEBOARD_PORT = String(primary);
|
|
1952
|
+
env.CONDUCTOR_PORT = String(primary);
|
|
1953
|
+
env.PORT = String(primary);
|
|
1954
|
+
for (let i = 1; i < ports.length; i++) {
|
|
1955
|
+
const p = ports[i];
|
|
1956
|
+
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
1957
|
+
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
1958
|
+
}
|
|
1730
1959
|
}
|
|
1731
|
-
|
|
1732
|
-
const workspace = await ensureWorkspace(repoPath);
|
|
1733
|
-
return { repoPath, workspace };
|
|
1960
|
+
return env;
|
|
1734
1961
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
function desktopHostPidPath() {
|
|
1740
|
-
return join7(appDataDir(), "desktop-host.pid");
|
|
1962
|
+
function pipeLines(stream, onLine) {
|
|
1963
|
+
if (!stream || !onLine) return;
|
|
1964
|
+
const rl = createInterface2({ input: stream });
|
|
1965
|
+
rl.on("line", onLine);
|
|
1741
1966
|
}
|
|
1742
|
-
function
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1967
|
+
function killScriptTree(child, ports = []) {
|
|
1968
|
+
const pid = child.pid;
|
|
1969
|
+
if (pid) {
|
|
1970
|
+
try {
|
|
1971
|
+
if (process.platform === "win32") {
|
|
1972
|
+
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
1973
|
+
} else {
|
|
1974
|
+
process.kill(-pid, "SIGTERM");
|
|
1975
|
+
setTimeout(() => {
|
|
1976
|
+
try {
|
|
1977
|
+
process.kill(-pid, "SIGKILL");
|
|
1978
|
+
} catch {
|
|
1979
|
+
}
|
|
1980
|
+
}, 2500).unref?.();
|
|
1981
|
+
}
|
|
1982
|
+
} catch {
|
|
1983
|
+
try {
|
|
1984
|
+
child.kill("SIGTERM");
|
|
1985
|
+
} catch {
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
for (const port of ports) {
|
|
1990
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
1991
|
+
if (process.platform === "win32") {
|
|
1992
|
+
void execa2(
|
|
1993
|
+
"powershell",
|
|
1994
|
+
[
|
|
1995
|
+
"-NoProfile",
|
|
1996
|
+
"-Command",
|
|
1997
|
+
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
1998
|
+
],
|
|
1999
|
+
{ reject: false }
|
|
2000
|
+
);
|
|
2001
|
+
} else {
|
|
2002
|
+
void execa2(
|
|
2003
|
+
"zsh",
|
|
2004
|
+
["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
|
|
2005
|
+
{ reject: false }
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
1749
2008
|
}
|
|
1750
2009
|
}
|
|
1751
|
-
function
|
|
2010
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
2011
|
+
const loginEnv = await captureLoginEnv();
|
|
2012
|
+
const env = buildWorkspaceScriptEnv(
|
|
2013
|
+
{
|
|
2014
|
+
worktreePath: opts.worktreePath,
|
|
2015
|
+
repoPath: opts.repoPath,
|
|
2016
|
+
workspaceName: opts.workspaceName,
|
|
2017
|
+
defaultBranch: opts.defaultBranch,
|
|
2018
|
+
ports: opts.ports
|
|
2019
|
+
},
|
|
2020
|
+
loginEnv
|
|
2021
|
+
);
|
|
1752
2022
|
try {
|
|
1753
|
-
|
|
1754
|
-
|
|
2023
|
+
mergeAgentGitAuthEnv(
|
|
2024
|
+
env,
|
|
2025
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2026
|
+
);
|
|
1755
2027
|
} catch {
|
|
1756
|
-
return false;
|
|
1757
2028
|
}
|
|
2029
|
+
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
2030
|
+
const child = execa2(shell, ["-lc", command], {
|
|
2031
|
+
cwd: opts.worktreePath,
|
|
2032
|
+
reject: false,
|
|
2033
|
+
env,
|
|
2034
|
+
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2035
|
+
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2036
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
2037
|
+
});
|
|
2038
|
+
pipeLines(child.stdout, opts.onLine);
|
|
2039
|
+
pipeLines(child.stderr, opts.onLine);
|
|
2040
|
+
const ports = opts.ports ?? [];
|
|
2041
|
+
return {
|
|
2042
|
+
pid: child.pid,
|
|
2043
|
+
kill: () => killScriptTree(child, ports),
|
|
2044
|
+
done: child.then((r) => r.exitCode ?? null),
|
|
2045
|
+
child
|
|
2046
|
+
};
|
|
1758
2047
|
}
|
|
1759
|
-
function
|
|
1760
|
-
|
|
1761
|
-
|
|
2048
|
+
async function attachAbort(handle, signal) {
|
|
2049
|
+
if (signal) {
|
|
2050
|
+
const onAbort = () => handle.kill();
|
|
2051
|
+
if (signal.aborted) onAbort();
|
|
2052
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2053
|
+
}
|
|
2054
|
+
return handle.done;
|
|
1762
2055
|
}
|
|
1763
|
-
function
|
|
1764
|
-
|
|
2056
|
+
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
2057
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2058
|
+
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
2059
|
+
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
2060
|
+
worktreePath,
|
|
2061
|
+
repoPath,
|
|
2062
|
+
defaultBranch: opts?.defaultBranch,
|
|
2063
|
+
onLine
|
|
2064
|
+
});
|
|
2065
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2066
|
+
return {
|
|
2067
|
+
ran: true,
|
|
2068
|
+
exitCode,
|
|
2069
|
+
source: workspaceSettingsSourceLabel(worktreePath, repoPath),
|
|
2070
|
+
kill: handle.kill
|
|
2071
|
+
};
|
|
1765
2072
|
}
|
|
1766
|
-
function
|
|
1767
|
-
|
|
1768
|
-
|
|
2073
|
+
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
2074
|
+
const found = findConventionSetup(worktreePath, repoPath);
|
|
2075
|
+
if (!found) return { ran: false, exitCode: null, source: null };
|
|
2076
|
+
onLine?.(`[setup] ${found.source}`);
|
|
2077
|
+
const handle = await spawnWorkspaceScript(found.command, {
|
|
2078
|
+
worktreePath,
|
|
2079
|
+
repoPath,
|
|
2080
|
+
defaultBranch: opts?.defaultBranch,
|
|
2081
|
+
onLine
|
|
2082
|
+
});
|
|
2083
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2084
|
+
return {
|
|
2085
|
+
ran: true,
|
|
2086
|
+
exitCode,
|
|
2087
|
+
source: found.source,
|
|
2088
|
+
kill: handle.kill
|
|
2089
|
+
};
|
|
1769
2090
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
|
|
1776
|
-
async function requireAgent(agent, opts) {
|
|
1777
|
-
ensureAgentPath();
|
|
1778
|
-
enrichPathWithNpmGlobalBin();
|
|
1779
|
-
const status = await Promise.race([
|
|
1780
|
-
getAdapter(agent).detect(),
|
|
1781
|
-
new Promise((_, reject) => {
|
|
1782
|
-
setTimeout(() => {
|
|
1783
|
-
reject(
|
|
1784
|
-
new Error(
|
|
1785
|
-
`${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
|
|
1786
|
-
)
|
|
1787
|
-
);
|
|
1788
|
-
}, REQUIRE_AGENT_TIMEOUT_MS);
|
|
1789
|
-
})
|
|
1790
|
-
]);
|
|
1791
|
-
if (!status.installed || !status.authenticated) {
|
|
1792
|
-
throw new Error(status.reason ?? `${agent} is not available`);
|
|
1793
|
-
}
|
|
1794
|
-
if (opts?.requireLinear && !status.linearMcp) {
|
|
1795
|
-
throw new Error(
|
|
1796
|
-
`${agent} has no Linear MCP server configured \u2014 ticket sources require it`
|
|
2091
|
+
async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
|
|
2092
|
+
const reviewSkill = ensureReviewSkillFile(worktreePath);
|
|
2093
|
+
if (reviewSkill.wrote) {
|
|
2094
|
+
onLine?.(
|
|
2095
|
+
`[setup] wrote ${REVIEW_SKILL_PATH} \u2014 commit it so later worktrees inherit Review guidelines`
|
|
1797
2096
|
);
|
|
1798
2097
|
}
|
|
1799
|
-
|
|
2098
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
2099
|
+
if (!setup.ran) {
|
|
2100
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
2101
|
+
}
|
|
2102
|
+
if (!setup.ran) {
|
|
2103
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
2104
|
+
}
|
|
2105
|
+
return setup;
|
|
2106
|
+
}
|
|
2107
|
+
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
2108
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2109
|
+
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
2110
|
+
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
2111
|
+
worktreePath,
|
|
2112
|
+
repoPath,
|
|
2113
|
+
onLine
|
|
2114
|
+
});
|
|
2115
|
+
const exitCode = await handle.done;
|
|
2116
|
+
return { ran: true, exitCode };
|
|
1800
2117
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
effort: input.effort,
|
|
1808
|
-
fast: input.fast
|
|
2118
|
+
function listRunScripts(worktreePath, repoPath) {
|
|
2119
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2120
|
+
if (!settings?.runScripts.length) return [];
|
|
2121
|
+
return settings.runScripts.filter((s) => {
|
|
2122
|
+
if (!s.availableIn?.length) return true;
|
|
2123
|
+
return s.availableIn.includes("local");
|
|
1809
2124
|
});
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2125
|
+
}
|
|
2126
|
+
function getDefaultRunScript(worktreePath, repoPath) {
|
|
2127
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2128
|
+
if (!scripts.length) return null;
|
|
2129
|
+
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
2130
|
+
}
|
|
2131
|
+
function getRunScript(worktreePath, repoPath, name) {
|
|
2132
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2133
|
+
if (!scripts.length) return null;
|
|
2134
|
+
if (name) {
|
|
2135
|
+
return scripts.find((s) => s.name === name) ?? null;
|
|
1814
2136
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
} else if (input.sourceType === "ticket") {
|
|
1830
|
-
sourceRef = await resolveDefaultBranch(repoPath);
|
|
1831
|
-
} else if (input.sourceType === "branch") {
|
|
1832
|
-
if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
|
|
1833
|
-
sourceRef = await resolveDefaultBranch(repoPath);
|
|
1834
|
-
} else {
|
|
1835
|
-
const existing = await getPrForHeadBranch(repoPath, sourceRef);
|
|
1836
|
-
if (existing?.url) {
|
|
1837
|
-
prUrl = existing.url;
|
|
1838
|
-
prTitle = existing.title;
|
|
2137
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
2138
|
+
}
|
|
2139
|
+
function getRunMode(worktreePath, repoPath) {
|
|
2140
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2141
|
+
}
|
|
2142
|
+
async function allocatePort() {
|
|
2143
|
+
return new Promise((resolve, reject) => {
|
|
2144
|
+
const server = createServer();
|
|
2145
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2146
|
+
const addr = server.address();
|
|
2147
|
+
if (!addr || typeof addr === "string") {
|
|
2148
|
+
server.close();
|
|
2149
|
+
reject(new Error("Failed to allocate port"));
|
|
2150
|
+
return;
|
|
1839
2151
|
}
|
|
2152
|
+
const port = addr.port;
|
|
2153
|
+
server.close((err) => err ? reject(err) : resolve(port));
|
|
2154
|
+
});
|
|
2155
|
+
server.on("error", reject);
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
async function allocatePortRange(size = PORT_RANGE_SIZE) {
|
|
2159
|
+
const base = await allocatePort();
|
|
2160
|
+
const ports = [base];
|
|
2161
|
+
for (let i = 1; i < size; i++) {
|
|
2162
|
+
const candidate = base + i;
|
|
2163
|
+
const free = await new Promise((resolve) => {
|
|
2164
|
+
const server = createServer();
|
|
2165
|
+
server.once("error", () => resolve(false));
|
|
2166
|
+
server.listen(candidate, "127.0.0.1", () => {
|
|
2167
|
+
server.close(() => resolve(true));
|
|
2168
|
+
});
|
|
2169
|
+
});
|
|
2170
|
+
if (free) {
|
|
2171
|
+
ports.push(candidate);
|
|
2172
|
+
} else {
|
|
2173
|
+
ports.push(await allocatePort());
|
|
1840
2174
|
}
|
|
1841
|
-
} else if (input.sourceType === "adopt") {
|
|
1842
|
-
throw new Error("Use adoptThread() for adopt sources");
|
|
1843
2175
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
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,
|
|
2176
|
+
return ports;
|
|
2177
|
+
}
|
|
2178
|
+
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
2179
|
+
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2180
|
+
if (!script) return null;
|
|
2181
|
+
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
2182
|
+
const handle = await spawnWorkspaceScript(script.command, {
|
|
1858
2183
|
worktreePath,
|
|
1859
2184
|
repoPath,
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
|
2185
|
+
ports,
|
|
2186
|
+
defaultBranch: opts?.defaultBranch,
|
|
2187
|
+
onLine
|
|
1872
2188
|
});
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
if (!adapter.listLinearIssues) {
|
|
1882
|
-
throw new Error(`${agent} does not support Linear issue listing`);
|
|
1883
|
-
}
|
|
1884
|
-
return adapter.listLinearIssues(repoPath);
|
|
2189
|
+
return {
|
|
2190
|
+
pid: handle.pid,
|
|
2191
|
+
port: ports[0],
|
|
2192
|
+
ports,
|
|
2193
|
+
scriptName: script.name,
|
|
2194
|
+
kill: handle.kill,
|
|
2195
|
+
done: handle.done
|
|
2196
|
+
};
|
|
1885
2197
|
}
|
|
1886
2198
|
|
|
1887
|
-
// src/
|
|
1888
|
-
import {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
var MAX_SUMMARY_CHARS = 6e3;
|
|
1893
|
-
async function summarizeConversation(transcript, opts) {
|
|
1894
|
-
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
1895
|
-
|
|
1896
|
-
[\u2026truncated\u2026]` : transcript;
|
|
1897
|
-
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
1898
|
-
if (claudeSummary) {
|
|
1899
|
-
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
1900
|
-
}
|
|
1901
|
-
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
2199
|
+
// src/git/orphan-cleanup.ts
|
|
2200
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, statSync } from "fs";
|
|
2201
|
+
import { join as join6 } from "path";
|
|
2202
|
+
function isSideboardWorktreePath(path) {
|
|
2203
|
+
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
1902
2204
|
}
|
|
1903
|
-
async function
|
|
1904
|
-
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
"
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2205
|
+
async function findOrphanWorktrees(repoPaths) {
|
|
2206
|
+
const threads = listThreads({ includeArchived: true });
|
|
2207
|
+
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
2208
|
+
const repos = new Set(
|
|
2209
|
+
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
2210
|
+
);
|
|
2211
|
+
const homeRoot = sideboardWorkspacesDir();
|
|
2212
|
+
if (existsSync5(homeRoot)) {
|
|
2213
|
+
try {
|
|
2214
|
+
for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
|
|
2215
|
+
if (!entry.isDirectory()) continue;
|
|
2216
|
+
void entry;
|
|
2217
|
+
}
|
|
2218
|
+
} catch {
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
const orphans = [];
|
|
2222
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2223
|
+
for (const repoPath of repos) {
|
|
2224
|
+
if (!repoPath || !existsSync5(repoPath)) continue;
|
|
2225
|
+
try {
|
|
2226
|
+
const wts = await listWorktrees(repoPath);
|
|
2227
|
+
for (const wt of wts) {
|
|
2228
|
+
const path = wt.path.replace(/\/$/, "");
|
|
2229
|
+
if (!isSideboardWorktreePath(path)) continue;
|
|
2230
|
+
if (known.has(path)) continue;
|
|
2231
|
+
if (seen.has(path)) continue;
|
|
2232
|
+
seen.add(path);
|
|
2233
|
+
let mtimeMs = 0;
|
|
2234
|
+
try {
|
|
2235
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
2236
|
+
} catch {
|
|
2237
|
+
mtimeMs = 0;
|
|
2238
|
+
}
|
|
2239
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
2240
|
+
}
|
|
2241
|
+
} catch {
|
|
2242
|
+
}
|
|
2243
|
+
try {
|
|
2244
|
+
const root = worktreesRoot(repoPath);
|
|
2245
|
+
if (existsSync5(root)) {
|
|
2246
|
+
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
2247
|
+
if (!entry.isDirectory()) continue;
|
|
2248
|
+
const path = join6(root, entry.name).replace(/\/$/, "");
|
|
2249
|
+
if (known.has(path) || seen.has(path)) continue;
|
|
2250
|
+
if (!existsSync5(join6(path, ".git"))) continue;
|
|
2251
|
+
seen.add(path);
|
|
2252
|
+
let mtimeMs = 0;
|
|
2253
|
+
try {
|
|
2254
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
2255
|
+
} catch {
|
|
2256
|
+
mtimeMs = Date.now();
|
|
2257
|
+
}
|
|
2258
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
} catch {
|
|
2262
|
+
}
|
|
1939
2263
|
}
|
|
2264
|
+
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
1940
2265
|
}
|
|
1941
|
-
function
|
|
1942
|
-
const
|
|
1943
|
-
const
|
|
1944
|
-
const
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
2266
|
+
async function cleanupOrphanWorktrees(opts) {
|
|
2267
|
+
const settings = loadAppSettings();
|
|
2268
|
+
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
2269
|
+
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
2270
|
+
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
2271
|
+
const budget = Math.max(0, maxCount - activeCount);
|
|
2272
|
+
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2273
|
+
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
2274
|
+
const removed = [];
|
|
2275
|
+
const kept = [];
|
|
2276
|
+
for (const orphan of orphans) {
|
|
2277
|
+
if (keep.has(orphan.path)) {
|
|
2278
|
+
kept.push(orphan.path);
|
|
1954
2279
|
continue;
|
|
1955
2280
|
}
|
|
1956
|
-
if (
|
|
1957
|
-
|
|
2281
|
+
if (opts?.dryRun) {
|
|
2282
|
+
removed.push(orphan.path);
|
|
1958
2283
|
continue;
|
|
1959
2284
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2285
|
+
try {
|
|
2286
|
+
await removeWorktree(orphan.repoPath, orphan.path);
|
|
2287
|
+
removed.push(orphan.path);
|
|
2288
|
+
} catch {
|
|
2289
|
+
kept.push(orphan.path);
|
|
1964
2290
|
}
|
|
1965
2291
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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}`));
|
|
2292
|
+
if (!opts?.dryRun) {
|
|
2293
|
+
updateAdvancedSettings({
|
|
2294
|
+
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2295
|
+
});
|
|
1978
2296
|
}
|
|
1979
|
-
|
|
1980
|
-
"",
|
|
1981
|
-
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
1982
|
-
);
|
|
1983
|
-
return parts.join("\n");
|
|
2297
|
+
return { removed, kept, orphans };
|
|
1984
2298
|
}
|
|
1985
|
-
function
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2299
|
+
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
2300
|
+
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
2301
|
+
const last = settings.advanced.worktreeLastCleanupAt;
|
|
2302
|
+
if (!last) return true;
|
|
2303
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
2304
|
+
return elapsed >= intervalHours * 36e5;
|
|
1990
2305
|
}
|
|
1991
2306
|
|
|
1992
|
-
// src/
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
switch (part.type) {
|
|
2006
|
-
case "text":
|
|
2007
|
-
case "thinking":
|
|
2008
|
-
return part.text.length;
|
|
2009
|
-
case "tool": {
|
|
2010
|
-
const input = part.input ? JSON.stringify(part.input) : "";
|
|
2011
|
-
return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
|
|
2012
|
-
}
|
|
2013
|
-
default:
|
|
2014
|
-
return 0;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
function estimateThreadChars(messages) {
|
|
2018
|
-
return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
|
|
2019
|
-
}
|
|
2020
|
-
function shouldCompactContext(messages, thresholds = {}) {
|
|
2021
|
-
const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
|
|
2022
|
-
const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
|
|
2023
|
-
if (messages.length < minMessages) return false;
|
|
2024
|
-
return estimateThreadChars(messages) >= maxChars;
|
|
2025
|
-
}
|
|
2026
|
-
function splitForCompaction(messages, thresholds = {}) {
|
|
2027
|
-
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
2028
|
-
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
2029
|
-
if (messages.length === 0) return { older: [], recent: [] };
|
|
2030
|
-
let recentChars = 0;
|
|
2031
|
-
let cut = messages.length;
|
|
2032
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2033
|
-
const next = recentChars + estimateMessageChars(messages[i]);
|
|
2034
|
-
const kept = messages.length - i;
|
|
2035
|
-
if (kept >= keepCount && next > keepChars) break;
|
|
2036
|
-
recentChars = next;
|
|
2037
|
-
cut = i;
|
|
2038
|
-
}
|
|
2039
|
-
const minRecent = Math.min(keepCount, messages.length);
|
|
2040
|
-
cut = Math.min(cut, messages.length - minRecent);
|
|
2041
|
-
if (cut <= 0) return { older: [], recent: messages };
|
|
2042
|
-
return {
|
|
2043
|
-
older: messages.slice(0, cut),
|
|
2044
|
-
recent: messages.slice(cut)
|
|
2045
|
-
};
|
|
2046
|
-
}
|
|
2047
|
-
function formatToolPart(t, detail) {
|
|
2048
|
-
if (detail === "summary") {
|
|
2049
|
-
const label = t.description || t.detail || t.name;
|
|
2050
|
-
const path = t.filePath ? ` (${t.filePath})` : "";
|
|
2051
|
-
return `- ${t.name}: ${label}${path}`;
|
|
2052
|
-
}
|
|
2053
|
-
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
2054
|
-
if (t.description) lines.push(`Description: ${t.description}`);
|
|
2055
|
-
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
2056
|
-
if (t.input && Object.keys(t.input).length > 0) {
|
|
2057
|
-
lines.push("Input:");
|
|
2058
|
-
lines.push("```json");
|
|
2059
|
-
lines.push(JSON.stringify(t.input, null, 2));
|
|
2060
|
-
lines.push("```");
|
|
2061
|
-
} else if (t.detail) {
|
|
2062
|
-
lines.push(`Detail: ${t.detail}`);
|
|
2307
|
+
// src/git/apply-into-main.ts
|
|
2308
|
+
import { execa as execa3 } from "execa";
|
|
2309
|
+
async function applyThreadIntoMain(thread, opts) {
|
|
2310
|
+
const method = opts?.method ?? "merge";
|
|
2311
|
+
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
2312
|
+
const status = await execa3("git", ["status", "--porcelain"], {
|
|
2313
|
+
cwd: thread.repoPath,
|
|
2314
|
+
reject: false
|
|
2315
|
+
});
|
|
2316
|
+
if (status.stdout.trim()) {
|
|
2317
|
+
throw new Error(
|
|
2318
|
+
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
2319
|
+
);
|
|
2063
2320
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2321
|
+
const checkout = await execa3("git", ["checkout", target], {
|
|
2322
|
+
cwd: thread.repoPath,
|
|
2323
|
+
reject: false
|
|
2324
|
+
});
|
|
2325
|
+
if (checkout.exitCode !== 0) {
|
|
2326
|
+
throw new Error(
|
|
2327
|
+
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
2328
|
+
);
|
|
2069
2329
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
for (const m of messages) {
|
|
2076
|
-
if (m.role === "summary") {
|
|
2077
|
-
blocks.push(`## Prior summary
|
|
2078
|
-
${m.text}`);
|
|
2079
|
-
continue;
|
|
2080
|
-
}
|
|
2081
|
-
if (m.role === "user") {
|
|
2082
|
-
blocks.push(`### User
|
|
2083
|
-
${m.text}`);
|
|
2084
|
-
continue;
|
|
2085
|
-
}
|
|
2086
|
-
const bits = [];
|
|
2087
|
-
if (m.text?.trim()) {
|
|
2088
|
-
bits.push(`### Agent
|
|
2089
|
-
${m.text}`);
|
|
2090
|
-
} else {
|
|
2091
|
-
bits.push("### Agent");
|
|
2092
|
-
}
|
|
2093
|
-
const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
|
|
2094
|
-
for (const th of thinking) {
|
|
2095
|
-
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
2096
|
-
bits.push("Thinking:");
|
|
2097
|
-
bits.push(th.text);
|
|
2098
|
-
}
|
|
2099
|
-
const toolParts = (m.parts ?? []).filter(
|
|
2100
|
-
(p) => p.type === "tool"
|
|
2330
|
+
if (method === "cherry-pick") {
|
|
2331
|
+
const log = await execa3(
|
|
2332
|
+
"git",
|
|
2333
|
+
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
2334
|
+
{ cwd: thread.repoPath, reject: false }
|
|
2101
2335
|
);
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2336
|
+
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2337
|
+
if (!shas.length) {
|
|
2338
|
+
return {
|
|
2339
|
+
applied: false,
|
|
2340
|
+
method,
|
|
2341
|
+
targetBranch: target,
|
|
2342
|
+
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
for (const sha of shas) {
|
|
2346
|
+
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
2347
|
+
cwd: thread.repoPath,
|
|
2348
|
+
reject: false
|
|
2349
|
+
});
|
|
2350
|
+
if (cp.exitCode !== 0) {
|
|
2351
|
+
await execa3("git", ["cherry-pick", "--abort"], {
|
|
2352
|
+
cwd: thread.repoPath,
|
|
2353
|
+
reject: false
|
|
2354
|
+
});
|
|
2355
|
+
throw new Error(
|
|
2356
|
+
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
2357
|
+
);
|
|
2106
2358
|
}
|
|
2107
2359
|
}
|
|
2108
|
-
|
|
2360
|
+
return {
|
|
2361
|
+
applied: true,
|
|
2362
|
+
method,
|
|
2363
|
+
targetBranch: target,
|
|
2364
|
+
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
const merge = await execa3(
|
|
2368
|
+
"git",
|
|
2369
|
+
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
2370
|
+
{ cwd: thread.repoPath, reject: false }
|
|
2371
|
+
);
|
|
2372
|
+
if (merge.exitCode !== 0) {
|
|
2373
|
+
await execa3("git", ["merge", "--abort"], {
|
|
2374
|
+
cwd: thread.repoPath,
|
|
2375
|
+
reject: false
|
|
2376
|
+
});
|
|
2377
|
+
throw new Error(
|
|
2378
|
+
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
2379
|
+
);
|
|
2109
2380
|
}
|
|
2110
|
-
return
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
if (!body.trim()) return null;
|
|
2116
|
-
return [
|
|
2117
|
-
"Sideboard conversation context (restored after compaction or a new session):",
|
|
2118
|
-
"",
|
|
2119
|
-
body,
|
|
2120
|
-
"",
|
|
2121
|
-
"Continue from this context. Do not repeat the summary unless asked."
|
|
2122
|
-
].join("\n");
|
|
2123
|
-
}
|
|
2124
|
-
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
2125
|
-
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
2126
|
-
if (older.length === 0) return messages;
|
|
2127
|
-
const summary = {
|
|
2128
|
-
role: "summary",
|
|
2129
|
-
text: summaryText.trim(),
|
|
2130
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
2381
|
+
return {
|
|
2382
|
+
applied: true,
|
|
2383
|
+
method: "merge",
|
|
2384
|
+
targetBranch: target,
|
|
2385
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
2131
2386
|
};
|
|
2132
|
-
return [summary, ...recent];
|
|
2133
2387
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2388
|
+
|
|
2389
|
+
// src/git/clone-repo.ts
|
|
2390
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2391
|
+
import { basename as basename2, join as join7 } from "path";
|
|
2392
|
+
import { execa as execa4 } from "execa";
|
|
2393
|
+
async function cloneRepoIntoSideboard(opts) {
|
|
2394
|
+
const url = opts.url.trim();
|
|
2395
|
+
if (!url) throw new Error("Clone URL is required");
|
|
2396
|
+
let name = opts.name?.trim();
|
|
2397
|
+
if (!name) {
|
|
2398
|
+
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
2399
|
+
name = leaf || "repo";
|
|
2137
2400
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2401
|
+
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
2402
|
+
const dest = join7(sideboardReposDir(), name);
|
|
2403
|
+
if (existsSync6(dest)) {
|
|
2404
|
+
const repoPath2 = await resolveRepoRoot(dest);
|
|
2405
|
+
const workspace2 = await ensureWorkspace(repoPath2);
|
|
2406
|
+
return { repoPath: repoPath2, workspace: workspace2 };
|
|
2141
2407
|
}
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2152
|
-
};
|
|
2153
|
-
return {
|
|
2154
|
-
didCompact: true,
|
|
2155
|
-
thread: next,
|
|
2156
|
-
summary,
|
|
2157
|
-
method,
|
|
2158
|
-
olderCount: older.length
|
|
2159
|
-
};
|
|
2408
|
+
const result = await execa4("git", ["clone", url, dest], { reject: false });
|
|
2409
|
+
if (result.exitCode !== 0) {
|
|
2410
|
+
throw new Error(
|
|
2411
|
+
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
2415
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
2416
|
+
return { repoPath, workspace };
|
|
2160
2417
|
}
|
|
2161
2418
|
|
|
2162
|
-
// src/
|
|
2163
|
-
|
|
2164
|
-
|
|
2419
|
+
// src/store/desktop-host.ts
|
|
2420
|
+
import { readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2421
|
+
import { join as join8 } from "path";
|
|
2422
|
+
function desktopHostPidPath() {
|
|
2423
|
+
return join8(appDataDir(), "desktop-host.pid");
|
|
2165
2424
|
}
|
|
2166
|
-
function
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
taken.add(slug);
|
|
2174
|
-
}
|
|
2425
|
+
function readDesktopHostPid() {
|
|
2426
|
+
try {
|
|
2427
|
+
const pid = Number.parseInt(readFileSync5(desktopHostPidPath(), "utf8").trim(), 10);
|
|
2428
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2429
|
+
return pid;
|
|
2430
|
+
} catch {
|
|
2431
|
+
return null;
|
|
2175
2432
|
}
|
|
2176
|
-
return [...taken];
|
|
2177
2433
|
}
|
|
2178
|
-
function
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
worktreePath: normalizeWorktreePath(from.worktreePath),
|
|
2186
|
-
repoPath: from.repoPath,
|
|
2187
|
-
branchName: from.branchName,
|
|
2188
|
-
sourceRef: from.sourceRef,
|
|
2189
|
-
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
2190
|
-
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
2191
|
-
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
2192
|
-
sourceIsFork: from.sourceIsFork,
|
|
2193
|
-
parentThreadId: from.parentThreadId,
|
|
2194
|
-
prUrl: from.prUrl,
|
|
2195
|
-
prTitle: from.prTitle,
|
|
2196
|
-
prState: from.prState,
|
|
2197
|
-
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
2198
|
-
stackId: from.stackId,
|
|
2199
|
-
stackLayer: from.stackLayer
|
|
2200
|
-
};
|
|
2434
|
+
function pidAlive(pid) {
|
|
2435
|
+
try {
|
|
2436
|
+
process.kill(pid, 0);
|
|
2437
|
+
return true;
|
|
2438
|
+
} catch {
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2201
2441
|
}
|
|
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));
|
|
2442
|
+
function isDesktopHostAlive() {
|
|
2443
|
+
const pid = readDesktopHostPid();
|
|
2444
|
+
return pid != null && pidAlive(pid);
|
|
2208
2445
|
}
|
|
2209
|
-
function
|
|
2210
|
-
|
|
2211
|
-
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
2446
|
+
function isThisProcessDesktopHost() {
|
|
2447
|
+
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
2212
2448
|
}
|
|
2213
|
-
function
|
|
2214
|
-
|
|
2215
|
-
return
|
|
2449
|
+
function thisProcessShouldDrainAgentQueues() {
|
|
2450
|
+
if (isThisProcessDesktopHost()) return true;
|
|
2451
|
+
return !isDesktopHostAlive();
|
|
2216
2452
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2453
|
+
|
|
2454
|
+
// src/threads/create.ts
|
|
2455
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2456
|
+
|
|
2457
|
+
// src/detect/detect.ts
|
|
2458
|
+
var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
|
|
2459
|
+
async function requireAgent(agent, opts) {
|
|
2460
|
+
ensureAgentPath();
|
|
2461
|
+
enrichPathWithNpmGlobalBin();
|
|
2462
|
+
const status = await Promise.race([
|
|
2463
|
+
getAdapter(agent).detect(),
|
|
2464
|
+
new Promise((_, reject) => {
|
|
2465
|
+
setTimeout(() => {
|
|
2466
|
+
reject(
|
|
2467
|
+
new Error(
|
|
2468
|
+
`${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
|
|
2469
|
+
)
|
|
2470
|
+
);
|
|
2471
|
+
}, REQUIRE_AGENT_TIMEOUT_MS);
|
|
2472
|
+
})
|
|
2473
|
+
]);
|
|
2474
|
+
if (!status.installed || !status.authenticated) {
|
|
2475
|
+
throw new Error(status.reason ?? `${agent} is not available`);
|
|
2476
|
+
}
|
|
2477
|
+
if (opts?.requireLinear && !status.linearMcp) {
|
|
2478
|
+
throw new Error(
|
|
2479
|
+
`${agent} has no Linear MCP server configured \u2014 ticket sources require it`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
return status;
|
|
2225
2483
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2484
|
+
|
|
2485
|
+
// src/threads/create.ts
|
|
2486
|
+
async function createThread(input, _onSetupLine) {
|
|
2487
|
+
const resolved = resolveNewThreadOptions({
|
|
2488
|
+
agent: input.agent,
|
|
2489
|
+
model: input.model,
|
|
2490
|
+
effort: input.effort,
|
|
2491
|
+
fast: input.fast
|
|
2492
|
+
});
|
|
2493
|
+
await requireAgent(resolved.agent);
|
|
2494
|
+
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
2495
|
+
if (!existsSync7(repoPath)) {
|
|
2496
|
+
throw new Error(`Repo not found: ${repoPath}`);
|
|
2234
2497
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2498
|
+
let sourceRef = input.sourceRef;
|
|
2499
|
+
let sourceIsFork = false;
|
|
2500
|
+
let prUrl = null;
|
|
2501
|
+
let prTitle = null;
|
|
2502
|
+
if (input.sourceType === "pr") {
|
|
2503
|
+
const num = Number(input.sourceRef.replace(/^#/, ""));
|
|
2504
|
+
if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
|
|
2505
|
+
const pr = await getPr(repoPath, num);
|
|
2506
|
+
if (!pr) throw new Error(`PR #${num} not found`);
|
|
2507
|
+
sourceIsFork = pr.isCrossRepository;
|
|
2508
|
+
prUrl = pr.url;
|
|
2509
|
+
const localFetchBranch = `sideboard-pr-${pr.number}`;
|
|
2510
|
+
await fetchPrHead(repoPath, pr.number, localFetchBranch);
|
|
2511
|
+
sourceRef = localFetchBranch;
|
|
2512
|
+
} else if (input.sourceType === "ticket") {
|
|
2513
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
2514
|
+
} else if (input.sourceType === "branch") {
|
|
2515
|
+
if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
|
|
2516
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
2517
|
+
} else {
|
|
2518
|
+
const existing = await getPrForHeadBranch(repoPath, sourceRef);
|
|
2519
|
+
if (existing?.url) {
|
|
2520
|
+
prUrl = existing.url;
|
|
2521
|
+
prTitle = existing.title;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
} else if (input.sourceType === "adopt") {
|
|
2525
|
+
throw new Error("Use adoptThread() for adopt sources");
|
|
2526
|
+
}
|
|
2527
|
+
const team = allocateTeamSlug(repoPath);
|
|
2528
|
+
const { branchName, worktreePath } = await createThreadWorktree({
|
|
2529
|
+
repoPath,
|
|
2530
|
+
sourceRef,
|
|
2531
|
+
slug: team.slug
|
|
2532
|
+
});
|
|
2533
|
+
copyConfiguredFiles(repoPath, worktreePath);
|
|
2534
|
+
const explicitTitle = input.title?.trim();
|
|
2535
|
+
const thread = createEmptyThread({
|
|
2536
|
+
title: explicitTitle || team.name,
|
|
2537
|
+
userSetTitle: Boolean(explicitTitle),
|
|
2538
|
+
sourceType: input.sourceType,
|
|
2539
|
+
sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
|
|
2540
|
+
branchName,
|
|
2541
|
+
worktreePath,
|
|
2542
|
+
repoPath,
|
|
2543
|
+
agent: resolved.agent,
|
|
2544
|
+
autonomy: input.autonomy ?? "default",
|
|
2545
|
+
model: resolved.model,
|
|
2546
|
+
effort: resolved.effort,
|
|
2547
|
+
fast: resolved.fast,
|
|
2548
|
+
planMode: Boolean(input.planMode),
|
|
2253
2549
|
attachments: input.attachments ?? [],
|
|
2254
|
-
|
|
2550
|
+
sourceIsFork,
|
|
2551
|
+
parentThreadId: input.parentThreadId ?? null,
|
|
2552
|
+
status: "idle",
|
|
2553
|
+
prUrl,
|
|
2554
|
+
prTitle
|
|
2255
2555
|
});
|
|
2256
2556
|
writeThread(thread);
|
|
2257
|
-
|
|
2557
|
+
await ensureWorkspace(repoPath);
|
|
2558
|
+
return readThread(thread.id) ?? thread;
|
|
2258
2559
|
}
|
|
2259
|
-
function
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
const
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
agent: input.agent ?? from.agent,
|
|
2266
|
-
model: input.model,
|
|
2267
|
-
title: input.title?.trim() || void 0,
|
|
2268
|
-
attachments: [attachment]
|
|
2269
|
-
});
|
|
2270
|
-
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
2271
|
-
const next = { ...tab, parentThreadId: from.id };
|
|
2272
|
-
writeThread(next);
|
|
2273
|
-
return next;
|
|
2560
|
+
async function listLinearIssues(agent, repoPath) {
|
|
2561
|
+
const { getAdapter: getAdapter2 } = await import("./agents-XB332VUJ.js");
|
|
2562
|
+
await requireAgent(agent, { requireLinear: true });
|
|
2563
|
+
const adapter = getAdapter2(agent);
|
|
2564
|
+
if (!adapter.listLinearIssues) {
|
|
2565
|
+
throw new Error(`${agent} does not support Linear issue listing`);
|
|
2274
2566
|
}
|
|
2275
|
-
return
|
|
2567
|
+
return adapter.listLinearIssues(repoPath);
|
|
2276
2568
|
}
|
|
2277
2569
|
|
|
2278
2570
|
// src/store/turn-live.ts
|
|
2279
2571
|
import {
|
|
2280
|
-
existsSync as
|
|
2572
|
+
existsSync as existsSync8,
|
|
2281
2573
|
renameSync,
|
|
2282
2574
|
unlinkSync as unlinkSync2,
|
|
2283
|
-
writeFileSync as
|
|
2284
|
-
readFileSync as
|
|
2575
|
+
writeFileSync as writeFileSync3,
|
|
2576
|
+
readFileSync as readFileSync6
|
|
2285
2577
|
} from "fs";
|
|
2286
2578
|
var buffers = /* @__PURE__ */ new Map();
|
|
2287
2579
|
var FLUSH_MS = 800;
|
|
@@ -2366,7 +2658,7 @@ function writeTurnLive(threadId, progress) {
|
|
|
2366
2658
|
const path = threadLivePath(threadId);
|
|
2367
2659
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
2368
2660
|
try {
|
|
2369
|
-
|
|
2661
|
+
writeFileSync3(tmp, JSON.stringify(progress), "utf8");
|
|
2370
2662
|
renameSync(tmp, path);
|
|
2371
2663
|
} catch {
|
|
2372
2664
|
try {
|
|
@@ -2377,9 +2669,9 @@ function writeTurnLive(threadId, progress) {
|
|
|
2377
2669
|
}
|
|
2378
2670
|
function readTurnLive(threadId) {
|
|
2379
2671
|
const path = threadLivePath(threadId);
|
|
2380
|
-
if (!
|
|
2672
|
+
if (!existsSync8(path)) return null;
|
|
2381
2673
|
try {
|
|
2382
|
-
const raw = JSON.parse(
|
|
2674
|
+
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
2383
2675
|
if (!raw || typeof raw.summary !== "string") return null;
|
|
2384
2676
|
return raw;
|
|
2385
2677
|
} catch {
|
|
@@ -2391,253 +2683,13 @@ function clearTurnLive(threadId) {
|
|
|
2391
2683
|
if (buf?.timer) clearTimeout(buf.timer);
|
|
2392
2684
|
buffers.delete(threadId);
|
|
2393
2685
|
const path = threadLivePath(threadId);
|
|
2394
|
-
if (!
|
|
2686
|
+
if (!existsSync8(path)) return;
|
|
2395
2687
|
try {
|
|
2396
2688
|
unlinkSync2(path);
|
|
2397
2689
|
} catch {
|
|
2398
2690
|
}
|
|
2399
2691
|
}
|
|
2400
2692
|
|
|
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
2693
|
// src/threads/fork-worktree.ts
|
|
2642
2694
|
function requireThread2(idOrRef) {
|
|
2643
2695
|
const thread = findThreadByRef(idOrRef) ?? null;
|
|
@@ -2879,7 +2931,7 @@ async function adoptThread(input) {
|
|
|
2879
2931
|
messages: input.messages ?? []
|
|
2880
2932
|
});
|
|
2881
2933
|
writeThread(thread);
|
|
2882
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
2934
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-I3554R3F.js");
|
|
2883
2935
|
await ensureWorkspace2(repoPath);
|
|
2884
2936
|
return thread;
|
|
2885
2937
|
}
|
|
@@ -4703,8 +4755,9 @@ function formatProcessGuideDirective() {
|
|
|
4703
4755
|
"Process guides (recurring work only):",
|
|
4704
4756
|
"- 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
4757
|
"- 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
|
-
"-
|
|
4758
|
+
"- 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).",
|
|
4759
|
+
"- 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.",
|
|
4760
|
+
"- 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
4761
|
].join("\n");
|
|
4709
4762
|
}
|
|
4710
4763
|
function formatArtifactDirective() {
|
|
@@ -4731,15 +4784,7 @@ function formatArtifactDirective() {
|
|
|
4731
4784
|
].join("\n");
|
|
4732
4785
|
}
|
|
4733
4786
|
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(" ");
|
|
4787
|
+
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
4788
|
}
|
|
4744
4789
|
|
|
4745
4790
|
// src/threads/sync-branch.ts
|
|
@@ -5010,7 +5055,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
5010
5055
|
${prompt}`;
|
|
5011
5056
|
}
|
|
5012
5057
|
async function defaultDeps() {
|
|
5013
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
5058
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-JVGVPKLI.js");
|
|
5014
5059
|
const orch = getOrchestrator2();
|
|
5015
5060
|
return {
|
|
5016
5061
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -5228,7 +5273,7 @@ var Orchestrator = class {
|
|
|
5228
5273
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
5229
5274
|
});
|
|
5230
5275
|
}
|
|
5231
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
5276
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-EBCVMFF6.js");
|
|
5232
5277
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
5233
5278
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
5234
5279
|
}
|
|
@@ -5397,7 +5442,7 @@ var Orchestrator = class {
|
|
|
5397
5442
|
}
|
|
5398
5443
|
}
|
|
5399
5444
|
await setup;
|
|
5400
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
5445
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-EBCVMFF6.js");
|
|
5401
5446
|
if (autoRunAfterSetupEnabled()) {
|
|
5402
5447
|
try {
|
|
5403
5448
|
await this.startDev(threadId);
|
|
@@ -5611,7 +5656,7 @@ var Orchestrator = class {
|
|
|
5611
5656
|
if (compact.didCompact) {
|
|
5612
5657
|
thread = updateThread(threadId, {
|
|
5613
5658
|
messages: compact.thread.messages,
|
|
5614
|
-
sessionId:
|
|
5659
|
+
sessionId: compact.thread.sessionId
|
|
5615
5660
|
});
|
|
5616
5661
|
this.emit({
|
|
5617
5662
|
type: "context_compacted",
|
|
@@ -5682,7 +5727,7 @@ var Orchestrator = class {
|
|
|
5682
5727
|
}
|
|
5683
5728
|
const isBrightsy = fresh.agent === "brightsy";
|
|
5684
5729
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
5685
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-
|
|
5730
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-EBCVMFF6.js");
|
|
5686
5731
|
const gitAuthMode = getGithubGitAuthMode();
|
|
5687
5732
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
5688
5733
|
githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
|
|
@@ -6441,7 +6486,7 @@ var Orchestrator = class {
|
|
|
6441
6486
|
updateThread(thread.id, { title: meta.title });
|
|
6442
6487
|
}
|
|
6443
6488
|
}
|
|
6444
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
6489
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-EBCVMFF6.js");
|
|
6445
6490
|
const latest = this.requireThread(thread.id);
|
|
6446
6491
|
if (!shouldAutoArchiveOnPrMerge({
|
|
6447
6492
|
previousPrState: prevState || null,
|
|
@@ -6744,7 +6789,7 @@ var Orchestrator = class {
|
|
|
6744
6789
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
6745
6790
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
6746
6791
|
try {
|
|
6747
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
6792
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-I3554R3F.js");
|
|
6748
6793
|
await ensureWorkspace2(thread.repoPath);
|
|
6749
6794
|
} catch {
|
|
6750
6795
|
}
|
|
@@ -6770,7 +6815,7 @@ var Orchestrator = class {
|
|
|
6770
6815
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
6771
6816
|
} catch {
|
|
6772
6817
|
}
|
|
6773
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
6818
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-EBCVMFF6.js");
|
|
6774
6819
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
6775
6820
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
6776
6821
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -6791,7 +6836,7 @@ var Orchestrator = class {
|
|
|
6791
6836
|
return restored2;
|
|
6792
6837
|
}
|
|
6793
6838
|
if (!existsSync16(thread.worktreePath)) {
|
|
6794
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
6839
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-7H6HB6GW.js");
|
|
6795
6840
|
const slug = thread.worktreePath.split("/").pop();
|
|
6796
6841
|
const dest = thread.worktreePath;
|
|
6797
6842
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
@@ -6899,7 +6944,7 @@ async function startOrchestration(opts) {
|
|
|
6899
6944
|
sourceRef: "default",
|
|
6900
6945
|
...createOpts
|
|
6901
6946
|
}).catch(async () => {
|
|
6902
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
6947
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-7H6HB6GW.js");
|
|
6903
6948
|
const repo = await resolveRepoRoot2(repoPath);
|
|
6904
6949
|
const def = await resolveDefaultBranch2(repo);
|
|
6905
6950
|
return orch.createThread({
|