@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.
Files changed (38) hide show
  1. package/dist/agents/cursor-runner.js +2 -2
  2. package/dist/{agents-T4RHL5UX.js → agents-XB332VUJ.js} +5 -5
  3. package/dist/{agents-4XQANGFO.js → agents-YIMHDI46.js} +7 -7
  4. package/dist/{app-settings-2WH6DVOW.js → app-settings-EBCVMFF6.js} +1 -1
  5. package/dist/{app-settings-6AXBWM3K.js → app-settings-IYSSVZHH.js} +2 -2
  6. package/dist/{chunk-7ZTQHC2Q.js → chunk-4LKD3PGO.js} +1 -1
  7. package/dist/{chunk-TLPJHLLM.js → chunk-4TR3HZFT.js} +1 -1
  8. package/dist/{chunk-SMWVSSE5.js → chunk-756EO4GK.js} +3 -3
  9. package/dist/{chunk-XOYH3OMI.js → chunk-AY2EQI2P.js} +16 -13
  10. package/dist/{chunk-PZVA2VDF.js → chunk-EW7COXYE.js} +1 -1
  11. package/dist/{chunk-VTTTV2LM.js → chunk-FBV6NK62.js} +2 -2
  12. package/dist/{chunk-GYFNFU62.js → chunk-I2OJ2YSP.js} +8 -16
  13. package/dist/{chunk-6JJTMI7G.js → chunk-JVIW55KT.js} +1 -1
  14. package/dist/{chunk-6HRR4T4P.js → chunk-KKHDPV45.js} +15 -14
  15. package/dist/{chunk-XEFHG6VG.js → chunk-LTPX5N6K.js} +1310 -1265
  16. package/dist/{chunk-EX4NZN3O.js → chunk-NE4TOOKS.js} +1 -1
  17. package/dist/{chunk-LRO7YVLJ.js → chunk-OQDIYVDD.js} +2 -2
  18. package/dist/{chunk-3SIFRHJ3.js → chunk-Q6B3NRCT.js} +1457 -1431
  19. package/dist/{chunk-THXSFETX.js → chunk-TROJ3PVH.js} +3 -3
  20. package/dist/{chunk-5XUKY2JY.js → chunk-VRTKGKUW.js} +8 -16
  21. package/dist/{chunk-HUKCGRAT.js → chunk-WD2TARQS.js} +7 -2
  22. package/dist/{coordinator-prompt-4TUQUEHY.js → coordinator-prompt-JKOU6BWV.js} +4 -4
  23. package/dist/{coordinator-prompt-YQWRXCPX.js → coordinator-prompt-V66BYTUV.js} +3 -3
  24. package/dist/{global-workspace-V6TMECDU.js → global-workspace-NCTBE7G7.js} +5 -5
  25. package/dist/{global-workspace-H37LFFXI.js → global-workspace-WBQWQQQY.js} +4 -4
  26. package/dist/index.cjs +1697 -1661
  27. package/dist/index.d.cts +65 -24
  28. package/dist/index.d.ts +65 -24
  29. package/dist/index.js +107 -85
  30. package/dist/mcp/run-stdio.cjs +1660 -1619
  31. package/dist/mcp/run-stdio.js +82 -79
  32. package/dist/{orchestrator-DEHSKERK.js → orchestrator-6W7EKSPO.js} +9 -9
  33. package/dist/{orchestrator-2P7DX44Q.js → orchestrator-JVGVPKLI.js} +7 -7
  34. package/dist/{workspaces-JU3P6FHB.js → workspaces-I3554R3F.js} +5 -5
  35. package/dist/{workspaces-UPEVANU4.js → workspaces-SUBEDSGB.js} +6 -6
  36. package/dist/{worktree-NR5YJG2E.js → worktree-2UWO7VEK.js} +3 -3
  37. package/dist/{worktree-6MFKE465.js → worktree-7H6HB6GW.js} +2 -2
  38. 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-6HRR4T4P.js";
11
+ } from "./chunk-KKHDPV45.js";
11
12
  import {
12
13
  addWorkspace,
13
14
  ensureWorkspace,
14
15
  removeWorkspace,
15
16
  syncWorkspacesFromThreads
16
- } from "./chunk-VTTTV2LM.js";
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-SMWVSSE5.js";
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-GYFNFU62.js";
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-HUKCGRAT.js";
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-PZVA2VDF.js";
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-7ZTQHC2Q.js";
119
+ } from "./chunk-4LKD3PGO.js";
119
120
  import {
120
121
  stripNestedElectronEnv
121
- } from "./chunk-TLPJHLLM.js";
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-V6TMECDU.js");
975
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-NCTBE7G7.js");
967
976
  if (isGlobalThread2(thread)) {
968
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-4TUQUEHY.js");
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 existsSync3,
1196
- mkdirSync,
1205
+ existsSync as existsSync4,
1206
+ mkdirSync as mkdirSync2,
1197
1207
  readdirSync,
1198
- readFileSync as readFileSync3
1208
+ readFileSync as readFileSync4
1199
1209
  } from "fs";
1200
1210
  import { createServer } from "net";
