@sideboard-ai/core 0.1.241 → 0.1.243

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 (52) hide show
  1. package/dist/{abletime-VSV6AUBV.js → abletime-CWIJ7INC.js} +4 -4
  2. package/dist/{abletime-UX4VUENQ.js → abletime-OI6FNSXX.js} +4 -4
  3. package/dist/{abletime-oauth-DKBUZV2C.js → abletime-oauth-CHVTUZBR.js} +3 -3
  4. package/dist/{abletime-oauth-QMDV45JC.js → abletime-oauth-PUIPZTA4.js} +3 -3
  5. package/dist/{abletime-rest-NL56UVK5.js → abletime-rest-32NIVXAY.js} +3 -3
  6. package/dist/{abletime-rest-VPBC2XS5.js → abletime-rest-APJUKFKL.js} +3 -3
  7. package/dist/{agents-CDQCQVWU.js → agents-CPME6O2H.js} +9 -5
  8. package/dist/{agents-PBRIHUYM.js → agents-VKO5UZO6.js} +9 -5
  9. package/dist/{app-settings-AGDLF75Z.js → app-settings-652J57CX.js} +1 -1
  10. package/dist/{app-settings-I27UBVI4.js → app-settings-VOPXJMGV.js} +1 -1
  11. package/dist/{chunk-BILEJG3V.js → chunk-3P5OCI2X.js} +3 -3
  12. package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
  13. package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
  14. package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
  15. package/dist/{chunk-TB6UMHIF.js → chunk-7UCZ4DRF.js} +2 -2
  16. package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
  17. package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
  18. package/dist/{chunk-O3UAQAYA.js → chunk-CCD2QIFK.js} +2 -2
  19. package/dist/{chunk-63UHRQ64.js → chunk-DARSRQUE.js} +1523 -1450
  20. package/dist/{chunk-OKHOYI53.js → chunk-ECZTXNYR.js} +3 -3
  21. package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
  22. package/dist/{chunk-3Q4PVZ3U.js → chunk-HR4RNC73.js} +32 -5
  23. package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
  24. package/dist/{chunk-QXYWLO6W.js → chunk-ISZERVDT.js} +28 -5
  25. package/dist/{chunk-EE3REM4B.js → chunk-NEOAHPMK.js} +1582 -1505
  26. package/dist/{chunk-DJU54RYW.js → chunk-PGPSIEMW.js} +13 -0
  27. package/dist/{chunk-P3DCZRER.js → chunk-PPJ5JTWA.js} +1 -0
  28. package/dist/{chunk-5ELC3KK3.js → chunk-QNJXJIWJ.js} +5 -5
  29. package/dist/{chunk-JFOPOMK7.js → chunk-WDDUIGFK.js} +13 -0
  30. package/dist/{chunk-IVXPXYH2.js → chunk-WURF67ID.js} +5 -5
  31. package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
  32. package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-AJ3E6LBX.js} +4 -4
  33. package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-PYIRQGU7.js} +4 -4
  34. package/dist/{global-workspace-TD5SARNW.js → global-workspace-GIQ6F6IJ.js} +4 -4
  35. package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-GQM653DP.js} +4 -4
  36. package/dist/index.cjs +609 -463
  37. package/dist/index.d.cts +56 -2
  38. package/dist/index.d.ts +56 -2
  39. package/dist/index.js +74 -38
  40. package/dist/mcp/run-stdio.cjs +1029 -908
  41. package/dist/mcp/run-stdio.js +50 -36
  42. package/dist/{orchestrator-ZUOEBJFN.js → orchestrator-3IOISDFS.js} +7 -7
  43. package/dist/{orchestrator-7AJEFWGQ.js → orchestrator-YDPDMXUY.js} +7 -7
  44. package/dist/{plan-file-UUKCZEYP.js → plan-file-7MMIPV7Y.js} +9 -1
  45. package/dist/{plan-file-2EPHYRX7.js → plan-file-CKXUFTQ2.js} +9 -1
  46. package/dist/{thread-store-YOPKY74K.js → thread-store-JQB7KZJR.js} +1 -1
  47. package/dist/{thread-store-AYPBEZT5.js → thread-store-SCV2IZYS.js} +1 -1
  48. package/dist/{workspaces-4SFSCYKT.js → workspaces-OIBSD5B6.js} +4 -4
  49. package/dist/{workspaces-JX5R4V24.js → workspaces-WYC5TLC3.js} +4 -4
  50. package/dist/{worktree-Y5FES5D6.js → worktree-5VRZVGP3.js} +3 -3
  51. package/dist/{worktree-AGVPKDBH.js → worktree-DGDM7LGV.js} +3 -3
  52. package/package.json +1 -1
@@ -2,13 +2,13 @@ import {
2
2
  extractPresentedPlan,
3
3
  readPlanFile,
4
4
  writePlanFile
5
- } from "./chunk-DJU54RYW.js";
5
+ } from "./chunk-PGPSIEMW.js";
6
6
  import {
7
- PLAN_MODE_INSTRUCTION,
8
7
  adjustBrightsyTurnExit,
9
8
  allAdapters,
10
9
  applyAgentEvent,
11
10
  clipAgentEventForPaint,
11
+ compactSummaryPart,
12
12
  finalizeParts,
13
13
  getAdapter,
14
14
  isBrightsyNdjsonLine,
@@ -21,10 +21,11 @@ import {
21
21
  parseBrightsyCliLine,
22
22
  parseSessionQuotaResetAt,
23
23
  partsToAssistantText,
24
+ planModeTurnInstruction,
24
25
  resolveQuotaFallbackAgent,
25
26
  stripBrightsyNdjsonNoise,
26
27
  toolDescription
27
- } from "./chunk-QXYWLO6W.js";
28
+ } from "./chunk-ISZERVDT.js";
28
29
  import {
29
30
  SLACK_REPLY_FORMATTING,
30
31
  addWorkspace,
@@ -47,7 +48,7 @@ import {
47
48
  stageAbsolutePathsAsAttachments,
48
49
  stageBuffersAsAttachments,
49
50
  syncWorkspacesFromThreads
50
- } from "./chunk-BILEJG3V.js";
51
+ } from "./chunk-3P5OCI2X.js";
51
52
  import {
52
53
  addPrStackLayer,
53
54
  allocateTeamName,
@@ -96,7 +97,7 @@ import {
96
97
  worktreeDisplayLabelForGroup,
97
98
  worktreeNameFromPath,
98
99
  worktreeSlugForTicket
99
- } from "./chunk-O3UAQAYA.js";
100
+ } from "./chunk-CCD2QIFK.js";
100
101
  import {
101
102
  ATTACHMENTS_DIR,
102
103
  DETACHED_JOBS_DIR,
@@ -159,7 +160,7 @@ import {
159
160
  sumUsageList,
160
161
  updateAdvancedSettings,
161
162
  usageOnLimit
162
- } from "./chunk-P3DCZRER.js";
163
+ } from "./chunk-PPJ5JTWA.js";
163
164
  import {
164
165
  examplePrefixedBranch,
165
166
  sanitizeGitBranchPrefix
@@ -181,7 +182,7 @@ import {
181
182
  updateThread,
182
183
  withThreadLock,
183
184
  writeThread
184
- } from "./chunk-3AJUH66R.js";
185
+ } from "./chunk-6I5YQTNK.js";
185
186
  import {
186
187
  annotateDevAccessRuns,
187
188
  appDataDir,
@@ -441,9 +442,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
441
442
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
442
443
  );
443
444
  }
444
- const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-ZJBBICFJ.js");
445
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-GQM653DP.js");
445
446
  if (isGlobalThread2(thread)) {
446
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-5UINTEA6.js");
447
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-AJ3E6LBX.js");
447
448
  ensureGlobalCoordinatorCwd2(
448
449
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
449
450
  );
@@ -744,15 +745,15 @@ function shouldAutoArchiveOnPrMerge(opts) {
744
745
  // src/hook/conductor.ts
745
746
  import {
746
747
  copyFileSync,
747
- existsSync as existsSync3,
748
- mkdirSync as mkdirSync4,
749
- readdirSync,
750
- readFileSync as readFileSync3,
748
+ existsSync as existsSync4,
749
+ mkdirSync as mkdirSync5,
750
+ readdirSync as readdirSync2,
751
+ readFileSync as readFileSync4,
751
752
  realpathSync
752
753
  } from "fs";
753
754
  import { execFileSync } from "child_process";
754
755
  import { createServer } from "net";
755
- import { basename, dirname as dirname3, join as join5, resolve, sep } from "path";
756
+ import { basename, dirname as dirname4, join as join6, resolve, sep } from "path";
756
757
  import { execa as execa2 } from "execa";
757
758
  import { createInterface as createInterface2 } from "readline";
758
759
 
@@ -1150,12 +1151,38 @@ ${match.text}`, "");
1150
1151
  blocks.push("Continue from this context. Do not repeat the summary unless asked.");
1151
1152
  return blocks.join("\n");
1152
1153
  }
1154
+ function compactArchiveProse(message) {
1155
+ if (message.origin === "continue") return null;
1156
+ if (message.role === "summary") return null;
1157
+ let text = (message.text ?? "").trim();
1158
+ if (!text && message.role === "agent") {
1159
+ text = lastAssistantMessageText(message.parts).trim();
1160
+ }
1161
+ if (!text || isInternalAgentStatusText(text)) return null;
1162
+ return { role: message.role, text, ts: message.ts };
1163
+ }
1164
+ function messagesForCompactArchive(older) {
1165
+ const out = [];
1166
+ for (const message of older) {
1167
+ const prose = compactArchiveProse(message);
1168
+ if (prose) out.push(prose);
1169
+ }
1170
+ return out;
1171
+ }
1172
+ function appendCompactedMessages(existing, older) {
1173
+ const next = messagesForCompactArchive(older);
1174
+ if (!existing?.length) return next;
1175
+ if (!next.length) return existing;
1176
+ return [...existing, ...next];
1177
+ }
1153
1178
  function applyCompaction(messages, summaryText, thresholds = {}) {
1154
1179
  const { older, recent } = splitForCompaction(messages, thresholds);
1155
1180
  if (older.length === 0) return messages;
1181
+ const text = summaryText.trim();
1156
1182
  const summary = {
1157
1183
  role: "summary",
1158
- text: summaryText.trim(),
1184
+ text,
1185
+ parts: [compactSummaryPart(text)],
1159
1186
  ts: (/* @__PURE__ */ new Date()).toISOString()
1160
1187
  };
1161
1188
  return [summary, ...recent];
@@ -1183,6 +1210,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
1183
1210
  cwd: thread.worktreePath
1184
1211
  });
1185
1212
  let messages = applyCompaction(thread.messages, summary, thresholds);
1213
+ const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
1186
1214
  const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
1187
1215
  if (resetSession) {
1188
1216
  messages = applyForwardOccupancy(messages);
@@ -1190,6 +1218,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
1190
1218
  const next = {
1191
1219
  ...thread,
1192
1220
  messages,
1221
+ ...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
1193
1222
  sessionId: resetSession ? null : thread.sessionId,
1194
1223
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1195
1224
  };
@@ -1725,1649 +1754,1669 @@ function saveWorktreeRunPorts(worktreePath, scriptName, ports) {
1725
1754
  `, "utf8");
1726
1755
  }
1727
1756
 
