@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
|
@@ -2,18 +2,19 @@ import {
|
|
|
2
2
|
PLAN_MODE_INSTRUCTION,
|
|
3
3
|
allAdapters,
|
|
4
4
|
applyTurnUsage,
|
|
5
|
+
contextTokens,
|
|
5
6
|
getAdapter,
|
|
6
7
|
isSessionQuotaLimit,
|
|
7
8
|
parseBrightsyCliLine,
|
|
8
9
|
parseSessionQuotaResetAt,
|
|
9
10
|
resolveQuotaFallbackAgent
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-KKHDPV45.js";
|
|
11
12
|
import {
|
|
12
13
|
addWorkspace,
|
|
13
14
|
ensureWorkspace,
|
|
14
15
|
removeWorkspace,
|
|
15
16
|
syncWorkspacesFromThreads
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-FBV6NK62.js";
|
|
17
18
|
import {
|
|
18
19
|
assertOrchestratorCapableAgent,
|
|
19
20
|
coerceOrchestratorAgent,
|
|
@@ -26,14 +27,14 @@ import {
|
|
|
26
27
|
isOrchestratorThread,
|
|
27
28
|
isSlackCoordinatorThread,
|
|
28
29
|
orchestratorSessionPoisonedByBuiltins
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-756EO4GK.js";
|
|
30
31
|
import {
|
|
31
32
|
SLACK_REPLY_FORMATTING,
|
|
32
33
|
coordinatorSystemPrompt,
|
|
33
34
|
coordinatorTurnReminder,
|
|
34
35
|
enrichWorkspacesWithGithub,
|
|
35
36
|
ensureGlobalCoordinatorCwd
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-I2OJ2YSP.js";
|
|
37
38
|
import {
|
|
38
39
|
extractPresentedPlan,
|
|
39
40
|
readPlanFile,
|
|
@@ -51,7 +52,7 @@ import {
|
|
|
51
52
|
shouldRetryFailedAgentTurn,
|
|
52
53
|
summarizeTurnStderr,
|
|
53
54
|
turnFailChatText
|
|
54
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-WD2TARQS.js";
|
|
55
56
|
import {
|
|
56
57
|
releaseCaffeinateHoldForThread
|
|
57
58
|
} from "./chunk-DCOZABNT.js";
|
|
@@ -97,7 +98,7 @@ import {
|
|
|
97
98
|
threadDisplayLabel,
|
|
98
99
|
withRepoGitLock,
|
|
99
100
|
worktreeNameFromPath
|
|
100
|
-
} from "./chunk-
|
|
101
|
+
} from "./chunk-EW7COXYE.js";
|
|
101
102
|
import {
|
|
102
103
|
ATTACHMENTS_DIR,
|
|
103
104
|
LEGACY_ATTACHMENTS_DIR,
|
|
@@ -115,10 +116,10 @@ import {
|
|
|
115
116
|
resolveVaultKey,
|
|
116
117
|
updateAdvancedSettings,
|
|
117
118
|
writeSecureJson
|
|
118
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-4LKD3PGO.js";
|
|
119
120
|
import {
|
|
120
121
|
stripNestedElectronEnv
|
|
121
|
-
} from "./chunk-
|
|
122
|
+
} from "./chunk-4TR3HZFT.js";
|
|
122
123
|
import {
|
|
123
124
|
writePrivateFile
|
|
124
125
|
} from "./chunk-JT7R45JB.js";
|
|
@@ -956,6 +957,14 @@ function normalizeParseResult(parsed) {
|
|
|
956
957
|
}
|
|
957
958
|
|
|
958
959
|
// src/agents/spawn.ts
|
|
960
|
+
function applyPromptCacheTtlEnv(agent, env) {
|
|
961
|
+
if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
|
|
962
|
+
env.ENABLE_PROMPT_CACHING_1H ??= "1";
|
|
963
|
+
}
|
|
964
|
+
if (agent === "opencode" && env.OPENCODE_ANTHROPIC_FORCE_PROMPT_CACHING_5M !== "1") {
|
|
965
|
+
env.OPENCODE_ANTHROPIC_PROMPT_CACHING_1H ??= "1";
|
|
966
|
+
}
|
|
967
|
+
}
|
|
959
968
|
async function spawnAgentTurn(thread, input, onEvent) {
|
|
960
969
|
ensureAgentPath();
|
|
961
970
|
if (!thread.worktreePath?.trim()) {
|
|
@@ -963,9 +972,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
963
972
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
964
973
|
);
|
|
965
974
|
}
|
|
966
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
975
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-NCTBE7G7.js");
|
|
967
976
|
if (isGlobalThread2(thread)) {
|
|
968
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
977
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-JKOU6BWV.js");
|
|
969
978
|
ensureGlobalCoordinatorCwd2(
|
|
970
979
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
971
980
|
);
|
|
@@ -981,6 +990,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
981
990
|
);
|
|
982
991
|
}
|
|
983
992
|
const env = childEnvWithAppSettings(cmd.env);
|
|
993
|
+
applyPromptCacheTtlEnv(thread.agent, env);
|
|
984
994
|
try {
|
|
985
995
|
if (isOrchestratorThread(thread)) {
|
|
986
996
|
mergeAgentGitAuthEnv(env, await resolveAgentGitAuthEnv(env));
|
|
@@ -1192,1540 +1202,1555 @@ function shouldAutoArchiveOnPrMerge(opts) {
|
|
|
1192
1202
|
// src/hook/conductor.ts
|
|
1193
1203
|
import {
|
|
1194
1204
|
copyFileSync,
|
|
1195
|
-
existsSync as
|
|
1196
|
-
mkdirSync,
|
|
1205
|
+
existsSync as existsSync4,
|
|
1206
|
+
mkdirSync as mkdirSync2,
|
|
1197
1207
|
readdirSync,
|
|
1198
|
-
readFileSync as
|
|
1208
|
+
readFileSync as readFileSync4
|
|
1199
1209
|
} from "fs";
|
|
1200
1210
|
import { createServer } from "net";
|
|
1201
|
-
import { basename, dirname, join as
|
|
1211
|
+
import { basename, dirname as dirname2, join as join5 } from "path";
|
|
1202
1212
|
import { execa as execa2 } from "execa";
|
|
1203
1213
|
import { createInterface as createInterface2 } from "readline";
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (!entry.isFile()) continue;
|
|
1224
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
1225
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
1226
|
-
matched.push(entry.name);
|
|
1227
|
-
break;
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
} catch {
|
|
1232
|
-
}
|
|
1233
|
-
if (matched.length) return [...new Set(matched)];
|
|
1214
|
+
|
|
1215
|
+
// src/review/request-review.ts
|
|
1216
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1217
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1218
|
+
import { dirname, join as join4 } from "path";
|
|
1219
|
+
|
|
1220
|
+
// src/threads/chat-tabs.ts
|
|
1221
|
+
import { randomUUID } from "crypto";
|
|
1222
|
+
|
|
1223
|
+
// src/composer/summarize.ts
|
|
1224
|
+
var MAX_TRANSCRIPT_CHARS = 12e4;
|
|
1225
|
+
var MAX_SUMMARY_CHARS = 6e3;
|
|
1226
|
+
async function summarizeConversation(transcript, opts) {
|
|
1227
|
+
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
1228
|
+
|
|
1229
|
+
[\u2026truncated\u2026]` : transcript;
|
|
1230
|
+
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
1231
|
+
if (claudeSummary) {
|
|
1232
|
+
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
1234
1233
|
}
|
|
1235
|
-
|
|
1234
|
+
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
1235
|
+
}
|
|
1236
|
+
async function tryClaudeSummary(transcript, opts) {
|
|
1237
|
+
ensureAgentPath();
|
|
1238
|
+
const prompt = [
|
|
1239
|
+
"Summarize this coding-agent conversation for continuity in a fresh session.",
|
|
1240
|
+
"Use concise markdown bullets covering:",
|
|
1241
|
+
"- Goal / request",
|
|
1242
|
+
"- Key decisions and constraints",
|
|
1243
|
+
"- Files / areas touched",
|
|
1244
|
+
"- Current state and open tasks",
|
|
1245
|
+
"- Anything the next turn must remember",
|
|
1246
|
+
"Do not invent details. No preamble.",
|
|
1247
|
+
"",
|
|
1248
|
+
"Transcript:",
|
|
1249
|
+
transcript
|
|
1250
|
+
].join("\n");
|
|
1236
1251
|
try {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1252
|
+
const { stdout, exitCode } = await run(
|
|
1253
|
+
"claude",
|
|
1254
|
+
[
|
|
1255
|
+
"-p",
|
|
1256
|
+
prompt,
|
|
1257
|
+
"--output-format",
|
|
1258
|
+
"text",
|
|
1259
|
+
"--permission-mode",
|
|
1260
|
+
"bypassPermissions",
|
|
1261
|
+
"--model",
|
|
1262
|
+
"haiku"
|
|
1263
|
+
],
|
|
1264
|
+
{ cwd: opts?.cwd, reject: false }
|
|
1265
|
+
);
|
|
1266
|
+
if (exitCode !== 0) return null;
|
|
1267
|
+
const text = stdout.trim();
|
|
1268
|
+
if (text.length < 40) return null;
|
|
1269
|
+
return text;
|
|
1242
1270
|
} catch {
|
|
1271
|
+
return null;
|
|
1243
1272
|
}
|
|
1244
|
-
if (defaults.length) return defaults;
|
|
1245
|
-
return [".env.local", ".env"];
|
|
1246
1273
|
}
|
|
1247
|
-
function
|
|
1248
|
-
const
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1274
|
+
function extractiveSummary(transcript) {
|
|
1275
|
+
const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1276
|
+
const users = [];
|
|
1277
|
+
const tools = [];
|
|
1278
|
+
const agents = [];
|
|
1279
|
+
let mode = "none";
|
|
1280
|
+
for (const line of lines) {
|
|
1281
|
+
if (line.startsWith("### User")) {
|
|
1282
|
+
mode = "user";
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
|
|
1286
|
+
mode = "agent";
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (line.startsWith("- ") && line.includes(":")) {
|
|
1290
|
+
tools.push(line.replace(/^- /, ""));
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (mode === "user" && users.length < 8) {
|
|
1294
|
+
users.push(line.slice(0, 240));
|
|
1295
|
+
} else if (mode === "agent" && agents.length < 6) {
|
|
1296
|
+
agents.push(line.slice(0, 240));
|
|
1297
|
+
}
|
|
1257
1298
|
}
|
|
1258
|
-
|
|
1299
|
+
const uniqueTools = [...new Set(tools)].slice(0, 20);
|
|
1300
|
+
const parts = [
|
|
1301
|
+
"## Compacted context (extractive)",
|
|
1302
|
+
"",
|
|
1303
|
+
"### User goals",
|
|
1304
|
+
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
1305
|
+
"",
|
|
1306
|
+
"### Recent agent notes",
|
|
1307
|
+
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
1308
|
+
];
|
|
1309
|
+
if (uniqueTools.length) {
|
|
1310
|
+
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
1311
|
+
}
|
|
1312
|
+
parts.push(
|
|
1313
|
+
"",
|
|
1314
|
+
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
1315
|
+
);
|
|
1316
|
+
return parts.join("\n");
|
|
1259
1317
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
if (key) env[key] = value;
|
|
1278
|
-
}
|
|
1279
|
-
cachedLoginEnv = env;
|
|
1280
|
-
return { ...env };
|
|
1281
|
-
}
|
|
1282
|
-
} catch {
|
|
1318
|
+
function clipSummary(text) {
|
|
1319
|
+
if (text.length <= MAX_SUMMARY_CHARS) return text;
|
|
1320
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}
|
|
1321
|
+
|
|
1322
|
+
[\u2026summary truncated\u2026]`;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/composer/context-compact.ts
|
|
1326
|
+
var CONTEXT_COMPACT_CHARS = 4e5;
|
|
1327
|
+
var CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
1328
|
+
var CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
1329
|
+
var CONTEXT_MIN_MESSAGES = 10;
|
|
1330
|
+
var SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
|
|
1331
|
+
function estimateMessageChars(message) {
|
|
1332
|
+
let n = message.text.length + 16;
|
|
1333
|
+
for (const part of message.parts ?? []) {
|
|
1334
|
+
n += estimatePartChars(part);
|
|
1283
1335
|
}
|
|
1284
|
-
|
|
1285
|
-
return { ...cachedLoginEnv };
|
|
1336
|
+
return n;
|
|
1286
1337
|
}
|
|
1287
|
-
function
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
1296
|
-
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
1297
|
-
env.SIDEBOARD_IS_LOCAL = "1";
|
|
1298
|
-
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1299
|
-
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
1300
|
-
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
1301
|
-
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
1302
|
-
env.CONDUCTOR_IS_LOCAL = "1";
|
|
1303
|
-
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1304
|
-
if (primary != null) {
|
|
1305
|
-
env.SIDEBOARD_PORT = String(primary);
|
|
1306
|
-
env.CONDUCTOR_PORT = String(primary);
|
|
1307
|
-
env.PORT = String(primary);
|
|
1308
|
-
for (let i = 1; i < ports.length; i++) {
|
|
1309
|
-
const p = ports[i];
|
|
1310
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
1311
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
1338
|
+
function estimatePartChars(part) {
|
|
1339
|
+
switch (part.type) {
|
|
1340
|
+
case "text":
|
|
1341
|
+
case "thinking":
|
|
1342
|
+
return part.text.length;
|
|
1343
|
+
case "tool": {
|
|
1344
|
+
const input = part.input ? JSON.stringify(part.input) : "";
|
|
1345
|
+
return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
|
|
1312
1346
|
}
|
|
1347
|
+
default:
|
|
1348
|
+
return 0;
|
|
1313
1349
|
}
|
|
1314
|
-
return env;
|
|
1315
1350
|
}
|
|
1316
|
-
function
|
|
1317
|
-
|
|
1318
|
-
const rl = createInterface2({ input: stream });
|
|
1319
|
-
rl.on("line", onLine);
|
|
1351
|
+
function estimateThreadChars(messages) {
|
|
1352
|
+
return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
|
|
1320
1353
|
}
|
|
1321
|
-
function
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
1327
|
-
} else {
|
|
1328
|
-
process.kill(-pid, "SIGTERM");
|
|
1329
|
-
setTimeout(() => {
|
|
1330
|
-
try {
|
|
1331
|
-
process.kill(-pid, "SIGKILL");
|
|
1332
|
-
} catch {
|
|
1333
|
-
}
|
|
1334
|
-
}, 2500).unref?.();
|
|
1335
|
-
}
|
|
1336
|
-
} catch {
|
|
1337
|
-
try {
|
|
1338
|
-
child.kill("SIGTERM");
|
|
1339
|
-
} catch {
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
for (const port of ports) {
|
|
1344
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
1345
|
-
if (process.platform === "win32") {
|
|
1346
|
-
void execa2(
|
|
1347
|
-
"powershell",
|
|
1348
|
-
[
|
|
1349
|
-
"-NoProfile",
|
|
1350
|
-
"-Command",
|
|
1351
|
-
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
1352
|
-
],
|
|
1353
|
-
{ reject: false }
|
|
1354
|
-
);
|
|
1355
|
-
} else {
|
|
1356
|
-
void execa2(
|
|
1357
|
-
"zsh",
|
|
1358
|
-
["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
|
|
1359
|
-
{ reject: false }
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1354
|
+
function shouldCompactContext(messages, thresholds = {}) {
|
|
1355
|
+
const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
|
|
1356
|
+
const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
|
|
1357
|
+
if (messages.length < minMessages) return false;
|
|
1358
|
+
return estimateThreadChars(messages) >= maxChars;
|
|
1363
1359
|
}
|
|
1364
|
-
|
|
1365
|
-
const
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
try {
|
|
1377
|
-
mergeAgentGitAuthEnv(
|
|
1378
|
-
env,
|
|
1379
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
1380
|
-
);
|
|
1381
|
-
} catch {
|
|
1360
|
+
function splitForCompaction(messages, thresholds = {}) {
|
|
1361
|
+
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
1362
|
+
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
1363
|
+
if (messages.length === 0) return { older: [], recent: [] };
|
|
1364
|
+
let recentChars = 0;
|
|
1365
|
+
let cut = messages.length;
|
|
1366
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1367
|
+
const next = recentChars + estimateMessageChars(messages[i]);
|
|
1368
|
+
const kept = messages.length - i;
|
|
1369
|
+
if (kept >= keepCount && next > keepChars) break;
|
|
1370
|
+
recentChars = next;
|
|
1371
|
+
cut = i;
|
|
1382
1372
|
}
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
reject: false,
|
|
1387
|
-
env,
|
|
1388
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
1389
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
1390
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
1391
|
-
});
|
|
1392
|
-
pipeLines(child.stdout, opts.onLine);
|
|
1393
|
-
pipeLines(child.stderr, opts.onLine);
|
|
1394
|
-
const ports = opts.ports ?? [];
|
|
1373
|
+
const minRecent = Math.min(keepCount, messages.length);
|
|
1374
|
+
cut = Math.min(cut, messages.length - minRecent);
|
|
1375
|
+
if (cut <= 0) return { older: [], recent: messages };
|
|
1395
1376
|
return {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
1399
|
-
child
|
|
1377
|
+
older: messages.slice(0, cut),
|
|
1378
|
+
recent: messages.slice(cut)
|
|
1400
1379
|
};
|
|
1401
1380
|
}
|
|
1402
|
-
|
|
1403
|
-
if (
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1381
|
+
function formatToolPart(t, detail) {
|
|
1382
|
+
if (detail === "summary") {
|
|
1383
|
+
const label = t.description || t.detail || t.name;
|
|
1384
|
+
const path = t.filePath ? ` (${t.filePath})` : "";
|
|
1385
|
+
return `- ${t.name}: ${label}${path}`;
|
|
1407
1386
|
}
|
|
1408
|
-
|
|
1387
|
+
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
1388
|
+
if (t.description) lines.push(`Description: ${t.description}`);
|
|
1389
|
+
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
1390
|
+
if (t.input && Object.keys(t.input).length > 0) {
|
|
1391
|
+
lines.push("Input:");
|
|
1392
|
+
lines.push("```json");
|
|
1393
|
+
lines.push(JSON.stringify(t.input, null, 2));
|
|
1394
|
+
lines.push("```");
|
|
1395
|
+
} else if (t.detail) {
|
|
1396
|
+
lines.push(`Detail: ${t.detail}`);
|
|
1397
|
+
}
|
|
1398
|
+
if (t.result != null && t.result !== "") {
|
|
1399
|
+
lines.push("Result:");
|
|
1400
|
+
lines.push("```");
|
|
1401
|
+
lines.push(t.result);
|
|
1402
|
+
lines.push("```");
|
|
1403
|
+
}
|
|
1404
|
+
return lines.join("\n");
|
|
1409
1405
|
}
|
|
1410
|
-
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1406
|
+
function formatMessagesAsTranscript(messages, opts) {
|
|
1407
|
+
const tools = opts?.tools ?? "full";
|
|
1408
|
+
const blocks = [];
|
|
1409
|
+
for (const m of messages) {
|
|
1410
|
+
if (m.role === "summary") {
|
|
1411
|
+
blocks.push(`## Prior summary
|
|
1412
|
+
${m.text}`);
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (m.role === "user") {
|
|
1416
|
+
blocks.push(`### User
|
|
1417
|
+
${m.text}`);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
const bits = [];
|
|
1421
|
+
if (m.text?.trim()) {
|
|
1422
|
+
bits.push(`### Agent
|
|
1423
|
+
${m.text}`);
|
|
1424
|
+
} else {
|
|
1425
|
+
bits.push("### Agent");
|
|
1426
|
+
}
|
|
1427
|
+
const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
|
|
1428
|
+
for (const th of thinking) {
|
|
1429
|
+
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
1430
|
+
bits.push("Thinking:");
|
|
1431
|
+
bits.push(th.text);
|
|
1432
|
+
}
|
|
1433
|
+
const toolParts = (m.parts ?? []).filter(
|
|
1434
|
+
(p) => p.type === "tool"
|
|
1435
|
+
);
|
|
1436
|
+
if (tools !== "none" && toolParts.length > 0) {
|
|
1437
|
+
if (tools === "summary") bits.push("Tools:");
|
|
1438
|
+
for (const t of toolParts) {
|
|
1439
|
+
bits.push(formatToolPart(t, tools));
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
blocks.push(bits.join("\n"));
|
|
1443
|
+
}
|
|
1444
|
+
return blocks.join("\n\n");
|
|
1426
1445
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1446
|
+
function buildSessionSeed(messages, opts) {
|
|
1447
|
+
if (messages.length === 0) return null;
|
|
1448
|
+
const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
|
|
1449
|
+
if (!body.trim()) return null;
|
|
1450
|
+
return [
|
|
1451
|
+
"Sideboard conversation context (restored after compaction or a new session):",
|
|
1452
|
+
"",
|
|
1453
|
+
body,
|
|
1454
|
+
"",
|
|
1455
|
+
"Continue from this context. Do not repeat the summary unless asked."
|
|
1456
|
+
].join("\n");
|
|
1457
|
+
}
|
|
1458
|
+
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
1459
|
+
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
1460
|
+
if (older.length === 0) return messages;
|
|
1461
|
+
const summary = {
|
|
1462
|
+
role: "summary",
|
|
1463
|
+
text: summaryText.trim(),
|
|
1464
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1443
1465
|
};
|
|
1466
|
+
return [summary, ...recent];
|
|
1444
1467
|
}
|
|
1445
|
-
|
|
1446
|
-
let
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
}
|
|
1450
|
-
if (!setup.ran) {
|
|
1451
|
-
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
1468
|
+
function lastRequestOccupancy(thread) {
|
|
1469
|
+
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
1470
|
+
const usage = thread.messages[i]?.usage;
|
|
1471
|
+
if (usage) return contextTokens(usage);
|
|
1452
1472
|
}
|
|
1453
|
-
return
|
|
1473
|
+
return 0;
|
|
1454
1474
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
1458
|
-
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
1459
|
-
worktreePath,
|
|
1460
|
-
repoPath,
|
|
1461
|
-
onLine
|
|
1462
|
-
});
|
|
1463
|
-
const exitCode = await handle.done;
|
|
1464
|
-
return { ran: true, exitCode };
|
|
1475
|
+
function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
|
|
1476
|
+
return lastRequestOccupancy(thread) >= occupancyTokens;
|
|
1465
1477
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1478
|
+
async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
|
|
1479
|
+
if (!shouldCompactContext(thread.messages, thresholds)) {
|
|
1480
|
+
return { didCompact: false, thread };
|
|
1481
|
+
}
|
|
1482
|
+
const { older } = splitForCompaction(thread.messages, thresholds);
|
|
1483
|
+
if (older.length === 0) {
|
|
1484
|
+
return { didCompact: false, thread };
|
|
1485
|
+
}
|
|
1486
|
+
const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
|
|
1487
|
+
const { summary, method } = await summarize(transcript, {
|
|
1488
|
+
cwd: thread.worktreePath
|
|
1472
1489
|
});
|
|
1490
|
+
const messages = applyCompaction(thread.messages, summary, thresholds);
|
|
1491
|
+
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
1492
|
+
const next = {
|
|
1493
|
+
...thread,
|
|
1494
|
+
messages,
|
|
1495
|
+
sessionId: resetSession ? null : thread.sessionId,
|
|
1496
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1497
|
+
};
|
|
1498
|
+
return {
|
|
1499
|
+
didCompact: true,
|
|
1500
|
+
thread: next,
|
|
1501
|
+
summary,
|
|
1502
|
+
method,
|
|
1503
|
+
olderCount: older.length
|
|
1504
|
+
};
|
|
1473
1505
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
return
|
|
1506
|
+
|
|
1507
|
+
// src/threads/chat-tabs.ts
|
|
1508
|
+
function sameWorktreePath(a, b) {
|
|
1509
|
+
return normalizeWorktreePath(a) === normalizeWorktreePath(b);
|
|
1478
1510
|
}
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1511
|
+
function takenTeamSlugsForChatTab(worktreePath) {
|
|
1512
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
1513
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1514
|
+
const dir = worktreeNameFromPath(key);
|
|
1515
|
+
if (dir) taken.add(dir.toLowerCase());
|
|
1516
|
+
for (const sibling of threadsSharingWorktree(key)) {
|
|
1517
|
+
for (const slug of takenSlugsFromThread(sibling)) {
|
|
1518
|
+
taken.add(slug);
|
|
1519
|
+
}
|
|
1484
1520
|
}
|
|
1485
|
-
return
|
|
1486
|
-
}
|
|
1487
|
-
function getRunMode(worktreePath, repoPath) {
|
|
1488
|
-
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
1489
|
-
}
|
|
1490
|
-
async function allocatePort() {
|
|
1491
|
-
return new Promise((resolve, reject) => {
|
|
1492
|
-
const server = createServer();
|
|
1493
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1494
|
-
const addr = server.address();
|
|
1495
|
-
if (!addr || typeof addr === "string") {
|
|
1496
|
-
server.close();
|
|
1497
|
-
reject(new Error("Failed to allocate port"));
|
|
1498
|
-
return;
|
|
1499
|
-
}
|
|
1500
|
-
const port = addr.port;
|
|
1501
|
-
server.close((err) => err ? reject(err) : resolve(port));
|
|
1502
|
-
});
|
|
1503
|
-
server.on("error", reject);
|
|
1504
|
-
});
|
|
1521
|
+
return [...taken];
|
|
1505
1522
|
}
|
|
1506
|
-
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const candidate = base + i;
|
|
1511
|
-
const free = await new Promise((resolve) => {
|
|
1512
|
-
const server = createServer();
|
|
1513
|
-
server.once("error", () => resolve(false));
|
|
1514
|
-
server.listen(candidate, "127.0.0.1", () => {
|
|
1515
|
-
server.close(() => resolve(true));
|
|
1516
|
-
});
|
|
1517
|
-
});
|
|
1518
|
-
if (free) {
|
|
1519
|
-
ports.push(candidate);
|
|
1520
|
-
} else {
|
|
1521
|
-
ports.push(await allocatePort());
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
return ports;
|
|
1523
|
+
function requireThread(idOrRef) {
|
|
1524
|
+
const thread = findThreadByRef(idOrRef) ?? null;
|
|
1525
|
+
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
1526
|
+
return thread;
|
|
1525
1527
|
}
|
|
1526
|
-
|
|
1527
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
1528
|
-
if (!script) return null;
|
|
1529
|
-
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
1530
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
1531
|
-
worktreePath,
|
|
1532
|
-
repoPath,
|
|
1533
|
-
ports,
|
|
1534
|
-
defaultBranch: opts?.defaultBranch,
|
|
1535
|
-
onLine
|
|
1536
|
-
});
|
|
1528
|
+
function worktreeBindingFrom(from) {
|
|
1537
1529
|
return {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1530
|
+
worktreePath: normalizeWorktreePath(from.worktreePath),
|
|
1531
|
+
repoPath: from.repoPath,
|
|
1532
|
+
branchName: from.branchName,
|
|
1533
|
+
sourceRef: from.sourceRef,
|
|
1534
|
+
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
1535
|
+
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
1536
|
+
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
1537
|
+
sourceIsFork: from.sourceIsFork,
|
|
1538
|
+
parentThreadId: from.parentThreadId,
|
|
1539
|
+
prUrl: from.prUrl,
|
|
1540
|
+
prTitle: from.prTitle,
|
|
1541
|
+
prState: from.prState,
|
|
1542
|
+
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
1543
|
+
stackId: from.stackId,
|
|
1544
|
+
stackLayer: from.stackLayer
|
|
1544
1545
|
};
|
|
1545
1546
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
}
|
|
1553
|
-
async function findOrphanWorktrees(repoPaths) {
|
|
1554
|
-
const threads = listThreads({ includeArchived: true });
|
|
1555
|
-
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
1556
|
-
const repos = new Set(
|
|
1557
|
-
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
1558
|
-
);
|
|
1559
|
-
const homeRoot = sideboardWorkspacesDir();
|
|
1560
|
-
if (existsSync4(homeRoot)) {
|
|
1561
|
-
try {
|
|
1562
|
-
for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
|
|
1563
|
-
if (!entry.isDirectory()) continue;
|
|
1564
|
-
void entry;
|
|
1565
|
-
}
|
|
1566
|
-
} catch {
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
const orphans = [];
|
|
1570
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1571
|
-
for (const repoPath of repos) {
|
|
1572
|
-
if (!repoPath || !existsSync4(repoPath)) continue;
|
|
1573
|
-
try {
|
|
1574
|
-
const wts = await listWorktrees(repoPath);
|
|
1575
|
-
for (const wt of wts) {
|
|
1576
|
-
const path = wt.path.replace(/\/$/, "");
|
|
1577
|
-
if (!isSideboardWorktreePath(path)) continue;
|
|
1578
|
-
if (known.has(path)) continue;
|
|
1579
|
-
if (seen.has(path)) continue;
|
|
1580
|
-
seen.add(path);
|
|
1581
|
-
let mtimeMs = 0;
|
|
1582
|
-
try {
|
|
1583
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
1584
|
-
} catch {
|
|
1585
|
-
mtimeMs = 0;
|
|
1586
|
-
}
|
|
1587
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
1588
|
-
}
|
|
1589
|
-
} catch {
|
|
1590
|
-
}
|
|
1591
|
-
try {
|
|
1592
|
-
const root = worktreesRoot(repoPath);
|
|
1593
|
-
if (existsSync4(root)) {
|
|
1594
|
-
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
1595
|
-
if (!entry.isDirectory()) continue;
|
|
1596
|
-
const path = join5(root, entry.name).replace(/\/$/, "");
|
|
1597
|
-
if (known.has(path) || seen.has(path)) continue;
|
|
1598
|
-
if (!existsSync4(join5(path, ".git"))) continue;
|
|
1599
|
-
seen.add(path);
|
|
1600
|
-
let mtimeMs = 0;
|
|
1601
|
-
try {
|
|
1602
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
1603
|
-
} catch {
|
|
1604
|
-
mtimeMs = Date.now();
|
|
1605
|
-
}
|
|
1606
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
} catch {
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
1547
|
+
function threadsSharingWorktree(worktreePath) {
|
|
1548
|
+
if (!worktreePath) return [];
|
|
1549
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
1550
|
+
return listThreads({ includeArchived: true }).filter(
|
|
1551
|
+
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
1552
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1613
1553
|
}
|
|
1614
|
-
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
1618
|
-
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
1619
|
-
const budget = Math.max(0, maxCount - activeCount);
|
|
1620
|
-
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1621
|
-
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
1622
|
-
const removed = [];
|
|
1623
|
-
const kept = [];
|
|
1624
|
-
for (const orphan of orphans) {
|
|
1625
|
-
if (keep.has(orphan.path)) {
|
|
1626
|
-
kept.push(orphan.path);
|
|
1627
|
-
continue;
|
|
1628
|
-
}
|
|
1629
|
-
if (opts?.dryRun) {
|
|
1630
|
-
removed.push(orphan.path);
|
|
1631
|
-
continue;
|
|
1632
|
-
}
|
|
1633
|
-
try {
|
|
1634
|
-
await removeWorktree(orphan.repoPath, orphan.path);
|
|
1635
|
-
removed.push(orphan.path);
|
|
1636
|
-
} catch {
|
|
1637
|
-
kept.push(orphan.path);
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
if (!opts?.dryRun) {
|
|
1641
|
-
updateAdvancedSettings({
|
|
1642
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
return { removed, kept, orphans };
|
|
1554
|
+
function formatTranscriptMarkdown(title, messages) {
|
|
1555
|
+
const body = formatMessagesAsTranscript(messages, { tools: "full" });
|
|
1556
|
+
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
1646
1557
|
}
|
|
1647
|
-
function
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
if (!last) return true;
|
|
1651
|
-
const elapsed = Date.now() - Date.parse(last);
|
|
1652
|
-
return elapsed >= intervalHours * 36e5;
|
|
1558
|
+
function forkMessageSlice(from, throughIndex) {
|
|
1559
|
+
const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
|
|
1560
|
+
return through >= 0 ? from.messages.slice(0, through + 1) : [];
|
|
1653
1561
|
}
|
|
1654
|
-
function
|
|
1655
|
-
const
|
|
1562
|
+
function buildForkTranscriptAttachment(baseTitle, messages) {
|
|
1563
|
+
const title = baseTitle || "Chat";
|
|
1656
1564
|
return {
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1565
|
+
id: randomUUID(),
|
|
1566
|
+
name: `Transcript of ${title}.md`,
|
|
1567
|
+
kind: "transcript",
|
|
1568
|
+
content: formatTranscriptMarkdown(title, messages)
|
|
1661
1569
|
};
|
|
1662
1570
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
const
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
reject: false
|
|
1672
|
-
});
|
|
1673
|
-
if (status.stdout.trim()) {
|
|
1674
|
-
throw new Error(
|
|
1675
|
-
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
1676
|
-
);
|
|
1571
|
+
function createChatTab(input) {
|
|
1572
|
+
const from = requireThread(input.fromThreadId);
|
|
1573
|
+
const binding = worktreeBindingFrom(from);
|
|
1574
|
+
const explicitTitle = input.title?.trim();
|
|
1575
|
+
const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
|
|
1576
|
+
const nextAgent = input.agent ?? from.agent;
|
|
1577
|
+
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
1578
|
+
assertOrchestratorCapableAgent(nextAgent);
|
|
1677
1579
|
}
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1580
|
+
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
1581
|
+
const thread = createEmptyThread({
|
|
1582
|
+
title,
|
|
1583
|
+
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
1584
|
+
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
1585
|
+
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
1586
|
+
userSetTitle: true,
|
|
1587
|
+
...binding,
|
|
1588
|
+
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
1589
|
+
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
1590
|
+
// the connected chat.
|
|
1591
|
+
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
1592
|
+
agent: nextAgent,
|
|
1593
|
+
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
1594
|
+
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
1595
|
+
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
1596
|
+
planMode: from.planMode,
|
|
1597
|
+
autonomy: input.autonomy ?? from.autonomy,
|
|
1598
|
+
attachments: input.attachments ?? [],
|
|
1599
|
+
status: "idle"
|
|
1681
1600
|
});
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
for (const sha of shas) {
|
|
1703
|
-
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
1704
|
-
cwd: thread.repoPath,
|
|
1705
|
-
reject: false
|
|
1706
|
-
});
|
|
1707
|
-
if (cp.exitCode !== 0) {
|
|
1708
|
-
await execa3("git", ["cherry-pick", "--abort"], {
|
|
1709
|
-
cwd: thread.repoPath,
|
|
1710
|
-
reject: false
|
|
1711
|
-
});
|
|
1712
|
-
throw new Error(
|
|
1713
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
1714
|
-
);
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
return {
|
|
1718
|
-
applied: true,
|
|
1719
|
-
method,
|
|
1720
|
-
targetBranch: target,
|
|
1721
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
const merge = await execa3(
|
|
1725
|
-
"git",
|
|
1726
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
1727
|
-
{ cwd: thread.repoPath, reject: false }
|
|
1728
|
-
);
|
|
1729
|
-
if (merge.exitCode !== 0) {
|
|
1730
|
-
await execa3("git", ["merge", "--abort"], {
|
|
1731
|
-
cwd: thread.repoPath,
|
|
1732
|
-
reject: false
|
|
1733
|
-
});
|
|
1734
|
-
throw new Error(
|
|
1735
|
-
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
1736
|
-
);
|
|
1737
|
-
}
|
|
1738
|
-
return {
|
|
1739
|
-
applied: true,
|
|
1740
|
-
method: "merge",
|
|
1741
|
-
targetBranch: target,
|
|
1742
|
-
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
1743
|
-
};
|
|
1601
|
+
writeThread(thread);
|
|
1602
|
+
return thread;
|
|
1603
|
+
}
|
|
1604
|
+
function forkChatTab(input) {
|
|
1605
|
+
const from = requireThread(input.threadId);
|
|
1606
|
+
const slice = forkMessageSlice(from, input.throughIndex);
|
|
1607
|
+
const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
|
|
1608
|
+
const tab = createChatTab({
|
|
1609
|
+
fromThreadId: input.threadId,
|
|
1610
|
+
agent: input.agent ?? from.agent,
|
|
1611
|
+
model: input.model,
|
|
1612
|
+
title: input.title?.trim() || void 0,
|
|
1613
|
+
attachments: [attachment]
|
|
1614
|
+
});
|
|
1615
|
+
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
1616
|
+
const next = { ...tab, parentThreadId: from.id };
|
|
1617
|
+
writeThread(next);
|
|
1618
|
+
return next;
|
|
1619
|
+
}
|
|
1620
|
+
return tab;
|
|
1744
1621
|
}
|
|
1745
1622
|
|
|
1746
|
-
// src/
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1623
|
+
// src/review/review-request-template.ts
|
|
1624
|
+
var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
|
|
1625
|
+
|
|
1626
|
+
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.
|
|
1627
|
+
|
|
1628
|
+
## Required outcome
|
|
1629
|
+
|
|
1630
|
+
Start your reply with a **Recommendation** section using exactly one of:
|
|
1631
|
+
|
|
1632
|
+
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
1633
|
+
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
1634
|
+
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
1635
|
+
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
1636
|
+
|
|
1637
|
+
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.
|
|
1638
|
+
|
|
1639
|
+
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
1640
|
+
|
|
1641
|
+
## Findings
|
|
1642
|
+
|
|
1643
|
+
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
1644
|
+
|
|
1645
|
+
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
1646
|
+
|
|
1647
|
+
Flag something as a bug / blocking finding only when:
|
|
1648
|
+
|
|
1649
|
+
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
1650
|
+
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
1651
|
+
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
1652
|
+
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
1653
|
+
5. The author would likely fix it if made aware.
|
|
1654
|
+
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
1655
|
+
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
1656
|
+
8. It is clearly not just an intentional change by the author.
|
|
1657
|
+
|
|
1658
|
+
When flagging an issue, include a short accompanying comment:
|
|
1659
|
+
|
|
1660
|
+
1. Clear about why it is a problem.
|
|
1661
|
+
2. Severity must match reality \u2014 do not inflate.
|
|
1662
|
+
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
1663
|
+
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
1664
|
+
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
1665
|
+
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
1666
|
+
7. Skimmable on first read.
|
|
1667
|
+
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
1668
|
+
|
|
1669
|
+
HOW MANY FINDINGS TO RETURN:
|
|
1670
|
+
|
|
1671
|
+
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.
|
|
1672
|
+
|
|
1673
|
+
GUIDELINES:
|
|
1674
|
+
|
|
1675
|
+
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
1676
|
+
- One comment per distinct issue (or a short multi-line range if needed).
|
|
1677
|
+
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
1678
|
+
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
1679
|
+
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
1680
|
+
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
1681
|
+
|
|
1682
|
+
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).
|
|
1683
|
+
|
|
1684
|
+
## Getting the diff
|
|
1685
|
+
|
|
1686
|
+
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.
|
|
1687
|
+
|
|
1688
|
+
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.
|
|
1689
|
+
|
|
1690
|
+
## Fallback: if you don't have access to the Sideboard diff tool
|
|
1691
|
+
|
|
1692
|
+
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
1693
|
+
|
|
1694
|
+
\`\`\`bash
|
|
1695
|
+
# Get the merge base between this branch and the target
|
|
1696
|
+
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
1697
|
+
|
|
1698
|
+
# Get the committed diff against the merge base
|
|
1699
|
+
git diff $MERGE_BASE HEAD
|
|
1700
|
+
|
|
1701
|
+
# Get any uncommitted changes (staged and unstaged)
|
|
1702
|
+
git diff HEAD
|
|
1703
|
+
\`\`\`
|
|
1704
|
+
|
|
1705
|
+
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.
|
|
1706
|
+
|
|
1707
|
+
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
1708
|
+
|
|
1709
|
+
## Output format
|
|
1710
|
+
|
|
1711
|
+
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
1712
|
+
|
|
1713
|
+
Only report ONE finding per unique issue.
|
|
1714
|
+
|
|
1715
|
+
<example>
|
|
1716
|
+
## Recommendation
|
|
1717
|
+
|
|
1718
|
+
**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.
|
|
1719
|
+
|
|
1720
|
+
## Findings
|
|
1721
|
+
|
|
1722
|
+
### **#1 Empty input causes crash** (blocking)
|
|
1723
|
+
|
|
1724
|
+
If the input field is empty when the page loads, the app will crash.
|
|
1725
|
+
|
|
1726
|
+
File: src/client/frontends/desktop/ui/Input.tsx
|
|
1727
|
+
|
|
1728
|
+
### **#2 Dead code** (nit)
|
|
1729
|
+
|
|
1730
|
+
The getUserData function is now unused. It should be deleted.
|
|
1731
|
+
|
|
1732
|
+
File: src/client/frontends/desktop/core/UserData.ts
|
|
1733
|
+
</example>
|
|
1734
|
+
|
|
1735
|
+
<example>
|
|
1736
|
+
## Recommendation
|
|
1737
|
+
|
|
1738
|
+
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
1739
|
+
</example>
|
|
1740
|
+
|
|
1741
|
+
## Growing the rules
|
|
1742
|
+
|
|
1743
|
+
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.
|
|
1744
|
+
`;
|
|
1745
|
+
var REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
|
|
1746
|
+
var REVIEW_SKILL_NAME = "review";
|
|
1747
|
+
var REVIEW_SKILL_FRONTMATTER = `---
|
|
1748
|
+
name: review
|
|
1749
|
+
description: >-
|
|
1750
|
+
Merge-readiness review for this repository. Use when reviewing a PR, worktree
|
|
1751
|
+
diff, or Sideboard Review (Approve / Approve with nits / Request changes).
|
|
1752
|
+
---
|
|
1753
|
+
|
|
1754
|
+
`;
|
|
1755
|
+
function wrapReviewSkillMarkdown(body) {
|
|
1756
|
+
const trimmed = body.replace(/^\uFEFF/, "").trim();
|
|
1757
|
+
const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
|
|
1758
|
+
if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
|
|
1759
|
+
return source.endsWith("\n") ? source : `${source}
|
|
1760
|
+
`;
|
|
1770
1761
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
return { repoPath, workspace };
|
|
1762
|
+
return `${REVIEW_SKILL_FRONTMATTER}${source}
|
|
1763
|
+
`;
|
|
1774
1764
|
}
|
|
1775
1765
|
|
|
1776
|
-
// src/
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1766
|
+
// src/review/request-review.ts
|
|
1767
|
+
var REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
1768
|
+
var REPO_REVIEW_NAME = "review.md";
|
|
1769
|
+
var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
1770
|
+
var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
1771
|
+
var REVIEW_REQUEST_NAME = "Review request.md";
|
|
1772
|
+
var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
1773
|
+
var LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
1774
|
+
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
1775
|
+
"HOW MANY FINDINGS TO RETURN:"
|
|
1776
|
+
];
|
|
1777
|
+
function shouldRefreshReviewRequestTemplate(content) {
|
|
1778
|
+
const trimmed = content.trim();
|
|
1779
|
+
if (!trimmed) return true;
|
|
1780
|
+
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
1781
|
+
return false;
|
|
1791
1782
|
}
|
|
1783
|
+
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
1792
1784
|
}
|
|
1793
|
-
function
|
|
1785
|
+
function readTextIfPresent(abs) {
|
|
1786
|
+
if (!existsSync3(abs)) return null;
|
|
1794
1787
|
try {
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
return pid;
|
|
1788
|
+
const content = readFileSync3(abs, "utf8");
|
|
1789
|
+
return content.trim() ? content : null;
|
|
1798
1790
|
} catch {
|
|
1799
1791
|
return null;
|
|
1800
1792
|
}
|
|
1801
1793
|
}
|
|
1802
|
-
function
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
return false;
|
|
1794
|
+
function readLocalGuidelines(worktreePath) {
|
|
1795
|
+
const localAbs = join4(worktreePath, REVIEW_REQUEST_PATH);
|
|
1796
|
+
const localContent = readTextIfPresent(localAbs);
|
|
1797
|
+
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
1798
|
+
return { path: REVIEW_REQUEST_PATH, content: localContent };
|
|
1808
1799
|
}
|
|
1800
|
+
const legacyAbs = join4(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
1801
|
+
const legacyContent = readTextIfPresent(legacyAbs);
|
|
1802
|
+
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
1803
|
+
return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
|
|
1804
|
+
}
|
|
1805
|
+
return null;
|
|
1809
1806
|
}
|
|
1810
|
-
function
|
|
1811
|
-
|
|
1812
|
-
|
|
1807
|
+
function skillGuidelines(content, source) {
|
|
1808
|
+
return {
|
|
1809
|
+
path: REVIEW_SKILL_PATH,
|
|
1810
|
+
name: REVIEW_SKILL_NAME,
|
|
1811
|
+
content,
|
|
1812
|
+
source
|
|
1813
|
+
};
|
|
1813
1814
|
}
|
|
1814
|
-
function
|
|
1815
|
-
|
|
1815
|
+
function ensureReviewSkillFile(worktreePath) {
|
|
1816
|
+
const abs = join4(worktreePath, REVIEW_SKILL_PATH);
|
|
1817
|
+
const existing = readTextIfPresent(abs);
|
|
1818
|
+
if (existing) {
|
|
1819
|
+
return { path: REVIEW_SKILL_PATH, content: existing, wrote: false };
|
|
1820
|
+
}
|
|
1821
|
+
const fromRepo = readTextIfPresent(join4(worktreePath, REPO_REVIEW_PATH));
|
|
1822
|
+
const fromLocal = readLocalGuidelines(worktreePath)?.content ?? null;
|
|
1823
|
+
const content = wrapReviewSkillMarkdown(fromRepo ?? fromLocal ?? REVIEW_REQUEST_TEMPLATE);
|
|
1824
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
1825
|
+
writeFileSync(abs, content, "utf8");
|
|
1826
|
+
return { path: REVIEW_SKILL_PATH, content, wrote: true };
|
|
1816
1827
|
}
|
|
1817
|
-
function
|
|
1818
|
-
|
|
1819
|
-
return
|
|
1828
|
+
function resolveReviewGuidelines(worktreePath) {
|
|
1829
|
+
const skillContent = readTextIfPresent(join4(worktreePath, REVIEW_SKILL_PATH));
|
|
1830
|
+
if (skillContent) return skillGuidelines(skillContent, "skill");
|
|
1831
|
+
const local = readLocalGuidelines(worktreePath);
|
|
1832
|
+
if (local) {
|
|
1833
|
+
return {
|
|
1834
|
+
path: local.path,
|
|
1835
|
+
name: REVIEW_REQUEST_NAME,
|
|
1836
|
+
content: local.content,
|
|
1837
|
+
source: "local"
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
const seeded = ensureReviewSkillFile(worktreePath);
|
|
1841
|
+
return skillGuidelines(seeded.content, seeded.wrote ? "stock" : "skill");
|
|
1820
1842
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
// src/detect/detect.ts
|
|
1826
|
-
async function detectAgents() {
|
|
1827
|
-
ensureAgentPath();
|
|
1828
|
-
enrichPathWithNpmGlobalBin();
|
|
1829
|
-
return Promise.all(allAdapters().map((a) => a.detect()));
|
|
1843
|
+
function ensureReviewRequestFile(worktreePath) {
|
|
1844
|
+
const seeded = ensureReviewSkillFile(worktreePath);
|
|
1845
|
+
return skillGuidelines(seeded.content, "skill");
|
|
1830
1846
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
if (!
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1850
|
-
if (opts?.requireLinear && !status.linearMcp) {
|
|
1847
|
+
function buildReviewRequestAttachment(content, opts) {
|
|
1848
|
+
const path = opts?.path ?? REVIEW_SKILL_PATH;
|
|
1849
|
+
const name = opts?.name ?? (path === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
|
|
1850
|
+
return {
|
|
1851
|
+
id: randomUUID2(),
|
|
1852
|
+
name,
|
|
1853
|
+
kind: "file",
|
|
1854
|
+
path,
|
|
1855
|
+
content
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
function readExistingReviewRequestFile(worktreePath) {
|
|
1859
|
+
return readTextIfPresent(join4(worktreePath, REVIEW_SKILL_PATH)) ?? readTextIfPresent(join4(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent(join4(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent(join4(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
|
|
1860
|
+
}
|
|
1861
|
+
async function requestReview(threadRef, send) {
|
|
1862
|
+
const from = findThreadByRef(threadRef);
|
|
1863
|
+
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
1864
|
+
if (isOrchestratorThread(from)) {
|
|
1851
1865
|
throw new Error(
|
|
1852
|
-
|
|
1866
|
+
"request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
1853
1867
|
);
|
|
1854
1868
|
}
|
|
1855
|
-
|
|
1869
|
+
if (from.status === "archived") {
|
|
1870
|
+
throw new Error(`Thread is archived: ${from.id}`);
|
|
1871
|
+
}
|
|
1872
|
+
const guidelines = resolveReviewGuidelines(from.worktreePath);
|
|
1873
|
+
const tab = createChatTab({
|
|
1874
|
+
fromThreadId: from.id,
|
|
1875
|
+
title: "Review",
|
|
1876
|
+
attachments: [
|
|
1877
|
+
buildReviewRequestAttachment(guidelines.content, {
|
|
1878
|
+
path: guidelines.path,
|
|
1879
|
+
name: guidelines.name
|
|
1880
|
+
})
|
|
1881
|
+
]
|
|
1882
|
+
});
|
|
1883
|
+
const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
|
|
1884
|
+
return { tab: started, from };
|
|
1856
1885
|
}
|
|
1857
1886
|
|
|
1858
|
-
// src/
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
} else if (input.sourceType === "ticket") {
|
|
1886
|
-
sourceRef = await resolveDefaultBranch(repoPath);
|
|
1887
|
-
} else if (input.sourceType === "branch") {
|
|
1888
|
-
if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
|
|
1889
|
-
sourceRef = await resolveDefaultBranch(repoPath);
|
|
1890
|
-
} else {
|
|
1891
|
-
const existing = await getPrForHeadBranch(repoPath, sourceRef);
|
|
1892
|
-
if (existing?.url) {
|
|
1893
|
-
prUrl = existing.url;
|
|
1894
|
-
prTitle = existing.title;
|
|
1887
|
+
// src/hook/conductor.ts
|
|
1888
|
+
var PORT_RANGE_SIZE = 10;
|
|
1889
|
+
function matchSimpleGlob(pattern, name) {
|
|
1890
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1891
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
1892
|
+
}
|
|
1893
|
+
function readWorktreeInclude(repoPath) {
|
|
1894
|
+
const path = join5(repoPath, ".worktreeinclude");
|
|
1895
|
+
if (!existsSync4(path)) return [];
|
|
1896
|
+
return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
1897
|
+
}
|
|
1898
|
+
function resolveFilesToCopy(repoPath) {
|
|
1899
|
+
const fromInclude = readWorktreeInclude(repoPath);
|
|
1900
|
+
if (fromInclude.length) return fromInclude;
|
|
1901
|
+
const settings = loadRepoSettings(repoPath);
|
|
1902
|
+
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
1903
|
+
if (settings?.fileIncludeGlobs?.length) {
|
|
1904
|
+
const matched = [];
|
|
1905
|
+
try {
|
|
1906
|
+
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1907
|
+
if (!entry.isFile()) continue;
|
|
1908
|
+
for (const glob of settings.fileIncludeGlobs) {
|
|
1909
|
+
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
1910
|
+
matched.push(entry.name);
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1895
1914
|
}
|
|
1915
|
+
} catch {
|
|
1896
1916
|
}
|
|
1897
|
-
|
|
1898
|
-
throw new Error("Use adoptThread() for adopt sources");
|
|
1917
|
+
if (matched.length) return [...new Set(matched)];
|
|
1899
1918
|
}
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
repoPath,
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
const thread = createEmptyThread({
|
|
1909
|
-
title: explicitTitle || team.name,
|
|
1910
|
-
userSetTitle: Boolean(explicitTitle),
|
|
1911
|
-
sourceType: input.sourceType,
|
|
1912
|
-
sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
|
|
1913
|
-
branchName,
|
|
1914
|
-
worktreePath,
|
|
1915
|
-
repoPath,
|
|
1916
|
-
agent: resolved.agent,
|
|
1917
|
-
autonomy: input.autonomy ?? "default",
|
|
1918
|
-
model: resolved.model,
|
|
1919
|
-
effort: resolved.effort,
|
|
1920
|
-
fast: resolved.fast,
|
|
1921
|
-
planMode: Boolean(input.planMode),
|
|
1922
|
-
attachments: input.attachments ?? [],
|
|
1923
|
-
sourceIsFork,
|
|
1924
|
-
parentThreadId: input.parentThreadId ?? null,
|
|
1925
|
-
status: "idle",
|
|
1926
|
-
prUrl,
|
|
1927
|
-
prTitle
|
|
1928
|
-
});
|
|
1929
|
-
writeThread(thread);
|
|
1930
|
-
await ensureWorkspace(repoPath);
|
|
1931
|
-
return readThread(thread.id) ?? thread;
|
|
1932
|
-
}
|
|
1933
|
-
async function listLinearIssues(agent, repoPath) {
|
|
1934
|
-
const { getAdapter: getAdapter2 } = await import("./agents-4XQANGFO.js");
|
|
1935
|
-
await requireAgent(agent, { requireLinear: true });
|
|
1936
|
-
const adapter = getAdapter2(agent);
|
|
1937
|
-
if (!adapter.listLinearIssues) {
|
|
1938
|
-
throw new Error(`${agent} does not support Linear issue listing`);
|
|
1919
|
+
const defaults = [];
|
|
1920
|
+
try {
|
|
1921
|
+
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1922
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
1923
|
+
defaults.push(entry.name);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
} catch {
|
|
1939
1927
|
}
|
|
1940
|
-
|
|
1928
|
+
if (defaults.length) return defaults;
|
|
1929
|
+
return [".env.local", ".env"];
|
|
1941
1930
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
[\u2026truncated\u2026]` : transcript;
|
|
1953
|
-
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
1954
|
-
if (claudeSummary) {
|
|
1955
|
-
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
1931
|
+
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
1932
|
+
const patterns = resolveFilesToCopy(repoPath);
|
|
1933
|
+
const copied = [];
|
|
1934
|
+
for (const rel of patterns) {
|
|
1935
|
+
const src = join5(repoPath, rel);
|
|
1936
|
+
if (!existsSync4(src)) continue;
|
|
1937
|
+
const dest = join5(worktreePath, rel);
|
|
1938
|
+
mkdirSync2(dirname2(dest), { recursive: true });
|
|
1939
|
+
copyFileSync(src, dest);
|
|
1940
|
+
copied.push(rel);
|
|
1956
1941
|
}
|
|
1957
|
-
return
|
|
1942
|
+
return copied;
|
|
1958
1943
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
"Use concise markdown bullets covering:",
|
|
1964
|
-
"- Goal / request",
|
|
1965
|
-
"- Key decisions and constraints",
|
|
1966
|
-
"- Files / areas touched",
|
|
1967
|
-
"- Current state and open tasks",
|
|
1968
|
-
"- Anything the next turn must remember",
|
|
1969
|
-
"Do not invent details. No preamble.",
|
|
1970
|
-
"",
|
|
1971
|
-
"Transcript:",
|
|
1972
|
-
transcript
|
|
1973
|
-
].join("\n");
|
|
1944
|
+
var cachedLoginEnv = null;
|
|
1945
|
+
async function captureLoginEnv() {
|
|
1946
|
+
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
1947
|
+
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
1974
1948
|
try {
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
"
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
return text;
|
|
1949
|
+
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
1950
|
+
reject: false,
|
|
1951
|
+
timeout: 5e3
|
|
1952
|
+
});
|
|
1953
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
1954
|
+
const env = { ...process.env };
|
|
1955
|
+
for (const entry of result.stdout.split("\0")) {
|
|
1956
|
+
if (!entry) continue;
|
|
1957
|
+
const eq = entry.indexOf("=");
|
|
1958
|
+
if (eq <= 0) continue;
|
|
1959
|
+
const key = entry.slice(0, eq);
|
|
1960
|
+
const value = entry.slice(eq + 1);
|
|
1961
|
+
if (key) env[key] = value;
|
|
1962
|
+
}
|
|
1963
|
+
cachedLoginEnv = env;
|
|
1964
|
+
return { ...env };
|
|
1965
|
+
}
|
|
1993
1966
|
} catch {
|
|
1994
|
-
return null;
|
|
1995
1967
|
}
|
|
1968
|
+
cachedLoginEnv = { ...process.env };
|
|
1969
|
+
return { ...cachedLoginEnv };
|
|
1996
1970
|
}
|
|
1997
|
-
function
|
|
1998
|
-
const
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
1971
|
+
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
1972
|
+
const env = stripNestedElectronEnv({
|
|
1973
|
+
...baseEnv ?? process.env
|
|
1974
|
+
});
|
|
1975
|
+
const name = opts.workspaceName ?? basename(opts.worktreePath);
|
|
1976
|
+
const ports = opts.ports ?? [];
|
|
1977
|
+
const primary = ports[0];
|
|
1978
|
+
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
1979
|
+
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
1980
|
+
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
1981
|
+
env.SIDEBOARD_IS_LOCAL = "1";
|
|
1982
|
+
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1983
|
+
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
1984
|
+
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
1985
|
+
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
1986
|
+
env.CONDUCTOR_IS_LOCAL = "1";
|
|
1987
|
+
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1988
|
+
if (primary != null) {
|
|
1989
|
+
env.SIDEBOARD_PORT = String(primary);
|
|
1990
|
+
env.CONDUCTOR_PORT = String(primary);
|
|
1991
|
+
env.PORT = String(primary);
|
|
1992
|
+
for (let i = 1; i < ports.length; i++) {
|
|
1993
|
+
const p = ports[i];
|
|
1994
|
+
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
1995
|
+
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
2020
1996
|
}
|
|
2021
1997
|
}
|
|
2022
|
-
|
|
2023
|
-
const parts = [
|
|
2024
|
-
"## Compacted context (extractive)",
|
|
2025
|
-
"",
|
|
2026
|
-
"### User goals",
|
|
2027
|
-
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
2028
|
-
"",
|
|
2029
|
-
"### Recent agent notes",
|
|
2030
|
-
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
2031
|
-
];
|
|
2032
|
-
if (uniqueTools.length) {
|
|
2033
|
-
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
2034
|
-
}
|
|
2035
|
-
parts.push(
|
|
2036
|
-
"",
|
|
2037
|
-
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
2038
|
-
);
|
|
2039
|
-
return parts.join("\n");
|
|
1998
|
+
return env;
|
|
2040
1999
|
}
|
|
2041
|
-
function
|
|
2042
|
-
if (
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
[\u2026summary truncated\u2026]`;
|
|
2000
|
+
function pipeLines(stream, onLine) {
|
|
2001
|
+
if (!stream || !onLine) return;
|
|
2002
|
+
const rl = createInterface2({ input: stream });
|
|
2003
|
+
rl.on("line", onLine);
|
|
2046
2004
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2005
|
+
function killScriptTree(child, ports = []) {
|
|
2006
|
+
const pid = child.pid;
|
|
2007
|
+
if (pid) {
|
|
2008
|
+
try {
|
|
2009
|
+
if (process.platform === "win32") {
|
|
2010
|
+
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
2011
|
+
} else {
|
|
2012
|
+
process.kill(-pid, "SIGTERM");
|
|
2013
|
+
setTimeout(() => {
|
|
2014
|
+
try {
|
|
2015
|
+
process.kill(-pid, "SIGKILL");
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
}, 2500).unref?.();
|
|
2019
|
+
}
|
|
2020
|
+
} catch {
|
|
2021
|
+
try {
|
|
2022
|
+
child.kill("SIGTERM");
|
|
2023
|
+
} catch {
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2057
2026
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2027
|
+
for (const port of ports) {
|
|
2028
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2029
|
+
if (process.platform === "win32") {
|
|
2030
|
+
void execa2(
|
|
2031
|
+
"powershell",
|
|
2032
|
+
[
|
|
2033
|
+
"-NoProfile",
|
|
2034
|
+
"-Command",
|
|
2035
|
+
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
2036
|
+
],
|
|
2037
|
+
{ reject: false }
|
|
2038
|
+
);
|
|
2039
|
+
} else {
|
|
2040
|
+
void execa2(
|
|
2041
|
+
"zsh",
|
|
2042
|
+
["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
|
|
2043
|
+
{ reject: false }
|
|
2044
|
+
);
|
|
2068
2045
|
}
|
|
2069
|
-
default:
|
|
2070
|
-
return 0;
|
|
2071
2046
|
}
|
|
2072
2047
|
}
|
|
2073
|
-
function
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
if (kept >= keepCount && next > keepChars) break;
|
|
2092
|
-
recentChars = next;
|
|
2093
|
-
cut = i;
|
|
2048
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
2049
|
+
const loginEnv = await captureLoginEnv();
|
|
2050
|
+
const env = buildWorkspaceScriptEnv(
|
|
2051
|
+
{
|
|
2052
|
+
worktreePath: opts.worktreePath,
|
|
2053
|
+
repoPath: opts.repoPath,
|
|
2054
|
+
workspaceName: opts.workspaceName,
|
|
2055
|
+
defaultBranch: opts.defaultBranch,
|
|
2056
|
+
ports: opts.ports
|
|
2057
|
+
},
|
|
2058
|
+
loginEnv
|
|
2059
|
+
);
|
|
2060
|
+
try {
|
|
2061
|
+
mergeAgentGitAuthEnv(
|
|
2062
|
+
env,
|
|
2063
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2064
|
+
);
|
|
2065
|
+
} catch {
|
|
2094
2066
|
}
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2067
|
+
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
2068
|
+
const child = execa2(shell, ["-lc", command], {
|
|
2069
|
+
cwd: opts.worktreePath,
|
|
2070
|
+
reject: false,
|
|
2071
|
+
env,
|
|
2072
|
+
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2073
|
+
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2074
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
2075
|
+
});
|
|
2076
|
+
pipeLines(child.stdout, opts.onLine);
|
|
2077
|
+
pipeLines(child.stderr, opts.onLine);
|
|
2078
|
+
const ports = opts.ports ?? [];
|
|
2098
2079
|
return {
|
|
2099
|
-
|
|
2100
|
-
|
|
2080
|
+
pid: child.pid,
|
|
2081
|
+
kill: () => killScriptTree(child, ports),
|
|
2082
|
+
done: child.then((r) => r.exitCode ?? null),
|
|
2083
|
+
child
|
|
2101
2084
|
};
|
|
2102
2085
|
}
|
|
2103
|
-
function
|
|
2104
|
-
if (
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
}
|
|
2109
|
-
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
2110
|
-
if (t.description) lines.push(`Description: ${t.description}`);
|
|
2111
|
-
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
2112
|
-
if (t.input && Object.keys(t.input).length > 0) {
|
|
2113
|
-
lines.push("Input:");
|
|
2114
|
-
lines.push("```json");
|
|
2115
|
-
lines.push(JSON.stringify(t.input, null, 2));
|
|
2116
|
-
lines.push("```");
|
|
2117
|
-
} else if (t.detail) {
|
|
2118
|
-
lines.push(`Detail: ${t.detail}`);
|
|
2119
|
-
}
|
|
2120
|
-
if (t.result != null && t.result !== "") {
|
|
2121
|
-
lines.push("Result:");
|
|
2122
|
-
lines.push("```");
|
|
2123
|
-
lines.push(t.result);
|
|
2124
|
-
lines.push("```");
|
|
2086
|
+
async function attachAbort(handle, signal) {
|
|
2087
|
+
if (signal) {
|
|
2088
|
+
const onAbort = () => handle.kill();
|
|
2089
|
+
if (signal.aborted) onAbort();
|
|
2090
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2125
2091
|
}
|
|
2126
|
-
return
|
|
2092
|
+
return handle.done;
|
|
2127
2093
|
}
|
|
2128
|
-
function
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2094
|
+
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
2095
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2096
|
+
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
2097
|
+
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
2098
|
+
worktreePath,
|
|
2099
|
+
repoPath,
|
|
2100
|
+
defaultBranch: opts?.defaultBranch,
|
|
2101
|
+
onLine
|
|
2102
|
+
});
|
|
2103
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2104
|
+
return {
|
|
2105
|
+
ran: true,
|
|
2106
|
+
exitCode,
|
|
2107
|
+
source: workspaceSettingsSourceLabel(worktreePath, repoPath),
|
|
2108
|
+
kill: handle.kill
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
2112
|
+
const found = findConventionSetup(worktreePath, repoPath);
|
|
2113
|
+
if (!found) return { ran: false, exitCode: null, source: null };
|
|
2114
|
+
onLine?.(`[setup] ${found.source}`);
|
|
2115
|
+
const handle = await spawnWorkspaceScript(found.command, {
|
|
2116
|
+
worktreePath,
|
|
2117
|
+
repoPath,
|
|
2118
|
+
defaultBranch: opts?.defaultBranch,
|
|
2119
|
+
onLine
|
|
2120
|
+
});
|
|
2121
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2122
|
+
return {
|
|
2123
|
+
ran: true,
|
|
2124
|
+
exitCode,
|
|
2125
|
+
source: found.source,
|
|
2126
|
+
kill: handle.kill
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
|
|
2130
|
+
const reviewSkill = ensureReviewSkillFile(worktreePath);
|
|
2131
|
+
if (reviewSkill.wrote) {
|
|
2132
|
+
onLine?.(
|
|
2133
|
+
`[setup] wrote ${REVIEW_SKILL_PATH} \u2014 commit it so later worktrees inherit Review guidelines`
|
|
2157
2134
|
);
|
|
2158
|
-
if (tools !== "none" && toolParts.length > 0) {
|
|
2159
|
-
if (tools === "summary") bits.push("Tools:");
|
|
2160
|
-
for (const t of toolParts) {
|
|
2161
|
-
bits.push(formatToolPart(t, tools));
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
blocks.push(bits.join("\n"));
|
|
2165
2135
|
}
|
|
2166
|
-
|
|
2136
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
2137
|
+
if (!setup.ran) {
|
|
2138
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
2139
|
+
}
|
|
2140
|
+
if (!setup.ran) {
|
|
2141
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
2142
|
+
}
|
|
2143
|
+
return setup;
|
|
2167
2144
|
}
|
|
2168
|
-
function
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
].join("\n");
|
|
2145
|
+
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
2146
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2147
|
+
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
2148
|
+
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
2149
|
+
worktreePath,
|
|
2150
|
+
repoPath,
|
|
2151
|
+
onLine
|
|
2152
|
+
});
|
|
2153
|
+
const exitCode = await handle.done;
|
|
2154
|
+
return { ran: true, exitCode };
|
|
2179
2155
|
}
|
|
2180
|
-
function
|
|
2181
|
-
const
|
|
2182
|
-
if (
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
};
|
|
2188
|
-
return [summary, ...recent];
|
|
2156
|
+
function listRunScripts(worktreePath, repoPath) {
|
|
2157
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2158
|
+
if (!settings?.runScripts.length) return [];
|
|
2159
|
+
return settings.runScripts.filter((s) => {
|
|
2160
|
+
if (!s.availableIn?.length) return true;
|
|
2161
|
+
return s.availableIn.includes("local");
|
|
2162
|
+
});
|
|
2189
2163
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2164
|
+
function getDefaultRunScript(worktreePath, repoPath) {
|
|
2165
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2166
|
+
if (!scripts.length) return null;
|
|
2167
|
+
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
2168
|
+
}
|
|
2169
|
+
function getRunScript(worktreePath, repoPath, name) {
|
|
2170
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2171
|
+
if (!scripts.length) return null;
|
|
2172
|
+
if (name) {
|
|
2173
|
+
return scripts.find((s) => s.name === name) ?? null;
|
|
2193
2174
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2175
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
2176
|
+
}
|
|
2177
|
+
function getRunMode(worktreePath, repoPath) {
|
|
2178
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2179
|
+
}
|
|
2180
|
+
async function allocatePort() {
|
|
2181
|
+
return new Promise((resolve, reject) => {
|
|
2182
|
+
const server = createServer();
|
|
2183
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2184
|
+
const addr = server.address();
|
|
2185
|
+
if (!addr || typeof addr === "string") {
|
|
2186
|
+
server.close();
|
|
2187
|
+
reject(new Error("Failed to allocate port"));
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const port = addr.port;
|
|
2191
|
+
server.close((err) => err ? reject(err) : resolve(port));
|
|
2192
|
+
});
|
|
2193
|
+
server.on("error", reject);
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
async function allocatePortRange(size = PORT_RANGE_SIZE) {
|
|
2197
|
+
const base = await allocatePort();
|
|
2198
|
+
const ports = [base];
|
|
2199
|
+
for (let i = 1; i < size; i++) {
|
|
2200
|
+
const candidate = base + i;
|
|
2201
|
+
const free = await new Promise((resolve) => {
|
|
2202
|
+
const server = createServer();
|
|
2203
|
+
server.once("error", () => resolve(false));
|
|
2204
|
+
server.listen(candidate, "127.0.0.1", () => {
|
|
2205
|
+
server.close(() => resolve(true));
|
|
2206
|
+
});
|
|
2207
|
+
});
|
|
2208
|
+
if (free) {
|
|
2209
|
+
ports.push(candidate);
|
|
2210
|
+
} else {
|
|
2211
|
+
ports.push(await allocatePort());
|
|
2212
|
+
}
|
|
2197
2213
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2214
|
+
return ports;
|
|
2215
|
+
}
|
|
2216
|
+
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
2217
|
+
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2218
|
+
if (!script) return null;
|
|
2219
|
+
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
2220
|
+
const handle = await spawnWorkspaceScript(script.command, {
|
|
2221
|
+
worktreePath,
|
|
2222
|
+
repoPath,
|
|
2223
|
+
ports,
|
|
2224
|
+
defaultBranch: opts?.defaultBranch,
|
|
2225
|
+
onLine
|
|
2201
2226
|
});
|
|
2202
|
-
const messages = applyCompaction(thread.messages, summary, thresholds);
|
|
2203
|
-
const next = {
|
|
2204
|
-
...thread,
|
|
2205
|
-
messages,
|
|
2206
|
-
sessionId: null,
|
|
2207
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2208
|
-
};
|
|
2209
2227
|
return {
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2228
|
+
pid: handle.pid,
|
|
2229
|
+
port: ports[0],
|
|
2230
|
+
ports,
|
|
2231
|
+
scriptName: script.name,
|
|
2232
|
+
kill: handle.kill,
|
|
2233
|
+
done: handle.done
|
|
2215
2234
|
};
|
|
2216
2235
|
}
|
|
2217
2236
|
|
|
2218
|
-
// src/
|
|
2219
|
-
|
|
2220
|
-
|
|
2237
|
+
// src/git/orphan-cleanup.ts
|
|
2238
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, statSync } from "fs";
|
|
2239
|
+
import { join as join6 } from "path";
|
|
2240
|
+
function isSideboardWorktreePath(path) {
|
|
2241
|
+
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
2221
2242
|
}
|
|
2222
|
-
function
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2243
|
+
async function findOrphanWorktrees(repoPaths) {
|
|
2244
|
+
const threads = listThreads({ includeArchived: true });
|
|
2245
|
+
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
2246
|
+
const repos = new Set(
|
|
2247
|
+
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
2248
|
+
);
|
|
2249
|
+
const homeRoot = sideboardWorkspacesDir();
|
|
2250
|
+
if (existsSync5(homeRoot)) {
|
|
2251
|
+
try {
|
|
2252
|
+
for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
|
|
2253
|
+
if (!entry.isDirectory()) continue;
|
|
2254
|
+
void entry;
|
|
2255
|
+
}
|
|
2256
|
+
} catch {
|
|
2230
2257
|
}
|
|
2231
2258
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2259
|
+
const orphans = [];
|
|
2260
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2261
|
+
for (const repoPath of repos) {
|
|
2262
|
+
if (!repoPath || !existsSync5(repoPath)) continue;
|
|
2263
|
+
try {
|
|
2264
|
+
const wts = await listWorktrees(repoPath);
|
|
2265
|
+
for (const wt of wts) {
|
|
2266
|
+
const path = wt.path.replace(/\/$/, "");
|
|
2267
|
+
if (!isSideboardWorktreePath(path)) continue;
|
|
2268
|
+
if (known.has(path)) continue;
|
|
2269
|
+
if (seen.has(path)) continue;
|
|
2270
|
+
seen.add(path);
|
|
2271
|
+
let mtimeMs = 0;
|
|
2272
|
+
try {
|
|
2273
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
2274
|
+
} catch {
|
|
2275
|
+
mtimeMs = 0;
|
|
2276
|
+
}
|
|
2277
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
2278
|
+
}
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
try {
|
|
2282
|
+
const root = worktreesRoot(repoPath);
|
|
2283
|
+
if (existsSync5(root)) {
|
|
2284
|
+
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
2285
|
+
if (!entry.isDirectory()) continue;
|
|
2286
|
+
const path = join6(root, entry.name).replace(/\/$/, "");
|
|
2287
|
+
if (known.has(path) || seen.has(path)) continue;
|
|
2288
|
+
if (!existsSync5(join6(path, ".git"))) continue;
|
|
2289
|
+
seen.add(path);
|
|
2290
|
+
let mtimeMs = 0;
|
|
2291
|
+
try {
|
|
2292
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
2293
|
+
} catch {
|
|
2294
|
+
mtimeMs = Date.now();
|
|
2295
|
+
}
|
|
2296
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
} catch {
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2264
2303
|
}
|
|
2265
|
-
function
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2304
|
+
async function cleanupOrphanWorktrees(opts) {
|
|
2305
|
+
const settings = loadAppSettings();
|
|
2306
|
+
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
2307
|
+
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
2308
|
+
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
2309
|
+
const budget = Math.max(0, maxCount - activeCount);
|
|
2310
|
+
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2311
|
+
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
2312
|
+
const removed = [];
|
|
2313
|
+
const kept = [];
|
|
2314
|
+
for (const orphan of orphans) {
|
|
2315
|
+
if (keep.has(orphan.path)) {
|
|
2316
|
+
kept.push(orphan.path);
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
if (opts?.dryRun) {
|
|
2320
|
+
removed.push(orphan.path);
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
try {
|
|
2324
|
+
await removeWorktree(orphan.repoPath, orphan.path);
|
|
2325
|
+
removed.push(orphan.path);
|
|
2326
|
+
} catch {
|
|
2327
|
+
kept.push(orphan.path);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (!opts?.dryRun) {
|
|
2331
|
+
updateAdvancedSettings({
|
|
2332
|
+
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
return { removed, kept, orphans };
|
|
2268
2336
|
}
|
|
2269
|
-
function
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2337
|
+
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
2338
|
+
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
2339
|
+
const last = settings.advanced.worktreeLastCleanupAt;
|
|
2340
|
+
if (!last) return true;
|
|
2341
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
2342
|
+
return elapsed >= intervalHours * 36e5;
|
|
2272
2343
|
}
|
|
2273
|
-
function
|
|
2274
|
-
const
|
|
2344
|
+
function worktreeCleanupSettings() {
|
|
2345
|
+
const a = loadAppSettings().advanced;
|
|
2275
2346
|
return {
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2347
|
+
worktreeMaxCount: a.worktreeMaxCount,
|
|
2348
|
+
worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
|
|
2349
|
+
worktreeLastCleanupAt: a.worktreeLastCleanupAt,
|
|
2350
|
+
autoCleanupOrphans: a.autoCleanupOrphans
|
|
2280
2351
|
};
|
|
2281
2352
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
2292
|
-
const thread = createEmptyThread({
|
|
2293
|
-
title,
|
|
2294
|
-
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
2295
|
-
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
2296
|
-
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
2297
|
-
userSetTitle: true,
|
|
2298
|
-
...binding,
|
|
2299
|
-
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
2300
|
-
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
2301
|
-
// the connected chat.
|
|
2302
|
-
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
2303
|
-
agent: nextAgent,
|
|
2304
|
-
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
2305
|
-
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
2306
|
-
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
2307
|
-
planMode: from.planMode,
|
|
2308
|
-
autonomy: input.autonomy ?? from.autonomy,
|
|
2309
|
-
attachments: input.attachments ?? [],
|
|
2310
|
-
status: "idle"
|
|
2353
|
+
|
|
2354
|
+
// src/git/apply-into-main.ts
|
|
2355
|
+
import { execa as execa3 } from "execa";
|
|
2356
|
+
async function applyThreadIntoMain(thread, opts) {
|
|
2357
|
+
const method = opts?.method ?? "merge";
|
|
2358
|
+
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
2359
|
+
const status = await execa3("git", ["status", "--porcelain"], {
|
|
2360
|
+
cwd: thread.repoPath,
|
|
2361
|
+
reject: false
|
|
2311
2362
|
});
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
const
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
fromThreadId: input.threadId,
|
|
2321
|
-
agent: input.agent ?? from.agent,
|
|
2322
|
-
model: input.model,
|
|
2323
|
-
title: input.title?.trim() || void 0,
|
|
2324
|
-
attachments: [attachment]
|
|
2363
|
+
if (status.stdout.trim()) {
|
|
2364
|
+
throw new Error(
|
|
2365
|
+
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
const checkout = await execa3("git", ["checkout", target], {
|
|
2369
|
+
cwd: thread.repoPath,
|
|
2370
|
+
reject: false
|
|
2325
2371
|
});
|
|
2326
|
-
if (
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2372
|
+
if (checkout.exitCode !== 0) {
|
|
2373
|
+
throw new Error(
|
|
2374
|
+
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
2375
|
+
);
|
|
2330
2376
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2377
|
+
if (method === "cherry-pick") {
|
|
2378
|
+
const log = await execa3(
|
|
2379
|
+
"git",
|
|
2380
|
+
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
2381
|
+
{ cwd: thread.repoPath, reject: false }
|
|
2382
|
+
);
|
|
2383
|
+
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2384
|
+
if (!shas.length) {
|
|
2385
|
+
return {
|
|
2386
|
+
applied: false,
|
|
2387
|
+
method,
|
|
2388
|
+
targetBranch: target,
|
|
2389
|
+
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
for (const sha of shas) {
|
|
2393
|
+
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
2394
|
+
cwd: thread.repoPath,
|
|
2395
|
+
reject: false
|
|
2396
|
+
});
|
|
2397
|
+
if (cp.exitCode !== 0) {
|
|
2398
|
+
await execa3("git", ["cherry-pick", "--abort"], {
|
|
2399
|
+
cwd: thread.repoPath,
|
|
2400
|
+
reject: false
|
|
2401
|
+
});
|
|
2402
|
+
throw new Error(
|
|
2403
|
+
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return {
|
|
2408
|
+
applied: true,
|
|
2409
|
+
method,
|
|
2410
|
+
targetBranch: target,
|
|
2411
|
+
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
2412
|
+
};
|
|
2353
2413
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
(p) => p.type === "tool"
|
|
2414
|
+
const merge = await execa3(
|
|
2415
|
+
"git",
|
|
2416
|
+
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
2417
|
+
{ cwd: thread.repoPath, reject: false }
|
|
2359
2418
|
);
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
if (lastToolLabel) {
|
|
2369
|
-
const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
|
|
2370
|
-
summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
|
|
2371
|
-
} else if (thinking) {
|
|
2372
|
-
summary = "Thinking\u2026";
|
|
2373
|
-
} else if (text) {
|
|
2374
|
-
summary = "Writing reply\u2026";
|
|
2419
|
+
if (merge.exitCode !== 0) {
|
|
2420
|
+
await execa3("git", ["merge", "--abort"], {
|
|
2421
|
+
cwd: thread.repoPath,
|
|
2422
|
+
reject: false
|
|
2423
|
+
});
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
2426
|
+
);
|
|
2375
2427
|
}
|
|
2376
2428
|
return {
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
excerpt
|
|
2429
|
+
applied: true,
|
|
2430
|
+
method: "merge",
|
|
2431
|
+
targetBranch: target,
|
|
2432
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
2382
2433
|
};
|
|
2383
2434
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2435
|
+
|
|
2436
|
+
// src/git/clone-repo.ts
|
|
2437
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2438
|
+
import { basename as basename2, join as join7 } from "path";
|
|
2439
|
+
import { execa as execa4 } from "execa";
|
|
2440
|
+
async function cloneRepoIntoSideboard(opts) {
|
|
2441
|
+
const url = opts.url.trim();
|
|
2442
|
+
if (!url) throw new Error("Clone URL is required");
|
|
2443
|
+
let name = opts.name?.trim();
|
|
2444
|
+
if (!name) {
|
|
2445
|
+
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
2446
|
+
name = leaf || "repo";
|
|
2390
2447
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
buf.timer = setTimeout(() => {
|
|
2398
|
-
const live = buffers.get(threadId);
|
|
2399
|
-
if (live) live.timer = null;
|
|
2400
|
-
flush(threadId);
|
|
2401
|
-
}, FLUSH_MS);
|
|
2402
|
-
}
|
|
2403
|
-
function noteTurnLiveEvent(threadId, event) {
|
|
2404
|
-
if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
|
|
2405
|
-
return;
|
|
2448
|
+
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
2449
|
+
const dest = join7(sideboardReposDir(), name);
|
|
2450
|
+
if (existsSync6(dest)) {
|
|
2451
|
+
const repoPath2 = await resolveRepoRoot(dest);
|
|
2452
|
+
const workspace2 = await ensureWorkspace(repoPath2);
|
|
2453
|
+
return { repoPath: repoPath2, workspace: workspace2 };
|
|
2406
2454
|
}
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
const cur = buffers.get(threadId);
|
|
2413
|
-
if (cur?.timer) clearTimeout(cur.timer);
|
|
2414
|
-
buffers.delete(threadId);
|
|
2415
|
-
return;
|
|
2455
|
+
const result = await execa4("git", ["clone", url, dest], { reject: false });
|
|
2456
|
+
if (result.exitCode !== 0) {
|
|
2457
|
+
throw new Error(
|
|
2458
|
+
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
2459
|
+
);
|
|
2416
2460
|
}
|
|
2417
|
-
const
|
|
2418
|
-
|
|
2419
|
-
|
|
2461
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
2462
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
2463
|
+
return { repoPath, workspace };
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// src/store/desktop-host.ts
|
|
2467
|
+
import { readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2468
|
+
import { join as join8 } from "path";
|
|
2469
|
+
function desktopHostPidPath() {
|
|
2470
|
+
return join8(appDataDir(), "desktop-host.pid");
|
|
2471
|
+
}
|
|
2472
|
+
function claimDesktopHost(pid = process.pid) {
|
|
2473
|
+
writeFileSync2(desktopHostPidPath(), `${pid}
|
|
2474
|
+
`, "utf8");
|
|
2420
2475
|
}
|
|
2421
|
-
function
|
|
2422
|
-
|
|
2423
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
2476
|
+
function releaseDesktopHost(pid = process.pid) {
|
|
2477
|
+
if (readDesktopHostPid() !== pid) return;
|
|
2424
2478
|
try {
|
|
2425
|
-
|
|
2426
|
-
renameSync(tmp, path);
|
|
2479
|
+
unlinkSync(desktopHostPidPath());
|
|
2427
2480
|
} catch {
|
|
2428
|
-
try {
|
|
2429
|
-
unlinkSync2(tmp);
|
|
2430
|
-
} catch {
|
|
2431
|
-
}
|
|
2432
2481
|
}
|
|
2433
2482
|
}
|
|
2434
|
-
function
|
|
2435
|
-
const path = threadLivePath(threadId);
|
|
2436
|
-
if (!existsSync7(path)) return null;
|
|
2483
|
+
function readDesktopHostPid() {
|
|
2437
2484
|
try {
|
|
2438
|
-
const
|
|
2439
|
-
if (!
|
|
2440
|
-
return
|
|
2485
|
+
const pid = Number.parseInt(readFileSync5(desktopHostPidPath(), "utf8").trim(), 10);
|
|
2486
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2487
|
+
return pid;
|
|
2441
2488
|
} catch {
|
|
2442
2489
|
return null;
|
|
2443
2490
|
}
|
|
2444
2491
|
}
|
|
2445
|
-
function
|
|
2446
|
-
const buf = buffers.get(threadId);
|
|
2447
|
-
if (buf?.timer) clearTimeout(buf.timer);
|
|
2448
|
-
buffers.delete(threadId);
|
|
2449
|
-
const path = threadLivePath(threadId);
|
|
2450
|
-
if (!existsSync7(path)) return;
|
|
2492
|
+
function pidAlive(pid) {
|
|
2451
2493
|
try {
|
|
2452
|
-
|
|
2494
|
+
process.kill(pid, 0);
|
|
2495
|
+
return true;
|
|
2453
2496
|
} catch {
|
|
2497
|
+
return false;
|
|
2454
2498
|
}
|
|
2455
2499
|
}
|
|
2500
|
+
function isDesktopHostAlive() {
|
|
2501
|
+
const pid = readDesktopHostPid();
|
|
2502
|
+
return pid != null && pidAlive(pid);
|
|
2503
|
+
}
|
|
2504
|
+
function isThisProcessDesktopHost() {
|
|
2505
|
+
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
2506
|
+
}
|
|
2507
|
+
function thisProcessShouldDrainAgentQueues() {
|
|
2508
|
+
if (isThisProcessDesktopHost()) return true;
|
|
2509
|
+
return !isDesktopHostAlive();
|
|
2510
|
+
}
|
|
2456
2511
|
|
|
2457
|
-
// src/
|
|
2458
|
-
import {
|
|
2459
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
2460
|
-
import { dirname as dirname2, join as join8 } from "path";
|
|
2461
|
-
|
|
2462
|
-
// src/review/review-request-template.ts
|
|
2463
|
-
var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
|
|
2464
|
-
|
|
2465
|
-
You are reviewing a proposed code change so a human can decide whether it is **ready to merge / land**. Findings matter, but the primary deliverable is a clear readiness recommendation \u2014 not a laundry list of style notes.
|
|
2466
|
-
|
|
2467
|
-
## Required outcome
|
|
2468
|
-
|
|
2469
|
-
Start your reply with a **Recommendation** section using exactly one of:
|
|
2470
|
-
|
|
2471
|
-
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
2472
|
-
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
2473
|
-
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
2474
|
-
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
2475
|
-
|
|
2476
|
-
In 1\u20133 sentences, say **why** \u2014 grounded in correctness, risk, test coverage, and scope \u2014 not vibes. If you request changes, name the blockers explicitly.
|
|
2477
|
-
|
|
2478
|
-
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
2479
|
-
|
|
2480
|
-
## Findings
|
|
2481
|
-
|
|
2482
|
-
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
2483
|
-
|
|
2484
|
-
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
2485
|
-
|
|
2486
|
-
Flag something as a bug / blocking finding only when:
|
|
2487
|
-
|
|
2488
|
-
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
2489
|
-
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
2490
|
-
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
2491
|
-
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
2492
|
-
5. The author would likely fix it if made aware.
|
|
2493
|
-
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
2494
|
-
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
2495
|
-
8. It is clearly not just an intentional change by the author.
|
|
2496
|
-
|
|
2497
|
-
When flagging an issue, include a short accompanying comment:
|
|
2498
|
-
|
|
2499
|
-
1. Clear about why it is a problem.
|
|
2500
|
-
2. Severity must match reality \u2014 do not inflate.
|
|
2501
|
-
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
2502
|
-
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
2503
|
-
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
2504
|
-
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
2505
|
-
7. Skimmable on first read.
|
|
2506
|
-
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
2507
|
-
|
|
2508
|
-
HOW MANY FINDINGS TO RETURN:
|
|
2509
|
-
|
|
2510
|
-
List every finding the author would fix if they knew about it. If nothing qualifies, say so and still give the Recommendation. Do not stop at the first finding.
|
|
2511
|
-
|
|
2512
|
-
GUIDELINES:
|
|
2513
|
-
|
|
2514
|
-
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
2515
|
-
- One comment per distinct issue (or a short multi-line range if needed).
|
|
2516
|
-
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
2517
|
-
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
2518
|
-
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
2519
|
-
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
2520
|
-
|
|
2521
|
-
The report appears in chat (and can become Sideboard diff comments). Avoid unnecessary location chatter in the body; keep line ranges as short as possible (prefer \u22645\u201310 lines).
|
|
2522
|
-
|
|
2523
|
-
## Getting the diff
|
|
2524
|
-
|
|
2525
|
-
Use Sideboard's diff for this thread's worktree. Prefer the \`get_diff\` MCP tool (pass this thread's ref) for a compact summary, then read specific files with Read/Glob as needed. In the Sideboard desktop app, the Changes panel shows the same worktree diff.
|
|
2526
|
-
|
|
2527
|
-
If the user asks you to address or read line comments they added in the Changes / file diff UI, those arrive as \`diff-comment\` attachments on the next turn \u2014 follow them precisely.
|
|
2528
|
-
|
|
2529
|
-
## Fallback: if you don't have access to the Sideboard diff tool
|
|
2530
|
-
|
|
2531
|
-
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
2532
|
-
|
|
2533
|
-
\`\`\`bash
|
|
2534
|
-
# Get the merge base between this branch and the target
|
|
2535
|
-
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
2536
|
-
|
|
2537
|
-
# Get the committed diff against the merge base
|
|
2538
|
-
git diff $MERGE_BASE HEAD
|
|
2539
|
-
|
|
2540
|
-
# Get any uncommitted changes (staged and unstaged)
|
|
2541
|
-
git diff HEAD
|
|
2542
|
-
\`\`\`
|
|
2543
|
-
|
|
2544
|
-
Review the combination of both outputs: the first shows all committed changes on this branch relative to the target, and the second shows any uncommitted work in progress.
|
|
2545
|
-
|
|
2546
|
-
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
2547
|
-
|
|
2548
|
-
## Output format
|
|
2549
|
-
|
|
2550
|
-
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
2551
|
-
|
|
2552
|
-
Only report ONE finding per unique issue.
|
|
2553
|
-
|
|
2554
|
-
<example>
|
|
2555
|
-
## Recommendation
|
|
2556
|
-
|
|
2557
|
-
**Request changes** \u2014 The empty-input crash on load will break first-run users; fix that before merge. The unused helper is a nit and can wait.
|
|
2558
|
-
|
|
2559
|
-
## Findings
|
|
2560
|
-
|
|
2561
|
-
### **#1 Empty input causes crash** (blocking)
|
|
2562
|
-
|
|
2563
|
-
If the input field is empty when the page loads, the app will crash.
|
|
2564
|
-
|
|
2565
|
-
File: src/client/frontends/desktop/ui/Input.tsx
|
|
2566
|
-
|
|
2567
|
-
### **#2 Dead code** (nit)
|
|
2568
|
-
|
|
2569
|
-
The getUserData function is now unused. It should be deleted.
|
|
2570
|
-
|
|
2571
|
-
File: src/client/frontends/desktop/core/UserData.ts
|
|
2572
|
-
</example>
|
|
2573
|
-
|
|
2574
|
-
<example>
|
|
2575
|
-
## Recommendation
|
|
2576
|
-
|
|
2577
|
-
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
2578
|
-
</example>
|
|
2579
|
-
|
|
2580
|
-
## Growing the rules
|
|
2512
|
+
// src/threads/create.ts
|
|
2513
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2581
2514
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2515
|
+
// src/detect/detect.ts
|
|
2516
|
+
async function detectAgents() {
|
|
2517
|
+
ensureAgentPath();
|
|
2518
|
+
enrichPathWithNpmGlobalBin();
|
|
2519
|
+
return Promise.all(allAdapters().map((a) => a.detect()));
|
|
2520
|
+
}
|
|
2521
|
+
var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
|
|
2522
|
+
async function requireAgent(agent, opts) {
|
|
2523
|
+
ensureAgentPath();
|
|
2524
|
+
enrichPathWithNpmGlobalBin();
|
|
2525
|
+
const status = await Promise.race([
|
|
2526
|
+
getAdapter(agent).detect(),
|
|
2527
|
+
new Promise((_, reject) => {
|
|
2528
|
+
setTimeout(() => {
|
|
2529
|
+
reject(
|
|
2530
|
+
new Error(
|
|
2531
|
+
`${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
|
|
2532
|
+
)
|
|
2533
|
+
);
|
|
2534
|
+
}, REQUIRE_AGENT_TIMEOUT_MS);
|
|
2535
|
+
})
|
|
2536
|
+
]);
|
|
2537
|
+
if (!status.installed || !status.authenticated) {
|
|
2538
|
+
throw new Error(status.reason ?? `${agent} is not available`);
|
|
2539
|
+
}
|
|
2540
|
+
if (opts?.requireLinear && !status.linearMcp) {
|
|
2541
|
+
throw new Error(
|
|
2542
|
+
`${agent} has no Linear MCP server configured \u2014 ticket sources require it`
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
return status;
|
|
2546
|
+
}
|
|
2584
2547
|
|
|
2585
|
-
// src/
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
const trimmed = content.trim();
|
|
2598
|
-
if (!trimmed) return true;
|
|
2599
|
-
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
2600
|
-
return false;
|
|
2548
|
+
// src/threads/create.ts
|
|
2549
|
+
async function createThread(input, _onSetupLine) {
|
|
2550
|
+
const resolved = resolveNewThreadOptions({
|
|
2551
|
+
agent: input.agent,
|
|
2552
|
+
model: input.model,
|
|
2553
|
+
effort: input.effort,
|
|
2554
|
+
fast: input.fast
|
|
2555
|
+
});
|
|
2556
|
+
await requireAgent(resolved.agent);
|
|
2557
|
+
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
2558
|
+
if (!existsSync7(repoPath)) {
|
|
2559
|
+
throw new Error(`Repo not found: ${repoPath}`);
|
|
2601
2560
|
}
|
|
2602
|
-
|
|
2561
|
+
let sourceRef = input.sourceRef;
|
|
2562
|
+
let sourceIsFork = false;
|
|
2563
|
+
let prUrl = null;
|
|
2564
|
+
let prTitle = null;
|
|
2565
|
+
if (input.sourceType === "pr") {
|
|
2566
|
+
const num = Number(input.sourceRef.replace(/^#/, ""));
|
|
2567
|
+
if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
|
|
2568
|
+
const pr = await getPr(repoPath, num);
|
|
2569
|
+
if (!pr) throw new Error(`PR #${num} not found`);
|
|
2570
|
+
sourceIsFork = pr.isCrossRepository;
|
|
2571
|
+
prUrl = pr.url;
|
|
2572
|
+
const localFetchBranch = `sideboard-pr-${pr.number}`;
|
|
2573
|
+
await fetchPrHead(repoPath, pr.number, localFetchBranch);
|
|
2574
|
+
sourceRef = localFetchBranch;
|
|
2575
|
+
} else if (input.sourceType === "ticket") {
|
|
2576
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
2577
|
+
} else if (input.sourceType === "branch") {
|
|
2578
|
+
if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
|
|
2579
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
2580
|
+
} else {
|
|
2581
|
+
const existing = await getPrForHeadBranch(repoPath, sourceRef);
|
|
2582
|
+
if (existing?.url) {
|
|
2583
|
+
prUrl = existing.url;
|
|
2584
|
+
prTitle = existing.title;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
} else if (input.sourceType === "adopt") {
|
|
2588
|
+
throw new Error("Use adoptThread() for adopt sources");
|
|
2589
|
+
}
|
|
2590
|
+
const team = allocateTeamSlug(repoPath);
|
|
2591
|
+
const { branchName, worktreePath } = await createThreadWorktree({
|
|
2592
|
+
repoPath,
|
|
2593
|
+
sourceRef,
|
|
2594
|
+
slug: team.slug
|
|
2595
|
+
});
|
|
2596
|
+
copyConfiguredFiles(repoPath, worktreePath);
|
|
2597
|
+
const explicitTitle = input.title?.trim();
|
|
2598
|
+
const thread = createEmptyThread({
|
|
2599
|
+
title: explicitTitle || team.name,
|
|
2600
|
+
userSetTitle: Boolean(explicitTitle),
|
|
2601
|
+
sourceType: input.sourceType,
|
|
2602
|
+
sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
|
|
2603
|
+
branchName,
|
|
2604
|
+
worktreePath,
|
|
2605
|
+
repoPath,
|
|
2606
|
+
agent: resolved.agent,
|
|
2607
|
+
autonomy: input.autonomy ?? "default",
|
|
2608
|
+
model: resolved.model,
|
|
2609
|
+
effort: resolved.effort,
|
|
2610
|
+
fast: resolved.fast,
|
|
2611
|
+
planMode: Boolean(input.planMode),
|
|
2612
|
+
attachments: input.attachments ?? [],
|
|
2613
|
+
sourceIsFork,
|
|
2614
|
+
parentThreadId: input.parentThreadId ?? null,
|
|
2615
|
+
status: "idle",
|
|
2616
|
+
prUrl,
|
|
2617
|
+
prTitle
|
|
2618
|
+
});
|
|
2619
|
+
writeThread(thread);
|
|
2620
|
+
await ensureWorkspace(repoPath);
|
|
2621
|
+
return readThread(thread.id) ?? thread;
|
|
2603
2622
|
}
|
|
2604
|
-
function
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
return null;
|
|
2623
|
+
async function listLinearIssues(agent, repoPath) {
|
|
2624
|
+
const { getAdapter: getAdapter2 } = await import("./agents-YIMHDI46.js");
|
|
2625
|
+
await requireAgent(agent, { requireLinear: true });
|
|
2626
|
+
const adapter = getAdapter2(agent);
|
|
2627
|
+
if (!adapter.listLinearIssues) {
|
|
2628
|
+
throw new Error(`${agent} does not support Linear issue listing`);
|
|
2611
2629
|
}
|
|
2630
|
+
return adapter.listLinearIssues(repoPath);
|
|
2612
2631
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2632
|
+
|
|
2633
|
+
// src/store/turn-live.ts
|
|
2634
|
+
import {
|
|
2635
|
+
existsSync as existsSync8,
|
|
2636
|
+
renameSync,
|
|
2637
|
+
unlinkSync as unlinkSync2,
|
|
2638
|
+
writeFileSync as writeFileSync3,
|
|
2639
|
+
readFileSync as readFileSync6
|
|
2640
|
+
} from "fs";
|
|
2641
|
+
var buffers = /* @__PURE__ */ new Map();
|
|
2642
|
+
var FLUSH_MS = 800;
|
|
2643
|
+
var MAX_PARTS = 80;
|
|
2644
|
+
function trimParts(parts) {
|
|
2645
|
+
if (parts.length <= MAX_PARTS) return parts;
|
|
2646
|
+
return parts.slice(-Math.floor(MAX_PARTS / 2));
|
|
2618
2647
|
}
|
|
2619
|
-
function
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
return {
|
|
2624
|
-
path: REPO_REVIEW_PATH,
|
|
2625
|
-
name: REPO_REVIEW_NAME,
|
|
2626
|
-
content: repoContent,
|
|
2627
|
-
source: "repo"
|
|
2628
|
-
};
|
|
2629
|
-
}
|
|
2630
|
-
const localAbs = join8(worktreePath, REVIEW_REQUEST_PATH);
|
|
2631
|
-
const localContent = readTextIfPresent(localAbs);
|
|
2632
|
-
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
2633
|
-
return {
|
|
2634
|
-
path: REVIEW_REQUEST_PATH,
|
|
2635
|
-
name: REVIEW_REQUEST_NAME,
|
|
2636
|
-
content: localContent,
|
|
2637
|
-
source: "local"
|
|
2638
|
-
};
|
|
2648
|
+
function lastText(parts, type) {
|
|
2649
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
2650
|
+
const p = parts[i];
|
|
2651
|
+
if (p.type === type && p.text.trim()) return p.text.trim();
|
|
2639
2652
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2653
|
+
return "";
|
|
2654
|
+
}
|
|
2655
|
+
function summarizeTurnLive(parts) {
|
|
2656
|
+
const tools = parts.filter(
|
|
2657
|
+
(p) => p.type === "tool"
|
|
2658
|
+
);
|
|
2659
|
+
const lastTool = tools[tools.length - 1];
|
|
2660
|
+
const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
|
|
2661
|
+
const running = lastTool?.status === "running";
|
|
2662
|
+
const thinking = lastText(parts, "thinking");
|
|
2663
|
+
const text = lastText(parts, "text");
|
|
2664
|
+
const excerptRaw = thinking || text;
|
|
2665
|
+
const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
|
|
2666
|
+
let summary = "Working\u2026";
|
|
2667
|
+
if (lastToolLabel) {
|
|
2668
|
+
const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
|
|
2669
|
+
summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
|
|
2670
|
+
} else if (thinking) {
|
|
2671
|
+
summary = "Thinking\u2026";
|
|
2672
|
+
} else if (text) {
|
|
2673
|
+
summary = "Writing reply\u2026";
|
|
2649
2674
|
}
|
|
2650
|
-
ensureAttachmentsGitignore(worktreePath);
|
|
2651
|
-
mkdirSync2(dirname2(localAbs), { recursive: true });
|
|
2652
|
-
writeFileSync3(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
|
|
2653
2675
|
return {
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2676
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2677
|
+
summary,
|
|
2678
|
+
lastTool: lastToolLabel,
|
|
2679
|
+
toolCount: tools.length,
|
|
2680
|
+
excerpt
|
|
2658
2681
|
};
|
|
2659
2682
|
}
|
|
2660
|
-
function
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
if (
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
name: REPO_REVIEW_NAME,
|
|
2667
|
-
content: repoContent,
|
|
2668
|
-
source: "repo"
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
const localAbs = join8(worktreePath, REVIEW_REQUEST_PATH);
|
|
2672
|
-
const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent(join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
|
|
2673
|
-
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
2674
|
-
const path = existsSync8(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
|
|
2675
|
-
return {
|
|
2676
|
-
path,
|
|
2677
|
-
name: REVIEW_REQUEST_NAME,
|
|
2678
|
-
content: localContent,
|
|
2679
|
-
source: "local"
|
|
2680
|
-
};
|
|
2683
|
+
function flush(threadId) {
|
|
2684
|
+
const buf = buffers.get(threadId);
|
|
2685
|
+
if (!buf) return;
|
|
2686
|
+
if (buf.timer) {
|
|
2687
|
+
clearTimeout(buf.timer);
|
|
2688
|
+
buf.timer = null;
|
|
2681
2689
|
}
|
|
2682
|
-
|
|
2683
|
-
writeFileSync3(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
|
|
2684
|
-
return {
|
|
2685
|
-
path: REPO_REVIEW_PATH,
|
|
2686
|
-
name: REPO_REVIEW_NAME,
|
|
2687
|
-
content: REVIEW_REQUEST_TEMPLATE,
|
|
2688
|
-
source: "repo"
|
|
2689
|
-
};
|
|
2690
|
+
writeTurnLive(threadId, summarizeTurnLive(buf.parts));
|
|
2690
2691
|
}
|
|
2691
|
-
function
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2694
|
-
return
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
};
|
|
2692
|
+
function scheduleFlush(threadId) {
|
|
2693
|
+
const buf = buffers.get(threadId);
|
|
2694
|
+
if (!buf) return;
|
|
2695
|
+
if (buf.timer) return;
|
|
2696
|
+
buf.timer = setTimeout(() => {
|
|
2697
|
+
const live = buffers.get(threadId);
|
|
2698
|
+
if (live) live.timer = null;
|
|
2699
|
+
flush(threadId);
|
|
2700
|
+
}, FLUSH_MS);
|
|
2701
2701
|
}
|
|
2702
|
-
function
|
|
2703
|
-
|
|
2702
|
+
function noteTurnLiveEvent(threadId, event) {
|
|
2703
|
+
if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const prev = buffers.get(threadId) ?? { parts: [], timer: null };
|
|
2707
|
+
prev.parts = trimParts(applyAgentEvent(prev.parts, event));
|
|
2708
|
+
buffers.set(threadId, prev);
|
|
2709
|
+
if (event.type === "exit") {
|
|
2710
|
+
flush(threadId);
|
|
2711
|
+
const cur = buffers.get(threadId);
|
|
2712
|
+
if (cur?.timer) clearTimeout(cur.timer);
|
|
2713
|
+
buffers.delete(threadId);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const immediate = event.type === "tool_use" || event.type === "tool_result";
|
|
2717
|
+
if (immediate) flush(threadId);
|
|
2718
|
+
else scheduleFlush(threadId);
|
|
2704
2719
|
}
|
|
2705
|
-
|
|
2706
|
-
const
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2720
|
+
function writeTurnLive(threadId, progress) {
|
|
2721
|
+
const path = threadLivePath(threadId);
|
|
2722
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2723
|
+
try {
|
|
2724
|
+
writeFileSync3(tmp, JSON.stringify(progress), "utf8");
|
|
2725
|
+
renameSync(tmp, path);
|
|
2726
|
+
} catch {
|
|
2727
|
+
try {
|
|
2728
|
+
unlinkSync2(tmp);
|
|
2729
|
+
} catch {
|
|
2730
|
+
}
|
|
2712
2731
|
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2732
|
+
}
|
|
2733
|
+
function readTurnLive(threadId) {
|
|
2734
|
+
const path = threadLivePath(threadId);
|
|
2735
|
+
if (!existsSync8(path)) return null;
|
|
2736
|
+
try {
|
|
2737
|
+
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
2738
|
+
if (!raw || typeof raw.summary !== "string") return null;
|
|
2739
|
+
return raw;
|
|
2740
|
+
} catch {
|
|
2741
|
+
return null;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function clearTurnLive(threadId) {
|
|
2745
|
+
const buf = buffers.get(threadId);
|
|
2746
|
+
if (buf?.timer) clearTimeout(buf.timer);
|
|
2747
|
+
buffers.delete(threadId);
|
|
2748
|
+
const path = threadLivePath(threadId);
|
|
2749
|
+
if (!existsSync8(path)) return;
|
|
2750
|
+
try {
|
|
2751
|
+
unlinkSync2(path);
|
|
2752
|
+
} catch {
|
|
2715
2753
|
}
|
|
2716
|
-
const guidelines = resolveReviewGuidelines(from.worktreePath);
|
|
2717
|
-
const tab = createChatTab({
|
|
2718
|
-
fromThreadId: from.id,
|
|
2719
|
-
title: "Review",
|
|
2720
|
-
attachments: [
|
|
2721
|
-
buildReviewRequestAttachment(guidelines.content, {
|
|
2722
|
-
path: guidelines.path,
|
|
2723
|
-
name: guidelines.name
|
|
2724
|
-
})
|
|
2725
|
-
]
|
|
2726
|
-
});
|
|
2727
|
-
const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
|
|
2728
|
-
return { tab: started, from };
|
|
2729
2754
|
}
|
|
2730
2755
|
|
|
2731
2756
|
// src/threads/fork-worktree.ts
|
|
@@ -2969,7 +2994,7 @@ async function adoptThread(input) {
|
|
|
2969
2994
|
messages: input.messages ?? []
|
|
2970
2995
|
});
|
|
2971
2996
|
writeThread(thread);
|
|
2972
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
2997
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-SUBEDSGB.js");
|
|
2973
2998
|
await ensureWorkspace2(repoPath);
|
|
2974
2999
|
return thread;
|
|
2975
3000
|
}
|
|
@@ -4845,8 +4870,9 @@ function formatProcessGuideDirective() {
|
|
|
4845
4870
|
"Process guides (recurring work only):",
|
|
4846
4871
|
"- If `.claude/skills/graph-engineering/SKILL.md` exists, follow it (`/graph-engineering`) for migrations, ports, batch fixes, and other fan-out. Judge first; state on disk; grow the rulebook; do not patch around it.",
|
|
4847
4872
|
"- If this same shape of work will happen again, write `.claude/skills/<kebab-name>/SKILL.md` in this worktree (Claude Code project skill). Sideboard `/name`, Claude Code, and `attach` all load that path. Do not leave the method only in chat.",
|
|
4848
|
-
"-
|
|
4849
|
-
"-
|
|
4873
|
+
"- 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).",
|
|
4874
|
+
"- 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.",
|
|
4875
|
+
"- Skip a guide for a one-off. If a matching skill exists, follow it. Same miss twice \u2192 edit the skill, do not patch around it."
|
|
4850
4876
|
].join("\n");
|
|
4851
4877
|
}
|
|
4852
4878
|
function formatArtifactDirective() {
|
|
@@ -4873,15 +4899,7 @@ function formatArtifactDirective() {
|
|
|
4873
4899
|
].join("\n");
|
|
4874
4900
|
}
|
|
4875
4901
|
function formatUiReminder() {
|
|
4876
|
-
return
|
|
4877
|
-
"Sideboard side column (important):",
|
|
4878
|
-
"There is no claude.ai Artifact tool here \u2014 that is normal.",
|
|
4879
|
-
"HTML/SVG/markdown: ```html fence or MCP present_artifact.",
|
|
4880
|
-
"CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
|
|
4881
|
-
"Files column: MCP present_files (brightsy account storage or memory).",
|
|
4882
|
-
"Do not say artifacts/CMS UI are unavailable.",
|
|
4883
|
-
"ask_user: only when blocked on a real multiple-choice (approach fork, which API). Never for hellos, check-ins, or \u201Cwhat next?\u201D menus \u2014 reply in chat."
|
|
4884
|
-
].join(" ");
|
|
4902
|
+
return "Sideboard UI: html fence or present_artifact / present_schema / present_files; ask_user only for a real multiple-choice (not hellos or \u201Cwhat next?\u201D) \u2014 reply in chat. Do not say artifacts/CMS UI are unavailable.";
|
|
4885
4903
|
}
|
|
4886
4904
|
var FILES_BY_AGENT = {
|
|
4887
4905
|
claude: [
|
|
@@ -5226,7 +5244,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
5226
5244
|
${prompt}`;
|
|
5227
5245
|
}
|
|
5228
5246
|
async function defaultDeps() {
|
|
5229
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
5247
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-6W7EKSPO.js");
|
|
5230
5248
|
const orch = getOrchestrator2();
|
|
5231
5249
|
return {
|
|
5232
5250
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -5444,7 +5462,7 @@ var Orchestrator = class {
|
|
|
5444
5462
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
5445
5463
|
});
|
|
5446
5464
|
}
|
|
5447
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
5465
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-IYSSVZHH.js");
|
|
5448
5466
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
5449
5467
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
5450
5468
|
}
|
|
@@ -5613,7 +5631,7 @@ var Orchestrator = class {
|
|
|
5613
5631
|
}
|
|
5614
5632
|
}
|
|
5615
5633
|
await setup;
|
|
5616
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
5634
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-IYSSVZHH.js");
|
|
5617
5635
|
if (autoRunAfterSetupEnabled()) {
|
|
5618
5636
|
try {
|
|
5619
5637
|
await this.startDev(threadId);
|
|
@@ -5827,7 +5845,7 @@ var Orchestrator = class {
|
|
|
5827
5845
|
if (compact.didCompact) {
|
|
5828
5846
|
thread = updateThread(threadId, {
|
|
5829
5847
|
messages: compact.thread.messages,
|
|
5830
|
-
sessionId:
|
|
5848
|
+
sessionId: compact.thread.sessionId
|
|
5831
5849
|
});
|
|
5832
5850
|
this.emit({
|
|
5833
5851
|
type: "context_compacted",
|
|
@@ -5898,7 +5916,7 @@ var Orchestrator = class {
|
|
|
5898
5916
|
}
|
|
5899
5917
|
const isBrightsy = fresh.agent === "brightsy";
|
|
5900
5918
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
5901
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-
|
|
5919
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("./app-settings-IYSSVZHH.js");
|
|
5902
5920
|
const gitAuthMode = getGithubGitAuthMode();
|
|
5903
5921
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
5904
5922
|
githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
|
|
@@ -6657,7 +6675,7 @@ var Orchestrator = class {
|
|
|
6657
6675
|
updateThread(thread.id, { title: meta.title });
|
|
6658
6676
|
}
|
|
6659
6677
|
}
|
|
6660
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
6678
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-IYSSVZHH.js");
|
|
6661
6679
|
const latest = this.requireThread(thread.id);
|
|
6662
6680
|
if (!shouldAutoArchiveOnPrMerge({
|
|
6663
6681
|
previousPrState: prevState || null,
|
|
@@ -6960,7 +6978,7 @@ var Orchestrator = class {
|
|
|
6960
6978
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
6961
6979
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
6962
6980
|
try {
|
|
6963
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
6981
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-SUBEDSGB.js");
|
|
6964
6982
|
await ensureWorkspace2(thread.repoPath);
|
|
6965
6983
|
} catch {
|
|
6966
6984
|
}
|
|
@@ -6986,7 +7004,7 @@ var Orchestrator = class {
|
|
|
6986
7004
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
6987
7005
|
} catch {
|
|
6988
7006
|
}
|
|
6989
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
7007
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-IYSSVZHH.js");
|
|
6990
7008
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
6991
7009
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
6992
7010
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -7007,7 +7025,7 @@ var Orchestrator = class {
|
|
|
7007
7025
|
return restored2;
|
|
7008
7026
|
}
|
|
7009
7027
|
if (!existsSync16(thread.worktreePath)) {
|
|
7010
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
7028
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-2UWO7VEK.js");
|
|
7011
7029
|
const slug = thread.worktreePath.split("/").pop();
|
|
7012
7030
|
const dest = thread.worktreePath;
|
|
7013
7031
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
@@ -7115,7 +7133,7 @@ async function startOrchestration(opts) {
|
|
|
7115
7133
|
sourceRef: "default",
|
|
7116
7134
|
...createOpts
|
|
7117
7135
|
}).catch(async () => {
|
|
7118
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
7136
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-2UWO7VEK.js");
|
|
7119
7137
|
const repo = await resolveRepoRoot2(repoPath);
|
|
7120
7138
|
const def = await resolveDefaultBranch2(repo);
|
|
7121
7139
|
return orch.createThread({
|
|
@@ -7188,41 +7206,17 @@ export {
|
|
|
7188
7206
|
isBrightsyNdjsonLine,
|
|
7189
7207
|
finalizeParts,
|
|
7190
7208
|
normalizeParseResult,
|
|
7209
|
+
applyPromptCacheTtlEnv,
|
|
7191
7210
|
spawnAgentTurn,
|
|
7192
7211
|
AGENT_GIT_ACTIONS,
|
|
7193
7212
|
agentGitPrompt,
|
|
7194
|
-
readWorktreeInclude,
|
|
7195
|
-
resolveFilesToCopy,
|
|
7196
|
-
copyConfiguredFiles,
|
|
7197
|
-
captureLoginEnv,
|
|
7198
|
-
buildWorkspaceScriptEnv,
|
|
7199
|
-
runSetupScript,
|
|
7200
|
-
runConventionSetup,
|
|
7201
|
-
runWorkspaceSetup,
|
|
7202
|
-
runArchiveScript,
|
|
7203
|
-
listRunScripts,
|
|
7204
|
-
getDefaultRunScript,
|
|
7205
|
-
getRunScript,
|
|
7206
|
-
getRunMode,
|
|
7207
|
-
allocatePort,
|
|
7208
|
-
allocatePortRange,
|
|
7209
|
-
startDevServer,
|
|
7210
|
-
findOrphanWorktrees,
|
|
7211
|
-
cleanupOrphanWorktrees,
|
|
7212
|
-
shouldRunWorktreeCleanup,
|
|
7213
|
-
worktreeCleanupSettings,
|
|
7214
|
-
applyThreadIntoMain,
|
|
7215
|
-
cloneRepoIntoSideboard,
|
|
7216
|
-
detectAgents,
|
|
7217
|
-
requireAgent,
|
|
7218
|
-
createThread,
|
|
7219
|
-
listLinearIssues,
|
|
7220
7213
|
summarizeConversation,
|
|
7221
7214
|
extractiveSummary,
|
|
7222
7215
|
CONTEXT_COMPACT_CHARS,
|
|
7223
7216
|
CONTEXT_KEEP_RECENT_CHARS,
|
|
7224
7217
|
CONTEXT_KEEP_RECENT_MESSAGES,
|
|
7225
7218
|
CONTEXT_MIN_MESSAGES,
|
|
7219
|
+
SESSION_RESET_OCCUPANCY_TOKENS,
|
|
7226
7220
|
estimateMessageChars,
|
|
7227
7221
|
estimateThreadChars,
|
|
7228
7222
|
shouldCompactContext,
|
|
@@ -7230,6 +7224,8 @@ export {
|
|
|
7230
7224
|
formatMessagesAsTranscript,
|
|
7231
7225
|
buildSessionSeed,
|
|
7232
7226
|
applyCompaction,
|
|
7227
|
+
lastRequestOccupancy,
|
|
7228
|
+
shouldResetSessionForOccupancy,
|
|
7233
7229
|
maybeCompactContext,
|
|
7234
7230
|
sameWorktreePath,
|
|
7235
7231
|
takenTeamSlugsForChatTab,
|
|
@@ -7239,8 +7235,10 @@ export {
|
|
|
7239
7235
|
buildForkTranscriptAttachment,
|
|
7240
7236
|
createChatTab,
|
|
7241
7237
|
forkChatTab,
|
|
7242
|
-
readTurnLive,
|
|
7243
7238
|
REVIEW_REQUEST_TEMPLATE,
|
|
7239
|
+
REVIEW_SKILL_PATH,
|
|
7240
|
+
REVIEW_SKILL_NAME,
|
|
7241
|
+
wrapReviewSkillMarkdown,
|
|
7244
7242
|
REPO_REVIEW_PATH,
|
|
7245
7243
|
REPO_REVIEW_NAME,
|
|
7246
7244
|
REVIEW_REQUEST_PATH,
|
|
@@ -7248,11 +7246,39 @@ export {
|
|
|
7248
7246
|
REVIEW_REQUEST_NAME,
|
|
7249
7247
|
REVIEW_REQUEST_PREFILL,
|
|
7250
7248
|
shouldRefreshReviewRequestTemplate,
|
|
7249
|
+
ensureReviewSkillFile,
|
|
7251
7250
|
resolveReviewGuidelines,
|
|
7252
7251
|
ensureReviewRequestFile,
|
|
7253
7252
|
buildReviewRequestAttachment,
|
|
7254
7253
|
readExistingReviewRequestFile,
|
|
7255
7254
|
requestReview,
|
|
7255
|
+
readWorktreeInclude,
|
|
7256
|
+
resolveFilesToCopy,
|
|
7257
|
+
copyConfiguredFiles,
|
|
7258
|
+
captureLoginEnv,
|
|
7259
|
+
buildWorkspaceScriptEnv,
|
|
7260
|
+
runSetupScript,
|
|
7261
|
+
runConventionSetup,
|
|
7262
|
+
runWorkspaceSetup,
|
|
7263
|
+
runArchiveScript,
|
|
7264
|
+
listRunScripts,
|
|
7265
|
+
getDefaultRunScript,
|
|
7266
|
+
getRunScript,
|
|
7267
|
+
getRunMode,
|
|
7268
|
+
allocatePort,
|
|
7269
|
+
allocatePortRange,
|
|
7270
|
+
startDevServer,
|
|
7271
|
+
findOrphanWorktrees,
|
|
7272
|
+
cleanupOrphanWorktrees,
|
|
7273
|
+
shouldRunWorktreeCleanup,
|
|
7274
|
+
worktreeCleanupSettings,
|
|
7275
|
+
applyThreadIntoMain,
|
|
7276
|
+
cloneRepoIntoSideboard,
|
|
7277
|
+
detectAgents,
|
|
7278
|
+
requireAgent,
|
|
7279
|
+
createThread,
|
|
7280
|
+
listLinearIssues,
|
|
7281
|
+
readTurnLive,
|
|
7256
7282
|
forkThreadWorktree,
|
|
7257
7283
|
resolveConductorCursorAgentId,
|
|
7258
7284
|
adoptThread,
|