1201
- import { basename, dirname, join as join4 } from "path";
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
- var PORT_RANGE_SIZE = 10;
1205
- function matchSimpleGlob(pattern, name) {
1206
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1207
- return new RegExp(`^${escaped}$`).test(name);
1208
- }
1209
- function readWorktreeInclude(repoPath) {
1210
- const path = join4(repoPath, ".worktreeinclude");
1211
- if (!existsSync3(path)) return [];
1212
- return readFileSync3(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
1213
- }
1214
- function resolveFilesToCopy(repoPath) {
1215
- const fromInclude = readWorktreeInclude(repoPath);
1216
- if (fromInclude.length) return fromInclude;
1217
- const settings = loadRepoSettings(repoPath);
1218
- if (settings?.filesToCopy?.length) return settings.filesToCopy;
1219
- if (settings?.fileIncludeGlobs?.length) {
1220
- const matched = [];
1221
- try {
1222
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1223
- if (!entry.isFile()) continue;
1224
- for (const glob of settings.fileIncludeGlobs) {
1225
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
1226
- matched.push(entry.name);
1227
- break;
1228
- }
1229
- }
1230
- }
1231
- } catch {
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
- const defaults = [];
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
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1238
- if (entry.isFile() && entry.name.startsWith(".env")) {
1239
- defaults.push(entry.name);
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 copyConfiguredFiles(repoPath, worktreePath) {
1248
- const patterns = resolveFilesToCopy(repoPath);
1249
- const copied = [];
1250
- for (const rel of patterns) {
1251
- const src = join4(repoPath, rel);
1252
- if (!existsSync3(src)) continue;
1253
- const dest = join4(worktreePath, rel);
1254
- mkdirSync(dirname(dest), { recursive: true });
1255
- copyFileSync(src, dest);
1256
- copied.push(rel);
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
- return copied;
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
- var cachedLoginEnv = null;
1261
- async function captureLoginEnv() {
1262
- if (cachedLoginEnv) return { ...cachedLoginEnv };
1263
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
1264
- try {
1265
- const result = await execa2(shell, ["-l", "-c", "env -0"], {
1266
- reject: false,
1267
- timeout: 5e3
1268
- });
1269
- if (result.exitCode === 0 && result.stdout) {
1270
- const env = { ...process.env };
1271
- for (const entry of result.stdout.split("\0")) {
1272
- if (!entry) continue;
1273
- const eq = entry.indexOf("=");
1274
- if (eq <= 0) continue;
1275
- const key = entry.slice(0, eq);
1276
- const value = entry.slice(eq + 1);
1277
- if (key) env[key] = value;
1278
- }
1279
- cachedLoginEnv = env;
1280
- return { ...env };
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
- cachedLoginEnv = { ...process.env };
1285
- return { ...cachedLoginEnv };
1336
+ return n;
1286
1337
  }
1287
- function buildWorkspaceScriptEnv(opts, baseEnv) {
1288
- const env = stripNestedElectronEnv({
1289
- ...baseEnv ?? process.env
1290
- });
1291
- const name = opts.workspaceName ?? basename(opts.worktreePath);
1292
- const ports = opts.ports ?? [];
1293
- const primary = ports[0];
1294
- env.SIDEBOARD_WORKSPACE_NAME = name;
1295
- env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
1296
- env.SIDEBOARD_ROOT_PATH = opts.repoPath;
1297
- env.SIDEBOARD_IS_LOCAL = "1";
1298
- if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
1299
- env.CONDUCTOR_WORKSPACE_NAME = name;
1300
- env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
1301
- env.CONDUCTOR_ROOT_PATH = opts.repoPath;
1302
- env.CONDUCTOR_IS_LOCAL = "1";
1303
- if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
1304
- if (primary != null) {
1305
- env.SIDEBOARD_PORT = String(primary);
1306
- env.CONDUCTOR_PORT = String(primary);
1307
- env.PORT = String(primary);
1308
- for (let i = 1; i < ports.length; i++) {
1309
- const p = ports[i];
1310
- env[`SIDEBOARD_PORT_${i}`] = String(p);
1311
- env[`CONDUCTOR_PORT_${i}`] = String(p);
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 pipeLines(stream, onLine) {
1317
- if (!stream || !onLine) return;
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 killScriptTree(child, ports = []) {
1322
- const pid = child.pid;
1323
- if (pid) {
1324
- try {
1325
- if (process.platform === "win32") {
1326
- void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
1327
- } else {
1328
- process.kill(-pid, "SIGTERM");
1329
- setTimeout(() => {
1330
- try {
1331
- process.kill(-pid, "SIGKILL");
1332
- } catch {
1333
- }
1334
- }, 2500).unref?.();
1335
- }
1336
- } catch {
1337
- try {
1338
- child.kill("SIGTERM");
1339
- } catch {
1340
- }
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
- async function spawnWorkspaceScript(command, opts) {
1365
- const loginEnv = await captureLoginEnv();
1366
- const env = buildWorkspaceScriptEnv(
1367
- {
1368
- worktreePath: opts.worktreePath,
1369
- repoPath: opts.repoPath,
1370
- workspaceName: opts.workspaceName,
1371
- defaultBranch: opts.defaultBranch,
1372
- ports: opts.ports
1373
- },
1374
- loginEnv
1375
- );
1376
- try {
1377
- mergeAgentGitAuthEnv(
1378
- env,
1379
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
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 shell = process.platform === "darwin" ? "zsh" : "bash";
1384
- const child = execa2(shell, ["-lc", command], {
1385
- cwd: opts.worktreePath,
1386
- reject: false,
1387
- env,
1388
- // Own process group so Stop can tear down the whole tree (not just the shell).
1389
- // There is no settings.toml `stop=` / teardown hook for run scripts.
1390
- ...process.platform === "win32" ? {} : { detached: true }
1391
- });
1392
- pipeLines(child.stdout, opts.onLine);
1393
- pipeLines(child.stderr, opts.onLine);
1394
- const ports = opts.ports ?? [];
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
- pid: child.pid,
1397
- kill: () => killScriptTree(child, ports),
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
- async function attachAbort(handle, signal) {
1403
- if (signal) {
1404
- const onAbort = () => handle.kill();
1405
- if (signal.aborted) onAbort();
1406
- else signal.addEventListener("abort", onAbort, { once: true });
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
- return handle.done;
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
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
1411
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1412
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
1413
- const handle = await spawnWorkspaceScript(settings.setup, {
1414
- worktreePath,
1415
- repoPath,
1416
- defaultBranch: opts?.defaultBranch,
1417
- onLine
1418
- });
1419
- const exitCode = await attachAbort(handle, opts?.signal);
1420
- return {
1421
- ran: true,
1422
- exitCode,
1423
- source: workspaceSettingsSourceLabel(worktreePath, repoPath),
1424
- kill: handle.kill
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
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
1428
- const found = findConventionSetup(worktreePath, repoPath);
1429
- if (!found) return { ran: false, exitCode: null, source: null };
1430
- onLine?.(`[setup] ${found.source}`);
1431
- const handle = await spawnWorkspaceScript(found.command, {
1432
- worktreePath,
1433
- repoPath,
1434
- defaultBranch: opts?.defaultBranch,
1435
- onLine
1436
- });
1437
- const exitCode = await attachAbort(handle, opts?.signal);
1438
- return {
1439
- ran: true,
1440
- exitCode,
1441
- source: found.source,
1442
- kill: handle.kill
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
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
1446
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
1447
- if (!setup.ran) {
1448
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
1449
- }
1450
- if (!setup.ran) {
1451
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
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 setup;
1473
+ return 0;
1454
1474
  }
1455
- async function runArchiveScript(repoPath, worktreePath, onLine) {
1456
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1457
- if (!settings?.archive) return { ran: false, exitCode: null };
1458
- const handle = await spawnWorkspaceScript(settings.archive, {
1459
- worktreePath,
1460
- repoPath,
1461
- onLine
1462
- });
1463
- const exitCode = await handle.done;
1464
- return { ran: true, exitCode };
1475
+ function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
1476
+ return lastRequestOccupancy(thread) >= occupancyTokens;
1465
1477
  }
1466
- function listRunScripts(worktreePath, repoPath) {
1467
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
1468
- if (!settings?.runScripts.length) return [];
1469
- return settings.runScripts.filter((s) => {
1470
- if (!s.availableIn?.length) return true;
1471
- return s.availableIn.includes("local");
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
- function getDefaultRunScript(worktreePath, repoPath) {
1475
- const scripts = listRunScripts(worktreePath, repoPath);
1476
- if (!scripts.length) return null;
1477
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
1506
+
1507
+ // src/threads/chat-tabs.ts
1508
+ function sameWorktreePath(a, b) {
1509
+ return normalizeWorktreePath(a) === normalizeWorktreePath(b);
1478
1510
  }
1479
- function getRunScript(worktreePath, repoPath, name) {
1480
- const scripts = listRunScripts(worktreePath, repoPath);
1481
- if (!scripts.length) return null;
1482
- if (name) {
1483
- return scripts.find((s) => s.name === name) ?? null;
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 getDefaultRunScript(worktreePath, repoPath);
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
- async function allocatePortRange(size = PORT_RANGE_SIZE) {
1507
- const base = await allocatePort();
1508
- const ports = [base];
1509
- for (let i = 1; i < size; i++) {
1510
- const candidate = base + i;
1511
- const free = await new Promise((resolve) => {
1512
- const server = createServer();
1513
- server.once("error", () => resolve(false));
1514
- server.listen(candidate, "127.0.0.1", () => {
1515
- server.close(() => resolve(true));
1516
- });
1517
- });
1518
- if (free) {
1519
- ports.push(candidate);
1520
- } else {
1521
- ports.push(await allocatePort());
1522
- }
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
- async function startDevServer(repoPath, worktreePath, onLine, opts) {
1527
- const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
1528
- if (!script) return null;
1529
- const ports = await allocatePortRange(PORT_RANGE_SIZE);
1530
- const handle = await spawnWorkspaceScript(script.command, {
1531
- worktreePath,
1532
- repoPath,
1533
- ports,
1534
- defaultBranch: opts?.defaultBranch,
1535
- onLine
1536
- });
1528
+ function worktreeBindingFrom(from) {
1537
1529
  return {
1538
- pid: handle.pid,
1539
- port: ports[0],
1540
- ports,
1541
- scriptName: script.name,
1542
- kill: handle.kill,
1543
- done: handle.done
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
- // src/git/orphan-cleanup.ts
1548
- import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "fs";
1549
- import { join as join5 } from "path";
1550
- function isSideboardWorktreePath(path) {
1551
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
1552
- }
1553
- async function findOrphanWorktrees(repoPaths) {
1554
- const threads = listThreads({ includeArchived: true });
1555
- const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
1556
- const repos = new Set(
1557
- repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
1558
- );
1559
- const homeRoot = sideboardWorkspacesDir();
1560
- if (existsSync4(homeRoot)) {
1561
- try {
1562
- for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
1563
- if (!entry.isDirectory()) continue;
1564
- void entry;
1565
- }
1566
- } catch {
1567
- }
1568
- }
1569
- const orphans = [];
1570
- const seen = /* @__PURE__ */ new Set();
1571
- for (const repoPath of repos) {
1572
- if (!repoPath || !existsSync4(repoPath)) continue;
1573
- try {
1574
- const wts = await listWorktrees(repoPath);
1575
- for (const wt of wts) {
1576
- const path = wt.path.replace(/\/$/, "");
1577
- if (!isSideboardWorktreePath(path)) continue;
1578
- if (known.has(path)) continue;
1579
- if (seen.has(path)) continue;
1580
- seen.add(path);
1581
- let mtimeMs = 0;
1582
- try {
1583
- mtimeMs = statSync(path).mtimeMs;
1584
- } catch {
1585
- mtimeMs = 0;
1586
- }
1587
- orphans.push({ path, repoPath, mtimeMs });
1588
- }
1589
- } catch {
1590
- }
1591
- try {
1592
- const root = worktreesRoot(repoPath);
1593
- if (existsSync4(root)) {
1594
- for (const entry of readdirSync2(root, { withFileTypes: true })) {
1595
- if (!entry.isDirectory()) continue;
1596
- const path = join5(root, entry.name).replace(/\/$/, "");
1597
- if (known.has(path) || seen.has(path)) continue;
1598
- if (!existsSync4(join5(path, ".git"))) continue;
1599
- seen.add(path);
1600
- let mtimeMs = 0;
1601
- try {
1602
- mtimeMs = statSync(path).mtimeMs;
1603
- } catch {
1604
- mtimeMs = Date.now();
1605
- }
1606
- orphans.push({ path, repoPath, mtimeMs });
1607
- }
1608
- }
1609
- } catch {
1610
- }
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
- async function cleanupOrphanWorktrees(opts) {
1615
- const settings = loadAppSettings();
1616
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
1617
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
1618
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
1619
- const budget = Math.max(0, maxCount - activeCount);
1620
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
1621
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
1622
- const removed = [];
1623
- const kept = [];
1624
- for (const orphan of orphans) {
1625
- if (keep.has(orphan.path)) {
1626
- kept.push(orphan.path);
1627
- continue;
1628
- }
1629
- if (opts?.dryRun) {
1630
- removed.push(orphan.path);
1631
- continue;
1632
- }
1633
- try {
1634
- await removeWorktree(orphan.repoPath, orphan.path);
1635
- removed.push(orphan.path);
1636
- } catch {
1637
- kept.push(orphan.path);
1638
- }
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 shouldRunWorktreeCleanup(settings = loadAppSettings()) {
1648
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
1649
- const last = settings.advanced.worktreeLastCleanupAt;
1650
- if (!last) return true;
1651
- const elapsed = Date.now() - Date.parse(last);
1652
- return elapsed >= intervalHours * 36e5;
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 worktreeCleanupSettings() {
1655
- const a = loadAppSettings().advanced;
1562
+ function buildForkTranscriptAttachment(baseTitle, messages) {
1563
+ const title = baseTitle || "Chat";
1656
1564
  return {
1657
- worktreeMaxCount: a.worktreeMaxCount,
1658
- worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
1659
- worktreeLastCleanupAt: a.worktreeLastCleanupAt,
1660
- autoCleanupOrphans: a.autoCleanupOrphans
1565
+ id: randomUUID(),
1566
+ name: `Transcript of ${title}.md`,
1567
+ kind: "transcript",
1568
+ content: formatTranscriptMarkdown(title, messages)
1661
1569
  };
1662
1570
  }
1663
-
1664
- // src/git/apply-into-main.ts
1665
- import { execa as execa3 } from "execa";
1666
- async function applyThreadIntoMain(thread, opts) {
1667
- const method = opts?.method ?? "merge";
1668
- const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
1669
- const status = await execa3("git", ["status", "--porcelain"], {
1670
- cwd: thread.repoPath,
1671
- reject: false
1672
- });
1673
- if (status.stdout.trim()) {
1674
- throw new Error(
1675
- `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
1676
- );
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 checkout = await execa3("git", ["checkout", target], {
1679
- cwd: thread.repoPath,
1680
- reject: false
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
- if (checkout.exitCode !== 0) {
1683
- throw new Error(
1684
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
1685
- );
1686
- }
1687
- if (method === "cherry-pick") {
1688
- const log = await execa3(
1689
- "git",
1690
- ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
1691
- { cwd: thread.repoPath, reject: false }
1692
- );
1693
- const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
1694
- if (!shas.length) {
1695
- return {
1696
- applied: false,
1697
- method,
1698
- targetBranch: target,
1699
- message: `No commits on ${thread.branchName} not already in ${target}`
1700
- };
1701
- }
1702
- for (const sha of shas) {
1703
- const cp = await execa3("git", ["cherry-pick", sha], {
1704
- cwd: thread.repoPath,
1705
- reject: false
1706
- });
1707
- if (cp.exitCode !== 0) {
1708
- await execa3("git", ["cherry-pick", "--abort"], {
1709
- cwd: thread.repoPath,
1710
- reject: false
1711
- });
1712
- throw new Error(
1713
- `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
1714
- );
1715
- }
1716
- }
1717
- return {
1718
- applied: true,
1719
- method,
1720
- targetBranch: target,
1721
- message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
1722
- };
1723
- }
1724
- const merge = await execa3(
1725
- "git",
1726
- ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
1727
- { cwd: thread.repoPath, reject: false }
1728
- );
1729
- if (merge.exitCode !== 0) {
1730
- await execa3("git", ["merge", "--abort"], {
1731
- cwd: thread.repoPath,
1732
- reject: false
1733
- });
1734
- throw new Error(
1735
- `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
1736
- );
1737
- }
1738
- return {
1739
- applied: true,
1740
- method: "merge",
1741
- targetBranch: target,
1742
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
1743
- };
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/git/clone-repo.ts
1747
- import { existsSync as existsSync5 } from "fs";
1748
- import { basename as basename2, join as join6 } from "path";
1749
- import { execa as execa4 } from "execa";
1750
- async function cloneRepoIntoSideboard(opts) {
1751
- const url = opts.url.trim();
1752
- if (!url) throw new Error("Clone URL is required");
1753
- let name = opts.name?.trim();
1754
- if (!name) {
1755
- const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
1756
- name = leaf || "repo";
1757
- }
1758
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
1759
- const dest = join6(sideboardReposDir(), name);
1760
- if (existsSync5(dest)) {
1761
- const repoPath2 = await resolveRepoRoot(dest);
1762
- const workspace2 = await ensureWorkspace(repoPath2);
1763
- return { repoPath: repoPath2, workspace: workspace2 };
1764
- }
1765
- const result = await execa4("git", ["clone", url, dest], { reject: false });
1766
- if (result.exitCode !== 0) {
1767
- throw new Error(
1768
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
1769
- );
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
- const repoPath = await resolveRepoRoot(dest);
1772
- const workspace = await ensureWorkspace(repoPath);
1773
- return { repoPath, workspace };
1762
+ return `${REVIEW_SKILL_FRONTMATTER}${source}
1763
+ `;
1774
1764
  }
1775
1765
 
1776
- // src/store/desktop-host.ts
1777
- import { readFileSync as readFileSync4, unlinkSync, writeFileSync } from "fs";
1778
- import { join as join7 } from "path";
1779
- function desktopHostPidPath() {
1780
- return join7(appDataDir(), "desktop-host.pid");
1781
- }
1782
- function claimDesktopHost(pid = process.pid) {
1783
- writeFileSync(desktopHostPidPath(), `${pid}
1784
- `, "utf8");
1785
- }
1786
- function releaseDesktopHost(pid = process.pid) {
1787
- if (readDesktopHostPid() !== pid) return;
1788
- try {
1789
- unlinkSync(desktopHostPidPath());
1790
- } catch {
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 readDesktopHostPid() {
1785
+ function readTextIfPresent(abs) {
1786
+ if (!existsSync3(abs)) return null;
1794
1787
  try {
1795
- const pid = Number.parseInt(readFileSync4(desktopHostPidPath(), "utf8").trim(), 10);
1796
- if (!Number.isFinite(pid) || pid <= 0) return null;
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 pidAlive(pid) {
1803
- try {
1804
- process.kill(pid, 0);
1805
- return true;
1806
- } catch {
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 isDesktopHostAlive() {
1811
- const pid = readDesktopHostPid();
1812
- return pid != null && pidAlive(pid);
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 isThisProcessDesktopHost() {
1815
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
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 thisProcessShouldDrainAgentQueues() {
1818
- if (isThisProcessDesktopHost()) return true;
1819
- return !isDesktopHostAlive();
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
- // src/threads/create.ts
1823
- import { existsSync as existsSync6 } from "fs";
1824
-
1825
- // src/detect/detect.ts
1826
- async function detectAgents() {
1827
- ensureAgentPath();
1828
- enrichPathWithNpmGlobalBin();
1829
- return Promise.all(allAdapters().map((a) => a.detect()));
1843
+ function ensureReviewRequestFile(worktreePath) {
1844
+ const seeded = ensureReviewSkillFile(worktreePath);
1845
+ return skillGuidelines(seeded.content, "skill");
1830
1846
  }
1831
- var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
1832
- async function requireAgent(agent, opts) {
1833
- ensureAgentPath();
1834
- enrichPathWithNpmGlobalBin();
1835
- const status = await Promise.race([
1836
- getAdapter(agent).detect(),
1837
- new Promise((_, reject) => {
1838
- setTimeout(() => {
1839
- reject(
1840
- new Error(
1841
- `${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
1842
- )
1843
- );
1844
- }, REQUIRE_AGENT_TIMEOUT_MS);
1845
- })
1846
- ]);
1847
- if (!status.installed || !status.authenticated) {
1848
- throw new Error(status.reason ?? `${agent} is not available`);
1849
- }
1850
- if (opts?.requireLinear && !status.linearMcp) {
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
- `${agent} has no Linear MCP server configured \u2014 ticket sources require it`
1866
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
1853
1867
  );
1854
1868
  }
1855
- return status;
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/threads/create.ts
1859
- async function createThread(input, _onSetupLine) {
1860
- const resolved = resolveNewThreadOptions({
1861
- agent: input.agent,
1862
- model: input.model,
1863
- effort: input.effort,
1864
- fast: input.fast
1865
- });
1866
- await requireAgent(resolved.agent);
1867
- const repoPath = await resolveRepoRoot(input.repoPath);
1868
- if (!existsSync6(repoPath)) {
1869
- throw new Error(`Repo not found: ${repoPath}`);
1870
- }
1871
- let sourceRef = input.sourceRef;
1872
- let sourceIsFork = false;
1873
- let prUrl = null;
1874
- let prTitle = null;
1875
- if (input.sourceType === "pr") {
1876
- const num = Number(input.sourceRef.replace(/^#/, ""));
1877
- if (!Number.isFinite(num)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
1878
- const pr = await getPr(repoPath, num);
1879
- if (!pr) throw new Error(`PR #${num} not found`);
1880
- sourceIsFork = pr.isCrossRepository;
1881
- prUrl = pr.url;
1882
- const localFetchBranch = `sideboard-pr-${pr.number}`;
1883
- await fetchPrHead(repoPath, pr.number, localFetchBranch);
1884
- sourceRef = localFetchBranch;
1885
- } else if (input.sourceType === "ticket") {
1886
- sourceRef = await resolveDefaultBranch(repoPath);
1887
- } else if (input.sourceType === "branch") {
1888
- if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
1889
- sourceRef = await resolveDefaultBranch(repoPath);
1890
- } else {
1891
- const existing = await getPrForHeadBranch(repoPath, sourceRef);
1892
- if (existing?.url) {
1893
- prUrl = existing.url;
1894
- prTitle = existing.title;
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
- } else if (input.sourceType === "adopt") {
1898
- throw new Error("Use adoptThread() for adopt sources");
1917
+ if (matched.length) return [...new Set(matched)];
1899
1918
  }
1900
- const team = allocateTeamSlug(repoPath);
1901
- const { branchName, worktreePath } = await createThreadWorktree({
1902
- repoPath,
1903
- sourceRef,
1904
- slug: team.slug
1905
- });
1906
- copyConfiguredFiles(repoPath, worktreePath);
1907
- const explicitTitle = input.title?.trim();
1908
- const thread = createEmptyThread({
1909
- title: explicitTitle || team.name,
1910
- userSetTitle: Boolean(explicitTitle),
1911
- sourceType: input.sourceType,
1912
- sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
1913
- branchName,
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
- return adapter.listLinearIssues(repoPath);
1928
+ if (defaults.length) return defaults;
1929
+ return [".env.local", ".env"];
1941
1930
  }
1942
-
1943
- // src/threads/chat-tabs.ts
1944
- import { randomUUID } from "crypto";
1945
-
1946
- // src/composer/summarize.ts
1947
- var MAX_TRANSCRIPT_CHARS = 12e4;
1948
- var MAX_SUMMARY_CHARS = 6e3;
1949
- async function summarizeConversation(transcript, opts) {
1950
- const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
1951
-
1952
- [\u2026truncated\u2026]` : transcript;
1953
- const claudeSummary = await tryClaudeSummary(clipped, opts);
1954
- if (claudeSummary) {
1955
- return { summary: clipSummary(claudeSummary), method: "claude" };
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 { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
1942
+ return copied;
1958
1943
  }
1959
- async function tryClaudeSummary(transcript, opts) {
1960
- ensureAgentPath();
1961
- const prompt = [
1962
- "Summarize this coding-agent conversation for continuity in a fresh session.",
1963
- "Use concise markdown bullets covering:",
1964
- "- Goal / request",
1965
- "- Key decisions and constraints",
1966
- "- Files / areas touched",
1967
- "- Current state and open tasks",
1968
- "- Anything the next turn must remember",
1969
- "Do not invent details. No preamble.",
1970
- "",
1971
- "Transcript:",
1972
- transcript
1973
- ].join("\n");
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 { stdout, exitCode } = await run(
1976
- "claude",
1977
- [
1978
- "-p",
1979
- prompt,
1980
- "--output-format",
1981
- "text",
1982
- "--permission-mode",
1983
- "bypassPermissions",
1984
- "--model",
1985
- "haiku"
1986
- ],
1987
- { cwd: opts?.cwd, reject: false }
1988
- );
1989
- if (exitCode !== 0) return null;
1990
- const text = stdout.trim();
1991
- if (text.length < 40) return null;
1992
- return text;
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 extractiveSummary(transcript) {
1998
- const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
1999
- const users = [];
2000
- const tools = [];
2001
- const agents = [];
2002
- let mode = "none";
2003
- for (const line of lines) {
2004
- if (line.startsWith("### User")) {
2005
- mode = "user";
2006
- continue;
2007
- }
2008
- if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
2009
- mode = "agent";
2010
- continue;
2011
- }
2012
- if (line.startsWith("- ") && line.includes(":")) {
2013
- tools.push(line.replace(/^- /, ""));
2014
- continue;
2015
- }
2016
- if (mode === "user" && users.length < 8) {
2017
- users.push(line.slice(0, 240));
2018
- } else if (mode === "agent" && agents.length < 6) {
2019
- agents.push(line.slice(0, 240));
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
- const uniqueTools = [...new Set(tools)].slice(0, 20);
2023
- const parts = [
2024
- "## Compacted context (extractive)",
2025
- "",
2026
- "### User goals",
2027
- ...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
2028
- "",
2029
- "### Recent agent notes",
2030
- ...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
2031
- ];
2032
- if (uniqueTools.length) {
2033
- parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
2034
- }
2035
- parts.push(
2036
- "",
2037
- "_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
2038
- );
2039
- return parts.join("\n");
1998
+ return env;
2040
1999
  }
2041
- function clipSummary(text) {
2042
- if (text.length <= MAX_SUMMARY_CHARS) return text;
2043
- return `${text.slice(0, MAX_SUMMARY_CHARS)}
2044
-
2045
- [\u2026summary truncated\u2026]`;
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
- // src/composer/context-compact.ts
2049
- var CONTEXT_COMPACT_CHARS = 1e5;
2050
- var CONTEXT_KEEP_RECENT_CHARS = 24e3;
2051
- var CONTEXT_KEEP_RECENT_MESSAGES = 12;
2052
- var CONTEXT_MIN_MESSAGES = 10;
2053
- function estimateMessageChars(message) {
2054
- let n = message.text.length + 16;
2055
- for (const part of message.parts ?? []) {
2056
- n += estimatePartChars(part);
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
- return n;
2059
- }
2060
- function estimatePartChars(part) {
2061
- switch (part.type) {
2062
- case "text":
2063
- case "thinking":
2064
- return part.text.length;
2065
- case "tool": {
2066
- const input = part.input ? JSON.stringify(part.input) : "";
2067
- return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
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 estimateThreadChars(messages) {
2074
- return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
2075
- }
2076
- function shouldCompactContext(messages, thresholds = {}) {
2077
- const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
2078
- const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
2079
- if (messages.length < minMessages) return false;
2080
- return estimateThreadChars(messages) >= maxChars;
2081
- }
2082
- function splitForCompaction(messages, thresholds = {}) {
2083
- const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
2084
- const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
2085
- if (messages.length === 0) return { older: [], recent: [] };
2086
- let recentChars = 0;
2087
- let cut = messages.length;
2088
- for (let i = messages.length - 1; i >= 0; i--) {
2089
- const next = recentChars + estimateMessageChars(messages[i]);
2090
- const kept = messages.length - i;
2091
- if (kept >= keepCount && next > keepChars) break;
2092
- recentChars = next;
2093
- cut = i;
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 minRecent = Math.min(keepCount, messages.length);
2096
- cut = Math.min(cut, messages.length - minRecent);
2097
- if (cut <= 0) return { older: [], recent: messages };
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
- older: messages.slice(0, cut),
2100
- recent: messages.slice(cut)
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 formatToolPart(t, detail) {
2104
- if (detail === "summary") {
2105
- const label = t.description || t.detail || t.name;
2106
- const path = t.filePath ? ` (${t.filePath})` : "";
2107
- return `- ${t.name}: ${label}${path}`;
2108
- }
2109
- const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
2110
- if (t.description) lines.push(`Description: ${t.description}`);
2111
- if (t.filePath) lines.push(`Path: ${t.filePath}`);
2112
- if (t.input && Object.keys(t.input).length > 0) {
2113
- lines.push("Input:");
2114
- lines.push("```json");
2115
- lines.push(JSON.stringify(t.input, null, 2));
2116
- lines.push("```");
2117
- } else if (t.detail) {
2118
- lines.push(`Detail: ${t.detail}`);
2119
- }
2120
- if (t.result != null && t.result !== "") {
2121
- lines.push("Result:");
2122
- lines.push("```");
2123
- lines.push(t.result);
2124
- lines.push("```");
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 lines.join("\n");
2092
+ return handle.done;
2127
2093
  }
2128
- function formatMessagesAsTranscript(messages, opts) {
2129
- const tools = opts?.tools ?? "full";
2130
- const blocks = [];
2131
- for (const m of messages) {
2132
- if (m.role === "summary") {
2133
- blocks.push(`## Prior summary
2134
- ${m.text}`);
2135
- continue;
2136
- }
2137
- if (m.role === "user") {
2138
- blocks.push(`### User
2139
- ${m.text}`);
2140
- continue;
2141
- }
2142
- const bits = [];
2143
- if (m.text?.trim()) {
2144
- bits.push(`### Agent
2145
- ${m.text}`);
2146
- } else {
2147
- bits.push("### Agent");
2148
- }
2149
- const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
2150
- for (const th of thinking) {
2151
- if (th.type !== "thinking" || !th.text.trim()) continue;
2152
- bits.push("Thinking:");
2153
- bits.push(th.text);
2154
- }
2155
- const toolParts = (m.parts ?? []).filter(
2156
- (p) => p.type === "tool"
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
- return blocks.join("\n\n");
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 buildSessionSeed(messages, opts) {
2169
- if (messages.length === 0) return null;
2170
- const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
2171
- if (!body.trim()) return null;
2172
- return [
2173
- "Sideboard conversation context (restored after compaction or a new session):",
2174
- "",
2175
- body,
2176
- "",
2177
- "Continue from this context. Do not repeat the summary unless asked."
2178
- ].join("\n");
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 applyCompaction(messages, summaryText, thresholds = {}) {
2181
- const { older, recent } = splitForCompaction(messages, thresholds);
2182
- if (older.length === 0) return messages;
2183
- const summary = {
2184
- role: "summary",
2185
- text: summaryText.trim(),
2186
- ts: (/* @__PURE__ */ new Date()).toISOString()
2187
- };
2188
- return [summary, ...recent];
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
- async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
2191
- if (!shouldCompactContext(thread.messages, thresholds)) {
2192
- return { didCompact: false, thread };
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
- const { older } = splitForCompaction(thread.messages, thresholds);
2195
- if (older.length === 0) {
2196
- return { didCompact: false, thread };
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
- const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
2199
- const { summary, method } = await summarize(transcript, {
2200
- cwd: thread.worktreePath
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
- didCompact: true,
2211
- thread: next,
2212
- summary,
2213
- method,
2214
- olderCount: older.length
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/threads/chat-tabs.ts
2219
- function sameWorktreePath(a, b) {
2220
- return normalizeWorktreePath(a) === normalizeWorktreePath(b);
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 takenTeamSlugsForChatTab(worktreePath) {
2223
- const key = normalizeWorktreePath(worktreePath);
2224
- const taken = /* @__PURE__ */ new Set();
2225
- const dir = worktreeNameFromPath(key);
2226
- if (dir) taken.add(dir.toLowerCase());
2227
- for (const sibling of threadsSharingWorktree(key)) {
2228
- for (const slug of takenSlugsFromThread(sibling)) {
2229
- taken.add(slug);
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
- return [...taken];
2233
- }
2234
- function requireThread(idOrRef) {
2235
- const thread = findThreadByRef(idOrRef) ?? null;
2236
- if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
2237
- return thread;
2238
- }
2239
- function worktreeBindingFrom(from) {
2240
- return {
2241
- worktreePath: normalizeWorktreePath(from.worktreePath),
2242
- repoPath: from.repoPath,
2243
- branchName: from.branchName,
2244
- sourceRef: from.sourceRef,
2245
- // Legacy pinned-repo orchestration: new tabs become normal worktree agents.
2246
- // Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
2247
- sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
2248
- sourceIsFork: from.sourceIsFork,
2249
- parentThreadId: from.parentThreadId,
2250
- prUrl: from.prUrl,
2251
- prTitle: from.prTitle,
2252
- prState: from.prState,
2253
- skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
2254
- stackId: from.stackId,
2255
- stackLayer: from.stackLayer
2256
- };
2257
- }
2258
- function threadsSharingWorktree(worktreePath) {
2259
- if (!worktreePath) return [];
2260
- const key = normalizeWorktreePath(worktreePath);
2261
- return listThreads({ includeArchived: true }).filter(
2262
- (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
2263
- ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
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 formatTranscriptMarkdown(title, messages) {
2266
- const body = formatMessagesAsTranscript(messages, { tools: "full" });
2267
- return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
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 forkMessageSlice(from, throughIndex) {
2270
- const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
2271
- return through >= 0 ? from.messages.slice(0, through + 1) : [];
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 buildForkTranscriptAttachment(baseTitle, messages) {
2274
- const title = baseTitle || "Chat";
2344
+ function worktreeCleanupSettings() {
2345
+ const a = loadAppSettings().advanced;
2275
2346
  return {
2276
- id: randomUUID(),
2277
- name: `Transcript of ${title}.md`,
2278
- kind: "transcript",
2279
- content: formatTranscriptMarkdown(title, messages)
2347
+ worktreeMaxCount: a.worktreeMaxCount,
2348
+ worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
2349
+ worktreeLastCleanupAt: a.worktreeLastCleanupAt,
2350
+ autoCleanupOrphans: a.autoCleanupOrphans
2280
2351
  };
2281
2352
  }
2282
- function createChatTab(input) {
2283
- const from = requireThread(input.fromThreadId);
2284
- const binding = worktreeBindingFrom(from);
2285
- const explicitTitle = input.title?.trim();
2286
- const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
2287
- const nextAgent = input.agent ?? from.agent;
2288
- if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
2289
- assertOrchestratorCapableAgent(nextAgent);
2290
- }
2291
- const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
2292
- const thread = createEmptyThread({
2293
- title,
2294
- // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
2295
- // syncThreadBranchFromGit would otherwise rewrite every sibling to the
2296
- // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
2297
- userSetTitle: true,
2298
- ...binding,
2299
- // Slack / Brightsy cloud identity stays on the original chat. A + tab
2300
- // that copies `slack:team:user` would steal inbound DMs after you close
2301
- // the connected chat.
2302
- sourceRef: singletonInbox ? title : binding.sourceRef,
2303
- agent: nextAgent,
2304
- model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
2305
- effort: input.effort !== void 0 ? input.effort : from.effort,
2306
- fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
2307
- planMode: from.planMode,
2308
- autonomy: input.autonomy ?? from.autonomy,
2309
- attachments: input.attachments ?? [],
2310
- status: "idle"
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
- writeThread(thread);
2313
- return thread;
2314
- }
2315
- function forkChatTab(input) {
2316
- const from = requireThread(input.threadId);
2317
- const slice = forkMessageSlice(from, input.throughIndex);
2318
- const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
2319
- const tab = createChatTab({
2320
- fromThreadId: input.threadId,
2321
- agent: input.agent ?? from.agent,
2322
- model: input.model,
2323
- title: input.title?.trim() || void 0,
2324
- attachments: [attachment]
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 (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
2327
- const next = { ...tab, parentThreadId: from.id };
2328
- writeThread(next);
2329
- return next;
2372
+ if (checkout.exitCode !== 0) {
2373
+ throw new Error(
2374
+ `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
2375
+ );
2330
2376
  }
2331
- return tab;
2332
- }
2333
-
2334
- // src/store/turn-live.ts
2335
- import {
2336
- existsSync as existsSync7,
2337
- renameSync,
2338
- unlinkSync as unlinkSync2,
2339
- writeFileSync as writeFileSync2,
2340
- readFileSync as readFileSync5
2341
- } from "fs";
2342
- var buffers = /* @__PURE__ */ new Map();
2343
- var FLUSH_MS = 800;
2344
- var MAX_PARTS = 80;
2345
- function trimParts(parts) {
2346
- if (parts.length <= MAX_PARTS) return parts;
2347
- return parts.slice(-Math.floor(MAX_PARTS / 2));
2348
- }
2349
- function lastText(parts, type) {
2350
- for (let i = parts.length - 1; i >= 0; i--) {
2351
- const p = parts[i];
2352
- if (p.type === type && p.text.trim()) return p.text.trim();
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
- return "";
2355
- }
2356
- function summarizeTurnLive(parts) {
2357
- const tools = parts.filter(
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
- const lastTool = tools[tools.length - 1];
2361
- const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
2362
- const running = lastTool?.status === "running";
2363
- const thinking = lastText(parts, "thinking");
2364
- const text = lastText(parts, "text");
2365
- const excerptRaw = thinking || text;
2366
- const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
2367
- let summary = "Working\u2026";
2368
- if (lastToolLabel) {
2369
- const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
2370
- summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
2371
- } else if (thinking) {
2372
- summary = "Thinking\u2026";
2373
- } else if (text) {
2374
- summary = "Writing reply\u2026";
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
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2378
- summary,
2379
- lastTool: lastToolLabel,
2380
- toolCount: tools.length,
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
- function flush(threadId) {
2385
- const buf = buffers.get(threadId);
2386
- if (!buf) return;
2387
- if (buf.timer) {
2388
- clearTimeout(buf.timer);
2389
- buf.timer = null;
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
- writeTurnLive(threadId, summarizeTurnLive(buf.parts));
2392
- }
2393
- function scheduleFlush(threadId) {
2394
- const buf = buffers.get(threadId);
2395
- if (!buf) return;
2396
- if (buf.timer) return;
2397
- buf.timer = setTimeout(() => {
2398
- const live = buffers.get(threadId);
2399
- if (live) live.timer = null;
2400
- flush(threadId);
2401
- }, FLUSH_MS);
2402
- }
2403
- function noteTurnLiveEvent(threadId, event) {
2404
- if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
2405
- return;
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 prev = buffers.get(threadId) ?? { parts: [], timer: null };
2408
- prev.parts = trimParts(applyAgentEvent(prev.parts, event));
2409
- buffers.set(threadId, prev);
2410
- if (event.type === "exit") {
2411
- flush(threadId);
2412
- const cur = buffers.get(threadId);
2413
- if (cur?.timer) clearTimeout(cur.timer);
2414
- buffers.delete(threadId);
2415
- return;
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 immediate = event.type === "tool_use" || event.type === "tool_result";
2418
- if (immediate) flush(threadId);
2419
- else scheduleFlush(threadId);
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 writeTurnLive(threadId, progress) {
2422
- const path = threadLivePath(threadId);
2423
- const tmp = `${path}.${process.pid}.tmp`;
2476
+ function releaseDesktopHost(pid = process.pid) {
2477
+ if (readDesktopHostPid() !== pid) return;
2424
2478
  try {
2425
- writeFileSync2(tmp, JSON.stringify(progress), "utf8");
2426
- renameSync(tmp, path);
2479
+ unlinkSync(desktopHostPidPath());
2427
2480
  } catch {
2428
- try {
2429
- unlinkSync2(tmp);
2430
- } catch {
2431
- }
2432
2481
  }
2433
2482
  }
2434
- function readTurnLive(threadId) {
2435
- const path = threadLivePath(threadId);
2436
- if (!existsSync7(path)) return null;
2483
+ function readDesktopHostPid() {
2437
2484
  try {
2438
- const raw = JSON.parse(readFileSync5(path, "utf8"));
2439
- if (!raw || typeof raw.summary !== "string") return null;
2440
- return raw;
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 clearTurnLive(threadId) {
2446
- const buf = buffers.get(threadId);
2447
- if (buf?.timer) clearTimeout(buf.timer);
2448
- buffers.delete(threadId);
2449
- const path = threadLivePath(threadId);
2450
- if (!existsSync7(path)) return;
2492
+ function pidAlive(pid) {
2451
2493
  try {
2452
- unlinkSync2(path);
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/review/request-review.ts
2458
- import { randomUUID as randomUUID2 } from "crypto";
2459
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2460
- import { dirname as dirname2, join as join8 } from "path";
2461
-
2462
- // src/review/review-request-template.ts
2463
- var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
2464
-
2465
- You are reviewing a proposed code change so a human can decide whether it is **ready to merge / land**. Findings matter, but the primary deliverable is a clear readiness recommendation \u2014 not a laundry list of style notes.
2466
-
2467
- ## Required outcome
2468
-
2469
- Start your reply with a **Recommendation** section using exactly one of:
2470
-
2471
- - **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
2472
- - **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
2473
- - **Request changes** \u2014 not ready; blocking issues must be fixed first.
2474
- - **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
2475
-
2476
- In 1\u20133 sentences, say **why** \u2014 grounded in correctness, risk, test coverage, and scope \u2014 not vibes. If you request changes, name the blockers explicitly.
2477
-
2478
- People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
2479
-
2480
- ## Findings
2481
-
2482
- Below are guidelines for determining whether an issue is worth flagging to the original author.
2483
-
2484
- These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
2485
-
2486
- Flag something as a bug / blocking finding only when:
2487
-
2488
- 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
2489
- 2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
2490
- 3. Fixing it does not demand rigor absent from the rest of the codebase.
2491
- 4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
2492
- 5. The author would likely fix it if made aware.
2493
- 6. It does not rely on unstated assumptions about the codebase or author intent.
2494
- 7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
2495
- 8. It is clearly not just an intentional change by the author.
2496
-
2497
- When flagging an issue, include a short accompanying comment:
2498
-
2499
- 1. Clear about why it is a problem.
2500
- 2. Severity must match reality \u2014 do not inflate.
2501
- 3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
2502
- 4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
2503
- 5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
2504
- 6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
2505
- 7. Skimmable on first read.
2506
- 8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
2507
-
2508
- HOW MANY FINDINGS TO RETURN:
2509
-
2510
- List every finding the author would fix if they knew about it. If nothing qualifies, say so and still give the Recommendation. Do not stop at the first finding.
2511
-
2512
- GUIDELINES:
2513
-
2514
- - Ignore trivial style unless it obscures meaning or violates documented standards.
2515
- - One comment per distinct issue (or a short multi-line range if needed).
2516
- - Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
2517
- - In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
2518
- - Do NOT introduce or remove outer indentation levels unless that is the actual fix.
2519
- - Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
2520
-
2521
- The report appears in chat (and can become Sideboard diff comments). Avoid unnecessary location chatter in the body; keep line ranges as short as possible (prefer \u22645\u201310 lines).
2522
-
2523
- ## Getting the diff
2524
-
2525
- Use Sideboard's diff for this thread's worktree. Prefer the \`get_diff\` MCP tool (pass this thread's ref) for a compact summary, then read specific files with Read/Glob as needed. In the Sideboard desktop app, the Changes panel shows the same worktree diff.
2526
-
2527
- If the user asks you to address or read line comments they added in the Changes / file diff UI, those arrive as \`diff-comment\` attachments on the next turn \u2014 follow them precisely.
2528
-
2529
- ## Fallback: if you don't have access to the Sideboard diff tool
2530
-
2531
- If you don't have access to \`get_diff\`, use the following git commands to get the diff:
2532
-
2533
- \`\`\`bash
2534
- # Get the merge base between this branch and the target
2535
- MERGE_BASE=$(git merge-base origin/main HEAD)
2536
-
2537
- # Get the committed diff against the merge base
2538
- git diff $MERGE_BASE HEAD
2539
-
2540
- # Get any uncommitted changes (staged and unstaged)
2541
- git diff HEAD
2542
- \`\`\`
2543
-
2544
- Review the combination of both outputs: the first shows all committed changes on this branch relative to the target, and the second shows any uncommitted work in progress.
2545
-
2546
- No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
2547
-
2548
- ## Output format
2549
-
2550
- **1. Recommendation first** (required), then **2. Findings** (may be empty).
2551
-
2552
- Only report ONE finding per unique issue.
2553
-
2554
- <example>
2555
- ## Recommendation
2556
-
2557
- **Request changes** \u2014 The empty-input crash on load will break first-run users; fix that before merge. The unused helper is a nit and can wait.
2558
-
2559
- ## Findings
2560
-
2561
- ### **#1 Empty input causes crash** (blocking)
2562
-
2563
- If the input field is empty when the page loads, the app will crash.
2564
-
2565
- File: src/client/frontends/desktop/ui/Input.tsx
2566
-
2567
- ### **#2 Dead code** (nit)
2568
-
2569
- The getUserData function is now unused. It should be deleted.
2570
-
2571
- File: src/client/frontends/desktop/core/UserData.ts
2572
- </example>
2573
-
2574
- <example>
2575
- ## Recommendation
2576
-
2577
- **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
2578
- </example>
2579
-
2580
- ## Growing the rules
2512
+ // src/threads/create.ts
2513
+ import { existsSync as existsSync7 } from "fs";
2581
2514
 
2582
- If a blocking issue is a missing or ambiguous repo rule that will recur, say so and propose one sentence for \`.sideboard/review.md\` or a \`.claude/skills/<name>/SKILL.md\`. Do not only patch this diff when the same miss will happen again. New skills go under \`.claude/skills\` (Claude Code / attach) \u2014 not \`.sideboard/skills\`.
2583
- `;
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/review/request-review.ts
2586
- var REPO_REVIEW_PATH = ".sideboard/review.md";
2587
- var REPO_REVIEW_NAME = "review.md";
2588
- var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
2589
- var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
2590
- var REVIEW_REQUEST_NAME = "Review request.md";
2591
- var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
2592
- var LEGACY_REVIEW_TEMPLATE_MARKERS = [
2593
- "You are acting as a reviewer for a proposed code change made by another engineer.",
2594
- "HOW MANY FINDINGS TO RETURN:"
2595
- ];
2596
- function shouldRefreshReviewRequestTemplate(content) {
2597
- const trimmed = content.trim();
2598
- if (!trimmed) return true;
2599
- if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
2600
- return false;
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
- return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
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 readTextIfPresent(abs) {
2605
- if (!existsSync8(abs)) return null;
2606
- try {
2607
- const content = readFileSync6(abs, "utf8");
2608
- return content.trim() ? content : null;
2609
- } catch {
2610
- return null;
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
- function ensureAttachmentsGitignore(worktreePath) {
2614
- const gitignoreAbs = join8(worktreePath, ATTACHMENTS_DIR, ".gitignore");
2615
- if (existsSync8(gitignoreAbs)) return;
2616
- mkdirSync2(dirname2(gitignoreAbs), { recursive: true });
2617
- writeFileSync3(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
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 resolveReviewGuidelines(worktreePath) {
2620
- const repoAbs = join8(worktreePath, REPO_REVIEW_PATH);
2621
- const repoContent = readTextIfPresent(repoAbs);
2622
- if (repoContent) {
2623
- return {
2624
- path: REPO_REVIEW_PATH,
2625
- name: REPO_REVIEW_NAME,
2626
- content: repoContent,
2627
- source: "repo"
2628
- };
2629
- }
2630
- const localAbs = join8(worktreePath, REVIEW_REQUEST_PATH);
2631
- const localContent = readTextIfPresent(localAbs);
2632
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
2633
- return {
2634
- path: REVIEW_REQUEST_PATH,
2635
- name: REVIEW_REQUEST_NAME,
2636
- content: localContent,
2637
- source: "local"
2638
- };
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
- const legacyAbs = join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
2641
- const legacyContent = readTextIfPresent(legacyAbs);
2642
- if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
2643
- return {
2644
- path: LEGACY_REVIEW_REQUEST_PATH,
2645
- name: REVIEW_REQUEST_NAME,
2646
- content: legacyContent,
2647
- source: "local"
2648
- };
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
- path: REVIEW_REQUEST_PATH,
2655
- name: REVIEW_REQUEST_NAME,
2656
- content: REVIEW_REQUEST_TEMPLATE,
2657
- source: "stock"
2676
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2677
+ summary,
2678
+ lastTool: lastToolLabel,
2679
+ toolCount: tools.length,
2680
+ excerpt
2658
2681
  };
2659
2682
  }
2660
- function ensureReviewRequestFile(worktreePath) {
2661
- const repoAbs = join8(worktreePath, REPO_REVIEW_PATH);
2662
- const repoContent = readTextIfPresent(repoAbs);
2663
- if (repoContent) {
2664
- return {
2665
- path: REPO_REVIEW_PATH,
2666
- name: REPO_REVIEW_NAME,
2667
- content: repoContent,
2668
- source: "repo"
2669
- };
2670
- }
2671
- const localAbs = join8(worktreePath, REVIEW_REQUEST_PATH);
2672
- const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent(join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
2673
- if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
2674
- const path = existsSync8(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
2675
- return {
2676
- path,
2677
- name: REVIEW_REQUEST_NAME,
2678
- content: localContent,
2679
- source: "local"
2680
- };
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
- mkdirSync2(dirname2(repoAbs), { recursive: true });
2683
- writeFileSync3(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
2684
- return {
2685
- path: REPO_REVIEW_PATH,
2686
- name: REPO_REVIEW_NAME,
2687
- content: REVIEW_REQUEST_TEMPLATE,
2688
- source: "repo"
2689
- };
2690
+ writeTurnLive(threadId, summarizeTurnLive(buf.parts));
2690
2691
  }
2691
- function buildReviewRequestAttachment(content, opts) {
2692
- const path = opts?.path ?? REVIEW_REQUEST_PATH;
2693
- const name = opts?.name ?? (path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
2694
- return {
2695
- id: randomUUID2(),
2696
- name,
2697
- kind: "file",
2698
- path,
2699
- content
2700
- };
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 readExistingReviewRequestFile(worktreePath) {
2703
- return readTextIfPresent(join8(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent(join8(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent(join8(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
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
- async function requestReview(threadRef, send) {
2706
- const from = findThreadByRef(threadRef);
2707
- if (!from) throw new Error(`Thread not found: ${threadRef}`);
2708
- if (isOrchestratorThread(from)) {
2709
- throw new Error(
2710
- "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
2711
- );
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
- if (from.status === "archived") {
2714
- throw new Error(`Thread is archived: ${from.id}`);
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-UPEVANU4.js");
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
- "- Do not write new skills under `.sideboard/skills` \u2014 native agents do not scan it. Point Codex/OpenCode at the file from `AGENTS.md`. Optional: symlink `.cursor/skills/<name>` to the Claude skill.",
4849
- "- Skip a guide for a one-off. If a matching skill exists, follow it. Same miss twice \u2192 edit the skill (or `.sideboard/review.md`), do not patch around it."
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-DEHSKERK.js");
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-6AXBWM3K.js");
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-6AXBWM3K.js");
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: null
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-6AXBWM3K.js");
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-6AXBWM3K.js");
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-UPEVANU4.js");
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-6AXBWM3K.js");
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-NR5YJG2E.js");
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-NR5YJG2E.js");
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,