1728
- // src/hook/conductor.ts
1729
- var PORT_RANGE_SIZE = 10;
1730
- var RECLAIM_RETRY_DELAY_MS = 40;
1731
- var RECLAIM_TRIES = 8;
1732
- function matchSimpleGlob(pattern, name) {
1733
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1734
- return new RegExp(`^${escaped}$`).test(name);
1735
- }
1736
- function readWorktreeInclude(repoPath) {
1737
- const path = join5(repoPath, ".worktreeinclude");
1738
- if (!existsSync3(path)) return [];
1739
- return readFileSync3(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
1757
+ // src/mcp/wait-for-job.ts
1758
+ import {
1759
+ appendFileSync as appendFileSync2,
1760
+ existsSync as existsSync3,
1761
+ mkdirSync as mkdirSync4,
1762
+ readdirSync,
1763
+ readFileSync as readFileSync3,
1764
+ writeFileSync as writeFileSync3
1765
+ } from "fs";
1766
+ import { dirname as dirname3, join as join5 } from "path";
1767
+
1768
+ // src/mcp/wait-for-turn.ts
1769
+ var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
1770
+ function mcpWaitForTurnTimeoutMs(requested) {
1771
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
1772
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
1773
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
1740
1774
  }
1741
- function resolveFilesToCopy(repoPath) {
1742
- const fromInclude = readWorktreeInclude(repoPath);
1743
- if (fromInclude.length) return fromInclude;
1744
- const settings = loadRepoSettings(repoPath);
1745
- if (settings?.filesToCopy?.length) return settings.filesToCopy;
1746
- if (settings?.fileIncludeGlobs?.length) {
1747
- const matched = [];
1748
- try {
1749
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1750
- if (!entry.isFile()) continue;
1751
- for (const glob of settings.fileIncludeGlobs) {
1752
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
1753
- matched.push(entry.name);
1754
- break;
1755
- }
1756
- }
1757
- }
1758
- } catch {
1759
- }
1760
- if (matched.length) return [...new Set(matched)];
1761
- }
1762
- const defaults = [];
1763
- try {
1764
- for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
1765
- if (entry.isFile() && entry.name.startsWith(".env")) {
1766
- defaults.push(entry.name);
1767
- }
1768
- }
1769
- } catch {
1775
+ var MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
1776
+ var MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
1777
+ var MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
1778
+ var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
1779
+ var MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
1780
+ var MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
1781
+ function mcpWaitTaskHint(taskState, status) {
1782
+ switch (taskState) {
1783
+ case "submitted":
1784
+ return MCP_WAIT_QUEUED_HINT;
1785
+ case "working":
1786
+ return MCP_WAIT_STILL_RUNNING_HINT;
1787
+ case "input-required":
1788
+ return MCP_WAIT_INPUT_REQUIRED_HINT;
1789
+ case "canceled":
1790
+ return MCP_WAIT_STOPPED_HINT;
1791
+ case "failed":
1792
+ return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
1793
+ default:
1794
+ return void 0;
1770
1795
  }
1771
- if (defaults.length) return defaults;
1772
- return [".env.local", ".env"];
1773
1796
  }
1774
- function copyConfiguredFiles(repoPath, worktreePath) {
1775
- const patterns = resolveFilesToCopy(repoPath);
1776
- const copied = [];
1777
- for (const rel of patterns) {
1778
- const src = join5(repoPath, rel);
1779
- if (!existsSync3(src)) continue;
1780
- const dest = join5(worktreePath, rel);
1781
- mkdirSync4(dirname3(dest), { recursive: true });
1782
- copyFileSync(src, dest);
1783
- copied.push(rel);
1797
+
1798
+ // src/mcp/wait-for-job.ts
1799
+ var MAX_JOB_CONTINUES = 8;
1800
+ var MAX_JOB_DELTA_LINES = 80;
1801
+ var MAX_JOB_DELTA_CHARS = 4e3;
1802
+ var DEFERRED_DONE_MAX_CHARS = 480;
1803
+ var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
1804
+ var MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. The type=log pane already appended this delta. Call wait_for_job again. If it is hanging, producing no useful output, or doing the wrong thing, call stop_job (same id) instead of looping forever. Do not end the turn or tell the user you will let them know later.";
1805
+ var MCP_STOP_JOB_HINT = "Stopped. The type=log pane is status=failed with the last delta. Do not wait again unless you start a new command.";
1806
+ var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
1807
+ function mcpWaitForJobTimeoutMs(requested) {
1808
+ return mcpWaitForTurnTimeoutMs(requested);
1809
+ }
1810
+ function sanitizeDetachedJobId(id) {
1811
+ const s = id.trim();
1812
+ if (!JOB_ID_RE.test(s)) {
1813
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
1784
1814
  }
1785
- return copied;
1815
+ return s;
1786
1816
  }
1787
- var cachedLoginEnv = null;
1788
- async function captureLoginEnv() {
1789
- if (cachedLoginEnv) return { ...cachedLoginEnv };
1790
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
1817
+ function jobAlive(pid) {
1818
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1791
1819
  try {
1792
- const result = await execa2(shell, ["-l", "-c", "env -0"], {
1793
- reject: false,
1794
- timeout: 5e3
1795
- });
1796
- if (result.exitCode === 0 && result.stdout) {
1797
- const env = { ...process.env };
1798
- for (const entry of result.stdout.split("\0")) {
1799
- if (!entry) continue;
1800
- const eq = entry.indexOf("=");
1801
- if (eq <= 0) continue;
1802
- const key = entry.slice(0, eq);
1803
- const value = entry.slice(eq + 1);
1804
- if (key) env[key] = value;
1805
- }
1806
- cachedLoginEnv = env;
1807
- return { ...env };
1808
- }
1820
+ process.kill(pid, 0);
1821
+ return true;
1809
1822
  } catch {
1823
+ return false;
1810
1824
  }
1811
- cachedLoginEnv = { ...process.env };
1812
- return { ...cachedLoginEnv };
1813
1825
  }
1814
- function buildWorkspaceScriptEnv(opts, baseEnv) {
1815
- const env = stripNestedElectronEnv({
1816
- ...baseEnv ?? process.env
1817
- });
1818
- const name = opts.workspaceName ?? basename(opts.worktreePath);
1819
- const ports = opts.ports ?? [];
1820
- const primary = ports[0];
1821
- env.SIDEBOARD_WORKSPACE_NAME = name;
1822
- env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
1823
- env.SIDEBOARD_ROOT_PATH = opts.repoPath;
1824
- env.SIDEBOARD_IS_LOCAL = "1";
1825
- if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
1826
- env.CONDUCTOR_WORKSPACE_NAME = name;
1827
- env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
1828
- env.CONDUCTOR_ROOT_PATH = opts.repoPath;
1829
- env.CONDUCTOR_IS_LOCAL = "1";
1830
- if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
1831
- if (primary != null) {
1832
- env.SIDEBOARD_PORT = String(primary);
1833
- env.CONDUCTOR_PORT = String(primary);
1834
- env.PORT = String(primary);
1835
- for (let i = 1; i < ports.length; i++) {
1836
- const p = ports[i];
1837
- env[`SIDEBOARD_PORT_${i}`] = String(p);
1838
- env[`CONDUCTOR_PORT_${i}`] = String(p);
1839
- }
1826
+ function processGroupAlive(pgid) {
1827
+ if (process.platform === "win32") return false;
1828
+ if (!Number.isInteger(pgid) || pgid <= 1) return false;
1829
+ try {
1830
+ process.kill(-pgid, 0);
1831
+ return true;
1832
+ } catch (err) {
1833
+ return err && typeof err === "object" && "code" in err && err.code === "EPERM";
1840
1834
  }
1841
- applyWorktreePkgCacheEnv(env, opts.worktreePath);
1842
- return env;
1843
- }
1844
- function pipeLines(stream, onLine) {
1845
- if (!stream || !onLine) return;
1846
- const rl = createInterface2({ input: stream });
1847
- rl.on("line", onLine);
1848
1835
  }
1849
- function killableListenerPids(raw, selfPid = process.pid) {
1850
- const pids = [];
1851
- for (const token of raw.trim().split(/\s+/)) {
1852
- const pid = Number(token);
1853
- if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
1854
- pids.push(pid);
1855
- }
1856
- return pids;
1836
+ function jobTreeAlive(pid) {
1837
+ if (pid == null) return false;
1838
+ return jobAlive(pid) || processGroupAlive(pid);
1857
1839
  }
1858
- function listUnixListenerPids(port, selfPid) {
1859
- try {
1860
- const raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
1861
- encoding: "utf8",
1862
- stdio: ["ignore", "pipe", "ignore"],
1863
- timeout: 2e3
1864
- });
1865
- return killableListenerPids(raw, selfPid);
1866
- } catch {
1867
- return [];
1868
- }
1840
+ function readIntFile(file) {
1841
+ if (!existsSync3(file)) return null;
1842
+ const n = Number.parseInt(readFileSync3(file, "utf8").trim(), 10);
1843
+ return Number.isInteger(n) ? n : null;
1869
1844
  }
1870
- function listenerCwd(pid) {
1871
- if (!Number.isFinite(pid) || pid <= 0) return null;
1872
- try {
1873
- const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
1874
- encoding: "utf8",
1875
- stdio: ["ignore", "pipe", "ignore"],
1876
- timeout: 2e3
1877
- });
1878
- for (const line of raw.split("\n")) {
1879
- if (line.startsWith("n") && line.length > 1) return line.slice(1);
1880
- }
1881
- } catch {
1882
- }
1883
- return null;
1845
+ function jobDir(root, id, legacy = false) {
1846
+ return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
1884
1847
  }
1885
- function realpathOrResolve(path) {
1886
- try {
1887
- return realpathSync(path);
1888
- } catch {
1889
- return resolve(path);
1890
- }
1848
+ function resolveJobDir(root, id) {
1849
+ const modern = jobDir(root, id, false);
1850
+ if (existsSync3(modern)) return modern;
1851
+ const legacy = jobDir(root, id, true);
1852
+ if (existsSync3(legacy)) return legacy;
1853
+ return modern;
1891
1854
  }
1892
- function isOtherWorktreeListenerCwd(cwd, worktreePath) {
1893
- const wt = realpathOrResolve(worktreePath);
1894
- const c = realpathOrResolve(cwd);
1895
- if (c === wt || c.startsWith(wt + sep)) return false;
1896
- const parent = dirname3(wt);
1897
- const thisName = basename(wt);
1898
- if (!thisName || c === parent) return false;
1899
- if (!c.startsWith(parent + sep)) return false;
1900
- const first = c.slice(parent.length + 1).split(sep)[0];
1901
- return Boolean(first) && first !== thisName;
1855
+ function listJobIdsIn(root, rel) {
1856
+ const dir = join5(root, rel);
1857
+ if (!existsSync3(dir)) return [];
1858
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
1902
1859
  }
1903
- function signalPid(pid, signal) {
1904
- try {
1905
- process.kill(pid, signal);
1906
- } catch {
1860
+ function listRunningDetachedJobs(worktreePath) {
1861
+ const root = worktreePath.trim();
1862
+ if (!root) return [];
1863
+ const ids = /* @__PURE__ */ new Set([
1864
+ ...listJobIdsIn(root, DETACHED_JOBS_DIR),
1865
+ ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
1866
+ ]);
1867
+ const running = [];
1868
+ for (const id of ids) {
1869
+ const dir = resolveJobDir(root, id);
1870
+ const pid = readIntFile(join5(dir, "pid"));
1871
+ if (jobTreeAlive(pid)) running.push(id);
1907
1872
  }
1873
+ return running.sort();
1908
1874
  }
1909
- function killListenersOnPorts(ports) {
1910
- const selfPid = process.pid;
1911
- for (const port of ports) {
1912
- if (!Number.isFinite(port) || port <= 0) continue;
1913
- try {
1914
- if (process.platform === "win32") {
1915
- execFileSync(
1916
- "powershell",
1917
- [
1918
- "-NoProfile",
1919
- "-Command",
1920
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
1921
- ],
1922
- { stdio: "ignore", timeout: 3e3 }
1923
- );
1924
- continue;
1925
- }
1926
- for (const pid of listUnixListenerPids(port, selfPid)) {
1927
- signalPid(pid, "SIGTERM");
1928
- }
1929
- } catch {
1930
- }
1931
- }
1875
+ function looksLikeDeferredDonePromise(text) {
1876
+ const t = (text ?? "").trim();
1877
+ if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
1878
+ const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
1879
+ if (!unquoted) return false;
1880
+ return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted);
1932
1881
  }
1933
- function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
1934
- if (!worktreePath.trim()) {
1935
- killListenersOnPorts(ports);
1936
- return;
1937
- }
1938
- if (process.platform === "win32") {
1939
- killListenersOnPorts(ports);
1940
- return;
1941
- }
1942
- const selfPid = process.pid;
1943
- for (const port of ports) {
1944
- if (!Number.isFinite(port) || port <= 0) continue;
1945
- for (const pid of listUnixListenerPids(port, selfPid)) {
1946
- const cwd = listenerCwd(pid);
1947
- if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
1948
- signalPid(pid, signal);
1949
- }
1950
- }
1882
+ function isJobContinuePrompt(text) {
1883
+ if (typeof text !== "string") return false;
1884
+ const t = text.trim();
1885
+ return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
1951
1886
  }
1952
- function killScriptTree(child, ports = []) {
1953
- const pid = child.pid;
1954
- if (pid) {
1955
- try {
1956
- if (process.platform === "win32") {
1957
- void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
1958
- } else {
1959
- process.kill(-pid, "SIGTERM");
1960
- setTimeout(() => {
1961
- try {
1962
- process.kill(-pid, "SIGKILL");
1963
- } catch {
1964
- }
1965
- }, 2500).unref?.();
1966
- }
1967
- } catch {
1968
- try {
1969
- child.kill("SIGTERM");
1970
- } catch {
1971
- }
1972
- }
1973
- }
1974
- killListenersOnPorts(ports);
1887
+ function collapseGhRunWatchLog(text) {
1888
+ if (!text.includes(GH_WATCH_REFRESH)) return text;
1889
+ const frames = text.split(/(?=Refreshing run status every 3 seconds\.)/);
1890
+ const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
1891
+ if (watchFrames.length <= 1) return text;
1892
+ const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
1893
+ const last = watchFrames[watchFrames.length - 1] ?? "";
1894
+ const omitted = watchFrames.length - 1;
1895
+ const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
1896
+ `;
1897
+ return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
1975
1898
  }
1976
- async function spawnWorkspaceScript(command, opts) {
1977
- const loginEnv = await captureLoginEnv();
1978
- const env = buildWorkspaceScriptEnv(
1979
- {
1980
- worktreePath: opts.worktreePath,
1981
- repoPath: opts.repoPath,
1982
- workspaceName: opts.workspaceName,
1983
- defaultBranch: opts.defaultBranch,
1984
- ports: opts.ports
1985
- },
1986
- loginEnv
1987
- );
1988
- try {
1989
- mergeAgentGitAuthEnv(
1990
- env,
1991
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
1992
- );
1993
- } catch {
1994
- }
1995
- if (opts.heldPorts?.length) {
1996
- await Promise.all(opts.heldPorts.map((h) => h.release()));
1899
+ function capJobLogDelta(text) {
1900
+ const collapsed = collapseGhRunWatchLog(text);
1901
+ const lines = collapsed.split("\n");
1902
+ let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
1903
+ ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
1904
+ if (out.length > MAX_JOB_DELTA_CHARS) {
1905
+ out = `\u2026(truncated ${out.length} chars)
1906
+ ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
1997
1907
  }
1998
- const shell = process.platform === "darwin" ? "zsh" : "bash";
1999
- const child = execa2(shell, ["-lc", command], {
2000
- cwd: opts.worktreePath,
2001
- reject: false,
2002
- env,
2003
- // Own process group so Stop can tear down the whole tree (not just the shell).
2004
- // There is no settings.toml `stop=` / teardown hook for run scripts.
2005
- ...process.platform === "win32" ? {} : { detached: true }
2006
- });
2007
- pipeLines(child.stdout, opts.onLine);
2008
- pipeLines(child.stderr, opts.onLine);
2009
- const ports = opts.ports ?? [];
2010
- return {
2011
- pid: child.pid,
2012
- kill: () => killScriptTree(child, ports),
2013
- done: child.then((r) => r.exitCode ?? null),
2014
- child
2015
- };
1908
+ return out;
2016
1909
  }
2017
- async function attachAbort(handle, signal) {
2018
- if (signal) {
2019
- const onAbort = () => handle.kill();
2020
- if (signal.aborted) onAbort();
2021
- else signal.addEventListener("abort", onAbort, { once: true });
2022
- }
2023
- return handle.done;
1910
+ function formatJobStillRunningContinuePrompt(jobIds) {
1911
+ const ids = jobIds.join(", ");
1912
+ return [
1913
+ `Detached job still running: ${ids}.`,
1914
+ "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
1915
+ "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
1916
+ "Then report the result. Do not tell the user you will let them know later."
1917
+ ].join(" ");
2024
1918
  }
2025
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
2026
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
2027
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
2028
- const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
2029
- onLine?.(`[setup] ${source ?? "settings.toml"}`);
2030
- const handle = await spawnWorkspaceScript(settings.setup, {
2031
- worktreePath,
2032
- repoPath,
2033
- defaultBranch: opts?.defaultBranch,
2034
- onLine
2035
- });
2036
- const exitCode = await attachAbort(handle, opts?.signal);
2037
- return {
2038
- ran: true,
2039
- exitCode,
2040
- source,
2041
- kill: handle.kill
2042
- };
1919
+ function formatDeferredDoneContinuePrompt() {
1920
+ return [
1921
+ "You ended the turn after promising to report later, but no detached job is running.",
1922
+ "If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
1923
+ "Do not say you will let the user know later."
1924
+ ].join(" ");
2043
1925
  }
2044
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
2045
- const found = findConventionSetup(worktreePath, repoPath);
2046
- if (!found) return { ran: false, exitCode: null, source: null };
2047
- onLine?.(`[setup] ${found.source}`);
2048
- const handle = await spawnWorkspaceScript(found.command, {
2049
- worktreePath,
2050
- repoPath,
2051
- defaultBranch: opts?.defaultBranch,
2052
- onLine
1926
+ function turnWatchedDetachedJob(parts) {
1927
+ return parts.some((p) => {
1928
+ if (p.type !== "tool") return false;
1929
+ if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
1930
+ const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
1931
+ return /detached-job\.(?:js|cjs)\b/i.test(blob);
2053
1932
  });
2054
- const exitCode = await attachAbort(handle, opts?.signal);
2055
- return {
2056
- ran: true,
2057
- exitCode,
2058
- source: found.source,
2059
- kill: handle.kill
2060
- };
2061
1933
  }
2062
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
2063
- await ensureWorktreeSideboardIgnored(worktreePath);
2064
- const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
2065
- if (review.wrote) {
2066
- onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
2067
- }
2068
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
2069
- if (!setup.ran) {
2070
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
1934
+ function planJobContinue(opts) {
1935
+ if (opts.isOrchestrator) return { action: "none" };
1936
+ if (opts.agent === "brightsy") return { action: "none" };
1937
+ if (opts.queueLength > 0) return { action: "none" };
1938
+ if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
1939
+ const farewell = looksLikeDeferredDonePromise(opts.chatText);
1940
+ if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
1941
+ return {
1942
+ action: "wait",
1943
+ jobIds: opts.runningJobIds,
1944
+ prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
1945
+ };
2071
1946
  }
2072
- if (!setup.ran) {
2073
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
1947
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
1948
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
2074
1949
  }
2075
- return setup;
2076
- }
2077
- async function runArchiveScript(repoPath, worktreePath, onLine) {
2078
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
2079
- if (!settings?.archive) return { ran: false, exitCode: null };
2080
- const handle = await spawnWorkspaceScript(settings.archive, {
2081
- worktreePath,
2082
- repoPath,
2083
- onLine
2084
- });
2085
- const exitCode = await handle.done;
2086
- return { ran: true, exitCode };
1950
+ return { action: "none" };
2087
1951
  }
2088
- function listRunScripts(worktreePath, repoPath) {
2089
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
2090
- if (!settings?.runScripts.length) return [];
2091
- return settings.runScripts.filter((s) => {
2092
- if (!s.availableIn?.length) return true;
2093
- return s.availableIn.includes("local");
2094
- });
1952
+ function tailProgress(logFile, maxLines = 12) {
1953
+ if (!existsSync3(logFile)) return "(no log yet)";
1954
+ const lines = readFileSync3(logFile, "utf8").split("\n");
1955
+ return lines.slice(-maxLines).join("\n");
2095
1956
  }
2096
- function getDefaultRunScript(worktreePath, repoPath) {
2097
- const scripts = listRunScripts(worktreePath, repoPath);
2098
- if (!scripts.length) return null;
2099
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
1957
+ function lastProgressLine(progress) {
1958
+ const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
1959
+ return last ? last.slice(0, 72) : void 0;
2100
1960
  }
2101
- function getRunScript(worktreePath, repoPath, name) {
2102
- const scripts = listRunScripts(worktreePath, repoPath);
2103
- if (!scripts.length) return null;
2104
- if (name) {
2105
- return scripts.find((s) => s.name === name) ?? null;
2106
- }
2107
- return getDefaultRunScript(worktreePath, repoPath);
1961
+ function readLogLines(file) {
1962
+ if (!existsSync3(file)) return [];
1963
+ const lines = readFileSync3(file, "utf8").split("\n");
1964
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
1965
+ return lines;
2108
1966
  }
2109
- function getRunMode(worktreePath, repoPath) {
2110
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
1967
+ function takeDelta(logFile, cursorFile) {
1968
+ const lines = readLogLines(logFile);
1969
+ const cursor = readIntFile(cursorFile) ?? 0;
1970
+ const start = Math.min(Math.max(0, cursor), lines.length);
1971
+ return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
2111
1972
  }
2112
- async function reservePort() {
2113
- return new Promise((resolve2, reject) => {
2114
- const server = createServer();
2115
- let released = false;
2116
- const release = () => new Promise((res) => {
2117
- if (released) {
2118
- res();
2119
- return;
2120
- }
2121
- released = true;
2122
- server.close(() => res());
2123
- });
2124
- server.listen(0, "127.0.0.1", () => {
2125
- const addr = server.address();
2126
- if (!addr || typeof addr === "string") {
2127
- void release().then(() => reject(new Error("Failed to allocate port")));
2128
- return;
2129
- }
2130
- resolve2({ port: addr.port, release });
2131
- });
2132
- server.on("error", reject);
2133
- });
1973
+ function snapshotJob(dir) {
1974
+ const pid = readIntFile(join5(dir, "pid"));
1975
+ const running = jobTreeAlive(pid);
1976
+ return {
1977
+ pid,
1978
+ running,
1979
+ exitCode: readIntFile(join5(dir, "exit")),
1980
+ log: join5(dir, "log"),
1981
+ cursor: join5(dir, "present.cursor"),
1982
+ progress: tailProgress(join5(dir, "log"))
1983
+ };
2134
1984
  }
2135
- async function allocatePort() {
2136
- const held = await reservePort();
2137
- await held.release();
2138
- return held.port;
1985
+ function toResult(id, snap, extra) {
1986
+ const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
1987
+ const ok = !snap.running && snap.exitCode === 0;
1988
+ const stillRunning = snap.running && !ok;
1989
+ const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
1990
+ try {
1991
+ mkdirSync4(dirname3(snap.cursor), { recursive: true });
1992
+ writeFileSync3(snap.cursor, `${nextCursor}
1993
+ `);
1994
+ } catch {
1995
+ }
1996
+ const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
1997
+ const progress = extra?.progress ?? snap.progress;
1998
+ return {
1999
+ stillRunning,
2000
+ ok,
2001
+ failed: Boolean(failed && !stillRunning && !ok),
2002
+ status,
2003
+ id,
2004
+ pid: snap.pid,
2005
+ exitCode: snap.exitCode ?? void 0,
2006
+ phase: lastProgressLine(progress),
2007
+ delta,
2008
+ progress,
2009
+ hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
2010
+ };
2139
2011
  }
2140
- async function tryReservePort(port) {
2141
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
2142
- return new Promise((resolve2) => {
2143
- const server = createServer();
2144
- let released = false;
2145
- const release = () => new Promise((res) => {
2146
- if (released) {
2147
- res();
2148
- return;
2149
- }
2150
- released = true;
2151
- server.close(() => res());
2152
- });
2153
- server.once("error", () => {
2154
- try {
2155
- server.close();
2156
- } catch {
2157
- }
2158
- resolve2(null);
2159
- });
2160
- server.listen(port, "127.0.0.1", () => {
2161
- resolve2({ port, release });
2012
+ async function sleepMs(ms) {
2013
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
2014
+ }
2015
+ async function waitForDetachedJob(cwd, id, opts) {
2016
+ const jobId = sanitizeDetachedJobId(id);
2017
+ const root = cwd.trim() || process.cwd();
2018
+ const dir = resolveJobDir(root, jobId);
2019
+ const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
2020
+ const sleep = opts?.sleep ?? sleepMs;
2021
+ if (!existsSync3(dir)) {
2022
+ return {
2023
+ stillRunning: false,
2024
+ ok: false,
2025
+ failed: true,
2026
+ status: "failed",
2027
+ id: jobId,
2028
+ delta: "",
2029
+ progress: "No detached job. Start one first.",
2030
+ hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
2031
+ };
2032
+ }
2033
+ const deadline = Date.now() + timeoutMs;
2034
+ let snap = snapshotJob(dir);
2035
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
2036
+ if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
2037
+ return toResult(jobId, snap, {
2038
+ failed: true,
2039
+ progress: "No detached job. Start one first."
2162
2040
  });
2163
- });
2041
+ }
2042
+ while (Date.now() < deadline) {
2043
+ snap = snapshotJob(dir);
2044
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
2045
+ if (!snap.running) return toResult(jobId, snap, { failed: true });
2046
+ await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
2047
+ }
2048
+ snap = snapshotJob(dir);
2049
+ return toResult(jobId, snap);
2164
2050
  }
2165
- async function tryReservePorts(ports) {
2166
- const held = [];
2167
- for (const port of ports) {
2168
- const next = await tryReservePort(port);
2169
- if (!next) {
2170
- await Promise.all(held.map((h) => h.release()));
2171
- return null;
2051
+ function killProcessTree(pid, signal) {
2052
+ if (!Number.isInteger(pid) || pid <= 0) return;
2053
+ if (process.platform !== "win32") {
2054
+ try {
2055
+ process.kill(-pid, signal);
2056
+ return;
2057
+ } catch {
2172
2058
  }
2173
- held.push(next);
2174
2059
  }
2175
- return held;
2176
- }
2177
- function normalizePreferredPorts(ports, size) {
2178
- if (!ports?.length) return [];
2179
- const seen = /* @__PURE__ */ new Set();
2180
- const out = [];
2181
- for (const p of ports) {
2182
- if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
2183
- seen.add(p);
2184
- out.push(p);
2185
- if (out.length >= size) break;
2060
+ try {
2061
+ process.kill(pid, signal);
2062
+ } catch {
2186
2063
  }
2187
- return out;
2188
2064
  }
2189
- function delay(ms) {
2190
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2191
- }
2192
- async function fillPortRange(held, size) {
2193
- if (held.length >= size) return held;
2194
- const base = held[0]?.port;
2195
- const taken = new Set(held.map((h) => h.port));
2065
+ function appendJobLog(logFile, line) {
2196
2066
  try {
2197
- let offset = 0;
2198
- while (held.length < size) {
2199
- const candidate = base != null ? base + offset : null;
2200
- offset += 1;
2201
- if (candidate != null && taken.has(candidate)) continue;
2202
- const next = candidate != null ? await tryReservePort(candidate) : null;
2203
- if (next) {
2204
- held.push(next);
2205
- taken.add(next.port);
2206
- } else {
2207
- const ephemeral = await reservePort();
2208
- held.push(ephemeral);
2209
- taken.add(ephemeral.port);
2210
- }
2211
- }
2212
- return held;
2213
- } catch (err) {
2214
- await Promise.all(held.map((h) => h.release()));
2215
- throw err;
2067
+ mkdirSync4(dirname3(logFile), { recursive: true });
2068
+ const body = line.endsWith("\n") ? line : `${line}
2069
+ `;
2070
+ appendFileSync2(logFile, body);
2071
+ } catch {
2216
2072
  }
2217
2073
  }
2218
- async function tryReserveAvailablePorts(ports) {
2219
- const held = [];
2220
- for (const port of ports) {
2221
- const next = await tryReservePort(port);
2222
- if (next) held.push(next);
2074
+ async function stopDetachedJob(cwd, id, opts) {
2075
+ const jobId = sanitizeDetachedJobId(id);
2076
+ const root = cwd.trim() || process.cwd();
2077
+ const dir = resolveJobDir(root, jobId);
2078
+ const sleep = opts?.sleep ?? sleepMs;
2079
+ const why = opts?.reason?.trim() ?? "";
2080
+ if (!existsSync3(dir)) {
2081
+ return {
2082
+ stillRunning: false,
2083
+ ok: false,
2084
+ failed: true,
2085
+ status: "failed",
2086
+ id: jobId,
2087
+ delta: "",
2088
+ progress: "No detached job. Start one first.",
2089
+ hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
2090
+ stopped: false,
2091
+ reason: "not-found"
2092
+ };
2223
2093
  }
2224
- return held;
2225
- }
2226
- async function reclaimPreferredPorts(ports, worktreePath) {
2227
- const primary = ports[0];
2228
- if (primary == null) return null;
2229
- const takePrimary = async () => {
2230
- const held2 = await tryReserveAvailablePorts(ports);
2231
- if (held2.some((h) => h.port === primary)) return held2;
2232
- await Promise.all(held2.map((h) => h.release()));
2233
- return null;
2234
- };
2235
- let held = await takePrimary();
2236
- if (held) return held;
2237
- if (worktreePath) {
2238
- killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
2239
- } else {
2240
- killListenersOnPorts(ports);
2094
+ let snap = snapshotJob(dir);
2095
+ if (!snap.running) {
2096
+ const result2 = toResult(jobId, snap);
2097
+ return {
2098
+ ...result2,
2099
+ stopped: false,
2100
+ reason: "not-running",
2101
+ hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
2102
+ };
2241
2103
  }
2242
- for (let i = 0; i < RECLAIM_TRIES; i++) {
2243
- await delay(RECLAIM_RETRY_DELAY_MS);
2244
- held = await takePrimary();
2245
- if (held) return held;
2246
- if (i === 2 && worktreePath) {
2247
- killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
2248
- }
2104
+ appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
2105
+ const targetPid = snap.pid;
2106
+ if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
2107
+ const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
2108
+ const deadline = Date.now() + graceMs;
2109
+ while (Date.now() < deadline && jobTreeAlive(targetPid)) {
2110
+ await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
2249
2111
  }
2250
- return null;
2251
- }
2252
- async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
2253
- const preferred = normalizePreferredPorts(opts?.preferred, size);
2254
- if (preferred.length) {
2255
- const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
2256
- if (reused) return fillPortRange(reused, size);
2112
+ if (targetPid != null) {
2113
+ killProcessTree(targetPid, "SIGKILL");
2114
+ await sleep(100);
2257
2115
  }
2258
- const held = [await reservePort()];
2259
- return fillPortRange(held, size);
2260
- }
2261
- async function allocatePortRange(size = PORT_RANGE_SIZE) {
2262
- const held = await reservePortRange(size);
2263
- const ports = held.map((h) => h.port);
2264
- await Promise.all(held.map((h) => h.release()));
2265
- return ports;
2266
- }
2267
- async function startDevServer(repoPath, worktreePath, onLine, opts) {
2268
- const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
2269
- if (!script) return null;
2270
- const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
2271
- const held = await reservePortRange(PORT_RANGE_SIZE, {
2272
- preferred: preferred ?? void 0,
2273
- worktreePath
2274
- });
2275
- const ports = held.map((h) => h.port);
2276
- saveWorktreeRunPorts(worktreePath, script.name, ports);
2277
- try {
2278
- const handle = await spawnWorkspaceScript(script.command, {
2279
- worktreePath,
2280
- repoPath,
2281
- ports,
2282
- heldPorts: held,
2283
- defaultBranch: opts?.defaultBranch,
2284
- onLine
2285
- });
2286
- return {
2287
- pid: handle.pid,
2288
- port: ports[0],
2289
- ports,
2290
- scriptName: script.name,
2291
- kill: handle.kill,
2292
- done: handle.done
2293
- };
2294
- } catch (err) {
2295
- await Promise.all(held.map((h) => h.release()));
2296
- throw err;
2297
- }
2298
- }
2299
-
2300
- // src/store/setup-log.ts
2301
- import { createHash } from "crypto";
2302
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2303
- import { join as join6 } from "path";
2304
- var EMPTY = {
2305
- output: "",
2306
- running: false,
2307
- exitCode: null,
2308
- source: null
2309
- };
2310
- var MAX_SETUP_LOG_CHARS = 256e3;
2311
- function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
2312
- const next = prev ? `${prev}
2313
- ${line}` : line;
2314
- if (next.length <= max) return next;
2315
- const cut = next.slice(next.length - max);
2316
- const nl = cut.indexOf("\n");
2317
- return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
2318
- }
2319
- var memory = /* @__PURE__ */ new Map();
2320
- var persistTimers = /* @__PURE__ */ new Map();
2321
- function fromSnapshot(snap) {
2322
- return {
2323
- chunks: snap.output ? [snap.output] : [],
2324
- length: snap.output.length,
2325
- running: snap.running,
2326
- exitCode: snap.exitCode,
2327
- source: snap.source
2328
- };
2329
- }
2330
- function materialize(mem) {
2331
- let output = mem.chunks.join("\n");
2332
- if (output.length > MAX_SETUP_LOG_CHARS) {
2333
- output = appendSetupOutput("", output);
2334
- mem.chunks = [output];
2335
- mem.length = output.length;
2116
+ const exitFile = join5(dir, "exit");
2117
+ if (!existsSync3(exitFile)) {
2118
+ try {
2119
+ writeFileSync3(exitFile, "1\n");
2120
+ } catch {
2121
+ }
2336
2122
  }
2123
+ snap = snapshotJob(dir);
2124
+ const stillRunning = jobTreeAlive(targetPid) || snap.running;
2125
+ const result = toResult(jobId, snap, { failed: true });
2126
+ const stopped = !stillRunning;
2337
2127
  return {
2338
- output,
2339
- running: mem.running,
2340
- exitCode: mem.exitCode,
2341
- source: mem.source
2128
+ ...result,
2129
+ stillRunning,
2130
+ ok: false,
2131
+ failed: true,
2132
+ status: stillRunning ? "running" : "failed",
2133
+ stopped,
2134
+ reason: stopped ? "stopped" : "still-running",
2135
+ stopReason: why || void 0,
2136
+ hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
2342
2137
  };
2343
2138
  }
2344
- function setupLogPath(threadId) {
2345
- return join6(threadsDir(), `${threadId}.setup.log.json`);
2346
- }
2347
- function setupLogKeyForWorktree(worktreePath) {
2348
- const norm = normalizeWorktreePath(worktreePath);
2349
- return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
2350
- }
2351
- function emptySetupLog() {
2352
- return { ...EMPTY };
2139
+
2140
+ // src/hook/conductor.ts
2141
+ var PORT_RANGE_SIZE = 10;
2142
+ var RECLAIM_RETRY_DELAY_MS = 40;
2143
+ var RECLAIM_TRIES = 8;
2144
+ function matchSimpleGlob(pattern, name) {
2145
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
2146
+ return new RegExp(`^${escaped}$`).test(name);
2353
2147
  }
2354
- function mergeSetupOutput(prev, incoming) {
2355
- if (!prev) return incoming;
2356
- if (!incoming) return prev;
2357
- if (prev === incoming) return prev;
2358
- if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
2359
- if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
2360
- return incoming.length >= prev.length ? incoming : prev;
2148
+ function readWorktreeInclude(repoPath) {
2149
+ const path = join6(repoPath, ".worktreeinclude");
2150
+ if (!existsSync4(path)) return [];
2151
+ return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
2361
2152
  }
2362
- var PERSIST_DEBOUNCE_MS = 500;
2363
- function persistNow(threadId) {
2364
- const timer = persistTimers.get(threadId);
2365
- if (timer) {
2366
- clearTimeout(timer);
2367
- persistTimers.delete(threadId);
2153
+ function resolveFilesToCopy(repoPath) {
2154
+ const fromInclude = readWorktreeInclude(repoPath);
2155
+ if (fromInclude.length) return fromInclude;
2156
+ const settings = loadRepoSettings(repoPath);
2157
+ if (settings?.filesToCopy?.length) return settings.filesToCopy;
2158
+ if (settings?.fileIncludeGlobs?.length) {
2159
+ const matched = [];
2160
+ try {
2161
+ for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
2162
+ if (!entry.isFile()) continue;
2163
+ for (const glob of settings.fileIncludeGlobs) {
2164
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
2165
+ matched.push(entry.name);
2166
+ break;
2167
+ }
2168
+ }
2169
+ }
2170
+ } catch {
2171
+ }
2172
+ if (matched.length) return [...new Set(matched)];
2368
2173
  }
2369
- const mem = memory.get(threadId);
2370
- if (!mem) return;
2174
+ const defaults = [];
2371
2175
  try {
2372
- writeFileSync3(
2373
- setupLogPath(threadId),
2374
- `${JSON.stringify(materialize(mem))}
2375
- `,
2376
- "utf8"
2377
- );
2176
+ for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
2177
+ if (entry.isFile() && entry.name.startsWith(".env")) {
2178
+ defaults.push(entry.name);
2179
+ }
2180
+ }
2378
2181
  } catch {
2379
2182
  }
2183
+ if (defaults.length) return defaults;
2184
+ return [".env.local", ".env"];
2380
2185
  }
2381
- function schedulePersist(threadId) {
2382
- if (persistTimers.has(threadId)) return;
2383
- persistTimers.set(
2384
- threadId,
2385
- setTimeout(() => {
2386
- persistTimers.delete(threadId);
2387
- persistNow(threadId);
2388
- }, PERSIST_DEBOUNCE_MS)
2389
- );
2186
+ function copyConfiguredFiles(repoPath, worktreePath) {
2187
+ const patterns = resolveFilesToCopy(repoPath);
2188
+ const copied = [];
2189
+ for (const rel of patterns) {
2190
+ const src = join6(repoPath, rel);
2191
+ if (!existsSync4(src)) continue;
2192
+ const dest = join6(worktreePath, rel);
2193
+ mkdirSync5(dirname4(dest), { recursive: true });
2194
+ copyFileSync(src, dest);
2195
+ copied.push(rel);
2196
+ }
2197
+ return copied;
2390
2198
  }
2391
- function loadFromDisk(threadId) {
2392
- const path = setupLogPath(threadId);
2393
- if (!existsSync4(path)) return null;
2199
+ var cachedLoginEnv = null;
2200
+ async function captureLoginEnv() {
2201
+ if (cachedLoginEnv) return { ...cachedLoginEnv };
2202
+ const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
2394
2203
  try {
2395
- const raw = JSON.parse(readFileSync4(path, "utf8"));
2396
- return {
2397
- output: typeof raw.output === "string" ? raw.output : "",
2398
- running: Boolean(raw.running),
2399
- exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
2400
- source: typeof raw.source === "string" ? raw.source : null
2401
- };
2204
+ const result = await execa2(shell, ["-l", "-c", "env -0"], {
2205
+ reject: false,
2206
+ timeout: 5e3
2207
+ });
2208
+ if (result.exitCode === 0 && result.stdout) {
2209
+ const env = { ...process.env };
2210
+ for (const entry of result.stdout.split("\0")) {
2211
+ if (!entry) continue;
2212
+ const eq = entry.indexOf("=");
2213
+ if (eq <= 0) continue;
2214
+ const key = entry.slice(0, eq);
2215
+ const value = entry.slice(eq + 1);
2216
+ if (key) env[key] = value;
2217
+ }
2218
+ cachedLoginEnv = env;
2219
+ return { ...env };
2220
+ }
2402
2221
  } catch {
2403
- return null;
2404
2222
  }
2223
+ cachedLoginEnv = { ...process.env };
2224
+ return { ...cachedLoginEnv };
2405
2225
  }
2406
- function memFor(threadId) {
2407
- const cached = memory.get(threadId);
2408
- if (cached) return cached;
2409
- const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
2410
- memory.set(threadId, mem);
2411
- return mem;
2412
- }
2413
- function readSetupLog(threadId) {
2414
- const cached = memory.get(threadId);
2415
- if (cached) return materialize(cached);
2416
- const disk = loadFromDisk(threadId);
2417
- if (disk) {
2418
- memory.set(threadId, fromSnapshot(disk));
2419
- return disk;
2226
+ function buildWorkspaceScriptEnv(opts, baseEnv) {
2227
+ const env = stripNestedElectronEnv({
2228
+ ...baseEnv ?? process.env
2229
+ });
2230
+ const name = opts.workspaceName ?? basename(opts.worktreePath);
2231
+ const ports = opts.ports ?? [];
2232
+ const primary = ports[0];
2233
+ env.SIDEBOARD_WORKSPACE_NAME = name;
2234
+ env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
2235
+ env.SIDEBOARD_ROOT_PATH = opts.repoPath;
2236
+ env.SIDEBOARD_IS_LOCAL = "1";
2237
+ if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
2238
+ env.CONDUCTOR_WORKSPACE_NAME = name;
2239
+ env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
2240
+ env.CONDUCTOR_ROOT_PATH = opts.repoPath;
2241
+ env.CONDUCTOR_IS_LOCAL = "1";
2242
+ if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
2243
+ if (primary != null) {
2244
+ env.SIDEBOARD_PORT = String(primary);
2245
+ env.CONDUCTOR_PORT = String(primary);
2246
+ env.PORT = String(primary);
2247
+ for (let i = 1; i < ports.length; i++) {
2248
+ const p = ports[i];
2249
+ env[`SIDEBOARD_PORT_${i}`] = String(p);
2250
+ env[`CONDUCTOR_PORT_${i}`] = String(p);
2251
+ }
2420
2252
  }
2421
- return emptySetupLog();
2253
+ applyWorktreePkgCacheEnv(env, opts.worktreePath);
2254
+ return env;
2422
2255
  }
2423
- function beginSetupLog(threadId) {
2424
- const mem = {
2425
- chunks: [],
2426
- length: 0,
2427
- running: true,
2428
- exitCode: null,
2429
- source: null
2430
- };
2431
- memory.set(threadId, mem);
2432
- persistNow(threadId);
2433
- return materialize(mem);
2434
- }
2435
- function appendSetupLog(threadId, line) {
2436
- const mem = memFor(threadId);
2437
- mem.chunks.push(line);
2438
- mem.length += line.length + 1;
2439
- mem.running = true;
2440
- if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
2441
- schedulePersist(threadId);
2442
- }
2443
- function finishSetupLog(threadId, exitCode, source) {
2444
- const mem = memFor(threadId);
2445
- mem.running = false;
2446
- mem.exitCode = exitCode;
2447
- mem.source = source ?? mem.source;
2448
- persistNow(threadId);
2449
- return materialize(mem);
2450
- }
2451
-
2452
- // src/store/run-log.ts
2453
- import { createHash as createHash2 } from "crypto";
2454
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2455
- import { join as join7 } from "path";
2456
- var EMPTY2 = {
2457
- output: "",
2458
- running: false,
2459
- exitCode: null,
2460
- scriptName: null
2461
- };
2462
- var MAX_RUN_LOG_CHARS = 256e3;
2463
- var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
2464
- var memory2 = /* @__PURE__ */ new Map();
2465
- var persistTimers2 = /* @__PURE__ */ new Map();
2466
- function fromSnapshot2(snap) {
2467
- return {
2468
- chunks: snap.output ? [snap.output] : [],
2469
- length: snap.output.length,
2470
- running: snap.running,
2471
- exitCode: snap.exitCode,
2472
- scriptName: snap.scriptName
2473
- };
2256
+ function pipeLines(stream, onLine) {
2257
+ if (!stream || !onLine) return;
2258
+ const rl = createInterface2({ input: stream });
2259
+ rl.on("line", onLine);
2474
2260
  }
2475
- function materialize2(mem) {
2476
- let output = mem.chunks.join("\n");
2477
- if (output.length > MAX_RUN_LOG_CHARS) {
2478
- output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
2479
- mem.chunks = [output];
2480
- mem.length = output.length;
2261
+ function killableListenerPids(raw, selfPid = process.pid) {
2262
+ const pids = [];
2263
+ for (const token of raw.trim().split(/\s+/)) {
2264
+ const pid = Number(token);
2265
+ if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
2266
+ pids.push(pid);
2481
2267
  }
2482
- return {
2483
- output,
2484
- running: mem.running,
2485
- exitCode: mem.exitCode,
2486
- scriptName: mem.scriptName
2487
- };
2488
- }
2489
- function safeScriptToken(scriptName) {
2490
- const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
2491
- return token || "dev";
2492
- }
2493
- function runLogPath(key) {
2494
- return join7(threadsDir(), `${key}.run.log.json`);
2495
- }
2496
- function runLogKeyForWorktree(worktreePath, scriptName) {
2497
- const norm = normalizeWorktreePath(worktreePath);
2498
- const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
2499
- return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
2500
- }
2501
- function emptyRunLog() {
2502
- return { ...EMPTY2 };
2503
- }
2504
- function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
2505
- const max = Math.max(1, tailChars);
2506
- const outputChars = output.length;
2507
- if (outputChars <= max) return { output, truncated: false, outputChars };
2508
- const cut = output.slice(outputChars - max);
2509
- const nl = cut.indexOf("\n");
2510
- const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
2511
- return { output: text, truncated: true, outputChars };
2268
+ return pids;
2512
2269
  }
2513
- var PERSIST_DEBOUNCE_MS2 = 500;
2514
- function persistNow2(key) {
2515
- const timer = persistTimers2.get(key);
2516
- if (timer) {
2517
- clearTimeout(timer);
2518
- persistTimers2.delete(key);
2519
- }
2520
- const mem = memory2.get(key);
2521
- if (!mem) return;
2270
+ function listUnixListenerPids(port, selfPid) {
2522
2271
  try {
2523
- writeFileSync4(runLogPath(key), `${JSON.stringify(materialize2(mem))}
2524
- `, "utf8");
2272
+ const raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
2273
+ encoding: "utf8",
2274
+ stdio: ["ignore", "pipe", "ignore"],
2275
+ timeout: 2e3
2276
+ });
2277
+ return killableListenerPids(raw, selfPid);
2525
2278
  } catch {
2279
+ return [];
2526
2280
  }
2527
2281
  }
2528
- function schedulePersist2(key) {
2529
- if (persistTimers2.has(key)) return;
2530
- persistTimers2.set(
2531
- key,
2532
- setTimeout(() => {
2533
- persistTimers2.delete(key);
2534
- persistNow2(key);
2535
- }, PERSIST_DEBOUNCE_MS2)
2536
- );
2537
- }
2538
- function loadFromDisk2(key) {
2539
- const path = runLogPath(key);
2540
- if (!existsSync5(path)) return null;
2282
+ function listenerCwd(pid) {
2283
+ if (!Number.isFinite(pid) || pid <= 0) return null;
2541
2284
  try {
2542
- const raw = JSON.parse(readFileSync5(path, "utf8"));
2543
- return {
2544
- output: typeof raw.output === "string" ? raw.output : "",
2545
- running: Boolean(raw.running),
2546
- exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
2547
- scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
2548
- };
2285
+ const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
2286
+ encoding: "utf8",
2287
+ stdio: ["ignore", "pipe", "ignore"],
2288
+ timeout: 2e3
2289
+ });
2290
+ for (const line of raw.split("\n")) {
2291
+ if (line.startsWith("n") && line.length > 1) return line.slice(1);
2292
+ }
2549
2293
  } catch {
2550
- return null;
2551
2294
  }
2295
+ return null;
2552
2296
  }
2553
- function memFor2(key) {
2554
- const cached = memory2.get(key);
2555
- if (cached) return cached;
2556
- const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
2557
- memory2.set(key, mem);
2558
- return mem;
2559
- }
2560
- function readRunLog(key) {
2561
- const cached = memory2.get(key);
2562
- if (cached) return materialize2(cached);
2563
- const disk = loadFromDisk2(key);
2564
- if (disk) {
2565
- memory2.set(key, fromSnapshot2(disk));
2566
- return disk;
2297
+ function realpathOrResolve(path) {
2298
+ try {
2299
+ return realpathSync(path);
2300
+ } catch {
2301
+ return resolve(path);
2567
2302
  }
2568
- return emptyRunLog();
2569
- }
2570
- function beginRunLog(key, scriptName) {
2571
- const mem = {
2572
- chunks: [],
2573
- length: 0,
2574
- running: true,
2575
- exitCode: null,
2576
- scriptName
2577
- };
2578
- memory2.set(key, mem);
2579
- persistNow2(key);
2580
- return materialize2(mem);
2581
- }
2582
- function appendRunLog(key, line) {
2583
- const mem = memFor2(key);
2584
- mem.chunks.push(line);
2585
- mem.length += line.length + 1;
2586
- mem.running = true;
2587
- if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
2588
- schedulePersist2(key);
2589
2303
  }
2590
- function finishRunLog(key, exitCode) {
2591
- const mem = memFor2(key);
2592
- mem.running = false;
2593
- mem.exitCode = exitCode;
2594
- persistNow2(key);
2595
- return materialize2(mem);
2304
+ function isOtherWorktreeListenerCwd(cwd, worktreePath) {
2305
+ const wt = realpathOrResolve(worktreePath);
2306
+ const c = realpathOrResolve(cwd);
2307
+ if (c === wt || c.startsWith(wt + sep)) return false;
2308
+ const parent = dirname4(wt);
2309
+ const thisName = basename(wt);
2310
+ if (!thisName || c === parent) return false;
2311
+ if (!c.startsWith(parent + sep)) return false;
2312
+ const first = c.slice(parent.length + 1).split(sep)[0];
2313
+ return Boolean(first) && first !== thisName;
2596
2314
  }
2597
- function flushRunLog(key) {
2598
- persistNow2(key);
2315
+ function signalPid(pid, signal) {
2316
+ try {
2317
+ process.kill(pid, signal);
2318
+ } catch {
2319
+ }
2599
2320
  }
2600
-
2601
- // src/git/orphan-cleanup.ts
2602
- import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "fs";
2603
- import { join as join8 } from "path";
2604
- function isSideboardWorktreePath(path) {
2605
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
2321
+ function isThisWorktreeListenerCwd(cwd, worktreePath) {
2322
+ const wt = realpathOrResolve(worktreePath);
2323
+ const c = realpathOrResolve(cwd);
2324
+ return c === wt || c.startsWith(wt + sep);
2325
+ }
2326
+ function killDetachedRunTree(pid, worktreePath) {
2327
+ if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
2328
+ const cwd = listenerCwd(pid);
2329
+ if (cwd) {
2330
+ if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
2331
+ } else if (!processGroupAlive(pid)) {
2332
+ return;
2333
+ }
2334
+ try {
2335
+ process.kill(-pid, "SIGTERM");
2336
+ } catch {
2337
+ signalPid(pid, "SIGTERM");
2338
+ }
2606
2339
  }
2607
- async function findOrphanWorktrees(repoPaths) {
2608
- const threads = listThreads({ includeArchived: true });
2609
- const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
2610
- const repos = new Set(
2611
- repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
2612
- );
2613
- const homeRoot = sideboardWorkspacesDir();
2614
- if (existsSync6(homeRoot)) {
2340
+ function killListenersOnPorts(ports) {
2341
+ const selfPid = process.pid;
2342
+ for (const port of ports) {
2343
+ if (!Number.isFinite(port) || port <= 0) continue;
2615
2344
  try {
2616
- for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
2617
- if (!entry.isDirectory()) continue;
2618
- void entry;
2345
+ if (process.platform === "win32") {
2346
+ execFileSync(
2347
+ "powershell",
2348
+ [
2349
+ "-NoProfile",
2350
+ "-Command",
2351
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
2352
+ ],
2353
+ { stdio: "ignore", timeout: 3e3 }
2354
+ );
2355
+ continue;
2356
+ }
2357
+ for (const pid of listUnixListenerPids(port, selfPid)) {
2358
+ signalPid(pid, "SIGTERM");
2619
2359
  }
2620
2360
  } catch {
2621
2361
  }
2622
2362
  }
2623
- const orphans = [];
2624
- const seen = /* @__PURE__ */ new Set();
2625
- for (const repoPath of repos) {
2626
- if (!repoPath || !existsSync6(repoPath)) continue;
2627
- try {
2628
- const wts = await listWorktrees(repoPath);
2629
- for (const wt of wts) {
2630
- const path = wt.path.replace(/\/$/, "");
2631
- if (!isSideboardWorktreePath(path)) continue;
2632
- if (known.has(path)) continue;
2633
- if (seen.has(path)) continue;
2634
- seen.add(path);
2635
- let mtimeMs = 0;
2636
- try {
2637
- mtimeMs = statSync(path).mtimeMs;
2638
- } catch {
2639
- mtimeMs = 0;
2640
- }
2641
- orphans.push({ path, repoPath, mtimeMs });
2642
- }
2643
- } catch {
2363
+ }
2364
+ function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
2365
+ if (!worktreePath.trim()) {
2366
+ killListenersOnPorts(ports);
2367
+ return;
2368
+ }
2369
+ if (process.platform === "win32") {
2370
+ killListenersOnPorts(ports);
2371
+ return;
2372
+ }
2373
+ const selfPid = process.pid;
2374
+ for (const port of ports) {
2375
+ if (!Number.isFinite(port) || port <= 0) continue;
2376
+ for (const pid of listUnixListenerPids(port, selfPid)) {
2377
+ const cwd = listenerCwd(pid);
2378
+ if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
2379
+ signalPid(pid, signal);
2644
2380
  }
2381
+ }
2382
+ }
2383
+ function killScriptTree(child, ports = []) {
2384
+ const pid = child.pid;
2385
+ if (pid) {
2645
2386
  try {
2646
- const root = worktreesRoot(repoPath);
2647
- if (existsSync6(root)) {
2648
- for (const entry of readdirSync2(root, { withFileTypes: true })) {
2649
- if (!entry.isDirectory()) continue;
2650
- const path = join8(root, entry.name).replace(/\/$/, "");
2651
- if (known.has(path) || seen.has(path)) continue;
2652
- if (!existsSync6(join8(path, ".git"))) continue;
2653
- seen.add(path);
2654
- let mtimeMs = 0;
2387
+ if (process.platform === "win32") {
2388
+ void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
2389
+ } else {
2390
+ process.kill(-pid, "SIGTERM");
2391
+ setTimeout(() => {
2655
2392
  try {
2656
- mtimeMs = statSync(path).mtimeMs;
2393
+ process.kill(-pid, "SIGKILL");
2657
2394
  } catch {
2658
- mtimeMs = Date.now();
2659
2395
  }
2660
- orphans.push({ path, repoPath, mtimeMs });
2661
- }
2396
+ }, 2500).unref?.();
2662
2397
  }
2663
2398
  } catch {
2399
+ try {
2400
+ child.kill("SIGTERM");
2401
+ } catch {
2402
+ }
2664
2403
  }
2665
2404
  }
2666
- return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
2405
+ killListenersOnPorts(ports);
2667
2406
  }
2668
- async function cleanupOrphanWorktrees(opts) {
2669
- const settings = loadAppSettings();
2670
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
2671
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
2672
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
2673
- const budget = Math.max(0, maxCount - activeCount);
2674
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
2675
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
2676
- const removed = [];
2677
- const kept = [];
2678
- for (const orphan of orphans) {
2679
- if (keep.has(orphan.path)) {
2680
- kept.push(orphan.path);
2681
- continue;
2682
- }
2683
- if (opts?.dryRun) {
2684
- removed.push(orphan.path);
2685
- continue;
2686
- }
2687
- try {
2688
- await removeWorktree(orphan.repoPath, orphan.path);
2689
- removed.push(orphan.path);
2690
- } catch {
2691
- kept.push(orphan.path);
2692
- }
2407
+ async function spawnWorkspaceScript(command, opts) {
2408
+ const loginEnv = await captureLoginEnv();
2409
+ const env = buildWorkspaceScriptEnv(
2410
+ {
2411
+ worktreePath: opts.worktreePath,
2412
+ repoPath: opts.repoPath,
2413
+ workspaceName: opts.workspaceName,
2414
+ defaultBranch: opts.defaultBranch,
2415
+ ports: opts.ports
2416
+ },
2417
+ loginEnv
2418
+ );
2419
+ try {
2420
+ mergeAgentGitAuthEnv(
2421
+ env,
2422
+ await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
2423
+ );
2424
+ } catch {
2693
2425
  }
2694
- if (!opts?.dryRun) {
2695
- updateAdvancedSettings({
2696
- worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
2697
- });
2426
+ if (opts.heldPorts?.length) {
2427
+ await Promise.all(opts.heldPorts.map((h) => h.release()));
2698
2428
  }
2699
- return { removed, kept, orphans };
2429
+ const shell = process.platform === "darwin" ? "zsh" : "bash";
2430
+ const child = execa2(shell, ["-lc", command], {
2431
+ cwd: opts.worktreePath,
2432
+ reject: false,
2433
+ env,
2434
+ // Own process group so Stop can tear down the whole tree (not just the shell).
2435
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
2436
+ ...process.platform === "win32" ? {} : { detached: true }
2437
+ });
2438
+ pipeLines(child.stdout, opts.onLine);
2439
+ pipeLines(child.stderr, opts.onLine);
2440
+ const ports = opts.ports ?? [];
2441
+ return {
2442
+ pid: child.pid,
2443
+ kill: () => killScriptTree(child, ports),
2444
+ done: child.then((r) => r.exitCode ?? null),
2445
+ child
2446
+ };
2700
2447
  }
2701
- function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
2702
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
2703
- const last = settings.advanced.worktreeLastCleanupAt;
2704
- if (!last) return true;
2705
- const elapsed = Date.now() - Date.parse(last);
2706
- return elapsed >= intervalHours * 36e5;
2448
+ async function attachAbort(handle, signal) {
2449
+ if (signal) {
2450
+ const onAbort = () => handle.kill();
2451
+ if (signal.aborted) onAbort();
2452
+ else signal.addEventListener("abort", onAbort, { once: true });
2453
+ }
2454
+ return handle.done;
2707
2455
  }
2708
- function worktreeCleanupSettings() {
2709
- const a = loadAppSettings().advanced;
2456
+ async function runSetupScript(repoPath, worktreePath, onLine, opts) {
2457
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
2458
+ if (!settings?.setup) return { ran: false, exitCode: null, source: null };
2459
+ const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
2460
+ onLine?.(`[setup] ${source ?? "settings.toml"}`);
2461
+ const handle = await spawnWorkspaceScript(settings.setup, {
2462
+ worktreePath,
2463
+ repoPath,
2464
+ defaultBranch: opts?.defaultBranch,
2465
+ onLine
2466
+ });
2467
+ const exitCode = await attachAbort(handle, opts?.signal);
2710
2468
  return {
2711
- worktreeMaxCount: a.worktreeMaxCount,
2712
- worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
2713
- worktreeLastCleanupAt: a.worktreeLastCleanupAt,
2714
- autoCleanupOrphans: a.autoCleanupOrphans
2469
+ ran: true,
2470
+ exitCode,
2471
+ source,
2472
+ kill: handle.kill
2715
2473
  };
2716
2474
  }
2717
-
2718
- // src/store/history-cleanup.ts
2719
- function stripArchivedTranscript(thread) {
2720
- writeThread(
2721
- {
2722
- ...thread,
2723
- attachments: [],
2724
- pendingTurnAttachments: [],
2725
- queueAttachments: [],
2726
- messages: [historyRetentionStubMessage(thread)]
2727
- },
2728
- { touch: false }
2729
- );
2730
- }
2731
- function cleanupArchivedHistory(opts = {}) {
2732
- const settings = loadAppSettings();
2733
- if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
2734
- return { stripped: [], purged: [] };
2735
- }
2736
- const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
2737
- const nowMs = opts.nowMs ?? Date.now();
2738
- const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
2739
- maxCount: opts.maxCount ?? historyMaxCount(settings),
2740
- maxDays: opts.maxDays ?? historyMaxDays(settings),
2741
- nowMs
2475
+ async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
2476
+ const found = findConventionSetup(worktreePath, repoPath);
2477
+ if (!found) return { ran: false, exitCode: null, source: null };
2478
+ onLine?.(`[setup] ${found.source}`);
2479
+ const handle = await spawnWorkspaceScript(found.command, {
2480
+ worktreePath,
2481
+ repoPath,
2482
+ defaultBranch: opts?.defaultBranch,
2483
+ onLine
2742
2484
  });
2743
- const stripped = [];
2744
- const purged = [];
2745
- if (opts.dryRun) {
2746
- return { stripped: plan.stripIds, purged: plan.purgeIds };
2747
- }
2748
- for (const id of plan.stripIds) {
2749
- const thread = readThread(id);
2750
- if (!thread || thread.status !== "archived") continue;
2751
- try {
2752
- stripArchivedTranscript(thread);
2753
- stripped.push(id);
2754
- } catch {
2755
- }
2485
+ const exitCode = await attachAbort(handle, opts?.signal);
2486
+ return {
2487
+ ran: true,
2488
+ exitCode,
2489
+ source: found.source,
2490
+ kill: handle.kill
2491
+ };
2492
+ }
2493
+ async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
2494
+ await ensureWorktreeSideboardIgnored(worktreePath);
2495
+ const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
2496
+ if (review.wrote) {
2497
+ onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
2756
2498
  }
2757
- for (const id of plan.purgeIds) {
2758
- const thread = readThread(id);
2759
- if (!thread || thread.status !== "archived") continue;
2760
- try {
2761
- deleteThreadRecord(id);
2762
- purged.push(id);
2763
- } catch {
2764
- }
2499
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
2500
+ if (!setup.ran) {
2501
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
2765
2502
  }
2766
- if (stripped.length > 0 || purged.length > 0) {
2767
- updateAdvancedSettings({
2768
- historyLastCleanupAt: new Date(nowMs).toISOString()
2769
- });
2503
+ if (!setup.ran) {
2504
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
2770
2505
  }
2771
- return { stripped, purged };
2506
+ return setup;
2772
2507
  }
2773
- function shouldRunHistoryCleanup(settings = loadAppSettings()) {
2774
- if (!autoCleanupHistoryEnabled(settings)) return false;
2775
- const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
2776
- const last = settings.advanced.historyLastCleanupAt;
2777
- if (!last) return true;
2778
- const elapsed = Date.now() - Date.parse(last);
2779
- return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
2508
+ async function runArchiveScript(repoPath, worktreePath, onLine) {
2509
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
2510
+ if (!settings?.archive) return { ran: false, exitCode: null };
2511
+ const handle = await spawnWorkspaceScript(settings.archive, {
2512
+ worktreePath,
2513
+ repoPath,
2514
+ onLine
2515
+ });
2516
+ const exitCode = await handle.done;
2517
+ return { ran: true, exitCode };
2780
2518
  }
2781
-
2782
- // src/git/apply-into-main.ts
2783
- import { execa as execa3 } from "execa";
2784
- async function applyThreadIntoMain(thread, opts) {
2785
- const method = opts?.method ?? "merge";
2786
- const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
2787
- const status = await execa3("git", ["status", "--porcelain"], {
2788
- cwd: thread.repoPath,
2789
- reject: false
2790
- });
2791
- if (status.stdout.trim()) {
2792
- throw new Error(
2793
- `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
2794
- );
2795
- }
2796
- const checkout = await execa3("git", ["checkout", target], {
2797
- cwd: thread.repoPath,
2798
- reject: false
2519
+ function listRunScripts(worktreePath, repoPath) {
2520
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
2521
+ if (!settings?.runScripts.length) return [];
2522
+ return settings.runScripts.filter((s) => {
2523
+ if (!s.availableIn?.length) return true;
2524
+ return s.availableIn.includes("local");
2799
2525
  });
2800
- if (checkout.exitCode !== 0) {
2801
- throw new Error(
2802
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
2803
- );
2804
- }
2805
- if (method === "cherry-pick") {
2806
- const log = await execa3(
2807
- "git",
2808
- ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
2809
- { cwd: thread.repoPath, reject: false }
2810
- );
2811
- const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
2812
- if (!shas.length) {
2813
- return {
2814
- applied: false,
2815
- method,
2816
- targetBranch: target,
2817
- message: `No commits on ${thread.branchName} not already in ${target}`
2818
- };
2819
- }
2820
- for (const sha of shas) {
2821
- const cp = await execa3("git", ["cherry-pick", sha], {
2822
- cwd: thread.repoPath,
2823
- reject: false
2824
- });
2825
- if (cp.exitCode !== 0) {
2826
- await execa3("git", ["cherry-pick", "--abort"], {
2827
- cwd: thread.repoPath,
2828
- reject: false
2829
- });
2830
- throw new Error(
2831
- `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
2832
- );
2833
- }
2834
- }
2835
- return {
2836
- applied: true,
2837
- method,
2838
- targetBranch: target,
2839
- message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
2840
- };
2841
- }
2842
- const merge = await execa3(
2843
- "git",
2844
- ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
2845
- { cwd: thread.repoPath, reject: false }
2846
- );
2847
- if (merge.exitCode !== 0) {
2848
- await execa3("git", ["merge", "--abort"], {
2849
- cwd: thread.repoPath,
2850
- reject: false
2851
- });
2852
- throw new Error(
2853
- `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
2854
- );
2855
- }
2856
- return {
2857
- applied: true,
2858
- method: "merge",
2859
- targetBranch: target,
2860
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
2861
- };
2862
2526
  }
2863
-
2864
- // src/git/clone-repo.ts
2865
- import { existsSync as existsSync7 } from "fs";
2866
- import { basename as basename2, join as join9 } from "path";
2867
- import { execa as execa4 } from "execa";
2868
- async function cloneRepoIntoSideboard(opts) {
2869
- const url = opts.url.trim();
2870
- if (!url) throw new Error("Clone URL is required");
2871
- let name = opts.name?.trim();
2872
- if (!name) {
2873
- const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
2874
- name = leaf || "repo";
2875
- }
2876
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
2877
- const dest = join9(sideboardReposDir(), name);
2878
- if (existsSync7(dest)) {
2879
- const repoPath2 = await resolveRepoRoot(dest);
2880
- const workspace2 = await ensureWorkspace(repoPath2);
2881
- return { repoPath: repoPath2, workspace: workspace2 };
2882
- }
2883
- const result = await execa4("git", ["clone", url, dest], { reject: false });
2884
- if (result.exitCode !== 0) {
2885
- throw new Error(
2886
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
2887
- );
2527
+ function getDefaultRunScript(worktreePath, repoPath) {
2528
+ const scripts = listRunScripts(worktreePath, repoPath);
2529
+ if (!scripts.length) return null;
2530
+ return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
2531
+ }
2532
+ function getRunScript(worktreePath, repoPath, name) {
2533
+ const scripts = listRunScripts(worktreePath, repoPath);
2534
+ if (!scripts.length) return null;
2535
+ if (name) {
2536
+ return scripts.find((s) => s.name === name) ?? null;
2888
2537
  }
2889
- const repoPath = await resolveRepoRoot(dest);
2890
- const workspace = await ensureWorkspace(repoPath);
2891
- return { repoPath, workspace };
2538
+ return getDefaultRunScript(worktreePath, repoPath);
2892
2539
  }
2893
-
2894
- // src/types/thread.ts
2895
- function isUnclaimedRunScriptRequest(req) {
2896
- return Boolean(req && !req.claimedAt && !req.fulfilledAt && !req.error);
2540
+ function getRunMode(worktreePath, repoPath) {
2541
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
2897
2542
  }
2898
- function isAdoptableRunScriptRequest(req) {
2899
- return Boolean(req && !req.fulfilledAt && !req.error);
2543
+ async function reservePort() {
2544
+ return new Promise((resolve2, reject) => {
2545
+ const server = createServer();
2546
+ let released = false;
2547
+ const release = () => new Promise((res) => {
2548
+ if (released) {
2549
+ res();
2550
+ return;
2551
+ }
2552
+ released = true;
2553
+ server.close(() => res());
2554
+ });
2555
+ server.listen(0, "127.0.0.1", () => {
2556
+ const addr = server.address();
2557
+ if (!addr || typeof addr === "string") {
2558
+ void release().then(() => reject(new Error("Failed to allocate port")));
2559
+ return;
2560
+ }
2561
+ resolve2({ port: addr.port, release });
2562
+ });
2563
+ server.on("error", reject);
2564
+ });
2900
2565
  }
2901
-
2902
- // src/store/desktop-host.ts
2903
- import { readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
2904
- import { join as join10 } from "path";
2905
- function desktopHostPidPath() {
2906
- return join10(appDataDir(), "desktop-host.pid");
2566
+ async function allocatePort() {
2567
+ const held = await reservePort();
2568
+ await held.release();
2569
+ return held.port;
2907
2570
  }
2908
- function claimDesktopHost(pid = process.pid) {
2909
- writeFileSync5(desktopHostPidPath(), `${pid}
2910
- `, "utf8");
2571
+ async function tryReservePort(port) {
2572
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
2573
+ return new Promise((resolve2) => {
2574
+ const server = createServer();
2575
+ let released = false;
2576
+ const release = () => new Promise((res) => {
2577
+ if (released) {
2578
+ res();
2579
+ return;
2580
+ }
2581
+ released = true;
2582
+ server.close(() => res());
2583
+ });
2584
+ server.once("error", () => {
2585
+ try {
2586
+ server.close();
2587
+ } catch {
2588
+ }
2589
+ resolve2(null);
2590
+ });
2591
+ server.listen(port, "127.0.0.1", () => {
2592
+ resolve2({ port, release });
2593
+ });
2594
+ });
2911
2595
  }
2912
- function releaseDesktopHost(pid = process.pid) {
2913
- if (readDesktopHostPid() !== pid) return;
2914
- try {
2915
- unlinkSync(desktopHostPidPath());
2916
- } catch {
2596
+ async function tryReservePorts(ports) {
2597
+ const held = [];
2598
+ for (const port of ports) {
2599
+ const next = await tryReservePort(port);
2600
+ if (!next) {
2601
+ await Promise.all(held.map((h) => h.release()));
2602
+ return null;
2603
+ }
2604
+ held.push(next);
2917
2605
  }
2606
+ return held;
2918
2607
  }
2919
- function readDesktopHostPid() {
2920
- try {
2921
- const pid = Number.parseInt(readFileSync6(desktopHostPidPath(), "utf8").trim(), 10);
2922
- if (!Number.isFinite(pid) || pid <= 0) return null;
2923
- return pid;
2924
- } catch {
2925
- return null;
2608
+ function normalizePreferredPorts(ports, size) {
2609
+ if (!ports?.length) return [];
2610
+ const seen = /* @__PURE__ */ new Set();
2611
+ const out = [];
2612
+ for (const p of ports) {
2613
+ if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
2614
+ seen.add(p);
2615
+ out.push(p);
2616
+ if (out.length >= size) break;
2926
2617
  }
2618
+ return out;
2927
2619
  }
2928
- function pidAlive(pid) {
2620
+ function delay(ms) {
2621
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2622
+ }
2623
+ async function fillPortRange(held, size) {
2624
+ if (held.length >= size) return held;
2625
+ const base = held[0]?.port;
2626
+ const taken = new Set(held.map((h) => h.port));
2929
2627
  try {
2930
- process.kill(pid, 0);
2931
- return true;
2932
- } catch {
2933
- return false;
2628
+ let offset = 0;
2629
+ while (held.length < size) {
2630
+ const candidate = base != null ? base + offset : null;
2631
+ offset += 1;
2632
+ if (candidate != null && taken.has(candidate)) continue;
2633
+ const next = candidate != null ? await tryReservePort(candidate) : null;
2634
+ if (next) {
2635
+ held.push(next);
2636
+ taken.add(next.port);
2637
+ } else {
2638
+ const ephemeral = await reservePort();
2639
+ held.push(ephemeral);
2640
+ taken.add(ephemeral.port);
2641
+ }
2642
+ }
2643
+ return held;
2644
+ } catch (err) {
2645
+ await Promise.all(held.map((h) => h.release()));
2646
+ throw err;
2934
2647
  }
2935
2648
  }
2936
- function isDesktopHostAlive() {
2937
- const pid = readDesktopHostPid();
2938
- return pid != null && pidAlive(pid);
2939
- }
2940
- function isThisProcessDesktopHost() {
2941
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
2649
+ async function tryReserveAvailablePorts(ports) {
2650
+ const held = [];
2651
+ for (const port of ports) {
2652
+ const next = await tryReservePort(port);
2653
+ if (next) held.push(next);
2654
+ }
2655
+ return held;
2942
2656
  }
2943
- function thisProcessShouldDrainAgentQueues() {
2944
- if (isThisProcessDesktopHost()) return true;
2945
- return !isDesktopHostAlive();
2946
- }
2947
-
2948
- // src/orchestrator/child-halt.ts
2949
- var HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
2950
- function isIncompleteChildStatus(status) {
2951
- return HALT_STATUSES.has(status);
2952
- }
2953
- function childHaltNotice(child, status) {
2954
- const title = child.title?.trim() || "Untitled";
2955
- const link = `[${title}](sideboard://thread/${child.id})`;
2956
- const why = child.lastError?.trim();
2957
- const extra = why ? ` lastError: ${why}` : "";
2958
- return [
2959
- `Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
2960
- "This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
2961
- ].join("\n");
2962
- }
2963
- function shouldNotifyParentAfterTurnError(opts) {
2964
- return !opts.quotaFailoverHandled && !opts.crashContinued;
2657
+ async function reclaimPreferredPorts(ports, worktreePath) {
2658
+ const primary = ports[0];
2659
+ if (primary == null) return null;
2660
+ const takePrimary = async () => {
2661
+ const held2 = await tryReserveAvailablePorts(ports);
2662
+ if (held2.some((h) => h.port === primary)) return held2;
2663
+ await Promise.all(held2.map((h) => h.release()));
2664
+ return null;
2665
+ };
2666
+ let held = await takePrimary();
2667
+ if (held) return held;
2668
+ if (worktreePath) {
2669
+ killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
2670
+ } else {
2671
+ killListenersOnPorts(ports);
2672
+ }
2673
+ for (let i = 0; i < RECLAIM_TRIES; i++) {
2674
+ await delay(RECLAIM_RETRY_DELAY_MS);
2675
+ held = await takePrimary();
2676
+ if (held) return held;
2677
+ if (i === 2 && worktreePath) {
2678
+ killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
2679
+ }
2680
+ }
2681
+ return null;
2965
2682
  }
2966
- function shouldNotifyParentOfChildHalt(opts) {
2967
- if (!isIncompleteChildStatus(opts.status)) return false;
2968
- if (!opts.child.parentThreadId) return false;
2969
- if (!opts.parent || opts.parent.status === "archived") return false;
2970
- if (opts.parent.id === opts.child.id) return false;
2971
- return isOrchestratorThread(opts.parent);
2683
+ async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
2684
+ const preferred = normalizePreferredPorts(opts?.preferred, size);
2685
+ if (preferred.length) {
2686
+ const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
2687
+ if (reused) return fillPortRange(reused, size);
2688
+ }
2689
+ const held = [await reservePort()];
2690
+ return fillPortRange(held, size);
2972
2691
  }
2973
- var notified = /* @__PURE__ */ new Set();
2974
- function noticeKey(childId, status) {
2975
- return `${childId}:${status}`;
2692
+ async function allocatePortRange(size = PORT_RANGE_SIZE) {
2693
+ const held = await reservePortRange(size);
2694
+ const ports = held.map((h) => h.port);
2695
+ await Promise.all(held.map((h) => h.release()));
2696
+ return ports;
2976
2697
  }
2977
- function notifyParentOfChildHalt(child, status, send) {
2978
- const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
2979
- if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
2980
- const key = noticeKey(child.id, status);
2981
- if (notified.has(key)) return false;
2982
- notified.add(key);
2983
- const parentId = parent.id;
2984
- void send(parentId, childHaltNotice(child, status)).catch(() => {
2985
- notified.delete(key);
2698
+ async function startDevServer(repoPath, worktreePath, onLine, opts) {
2699
+ const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
2700
+ if (!script) return null;
2701
+ const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
2702
+ const held = await reservePortRange(PORT_RANGE_SIZE, {
2703
+ preferred: preferred ?? void 0,
2704
+ worktreePath
2986
2705
  });
2987
- return true;
2988
- }
2989
-
2990
- // src/mcp/wait-for-job.ts
2991
- import {
2992
- appendFileSync as appendFileSync2,
2993
- existsSync as existsSync8,
2994
- mkdirSync as mkdirSync5,
2995
- readdirSync as readdirSync3,
2996
- readFileSync as readFileSync7,
2997
- writeFileSync as writeFileSync6
2998
- } from "fs";
2999
- import { dirname as dirname4, join as join11 } from "path";
3000
-
3001
- // src/mcp/wait-for-turn.ts
3002
- var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
3003
- function mcpWaitForTurnTimeoutMs(requested) {
3004
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
3005
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
3006
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
3007
- }
3008
- var MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
3009
- var MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
3010
- var MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
3011
- var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
3012
- var MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
3013
- var MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
3014
- function mcpWaitTaskHint(taskState, status) {
3015
- switch (taskState) {
3016
- case "submitted":
3017
- return MCP_WAIT_QUEUED_HINT;
3018
- case "working":
3019
- return MCP_WAIT_STILL_RUNNING_HINT;
3020
- case "input-required":
3021
- return MCP_WAIT_INPUT_REQUIRED_HINT;
3022
- case "canceled":
3023
- return MCP_WAIT_STOPPED_HINT;
3024
- case "failed":
3025
- return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
3026
- default:
3027
- return void 0;
2706
+ const ports = held.map((h) => h.port);
2707
+ saveWorktreeRunPorts(worktreePath, script.name, ports);
2708
+ try {
2709
+ const handle = await spawnWorkspaceScript(script.command, {
2710
+ worktreePath,
2711
+ repoPath,
2712
+ ports,
2713
+ heldPorts: held,
2714
+ defaultBranch: opts?.defaultBranch,
2715
+ onLine
2716
+ });
2717
+ return {
2718
+ pid: handle.pid,
2719
+ port: ports[0],
2720
+ ports,
2721
+ scriptName: script.name,
2722
+ kill: handle.kill,
2723
+ done: handle.done
2724
+ };
2725
+ } catch (err) {
2726
+ await Promise.all(held.map((h) => h.release()));
2727
+ throw err;
3028
2728
  }
3029
2729
  }
3030
2730
 
3031
- // src/mcp/wait-for-job.ts
3032
- var MAX_JOB_CONTINUES = 8;
3033
- var MAX_JOB_DELTA_LINES = 80;
3034
- var MAX_JOB_DELTA_CHARS = 4e3;
3035
- var DEFERRED_DONE_MAX_CHARS = 480;
3036
- var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
3037
- var MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. The type=log pane already appended this delta. Call wait_for_job again. If it is hanging, producing no useful output, or doing the wrong thing, call stop_job (same id) instead of looping forever. Do not end the turn or tell the user you will let them know later.";
3038
- var MCP_STOP_JOB_HINT = "Stopped. The type=log pane is status=failed with the last delta. Do not wait again unless you start a new command.";
3039
- var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
3040
- function mcpWaitForJobTimeoutMs(requested) {
3041
- return mcpWaitForTurnTimeoutMs(requested);
3042
- }
3043
- function sanitizeDetachedJobId(id) {
3044
- const s = id.trim();
3045
- if (!JOB_ID_RE.test(s)) {
3046
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
3047
- }
3048
- return s;
2731
+ // src/store/setup-log.ts
2732
+ import { createHash } from "crypto";
2733
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2734
+ import { join as join7 } from "path";
2735
+ var EMPTY = {
2736
+ output: "",
2737
+ running: false,
2738
+ exitCode: null,
2739
+ source: null
2740
+ };
2741
+ var MAX_SETUP_LOG_CHARS = 256e3;
2742
+ function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
2743
+ const next = prev ? `${prev}
2744
+ ${line}` : line;
2745
+ if (next.length <= max) return next;
2746
+ const cut = next.slice(next.length - max);
2747
+ const nl = cut.indexOf("\n");
2748
+ return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
3049
2749
  }
3050
- function jobAlive(pid) {
3051
- if (!Number.isInteger(pid) || pid <= 0) return false;
3052
- try {
3053
- process.kill(pid, 0);
3054
- return true;
3055
- } catch {
3056
- return false;
3057
- }
2750
+ var memory = /* @__PURE__ */ new Map();
2751
+ var persistTimers = /* @__PURE__ */ new Map();
2752
+ function fromSnapshot(snap) {
2753
+ return {
2754
+ chunks: snap.output ? [snap.output] : [],
2755
+ length: snap.output.length,
2756
+ running: snap.running,
2757
+ exitCode: snap.exitCode,
2758
+ source: snap.source
2759
+ };
3058
2760
  }
3059
- function processGroupAlive(pgid) {
3060
- if (process.platform === "win32") return false;
3061
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
3062
- try {
3063
- process.kill(-pgid, 0);
3064
- return true;
3065
- } catch (err) {
3066
- return err && typeof err === "object" && "code" in err && err.code === "EPERM";
2761
+ function materialize(mem) {
2762
+ let output = mem.chunks.join("\n");
2763
+ if (output.length > MAX_SETUP_LOG_CHARS) {
2764
+ output = appendSetupOutput("", output);
2765
+ mem.chunks = [output];
2766
+ mem.length = output.length;
3067
2767
  }
2768
+ return {
2769
+ output,
2770
+ running: mem.running,
2771
+ exitCode: mem.exitCode,
2772
+ source: mem.source
2773
+ };
3068
2774
  }
3069
- function jobTreeAlive(pid) {
3070
- if (pid == null) return false;
3071
- return jobAlive(pid) || processGroupAlive(pid);
3072
- }
3073
- function readIntFile(file) {
3074
- if (!existsSync8(file)) return null;
3075
- const n = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
3076
- return Number.isInteger(n) ? n : null;
2775
+ function setupLogPath(threadId) {
2776
+ return join7(threadsDir(), `${threadId}.setup.log.json`);
3077
2777
  }
3078
- function jobDir(root, id, legacy = false) {
3079
- return join11(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
2778
+ function setupLogKeyForWorktree(worktreePath) {
2779
+ const norm = normalizeWorktreePath(worktreePath);
2780
+ return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
3080
2781
  }
3081
- function resolveJobDir(root, id) {
3082
- const modern = jobDir(root, id, false);
3083
- if (existsSync8(modern)) return modern;
3084
- const legacy = jobDir(root, id, true);
3085
- if (existsSync8(legacy)) return legacy;
3086
- return modern;
2782
+ function emptySetupLog() {
2783
+ return { ...EMPTY };
3087
2784
  }
3088
- function listJobIdsIn(root, rel) {
3089
- const dir = join11(root, rel);
3090
- if (!existsSync8(dir)) return [];
3091
- return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
2785
+ function mergeSetupOutput(prev, incoming) {
2786
+ if (!prev) return incoming;
2787
+ if (!incoming) return prev;
2788
+ if (prev === incoming) return prev;
2789
+ if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
2790
+ if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
2791
+ return incoming.length >= prev.length ? incoming : prev;
3092
2792
  }
3093
- function listRunningDetachedJobs(worktreePath) {
3094
- const root = worktreePath.trim();
3095
- if (!root) return [];
3096
- const ids = /* @__PURE__ */ new Set([
3097
- ...listJobIdsIn(root, DETACHED_JOBS_DIR),
3098
- ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
3099
- ]);
3100
- const running = [];
3101
- for (const id of ids) {
3102
- const dir = resolveJobDir(root, id);
3103
- const pid = readIntFile(join11(dir, "pid"));
3104
- if (jobTreeAlive(pid)) running.push(id);
2793
+ var PERSIST_DEBOUNCE_MS = 500;
2794
+ function persistNow(threadId) {
2795
+ const timer = persistTimers.get(threadId);
2796
+ if (timer) {
2797
+ clearTimeout(timer);
2798
+ persistTimers.delete(threadId);
2799
+ }
2800
+ const mem = memory.get(threadId);
2801
+ if (!mem) return;
2802
+ try {
2803
+ writeFileSync4(
2804
+ setupLogPath(threadId),
2805
+ `${JSON.stringify(materialize(mem))}
2806
+ `,
2807
+ "utf8"
2808
+ );
2809
+ } catch {
3105
2810
  }
3106
- return running.sort();
3107
2811
  }
3108
- function looksLikeDeferredDonePromise(text) {
3109
- const t = (text ?? "").trim();
3110
- if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
3111
- const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
3112
- if (!unquoted) return false;
3113
- return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted);
2812
+ function schedulePersist(threadId) {
2813
+ if (persistTimers.has(threadId)) return;
2814
+ persistTimers.set(
2815
+ threadId,
2816
+ setTimeout(() => {
2817
+ persistTimers.delete(threadId);
2818
+ persistNow(threadId);
2819
+ }, PERSIST_DEBOUNCE_MS)
2820
+ );
3114
2821
  }
3115
- function isJobContinuePrompt(text) {
3116
- if (typeof text !== "string") return false;
3117
- const t = text.trim();
3118
- return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
2822
+ function loadFromDisk(threadId) {
2823
+ const path = setupLogPath(threadId);
2824
+ if (!existsSync5(path)) return null;
2825
+ try {
2826
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
2827
+ return {
2828
+ output: typeof raw.output === "string" ? raw.output : "",
2829
+ running: Boolean(raw.running),
2830
+ exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
2831
+ source: typeof raw.source === "string" ? raw.source : null
2832
+ };
2833
+ } catch {
2834
+ return null;
2835
+ }
3119
2836
  }
3120
- function collapseGhRunWatchLog(text) {
3121
- if (!text.includes(GH_WATCH_REFRESH)) return text;
3122
- const frames = text.split(/(?=Refreshing run status every 3 seconds\.)/);
3123
- const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
3124
- if (watchFrames.length <= 1) return text;
3125
- const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
3126
- const last = watchFrames[watchFrames.length - 1] ?? "";
3127
- const omitted = watchFrames.length - 1;
3128
- const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
3129
- `;
3130
- return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
2837
+ function memFor(threadId) {
2838
+ const cached = memory.get(threadId);
2839
+ if (cached) return cached;
2840
+ const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
2841
+ memory.set(threadId, mem);
2842
+ return mem;
3131
2843
  }
3132
- function capJobLogDelta(text) {
3133
- const collapsed = collapseGhRunWatchLog(text);
3134
- const lines = collapsed.split("\n");
3135
- let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
3136
- ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
3137
- if (out.length > MAX_JOB_DELTA_CHARS) {
3138
- out = `\u2026(truncated ${out.length} chars)
3139
- ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
2844
+ function readSetupLog(threadId) {
2845
+ const cached = memory.get(threadId);
2846
+ if (cached) return materialize(cached);
2847
+ const disk = loadFromDisk(threadId);
2848
+ if (disk) {
2849
+ memory.set(threadId, fromSnapshot(disk));
2850
+ return disk;
3140
2851
  }
3141
- return out;
2852
+ return emptySetupLog();
3142
2853
  }
3143
- function formatJobStillRunningContinuePrompt(jobIds) {
3144
- const ids = jobIds.join(", ");
3145
- return [
3146
- `Detached job still running: ${ids}.`,
3147
- "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
3148
- "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
3149
- "Then report the result. Do not tell the user you will let them know later."
3150
- ].join(" ");
2854
+ function beginSetupLog(threadId) {
2855
+ const mem = {
2856
+ chunks: [],
2857
+ length: 0,
2858
+ running: true,
2859
+ exitCode: null,
2860
+ source: null
2861
+ };
2862
+ memory.set(threadId, mem);
2863
+ persistNow(threadId);
2864
+ return materialize(mem);
3151
2865
  }
3152
- function formatDeferredDoneContinuePrompt() {
3153
- return [
3154
- "You ended the turn after promising to report later, but no detached job is running.",
3155
- "If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
3156
- "Do not say you will let the user know later."
3157
- ].join(" ");
2866
+ function appendSetupLog(threadId, line) {
2867
+ const mem = memFor(threadId);
2868
+ mem.chunks.push(line);
2869
+ mem.length += line.length + 1;
2870
+ mem.running = true;
2871
+ if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
2872
+ schedulePersist(threadId);
3158
2873
  }
3159
- function turnWatchedDetachedJob(parts) {
3160
- return parts.some((p) => {
3161
- if (p.type !== "tool") return false;
3162
- if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
3163
- const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
3164
- return /detached-job\.(?:js|cjs)\b/i.test(blob);
3165
- });
2874
+ function finishSetupLog(threadId, exitCode, source) {
2875
+ const mem = memFor(threadId);
2876
+ mem.running = false;
2877
+ mem.exitCode = exitCode;
2878
+ mem.source = source ?? mem.source;
2879
+ persistNow(threadId);
2880
+ return materialize(mem);
3166
2881
  }
3167
- function planJobContinue(opts) {
3168
- if (opts.isOrchestrator) return { action: "none" };
3169
- if (opts.agent === "brightsy") return { action: "none" };
3170
- if (opts.queueLength > 0) return { action: "none" };
3171
- if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
3172
- const farewell = looksLikeDeferredDonePromise(opts.chatText);
3173
- if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
3174
- return {
3175
- action: "wait",
3176
- jobIds: opts.runningJobIds,
3177
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
3178
- };
3179
- }
3180
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
3181
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
2882
+
2883
+ // src/store/run-log.ts
2884
+ import { createHash as createHash2 } from "crypto";
2885
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2886
+ import { join as join8 } from "path";
2887
+ var EMPTY2 = {
2888
+ output: "",
2889
+ running: false,
2890
+ exitCode: null,
2891
+ scriptName: null
2892
+ };
2893
+ var MAX_RUN_LOG_CHARS = 256e3;
2894
+ var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
2895
+ var memory2 = /* @__PURE__ */ new Map();
2896
+ var persistTimers2 = /* @__PURE__ */ new Map();
2897
+ function fromSnapshot2(snap) {
2898
+ return {
2899
+ chunks: snap.output ? [snap.output] : [],
2900
+ length: snap.output.length,
2901
+ running: snap.running,
2902
+ exitCode: snap.exitCode,
2903
+ scriptName: snap.scriptName
2904
+ };
2905
+ }
2906
+ function materialize2(mem) {
2907
+ let output = mem.chunks.join("\n");
2908
+ if (output.length > MAX_RUN_LOG_CHARS) {
2909
+ output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
2910
+ mem.chunks = [output];
2911
+ mem.length = output.length;
3182
2912
  }
3183
- return { action: "none" };
2913
+ return {
2914
+ output,
2915
+ running: mem.running,
2916
+ exitCode: mem.exitCode,
2917
+ scriptName: mem.scriptName
2918
+ };
3184
2919
  }
3185
- function tailProgress(logFile, maxLines = 12) {
3186
- if (!existsSync8(logFile)) return "(no log yet)";
3187
- const lines = readFileSync7(logFile, "utf8").split("\n");
3188
- return lines.slice(-maxLines).join("\n");
2920
+ function safeScriptToken(scriptName) {
2921
+ const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
2922
+ return token || "dev";
3189
2923
  }
3190
- function lastProgressLine(progress) {
3191
- const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
3192
- return last ? last.slice(0, 72) : void 0;
2924
+ function runLogPath(key) {
2925
+ return join8(threadsDir(), `${key}.run.log.json`);
3193
2926
  }
3194
- function readLogLines(file) {
3195
- if (!existsSync8(file)) return [];
3196
- const lines = readFileSync7(file, "utf8").split("\n");
3197
- if (lines.length && lines[lines.length - 1] === "") lines.pop();
3198
- return lines;
2927
+ function runLogKeyForWorktree(worktreePath, scriptName) {
2928
+ const norm = normalizeWorktreePath(worktreePath);
2929
+ const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
2930
+ return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
3199
2931
  }
3200
- function takeDelta(logFile, cursorFile) {
3201
- const lines = readLogLines(logFile);
3202
- const cursor = readIntFile(cursorFile) ?? 0;
3203
- const start = Math.min(Math.max(0, cursor), lines.length);
3204
- return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
2932
+ function emptyRunLog() {
2933
+ return { ...EMPTY2 };
3205
2934
  }
3206
- function snapshotJob(dir) {
3207
- const pid = readIntFile(join11(dir, "pid"));
3208
- const running = jobTreeAlive(pid);
3209
- return {
3210
- pid,
3211
- running,
3212
- exitCode: readIntFile(join11(dir, "exit")),
3213
- log: join11(dir, "log"),
3214
- cursor: join11(dir, "present.cursor"),
3215
- progress: tailProgress(join11(dir, "log"))
3216
- };
2935
+ function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
2936
+ const max = Math.max(1, tailChars);
2937
+ const outputChars = output.length;
2938
+ if (outputChars <= max) return { output, truncated: false, outputChars };
2939
+ const cut = output.slice(outputChars - max);
2940
+ const nl = cut.indexOf("\n");
2941
+ const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
2942
+ return { output: text, truncated: true, outputChars };
3217
2943
  }
3218
- function toResult(id, snap, extra) {
3219
- const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
3220
- const ok = !snap.running && snap.exitCode === 0;
3221
- const stillRunning = snap.running && !ok;
3222
- const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
2944
+ var PERSIST_DEBOUNCE_MS2 = 500;
2945
+ function persistNow2(key) {
2946
+ const timer = persistTimers2.get(key);
2947
+ if (timer) {
2948
+ clearTimeout(timer);
2949
+ persistTimers2.delete(key);
2950
+ }
2951
+ const mem = memory2.get(key);
2952
+ if (!mem) return;
3223
2953
  try {
3224
- mkdirSync5(dirname4(snap.cursor), { recursive: true });
3225
- writeFileSync6(snap.cursor, `${nextCursor}
3226
- `);
2954
+ writeFileSync5(runLogPath(key), `${JSON.stringify(materialize2(mem))}
2955
+ `, "utf8");
2956
+ } catch {
2957
+ }
2958
+ }
2959
+ function schedulePersist2(key) {
2960
+ if (persistTimers2.has(key)) return;
2961
+ persistTimers2.set(
2962
+ key,
2963
+ setTimeout(() => {
2964
+ persistTimers2.delete(key);
2965
+ persistNow2(key);
2966
+ }, PERSIST_DEBOUNCE_MS2)
2967
+ );
2968
+ }
2969
+ function loadFromDisk2(key) {
2970
+ const path = runLogPath(key);
2971
+ if (!existsSync6(path)) return null;
2972
+ try {
2973
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
2974
+ return {
2975
+ output: typeof raw.output === "string" ? raw.output : "",
2976
+ running: Boolean(raw.running),
2977
+ exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
2978
+ scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
2979
+ };
3227
2980
  } catch {
2981
+ return null;
2982
+ }
2983
+ }
2984
+ function memFor2(key) {
2985
+ const cached = memory2.get(key);
2986
+ if (cached) return cached;
2987
+ const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
2988
+ memory2.set(key, mem);
2989
+ return mem;
2990
+ }
2991
+ function readRunLog(key) {
2992
+ const cached = memory2.get(key);
2993
+ if (cached) return materialize2(cached);
2994
+ const disk = loadFromDisk2(key);
2995
+ if (disk) {
2996
+ memory2.set(key, fromSnapshot2(disk));
2997
+ return disk;
2998
+ }
2999
+ return emptyRunLog();
3000
+ }
3001
+ function beginRunLog(key, scriptName) {
3002
+ const mem = {
3003
+ chunks: [],
3004
+ length: 0,
3005
+ running: true,
3006
+ exitCode: null,
3007
+ scriptName
3008
+ };
3009
+ memory2.set(key, mem);
3010
+ persistNow2(key);
3011
+ return materialize2(mem);
3012
+ }
3013
+ function appendRunLog(key, line) {
3014
+ const mem = memFor2(key);
3015
+ mem.chunks.push(line);
3016
+ mem.length += line.length + 1;
3017
+ mem.running = true;
3018
+ if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
3019
+ schedulePersist2(key);
3020
+ }
3021
+ function finishRunLog(key, exitCode) {
3022
+ const mem = memFor2(key);
3023
+ mem.running = false;
3024
+ mem.exitCode = exitCode;
3025
+ persistNow2(key);
3026
+ return materialize2(mem);
3027
+ }
3028
+ function flushRunLog(key) {
3029
+ persistNow2(key);
3030
+ }
3031
+
3032
+ // src/git/orphan-cleanup.ts
3033
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync } from "fs";
3034
+ import { join as join9 } from "path";
3035
+ function isSideboardWorktreePath(path) {
3036
+ return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
3037
+ }
3038
+ async function findOrphanWorktrees(repoPaths) {
3039
+ const threads = listThreads({ includeArchived: true });
3040
+ const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
3041
+ const repos = new Set(
3042
+ repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
3043
+ );
3044
+ const homeRoot = sideboardWorkspacesDir();
3045
+ if (existsSync7(homeRoot)) {
3046
+ try {
3047
+ for (const entry of readdirSync3(homeRoot, { withFileTypes: true })) {
3048
+ if (!entry.isDirectory()) continue;
3049
+ void entry;
3050
+ }
3051
+ } catch {
3052
+ }
3053
+ }
3054
+ const orphans = [];
3055
+ const seen = /* @__PURE__ */ new Set();
3056
+ for (const repoPath of repos) {
3057
+ if (!repoPath || !existsSync7(repoPath)) continue;
3058
+ try {
3059
+ const wts = await listWorktrees(repoPath);
3060
+ for (const wt of wts) {
3061
+ const path = wt.path.replace(/\/$/, "");
3062
+ if (!isSideboardWorktreePath(path)) continue;
3063
+ if (known.has(path)) continue;
3064
+ if (seen.has(path)) continue;
3065
+ seen.add(path);
3066
+ let mtimeMs = 0;
3067
+ try {
3068
+ mtimeMs = statSync(path).mtimeMs;
3069
+ } catch {
3070
+ mtimeMs = 0;
3071
+ }
3072
+ orphans.push({ path, repoPath, mtimeMs });
3073
+ }
3074
+ } catch {
3075
+ }
3076
+ try {
3077
+ const root = worktreesRoot(repoPath);
3078
+ if (existsSync7(root)) {
3079
+ for (const entry of readdirSync3(root, { withFileTypes: true })) {
3080
+ if (!entry.isDirectory()) continue;
3081
+ const path = join9(root, entry.name).replace(/\/$/, "");
3082
+ if (known.has(path) || seen.has(path)) continue;
3083
+ if (!existsSync7(join9(path, ".git"))) continue;
3084
+ seen.add(path);
3085
+ let mtimeMs = 0;
3086
+ try {
3087
+ mtimeMs = statSync(path).mtimeMs;
3088
+ } catch {
3089
+ mtimeMs = Date.now();
3090
+ }
3091
+ orphans.push({ path, repoPath, mtimeMs });
3092
+ }
3093
+ }
3094
+ } catch {
3095
+ }
3096
+ }
3097
+ return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
3098
+ }
3099
+ async function cleanupOrphanWorktrees(opts) {
3100
+ const settings = loadAppSettings();
3101
+ const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
3102
+ const orphans = await findOrphanWorktrees(opts?.repoPaths);
3103
+ const activeCount = listThreads().filter((t) => t.status !== "archived").length;
3104
+ const budget = Math.max(0, maxCount - activeCount);
3105
+ const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
3106
+ const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
3107
+ const removed = [];
3108
+ const kept = [];
3109
+ for (const orphan of orphans) {
3110
+ if (keep.has(orphan.path)) {
3111
+ kept.push(orphan.path);
3112
+ continue;
3113
+ }
3114
+ if (opts?.dryRun) {
3115
+ removed.push(orphan.path);
3116
+ continue;
3117
+ }
3118
+ try {
3119
+ await removeWorktree(orphan.repoPath, orphan.path);
3120
+ removed.push(orphan.path);
3121
+ } catch {
3122
+ kept.push(orphan.path);
3123
+ }
3124
+ }
3125
+ if (!opts?.dryRun) {
3126
+ updateAdvancedSettings({
3127
+ worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
3128
+ });
3129
+ }
3130
+ return { removed, kept, orphans };
3131
+ }
3132
+ function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
3133
+ const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
3134
+ const last = settings.advanced.worktreeLastCleanupAt;
3135
+ if (!last) return true;
3136
+ const elapsed = Date.now() - Date.parse(last);
3137
+ return elapsed >= intervalHours * 36e5;
3138
+ }
3139
+ function worktreeCleanupSettings() {
3140
+ const a = loadAppSettings().advanced;
3141
+ return {
3142
+ worktreeMaxCount: a.worktreeMaxCount,
3143
+ worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
3144
+ worktreeLastCleanupAt: a.worktreeLastCleanupAt,
3145
+ autoCleanupOrphans: a.autoCleanupOrphans
3146
+ };
3147
+ }
3148
+
3149
+ // src/store/history-cleanup.ts
3150
+ function stripArchivedTranscript(thread) {
3151
+ writeThread(
3152
+ {
3153
+ ...thread,
3154
+ attachments: [],
3155
+ pendingTurnAttachments: [],
3156
+ queueAttachments: [],
3157
+ compactedMessages: void 0,
3158
+ messages: [historyRetentionStubMessage(thread)]
3159
+ },
3160
+ { touch: false }
3161
+ );
3162
+ }
3163
+ function cleanupArchivedHistory(opts = {}) {
3164
+ const settings = loadAppSettings();
3165
+ if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
3166
+ return { stripped: [], purged: [] };
3167
+ }
3168
+ const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
3169
+ const nowMs = opts.nowMs ?? Date.now();
3170
+ const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
3171
+ maxCount: opts.maxCount ?? historyMaxCount(settings),
3172
+ maxDays: opts.maxDays ?? historyMaxDays(settings),
3173
+ nowMs
3174
+ });
3175
+ const stripped = [];
3176
+ const purged = [];
3177
+ if (opts.dryRun) {
3178
+ return { stripped: plan.stripIds, purged: plan.purgeIds };
3179
+ }
3180
+ for (const id of plan.stripIds) {
3181
+ const thread = readThread(id);
3182
+ if (!thread || thread.status !== "archived") continue;
3183
+ try {
3184
+ stripArchivedTranscript(thread);
3185
+ stripped.push(id);
3186
+ } catch {
3187
+ }
3188
+ }
3189
+ for (const id of plan.purgeIds) {
3190
+ const thread = readThread(id);
3191
+ if (!thread || thread.status !== "archived") continue;
3192
+ try {
3193
+ deleteThreadRecord(id);
3194
+ purged.push(id);
3195
+ } catch {
3196
+ }
3197
+ }
3198
+ if (stripped.length > 0 || purged.length > 0) {
3199
+ updateAdvancedSettings({
3200
+ historyLastCleanupAt: new Date(nowMs).toISOString()
3201
+ });
3202
+ }
3203
+ return { stripped, purged };
3204
+ }
3205
+ function shouldRunHistoryCleanup(settings = loadAppSettings()) {
3206
+ if (!autoCleanupHistoryEnabled(settings)) return false;
3207
+ const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
3208
+ const last = settings.advanced.historyLastCleanupAt;
3209
+ if (!last) return true;
3210
+ const elapsed = Date.now() - Date.parse(last);
3211
+ return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
3212
+ }
3213
+
3214
+ // src/git/apply-into-main.ts
3215
+ import { execa as execa3 } from "execa";
3216
+ async function applyThreadIntoMain(thread, opts) {
3217
+ const method = opts?.method ?? "merge";
3218
+ const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
3219
+ const status = await execa3("git", ["status", "--porcelain"], {
3220
+ cwd: thread.repoPath,
3221
+ reject: false
3222
+ });
3223
+ if (status.stdout.trim()) {
3224
+ throw new Error(
3225
+ `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
3226
+ );
3227
+ }
3228
+ const checkout = await execa3("git", ["checkout", target], {
3229
+ cwd: thread.repoPath,
3230
+ reject: false
3231
+ });
3232
+ if (checkout.exitCode !== 0) {
3233
+ throw new Error(
3234
+ `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
3235
+ );
3236
+ }
3237
+ if (method === "cherry-pick") {
3238
+ const log = await execa3(
3239
+ "git",
3240
+ ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
3241
+ { cwd: thread.repoPath, reject: false }
3242
+ );
3243
+ const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
3244
+ if (!shas.length) {
3245
+ return {
3246
+ applied: false,
3247
+ method,
3248
+ targetBranch: target,
3249
+ message: `No commits on ${thread.branchName} not already in ${target}`
3250
+ };
3251
+ }
3252
+ for (const sha of shas) {
3253
+ const cp = await execa3("git", ["cherry-pick", sha], {
3254
+ cwd: thread.repoPath,
3255
+ reject: false
3256
+ });
3257
+ if (cp.exitCode !== 0) {
3258
+ await execa3("git", ["cherry-pick", "--abort"], {
3259
+ cwd: thread.repoPath,
3260
+ reject: false
3261
+ });
3262
+ throw new Error(
3263
+ `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
3264
+ );
3265
+ }
3266
+ }
3267
+ return {
3268
+ applied: true,
3269
+ method,
3270
+ targetBranch: target,
3271
+ message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
3272
+ };
3273
+ }
3274
+ const merge = await execa3(
3275
+ "git",
3276
+ ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
3277
+ { cwd: thread.repoPath, reject: false }
3278
+ );
3279
+ if (merge.exitCode !== 0) {
3280
+ await execa3("git", ["merge", "--abort"], {
3281
+ cwd: thread.repoPath,
3282
+ reject: false
3283
+ });
3284
+ throw new Error(
3285
+ `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
3286
+ );
3228
3287
  }
3229
- const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
3230
- const progress = extra?.progress ?? snap.progress;
3231
3288
  return {
3232
- stillRunning,
3233
- ok,
3234
- failed: Boolean(failed && !stillRunning && !ok),
3235
- status,
3236
- id,
3237
- pid: snap.pid,
3238
- exitCode: snap.exitCode ?? void 0,
3239
- phase: lastProgressLine(progress),
3240
- delta,
3241
- progress,
3242
- hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
3289
+ applied: true,
3290
+ method: "merge",
3291
+ targetBranch: target,
3292
+ message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
3243
3293
  };
3244
3294
  }
3245
- async function sleepMs(ms) {
3246
- await new Promise((resolve2) => setTimeout(resolve2, ms));
3247
- }
3248
- async function waitForDetachedJob(cwd, id, opts) {
3249
- const jobId = sanitizeDetachedJobId(id);
3250
- const root = cwd.trim() || process.cwd();
3251
- const dir = resolveJobDir(root, jobId);
3252
- const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
3253
- const sleep = opts?.sleep ?? sleepMs;
3254
- if (!existsSync8(dir)) {
3255
- return {
3256
- stillRunning: false,
3257
- ok: false,
3258
- failed: true,
3259
- status: "failed",
3260
- id: jobId,
3261
- delta: "",
3262
- progress: "No detached job. Start one first.",
3263
- hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
3264
- };
3295
+
3296
+ // src/git/clone-repo.ts
3297
+ import { existsSync as existsSync8 } from "fs";
3298
+ import { basename as basename2, join as join10 } from "path";
3299
+ import { execa as execa4 } from "execa";
3300
+ async function cloneRepoIntoSideboard(opts) {
3301
+ const url = opts.url.trim();
3302
+ if (!url) throw new Error("Clone URL is required");
3303
+ let name = opts.name?.trim();
3304
+ if (!name) {
3305
+ const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
3306
+ name = leaf || "repo";
3265
3307
  }
3266
- const deadline = Date.now() + timeoutMs;
3267
- let snap = snapshotJob(dir);
3268
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
3269
- if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
3270
- return toResult(jobId, snap, {
3271
- failed: true,
3272
- progress: "No detached job. Start one first."
3273
- });
3308
+ name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
3309
+ const dest = join10(sideboardReposDir(), name);
3310
+ if (existsSync8(dest)) {
3311
+ const repoPath2 = await resolveRepoRoot(dest);
3312
+ const workspace2 = await ensureWorkspace(repoPath2);
3313
+ return { repoPath: repoPath2, workspace: workspace2 };
3274
3314
  }
3275
- while (Date.now() < deadline) {
3276
- snap = snapshotJob(dir);
3277
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
3278
- if (!snap.running) return toResult(jobId, snap, { failed: true });
3279
- await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
3315
+ const result = await execa4("git", ["clone", url, dest], { reject: false });
3316
+ if (result.exitCode !== 0) {
3317
+ throw new Error(
3318
+ `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
3319
+ );
3280
3320
  }
3281
- snap = snapshotJob(dir);
3282
- return toResult(jobId, snap);
3321
+ const repoPath = await resolveRepoRoot(dest);
3322
+ const workspace = await ensureWorkspace(repoPath);
3323
+ return { repoPath, workspace };
3283
3324
  }
3284
- function killProcessTree(pid, signal) {
3285
- if (!Number.isInteger(pid) || pid <= 0) return;
3286
- if (process.platform !== "win32") {
3287
- try {
3288
- process.kill(-pid, signal);
3289
- return;
3290
- } catch {
3291
- }
3292
- }
3325
+
3326
+ // src/types/thread.ts
3327
+ function isUnclaimedRunScriptRequest(req) {
3328
+ return Boolean(req && !req.claimedAt && !req.fulfilledAt && !req.error);
3329
+ }
3330
+ function isAdoptableRunScriptRequest(req) {
3331
+ return Boolean(req && !req.fulfilledAt && !req.error);
3332
+ }
3333
+
3334
+ // src/store/desktop-host.ts
3335
+ import { readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
3336
+ import { join as join11 } from "path";
3337
+ function desktopHostPidPath() {
3338
+ return join11(appDataDir(), "desktop-host.pid");
3339
+ }
3340
+ function claimDesktopHost(pid = process.pid) {
3341
+ writeFileSync6(desktopHostPidPath(), `${pid}
3342
+ `, "utf8");
3343
+ }
3344
+ function releaseDesktopHost(pid = process.pid) {
3345
+ if (readDesktopHostPid() !== pid) return;
3293
3346
  try {
3294
- process.kill(pid, signal);
3347
+ unlinkSync(desktopHostPidPath());
3295
3348
  } catch {
3296
3349
  }
3297
3350
  }
3298
- function appendJobLog(logFile, line) {
3351
+ function readDesktopHostPid() {
3299
3352
  try {
3300
- mkdirSync5(dirname4(logFile), { recursive: true });
3301
- const body = line.endsWith("\n") ? line : `${line}
3302
- `;
3303
- appendFileSync2(logFile, body);
3353
+ const pid = Number.parseInt(readFileSync7(desktopHostPidPath(), "utf8").trim(), 10);
3354
+ if (!Number.isFinite(pid) || pid <= 0) return null;
3355
+ return pid;
3304
3356
  } catch {
3357
+ return null;
3305
3358
  }
3306
3359
  }
3307
- async function stopDetachedJob(cwd, id, opts) {
3308
- const jobId = sanitizeDetachedJobId(id);
3309
- const root = cwd.trim() || process.cwd();
3310
- const dir = resolveJobDir(root, jobId);
3311
- const sleep = opts?.sleep ?? sleepMs;
3312
- const why = opts?.reason?.trim() ?? "";
3313
- if (!existsSync8(dir)) {
3314
- return {
3315
- stillRunning: false,
3316
- ok: false,
3317
- failed: true,
3318
- status: "failed",
3319
- id: jobId,
3320
- delta: "",
3321
- progress: "No detached job. Start one first.",
3322
- hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
3323
- stopped: false,
3324
- reason: "not-found"
3325
- };
3326
- }
3327
- let snap = snapshotJob(dir);
3328
- if (!snap.running) {
3329
- const result2 = toResult(jobId, snap);
3330
- return {
3331
- ...result2,
3332
- stopped: false,
3333
- reason: "not-running",
3334
- hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
3335
- };
3336
- }
3337
- appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
3338
- const targetPid = snap.pid;
3339
- if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
3340
- const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
3341
- const deadline = Date.now() + graceMs;
3342
- while (Date.now() < deadline && jobTreeAlive(targetPid)) {
3343
- await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
3344
- }
3345
- if (targetPid != null) {
3346
- killProcessTree(targetPid, "SIGKILL");
3347
- await sleep(100);
3348
- }
3349
- const exitFile = join11(dir, "exit");
3350
- if (!existsSync8(exitFile)) {
3351
- try {
3352
- writeFileSync6(exitFile, "1\n");
3353
- } catch {
3354
- }
3360
+ function pidAlive(pid) {
3361
+ try {
3362
+ process.kill(pid, 0);
3363
+ return true;
3364
+ } catch {
3365
+ return false;
3355
3366
  }
3356
- snap = snapshotJob(dir);
3357
- const stillRunning = jobTreeAlive(targetPid) || snap.running;
3358
- const result = toResult(jobId, snap, { failed: true });
3359
- const stopped = !stillRunning;
3360
- return {
3361
- ...result,
3362
- stillRunning,
3363
- ok: false,
3364
- failed: true,
3365
- status: stillRunning ? "running" : "failed",
3366
- stopped,
3367
- reason: stopped ? "stopped" : "still-running",
3368
- stopReason: why || void 0,
3369
- hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
3370
- };
3367
+ }
3368
+ function isDesktopHostAlive() {
3369
+ const pid = readDesktopHostPid();
3370
+ return pid != null && pidAlive(pid);
3371
+ }
3372
+ function isThisProcessDesktopHost() {
3373
+ return readDesktopHostPid() === process.pid && pidAlive(process.pid);
3374
+ }
3375
+ function thisProcessShouldDrainAgentQueues() {
3376
+ if (isThisProcessDesktopHost()) return true;
3377
+ return !isDesktopHostAlive();
3378
+ }
3379
+
3380
+ // src/orchestrator/child-halt.ts
3381
+ var HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
3382
+ function isIncompleteChildStatus(status) {
3383
+ return HALT_STATUSES.has(status);
3384
+ }
3385
+ function childHaltNotice(child, status) {
3386
+ const title = child.title?.trim() || "Untitled";
3387
+ const link = `[${title}](sideboard://thread/${child.id})`;
3388
+ const why = child.lastError?.trim();
3389
+ const extra = why ? ` lastError: ${why}` : "";
3390
+ return [
3391
+ `Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
3392
+ "This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
3393
+ ].join("\n");
3394
+ }
3395
+ function shouldNotifyParentAfterTurnError(opts) {
3396
+ return !opts.quotaFailoverHandled && !opts.crashContinued;
3397
+ }
3398
+ function shouldNotifyParentOfChildHalt(opts) {
3399
+ if (!isIncompleteChildStatus(opts.status)) return false;
3400
+ if (!opts.child.parentThreadId) return false;
3401
+ if (!opts.parent || opts.parent.status === "archived") return false;
3402
+ if (opts.parent.id === opts.child.id) return false;
3403
+ return isOrchestratorThread(opts.parent);
3404
+ }
3405
+ var notified = /* @__PURE__ */ new Set();
3406
+ function noticeKey(childId, status) {
3407
+ return `${childId}:${status}`;
3408
+ }
3409
+ function notifyParentOfChildHalt(child, status, send) {
3410
+ const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
3411
+ if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
3412
+ const key = noticeKey(child.id, status);
3413
+ if (notified.has(key)) return false;
3414
+ notified.add(key);
3415
+ const parentId = parent.id;
3416
+ void send(parentId, childHaltNotice(child, status)).catch(() => {
3417
+ notified.delete(key);
3418
+ });
3419
+ return true;
3371
3420
  }
3372
3421
 
3373
3422
  // src/threads/create.ts
@@ -4012,7 +4061,7 @@ async function createThread(input, _onSetupLine) {
4012
4061
  });
4013
4062
  await requireAgent(resolved.agent);
4014
4063
  if (input.cowboy) {
4015
- const { cowboyModeEnabled } = await import("./app-settings-I27UBVI4.js");
4064
+ const { cowboyModeEnabled } = await import("./app-settings-VOPXJMGV.js");
4016
4065
  if (!cowboyModeEnabled()) {
4017
4066
  throw new Error(
4018
4067
  "Cowboy mode is off. Enable it in Settings \u2192 Advanced."
@@ -4070,7 +4119,7 @@ async function createThread(input, _onSetupLine) {
4070
4119
  let sourceIsFork = false;
4071
4120
  if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
4072
4121
  if (isAbleTimeConnected() && getIssueSource() === "abletime") {
4073
- const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-VSV6AUBV.js");
4122
+ const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-CWIJ7INC.js");
4074
4123
  const task = await ensureAbleTimeTask({
4075
4124
  title: title?.trim() || "Untitled work",
4076
4125
  description: input.prompt?.trim() || void 0
@@ -4172,7 +4221,7 @@ async function createThread(input, _onSetupLine) {
4172
4221
  return readThread(thread.id) ?? thread;
4173
4222
  }
4174
4223
  async function listLinearIssues(agent, repoPath) {
4175
- const { getAdapter: getAdapter2 } = await import("./agents-CDQCQVWU.js");
4224
+ const { getAdapter: getAdapter2 } = await import("./agents-CPME6O2H.js");
4176
4225
  await requireAgent(agent, { requireLinear: true });
4177
4226
  const adapter = getAdapter2(agent);
4178
4227
  if (!adapter.listLinearIssues) {
@@ -4645,7 +4694,7 @@ async function adoptThread(input) {
4645
4694
  messages: input.messages ?? []
4646
4695
  });
4647
4696
  writeThread(thread);
4648
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-JX5R4V24.js");
4697
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-WYC5TLC3.js");
4649
4698
  await ensureWorkspace2(repoPath);
4650
4699
  const { ensureWorktreeSideboardIgnored: ensureWorktreeSideboardIgnored2 } = await import("./worktree-exclude-RXZPTCNH.js");
4651
4700
  await ensureWorktreeSideboardIgnored2(input.worktreePath);
@@ -7357,7 +7406,7 @@ function formatScheduledPrompt(name, prompt) {
7357
7406
  ${prompt}`;
7358
7407
  }
7359
7408
  async function defaultDeps() {
7360
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-7AJEFWGQ.js");
7409
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-YDPDMXUY.js");
7361
7410
  const orch = getOrchestrator2();
7362
7411
  return {
7363
7412
  findThread: (id) => findThreadByRef(id),
@@ -7682,7 +7731,7 @@ var Orchestrator = class {
7682
7731
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
7683
7732
  });
7684
7733
  }
7685
- const { autoCleanupOrphansEnabled } = await import("./app-settings-I27UBVI4.js");
7734
+ const { autoCleanupOrphansEnabled } = await import("./app-settings-VOPXJMGV.js");
7686
7735
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
7687
7736
  await cleanupOrphanWorktrees({ repoPaths });
7688
7737
  }
@@ -8029,7 +8078,7 @@ var Orchestrator = class {
8029
8078
  }
8030
8079
  await setup;
8031
8080
  if (skipSetup) return;
8032
- const { autoRunAfterSetupEnabled } = await import("./app-settings-I27UBVI4.js");
8081
+ const { autoRunAfterSetupEnabled } = await import("./app-settings-VOPXJMGV.js");
8033
8082
  if (autoRunAfterSetupEnabled()) {
8034
8083
  try {
8035
8084
  await this.startDev(threadId);
@@ -8280,7 +8329,8 @@ var Orchestrator = class {
8280
8329
  if (compact.didCompact) {
8281
8330
  thread = updateThread(threadId, {
8282
8331
  messages: compact.thread.messages,
8283
- sessionId: compact.thread.sessionId
8332
+ sessionId: compact.thread.sessionId,
8333
+ compactedMessages: compact.thread.compactedMessages
8284
8334
  });
8285
8335
  this.emit({
8286
8336
  type: "context_compacted",
@@ -8294,9 +8344,6 @@ var Orchestrator = class {
8294
8344
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
8295
8345
  thread = this.requireThread(threadId);
8296
8346
  }
8297
- if (this.stoppedTurns.has(threadId)) {
8298
- return;
8299
- }
8300
8347
  const promptText = typeof prompt === "string" ? prompt : "";
8301
8348
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
8302
8349
  const parked = takePendingTurnAttachments(thread);
@@ -8313,6 +8360,14 @@ var Orchestrator = class {
8313
8360
  agent: thread.agent,
8314
8361
  orch: isOrchestratorThread(thread)
8315
8362
  });
8363
+ if (this.stoppedTurns.has(threadId)) {
8364
+ const stopped = writeLiveStatus(threadId, "stopped");
8365
+ if (stopped?.status === "stopped") {
8366
+ this.emit({ type: "status_changed", threadId, status: "stopped" });
8367
+ }
8368
+ this.emit({ type: "turn_finished", threadId, exitCode: 1 });
8369
+ return;
8370
+ }
8316
8371
  thread = this.requireThread(threadId);
8317
8372
  const gitPrompt = expandCanonicalGitRequest(promptText);
8318
8373
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -8371,7 +8426,7 @@ var Orchestrator = class {
8371
8426
  ].filter(Boolean).join("\n\n") || void 0;
8372
8427
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
8373
8428
  const agentPrompt = [
8374
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
8429
+ planModeTurnInstruction(thread.planMode),
8375
8430
  orchestrationReminder,
8376
8431
  prGateDirective,
8377
8432
  injectedNoticeContext,
@@ -8393,7 +8448,7 @@ var Orchestrator = class {
8393
8448
  }
8394
8449
  const isBrightsy = fresh.agent === "brightsy";
8395
8450
  const isOrchestration = isOrchestratorThread(fresh);
8396
- const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-I27UBVI4.js");
8451
+ const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-VOPXJMGV.js");
8397
8452
  const gitAuthMode = getGithubGitAuthMode();
8398
8453
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
8399
8454
  githubSlug: await withTimeout(
@@ -8874,6 +8929,9 @@ var Orchestrator = class {
8874
8929
  }
8875
8930
  const preferredPorts = active?.ports?.length ? active.ports : active?.port != null ? [active.port] : void 0;
8876
8931
  if (active && !existing) {
8932
+ if (typeof active.pid === "number") {
8933
+ killDetachedRunTree(active.pid, thread.worktreePath);
8934
+ }
8877
8935
  killListenersOnPorts(collectActiveRunPorts([active]));
8878
8936
  this.syncWorktreeRuns(
8879
8937
  thread.worktreePath,
@@ -8946,7 +9004,8 @@ var Orchestrator = class {
8946
9004
  scriptName: resolvedName,
8947
9005
  port: handle.port,
8948
9006
  ports: handle.ports,
8949
- startedAt
9007
+ startedAt,
9008
+ ...typeof handle.pid === "number" && handle.pid > 1 ? { pid: handle.pid } : {}
8950
9009
  };
8951
9010
  const nextRuns = [
8952
9011
  ...latestShared.activeRuns.filter((r) => r.scriptName !== resolvedName),
@@ -9092,6 +9151,9 @@ var Orchestrator = class {
9092
9151
  }
9093
9152
  const run2 = shared.activeRuns.find((r) => r.scriptName === scriptName);
9094
9153
  if (!hadHandle2 && run2) {
9154
+ if (typeof run2.pid === "number") {
9155
+ killDetachedRunTree(run2.pid, thread.worktreePath);
9156
+ }
9095
9157
  killListenersOnPorts(collectActiveRunPorts([run2]));
9096
9158
  }
9097
9159
  const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
@@ -9113,6 +9175,11 @@ var Orchestrator = class {
9113
9175
  }
9114
9176
  }
9115
9177
  if (!hadHandle && shared.activeRuns.length > 0) {
9178
+ for (const run2 of shared.activeRuns) {
9179
+ if (typeof run2.pid === "number") {
9180
+ killDetachedRunTree(run2.pid, thread.worktreePath);
9181
+ }
9182
+ }
9116
9183
  killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
9117
9184
  }
9118
9185
  this.syncWorktreeRuns(thread.worktreePath, [], null);
@@ -9154,6 +9221,11 @@ var Orchestrator = class {
9154
9221
  (k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
9155
9222
  );
9156
9223
  if (!force && hasLiveHandle) continue;
9224
+ for (const run2 of shared.activeRuns) {
9225
+ if (typeof run2.pid === "number") {
9226
+ killDetachedRunTree(run2.pid, thread.worktreePath);
9227
+ }
9228
+ }
9157
9229
  killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
9158
9230
  if (shared.devPort != null) {
9159
9231
  killListenersOnPorts([shared.devPort]);
@@ -9667,7 +9739,7 @@ var Orchestrator = class {
9667
9739
  if (!titled.userSetTitle && meta.title && titled.title !== meta.title) {
9668
9740
  updateThread(thread.id, { title: meta.title });
9669
9741
  }
9670
- const { autoArchiveOnMergeEnabled } = await import("./app-settings-I27UBVI4.js");
9742
+ const { autoArchiveOnMergeEnabled } = await import("./app-settings-VOPXJMGV.js");
9671
9743
  const latest = this.requireThread(thread.id);
9672
9744
  if (!shouldAutoArchiveOnPrMerge({
9673
9745
  previousPrState: prevState || null,
@@ -9949,7 +10021,7 @@ var Orchestrator = class {
9949
10021
  this.enforceHistoryRetention();
9950
10022
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
9951
10023
  try {
9952
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-JX5R4V24.js");
10024
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-WYC5TLC3.js");
9953
10025
  await ensureWorkspace2(thread.repoPath);
9954
10026
  } catch {
9955
10027
  }
@@ -9976,7 +10048,7 @@ var Orchestrator = class {
9976
10048
  await runArchiveScript(thread.repoPath, thread.worktreePath);
9977
10049
  } catch {
9978
10050
  }
9979
- const { deleteBranchOnPurgeEnabled } = await import("./app-settings-I27UBVI4.js");
10051
+ const { deleteBranchOnPurgeEnabled } = await import("./app-settings-VOPXJMGV.js");
9980
10052
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
9981
10053
  await removeWorktree(thread.repoPath, thread.worktreePath, {
9982
10054
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -10003,7 +10075,7 @@ var Orchestrator = class {
10003
10075
  `Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
10004
10076
  );
10005
10077
  }
10006
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-AGVPKDBH.js");
10078
+ const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-DGDM7LGV.js");
10007
10079
  const slug = thread.worktreePath.split("/").pop();
10008
10080
  const dest = thread.worktreePath;
10009
10081
  await withRepoGitLock(thread.repoPath, async () => {
@@ -10113,7 +10185,7 @@ async function startOrchestration(opts) {
10113
10185
  sourceRef: "default",
10114
10186
  ...createOpts
10115
10187
  }).catch(async () => {
10116
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-AGVPKDBH.js");
10188
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-DGDM7LGV.js");
10117
10189
  const repo = await resolveRepoRoot2(repoPath);
10118
10190
  const def = await resolveDefaultBranch2(repo);
10119
10191
  return orch.createThread({
@@ -10123,7 +10195,7 @@ async function startOrchestration(opts) {
10123
10195
  repoPath: repo
10124
10196
  });
10125
10197
  });
10126
- const { updateThread: upd } = await import("./thread-store-AYPBEZT5.js");
10198
+ const { updateThread: upd } = await import("./thread-store-SCV2IZYS.js");
10127
10199
  const updated = upd(thread.id, {
10128
10200
  sourceType: "orchestration",
10129
10201
  sourceRef: goal
@@ -10228,6 +10300,9 @@ export {
10228
10300
  findLastBrightsyContextSummary,
10229
10301
  messagesSinceLastBrightsyContextSummary,
10230
10302
  buildBrightsySessionSeed,
10303
+ compactArchiveProse,
10304
+ messagesForCompactArchive,
10305
+ appendCompactedMessages,
10231
10306
  applyCompaction,
10232
10307
  lastRequestOccupancy,
10233
10308
  shouldResetSessionForOccupancy,
@@ -10263,6 +10338,11 @@ export {
10263
10338
  worktreeRunPortsPath,
10264
10339
  loadWorktreeRunPorts,
10265
10340
  saveWorktreeRunPorts,
10341
+ mcpWaitForTurnTimeoutMs,
10342
+ mcpWaitTaskHint,
10343
+ mcpWaitForJobTimeoutMs,
10344
+ waitForDetachedJob,
10345
+ stopDetachedJob,
10266
10346
  PORT_RANGE_SIZE,
10267
10347
  readWorktreeInclude,
10268
10348
  resolveFilesToCopy,
@@ -10272,6 +10352,8 @@ export {
10272
10352
  killableListenerPids,
10273
10353
  listenerCwd,
10274
10354
  isOtherWorktreeListenerCwd,
10355
+ isThisWorktreeListenerCwd,
10356
+ killDetachedRunTree,
10275
10357
  killListenersOnPorts,
10276
10358
  killStaleWorktreeListeners,
10277
10359
  runSetupScript,
@@ -10297,11 +10379,6 @@ export {
10297
10379
  shouldRunHistoryCleanup,
10298
10380
  applyThreadIntoMain,
10299
10381
  cloneRepoIntoSideboard,
10300
- mcpWaitForTurnTimeoutMs,
10301
- mcpWaitTaskHint,
10302
- mcpWaitForJobTimeoutMs,
10303
- waitForDetachedJob,
10304
- stopDetachedJob,
10305
10382
  detectAgents,
10306
10383
  requireAgent,
10307
10384
  createThread,