@sideboard-ai/core 0.1.242 → 0.1.244

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-PBRIHUYM.js → agents-BQZ2CHXK.js} +9 -5
  8. package/dist/{agents-CDQCQVWU.js → agents-Q23R4WOR.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-3Q4PVZ3U.js → chunk-2QRU4IFI.js} +32 -5
  12. package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
  13. package/dist/{chunk-LRMJ6V6U.js → chunk-4HMV53AB.js} +1506 -1465
  14. package/dist/{chunk-OKHOYI53.js → chunk-576S3HJ7.js} +5 -5
  15. package/dist/{chunk-TB6UMHIF.js → chunk-5ZFULOTC.js} +10 -2
  16. package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
  17. package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
  18. package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
  19. package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
  20. package/dist/{chunk-QXYWLO6W.js → chunk-CYYQ6577.js} +28 -5
  21. package/dist/{chunk-O3UAQAYA.js → chunk-E6C5NR46.js} +10 -2
  22. package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
  23. package/dist/{chunk-BILEJG3V.js → chunk-GFRC6JQN.js} +5 -5
  24. package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
  25. package/dist/{chunk-DJU54RYW.js → chunk-PGPSIEMW.js} +13 -0
  26. package/dist/{chunk-P3DCZRER.js → chunk-PPJ5JTWA.js} +1 -0
  27. package/dist/{chunk-5ELC3KK3.js → chunk-QNJXJIWJ.js} +5 -5
  28. package/dist/{chunk-JFOPOMK7.js → chunk-WDDUIGFK.js} +13 -0
  29. package/dist/{chunk-IVXPXYH2.js → chunk-WURF67ID.js} +5 -5
  30. package/dist/{chunk-R3YKDTVE.js → chunk-YJJ2A5CI.js} +1572 -1529
  31. package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
  32. package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-WQIICPT6.js} +4 -4
  33. package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-ZBYRREHD.js} +4 -4
  34. package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-2N6KISDO.js} +4 -4
  35. package/dist/{global-workspace-TD5SARNW.js → global-workspace-3SQU4GL4.js} +4 -4
  36. package/dist/index.cjs +590 -470
  37. package/dist/index.d.cts +50 -3
  38. package/dist/index.d.ts +50 -3
  39. package/dist/index.js +73 -39
  40. package/dist/mcp/run-stdio.cjs +1649 -1552
  41. package/dist/mcp/run-stdio.js +51 -37
  42. package/dist/{orchestrator-VYNPNHY4.js → orchestrator-ABF6QFE6.js} +7 -7
  43. package/dist/{orchestrator-66ZFQHW5.js → orchestrator-ZJ6SYGW7.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-JX5R4V24.js → workspaces-CFI7XVAE.js} +4 -4
  49. package/dist/{workspaces-4SFSCYKT.js → workspaces-QLKWW3O2.js} +4 -4
  50. package/dist/{worktree-AGVPKDBH.js → worktree-4UAOZUJ4.js} +5 -3
  51. package/dist/{worktree-Y5FES5D6.js → worktree-PK3QETXO.js} +5 -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-CYYQ6577.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-GFRC6JQN.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-E6C5NR46.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-2N6KISDO.js");
445
446
  if (isGlobalThread2(thread)) {
446
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-5UINTEA6.js");
447
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-WQIICPT6.js");
447
448
  ensureGlobalCoordinatorCwd2(
448
449
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
449
450
  );
@@ -634,9 +635,9 @@ function gitActionExpansion(action, base) {
634
635
  case "commit-push":
635
636
  return "Commit any uncommitted work with a purpose-stating message, then push to origin (`git push -u origin HEAD`; this updates an existing PR when one is linked). Do not start a checks loop unless a goal was given.";
636
637
  case "create-draft":
637
- return "Commit, push, then create a draft PR with `gh pr create --draft -R <origin-owner/name>` (title/body from the change purpose). Update the existing PR instead if one is linked.";
638
+ return "Commit, push, then create a draft PR with `gh pr create --draft --assignee @me -R <origin-owner/name>` (title/body from the change purpose). Update the existing PR instead if one is linked. Use a different `--assignee` only if the user named someone else.";
638
639
  case "create-web":
639
- return "Commit, push, then `gh pr create --web -R <origin-owner/name>`.";
640
+ return "Commit, push, then `gh pr create --web --assignee @me -R <origin-owner/name>`. Use a different `--assignee` only if the user named someone else.";
640
641
  case "resolve-conflicts":
641
642
  return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
642
643
  case "ready-for-review":
@@ -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,1664 +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("#"));
1740
- }
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 {
1770
- }
1771
- if (defaults.length) return defaults;
1772
- return [".env.local", ".env"];
1773
- }
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);
1784
- }
1785
- return copied;
1786
- }
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");
1791
- 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
- }
1809
- } catch {
1810
- }
1811
- cachedLoginEnv = { ...process.env };
1812
- return { ...cachedLoginEnv };
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);
1813
1774
  }
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
- }
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;
1840
1795
  }
1841
- applyWorktreePkgCacheEnv(env, opts.worktreePath);
1842
- return env;
1843
1796
  }
1844
- function pipeLines(stream, onLine) {
1845
- if (!stream || !onLine) return;
1846
- const rl = createInterface2({ input: stream });
1847
- rl.on("line", onLine);
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);
1848
1809
  }
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);
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)}`);
1855
1814
  }
1856
- return pids;
1815
+ return s;
1857
1816
  }
1858
- function listUnixListenerPids(port, selfPid) {
1817
+ function jobAlive(pid) {
1818
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1859
1819
  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);
1820
+ process.kill(pid, 0);
1821
+ return true;
1866
1822
  } catch {
1867
- return [];
1823
+ return false;
1868
1824
  }
1869
1825
  }
1870
- function listenerCwd(pid) {
1871
- if (!Number.isFinite(pid) || pid <= 0) return null;
1826
+ function processGroupAlive(pgid) {
1827
+ if (process.platform === "win32") return false;
1828
+ if (!Number.isInteger(pgid) || pgid <= 1) return false;
1872
1829
  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 {
1830
+ process.kill(-pgid, 0);
1831
+ return true;
1832
+ } catch (err) {
1833
+ return err && typeof err === "object" && "code" in err && err.code === "EPERM";
1882
1834
  }
1883
- return null;
1884
1835
  }
1885
- function realpathOrResolve(path) {
1886
- try {
1887
- return realpathSync(path);
1888
- } catch {
1889
- return resolve(path);
1890
- }
1836
+ function jobTreeAlive(pid) {
1837
+ if (pid == null) return false;
1838
+ return jobAlive(pid) || processGroupAlive(pid);
1891
1839
  }
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;
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;
1902
1844
  }
1903
- function signalPid(pid, signal) {
1904
- try {
1905
- process.kill(pid, signal);
1906
- } catch {
1907
- }
1845
+ function jobDir(root, id, legacy = false) {
1846
+ return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
1908
1847
  }
1909
- function isThisWorktreeListenerCwd(cwd, worktreePath) {
1910
- const wt = realpathOrResolve(worktreePath);
1911
- const c = realpathOrResolve(cwd);
1912
- return c === wt || c.startsWith(wt + sep);
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;
1913
1854
  }
1914
- function killDetachedRunTree(pid, worktreePath) {
1915
- if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
1916
- const cwd = listenerCwd(pid);
1917
- if (!cwd || !isThisWorktreeListenerCwd(cwd, worktreePath)) return;
1918
- try {
1919
- process.kill(-pid, "SIGTERM");
1920
- } catch {
1921
- signalPid(pid, "SIGTERM");
1922
- }
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);
1923
1859
  }
1924
- function killListenersOnPorts(ports) {
1925
- const selfPid = process.pid;
1926
- for (const port of ports) {
1927
- if (!Number.isFinite(port) || port <= 0) continue;
1928
- try {
1929
- if (process.platform === "win32") {
1930
- execFileSync(
1931
- "powershell",
1932
- [
1933
- "-NoProfile",
1934
- "-Command",
1935
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
1936
- ],
1937
- { stdio: "ignore", timeout: 3e3 }
1938
- );
1939
- continue;
1940
- }
1941
- for (const pid of listUnixListenerPids(port, selfPid)) {
1942
- signalPid(pid, "SIGTERM");
1943
- }
1944
- } catch {
1945
- }
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);
1946
1872
  }
1873
+ return running.sort();
1947
1874
  }
1948
- function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
1949
- if (!worktreePath.trim()) {
1950
- killListenersOnPorts(ports);
1951
- return;
1952
- }
1953
- if (process.platform === "win32") {
1954
- killListenersOnPorts(ports);
1955
- return;
1956
- }
1957
- const selfPid = process.pid;
1958
- for (const port of ports) {
1959
- if (!Number.isFinite(port) || port <= 0) continue;
1960
- for (const pid of listUnixListenerPids(port, selfPid)) {
1961
- const cwd = listenerCwd(pid);
1962
- if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
1963
- signalPid(pid, signal);
1964
- }
1965
- }
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);
1966
1881
  }
1967
- function killScriptTree(child, ports = []) {
1968
- const pid = child.pid;
1969
- if (pid) {
1970
- try {
1971
- if (process.platform === "win32") {
1972
- void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
1973
- } else {
1974
- process.kill(-pid, "SIGTERM");
1975
- setTimeout(() => {
1976
- try {
1977
- process.kill(-pid, "SIGKILL");
1978
- } catch {
1979
- }
1980
- }, 2500).unref?.();
1981
- }
1982
- } catch {
1983
- try {
1984
- child.kill("SIGTERM");
1985
- } catch {
1986
- }
1987
- }
1988
- }
1989
- killListenersOnPorts(ports);
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");
1990
1886
  }
1991
- async function spawnWorkspaceScript(command, opts) {
1992
- const loginEnv = await captureLoginEnv();
1993
- const env = buildWorkspaceScriptEnv(
1994
- {
1995
- worktreePath: opts.worktreePath,
1996
- repoPath: opts.repoPath,
1997
- workspaceName: opts.workspaceName,
1998
- defaultBranch: opts.defaultBranch,
1999
- ports: opts.ports
2000
- },
2001
- loginEnv
2002
- );
2003
- try {
2004
- mergeAgentGitAuthEnv(
2005
- env,
2006
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
2007
- );
2008
- } catch {
2009
- }
2010
- if (opts.heldPorts?.length) {
2011
- await Promise.all(opts.heldPorts.map((h) => h.release()));
2012
- }
2013
- const shell = process.platform === "darwin" ? "zsh" : "bash";
2014
- const child = execa2(shell, ["-lc", command], {
2015
- cwd: opts.worktreePath,
2016
- reject: false,
2017
- env,
2018
- // Own process group so Stop can tear down the whole tree (not just the shell).
2019
- // There is no settings.toml `stop=` / teardown hook for run scripts.
2020
- ...process.platform === "win32" ? {} : { detached: true }
2021
- });
2022
- pipeLines(child.stdout, opts.onLine);
2023
- pipeLines(child.stderr, opts.onLine);
2024
- const ports = opts.ports ?? [];
2025
- return {
2026
- pid: child.pid,
2027
- kill: () => killScriptTree(child, ports),
2028
- done: child.then((r) => r.exitCode ?? null),
2029
- child
2030
- };
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");
2031
1898
  }
2032
- async function attachAbort(handle, signal) {
2033
- if (signal) {
2034
- const onAbort = () => handle.kill();
2035
- if (signal.aborted) onAbort();
2036
- else signal.addEventListener("abort", onAbort, { once: true });
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)}`;
2037
1907
  }
2038
- return handle.done;
1908
+ return out;
2039
1909
  }
2040
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
2041
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
2042
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
2043
- const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
2044
- onLine?.(`[setup] ${source ?? "settings.toml"}`);
2045
- const handle = await spawnWorkspaceScript(settings.setup, {
2046
- worktreePath,
2047
- repoPath,
2048
- defaultBranch: opts?.defaultBranch,
2049
- onLine
2050
- });
2051
- const exitCode = await attachAbort(handle, opts?.signal);
2052
- return {
2053
- ran: true,
2054
- exitCode,
2055
- source,
2056
- kill: handle.kill
2057
- };
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(" ");
2058
1918
  }
2059
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
2060
- const found = findConventionSetup(worktreePath, repoPath);
2061
- if (!found) return { ran: false, exitCode: null, source: null };
2062
- onLine?.(`[setup] ${found.source}`);
2063
- const handle = await spawnWorkspaceScript(found.command, {
2064
- worktreePath,
2065
- repoPath,
2066
- defaultBranch: opts?.defaultBranch,
2067
- onLine
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(" ");
1925
+ }
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);
2068
1932
  });
2069
- const exitCode = await attachAbort(handle, opts?.signal);
2070
- return {
2071
- ran: true,
2072
- exitCode,
2073
- source: found.source,
2074
- kill: handle.kill
2075
- };
2076
1933
  }
2077
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
2078
- await ensureWorktreeSideboardIgnored(worktreePath);
2079
- const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
2080
- if (review.wrote) {
2081
- onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
2082
- }
2083
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
2084
- if (!setup.ran) {
2085
- 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
+ };
2086
1946
  }
2087
- if (!setup.ran) {
2088
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
1947
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
1948
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
2089
1949
  }
2090
- return setup;
1950
+ return { action: "none" };
2091
1951
  }
2092
- async function runArchiveScript(repoPath, worktreePath, onLine) {
2093
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
2094
- if (!settings?.archive) return { ran: false, exitCode: null };
2095
- const handle = await spawnWorkspaceScript(settings.archive, {
2096
- worktreePath,
2097
- repoPath,
2098
- onLine
2099
- });
2100
- const exitCode = await handle.done;
2101
- return { ran: true, exitCode };
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");
2102
1956
  }
2103
- function listRunScripts(worktreePath, repoPath) {
2104
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
2105
- if (!settings?.runScripts.length) return [];
2106
- return settings.runScripts.filter((s) => {
2107
- if (!s.availableIn?.length) return true;
2108
- return s.availableIn.includes("local");
2109
- });
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;
2110
1960
  }
2111
- function getDefaultRunScript(worktreePath, repoPath) {
2112
- const scripts = listRunScripts(worktreePath, repoPath);
2113
- if (!scripts.length) return null;
2114
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
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;
2115
1966
  }
2116
- function getRunScript(worktreePath, repoPath, name) {
2117
- const scripts = listRunScripts(worktreePath, repoPath);
2118
- if (!scripts.length) return null;
2119
- if (name) {
2120
- return scripts.find((s) => s.name === name) ?? null;
2121
- }
2122
- return getDefaultRunScript(worktreePath, repoPath);
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 };
2123
1972
  }
2124
- function getRunMode(worktreePath, repoPath) {
2125
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
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
+ };
2126
1984
  }
2127
- async function reservePort() {
2128
- return new Promise((resolve2, reject) => {
2129
- const server = createServer();
2130
- let released = false;
2131
- const release = () => new Promise((res) => {
2132
- if (released) {
2133
- res();
2134
- return;
2135
- }
2136
- released = true;
2137
- server.close(() => res());
2138
- });
2139
- server.listen(0, "127.0.0.1", () => {
2140
- const addr = server.address();
2141
- if (!addr || typeof addr === "string") {
2142
- void release().then(() => reject(new Error("Failed to allocate port")));
2143
- return;
2144
- }
2145
- resolve2({ port: addr.port, release });
2146
- });
2147
- server.on("error", reject);
2148
- });
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
+ };
2149
2011
  }
2150
- async function allocatePort() {
2151
- const held = await reservePort();
2152
- await held.release();
2153
- return held.port;
2012
+ async function sleepMs(ms) {
2013
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
2154
2014
  }
2155
- async function tryReservePort(port) {
2156
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
2157
- return new Promise((resolve2) => {
2158
- const server = createServer();
2159
- let released = false;
2160
- const release = () => new Promise((res) => {
2161
- if (released) {
2162
- res();
2163
- return;
2164
- }
2165
- released = true;
2166
- server.close(() => res());
2167
- });
2168
- server.once("error", () => {
2169
- try {
2170
- server.close();
2171
- } catch {
2172
- }
2173
- resolve2(null);
2174
- });
2175
- server.listen(port, "127.0.0.1", () => {
2176
- resolve2({ port, release });
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."
2177
2040
  });
2178
- });
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);
2179
2050
  }
2180
- async function tryReservePorts(ports) {
2181
- const held = [];
2182
- for (const port of ports) {
2183
- const next = await tryReservePort(port);
2184
- if (!next) {
2185
- await Promise.all(held.map((h) => h.release()));
2186
- 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 {
2187
2058
  }
2188
- held.push(next);
2189
2059
  }
2190
- return held;
2191
- }
2192
- function normalizePreferredPorts(ports, size) {
2193
- if (!ports?.length) return [];
2194
- const seen = /* @__PURE__ */ new Set();
2195
- const out = [];
2196
- for (const p of ports) {
2197
- if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
2198
- seen.add(p);
2199
- out.push(p);
2200
- if (out.length >= size) break;
2060
+ try {
2061
+ process.kill(pid, signal);
2062
+ } catch {
2201
2063
  }
2202
- return out;
2203
- }
2204
- function delay(ms) {
2205
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2206
2064
  }
2207
- async function fillPortRange(held, size) {
2208
- if (held.length >= size) return held;
2209
- const base = held[0]?.port;
2210
- const taken = new Set(held.map((h) => h.port));
2065
+ function appendJobLog(logFile, line) {
2211
2066
  try {
2212
- let offset = 0;
2213
- while (held.length < size) {
2214
- const candidate = base != null ? base + offset : null;
2215
- offset += 1;
2216
- if (candidate != null && taken.has(candidate)) continue;
2217
- const next = candidate != null ? await tryReservePort(candidate) : null;
2218
- if (next) {
2219
- held.push(next);
2220
- taken.add(next.port);
2221
- } else {
2222
- const ephemeral = await reservePort();
2223
- held.push(ephemeral);
2224
- taken.add(ephemeral.port);
2225
- }
2226
- }
2227
- return held;
2228
- } catch (err) {
2229
- await Promise.all(held.map((h) => h.release()));
2230
- throw err;
2067
+ mkdirSync4(dirname3(logFile), { recursive: true });
2068
+ const body = line.endsWith("\n") ? line : `${line}
2069
+ `;
2070
+ appendFileSync2(logFile, body);
2071
+ } catch {
2231
2072
  }
2232
2073
  }
2233
- async function tryReserveAvailablePorts(ports) {
2234
- const held = [];
2235
- for (const port of ports) {
2236
- const next = await tryReservePort(port);
2237
- 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
+ };
2238
2093
  }
2239
- return held;
2240
- }
2241
- async function reclaimPreferredPorts(ports, worktreePath) {
2242
- const primary = ports[0];
2243
- if (primary == null) return null;
2244
- const takePrimary = async () => {
2245
- const held2 = await tryReserveAvailablePorts(ports);
2246
- if (held2.some((h) => h.port === primary)) return held2;
2247
- await Promise.all(held2.map((h) => h.release()));
2248
- return null;
2249
- };
2250
- let held = await takePrimary();
2251
- if (held) return held;
2252
- if (worktreePath) {
2253
- killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
2254
- } else {
2255
- 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
+ };
2256
2103
  }
2257
- for (let i = 0; i < RECLAIM_TRIES; i++) {
2258
- await delay(RECLAIM_RETRY_DELAY_MS);
2259
- held = await takePrimary();
2260
- if (held) return held;
2261
- if (i === 2 && worktreePath) {
2262
- killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
2263
- }
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())));
2264
2111
  }
2265
- return null;
2266
- }
2267
- async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
2268
- const preferred = normalizePreferredPorts(opts?.preferred, size);
2269
- if (preferred.length) {
2270
- const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
2271
- if (reused) return fillPortRange(reused, size);
2272
- }
2273
- const held = [await reservePort()];
2274
- return fillPortRange(held, size);
2275
- }
2276
- async function allocatePortRange(size = PORT_RANGE_SIZE) {
2277
- const held = await reservePortRange(size);
2278
- const ports = held.map((h) => h.port);
2279
- await Promise.all(held.map((h) => h.release()));
2280
- return ports;
2281
- }
2282
- async function startDevServer(repoPath, worktreePath, onLine, opts) {
2283
- const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
2284
- if (!script) return null;
2285
- const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
2286
- const held = await reservePortRange(PORT_RANGE_SIZE, {
2287
- preferred: preferred ?? void 0,
2288
- worktreePath
2289
- });
2290
- const ports = held.map((h) => h.port);
2291
- saveWorktreeRunPorts(worktreePath, script.name, ports);
2292
- try {
2293
- const handle = await spawnWorkspaceScript(script.command, {
2294
- worktreePath,
2295
- repoPath,
2296
- ports,
2297
- heldPorts: held,
2298
- defaultBranch: opts?.defaultBranch,
2299
- onLine
2300
- });
2301
- return {
2302
- pid: handle.pid,
2303
- port: ports[0],
2304
- ports,
2305
- scriptName: script.name,
2306
- kill: handle.kill,
2307
- done: handle.done
2308
- };
2309
- } catch (err) {
2310
- await Promise.all(held.map((h) => h.release()));
2311
- throw err;
2112
+ if (targetPid != null) {
2113
+ killProcessTree(targetPid, "SIGKILL");
2114
+ await sleep(100);
2312
2115
  }
2313
- }
2314
-
2315
- // src/store/setup-log.ts
2316
- import { createHash } from "crypto";
2317
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2318
- import { join as join6 } from "path";
2319
- var EMPTY = {
2320
- output: "",
2321
- running: false,
2322
- exitCode: null,
2323
- source: null
2324
- };
2325
- var MAX_SETUP_LOG_CHARS = 256e3;
2326
- function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
2327
- const next = prev ? `${prev}
2328
- ${line}` : line;
2329
- if (next.length <= max) return next;
2330
- const cut = next.slice(next.length - max);
2331
- const nl = cut.indexOf("\n");
2332
- return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
2333
- }
2334
- var memory = /* @__PURE__ */ new Map();
2335
- var persistTimers = /* @__PURE__ */ new Map();
2336
- function fromSnapshot(snap) {
2337
- return {
2338
- chunks: snap.output ? [snap.output] : [],
2339
- length: snap.output.length,
2340
- running: snap.running,
2341
- exitCode: snap.exitCode,
2342
- source: snap.source
2343
- };
2344
- }
2345
- function materialize(mem) {
2346
- let output = mem.chunks.join("\n");
2347
- if (output.length > MAX_SETUP_LOG_CHARS) {
2348
- output = appendSetupOutput("", output);
2349
- mem.chunks = [output];
2350
- mem.length = output.length;
2116
+ const exitFile = join5(dir, "exit");
2117
+ if (!existsSync3(exitFile)) {
2118
+ try {
2119
+ writeFileSync3(exitFile, "1\n");
2120
+ } catch {
2121
+ }
2351
2122
  }
2123
+ snap = snapshotJob(dir);
2124
+ const stillRunning = jobTreeAlive(targetPid) || snap.running;
2125
+ const result = toResult(jobId, snap, { failed: true });
2126
+ const stopped = !stillRunning;
2352
2127
  return {
2353
- output,
2354
- running: mem.running,
2355
- exitCode: mem.exitCode,
2356
- 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
2357
2137
  };
2358
2138
  }
2359
- function setupLogPath(threadId) {
2360
- return join6(threadsDir(), `${threadId}.setup.log.json`);
2361
- }
2362
- function setupLogKeyForWorktree(worktreePath) {
2363
- const norm = normalizeWorktreePath(worktreePath);
2364
- return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
2365
- }
2366
- function emptySetupLog() {
2367
- 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);
2368
2147
  }
2369
- function mergeSetupOutput(prev, incoming) {
2370
- if (!prev) return incoming;
2371
- if (!incoming) return prev;
2372
- if (prev === incoming) return prev;
2373
- if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
2374
- if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
2375
- 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("#"));
2376
2152
  }
2377
- var PERSIST_DEBOUNCE_MS = 500;
2378
- function persistNow(threadId) {
2379
- const timer = persistTimers.get(threadId);
2380
- if (timer) {
2381
- clearTimeout(timer);
2382
- 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)];
2383
2173
  }
2384
- const mem = memory.get(threadId);
2385
- if (!mem) return;
2174
+ const defaults = [];
2386
2175
  try {
2387
- writeFileSync3(
2388
- setupLogPath(threadId),
2389
- `${JSON.stringify(materialize(mem))}
2390
- `,
2391
- "utf8"
2392
- );
2176
+ for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
2177
+ if (entry.isFile() && entry.name.startsWith(".env")) {
2178
+ defaults.push(entry.name);
2179
+ }
2180
+ }
2393
2181
  } catch {
2394
2182
  }
2183
+ if (defaults.length) return defaults;
2184
+ return [".env.local", ".env"];
2395
2185
  }
2396
- function schedulePersist(threadId) {
2397
- if (persistTimers.has(threadId)) return;
2398
- persistTimers.set(
2399
- threadId,
2400
- setTimeout(() => {
2401
- persistTimers.delete(threadId);
2402
- persistNow(threadId);
2403
- }, PERSIST_DEBOUNCE_MS)
2404
- );
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;
2405
2198
  }
2406
- function loadFromDisk(threadId) {
2407
- const path = setupLogPath(threadId);
2408
- 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");
2409
2203
  try {
2410
- const raw = JSON.parse(readFileSync4(path, "utf8"));
2411
- return {
2412
- output: typeof raw.output === "string" ? raw.output : "",
2413
- running: Boolean(raw.running),
2414
- exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
2415
- source: typeof raw.source === "string" ? raw.source : null
2416
- };
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
+ }
2417
2221
  } catch {
2418
- return null;
2419
2222
  }
2223
+ cachedLoginEnv = { ...process.env };
2224
+ return { ...cachedLoginEnv };
2420
2225
  }
2421
- function memFor(threadId) {
2422
- const cached = memory.get(threadId);
2423
- if (cached) return cached;
2424
- const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
2425
- memory.set(threadId, mem);
2426
- return mem;
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
+ }
2252
+ }
2253
+ applyWorktreePkgCacheEnv(env, opts.worktreePath);
2254
+ return env;
2427
2255
  }
2428
- function readSetupLog(threadId) {
2429
- const cached = memory.get(threadId);
2430
- if (cached) return materialize(cached);
2431
- const disk = loadFromDisk(threadId);
2432
- if (disk) {
2433
- memory.set(threadId, fromSnapshot(disk));
2434
- return disk;
2435
- }
2436
- return emptySetupLog();
2437
- }
2438
- function beginSetupLog(threadId) {
2439
- const mem = {
2440
- chunks: [],
2441
- length: 0,
2442
- running: true,
2443
- exitCode: null,
2444
- source: null
2445
- };
2446
- memory.set(threadId, mem);
2447
- persistNow(threadId);
2448
- return materialize(mem);
2449
- }
2450
- function appendSetupLog(threadId, line) {
2451
- const mem = memFor(threadId);
2452
- mem.chunks.push(line);
2453
- mem.length += line.length + 1;
2454
- mem.running = true;
2455
- if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
2456
- schedulePersist(threadId);
2457
- }
2458
- function finishSetupLog(threadId, exitCode, source) {
2459
- const mem = memFor(threadId);
2460
- mem.running = false;
2461
- mem.exitCode = exitCode;
2462
- mem.source = source ?? mem.source;
2463
- persistNow(threadId);
2464
- return materialize(mem);
2465
- }
2466
-
2467
- // src/store/run-log.ts
2468
- import { createHash as createHash2 } from "crypto";
2469
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2470
- import { join as join7 } from "path";
2471
- var EMPTY2 = {
2472
- output: "",
2473
- running: false,
2474
- exitCode: null,
2475
- scriptName: null
2476
- };
2477
- var MAX_RUN_LOG_CHARS = 256e3;
2478
- var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
2479
- var memory2 = /* @__PURE__ */ new Map();
2480
- var persistTimers2 = /* @__PURE__ */ new Map();
2481
- function fromSnapshot2(snap) {
2482
- return {
2483
- chunks: snap.output ? [snap.output] : [],
2484
- length: snap.output.length,
2485
- running: snap.running,
2486
- exitCode: snap.exitCode,
2487
- scriptName: snap.scriptName
2488
- };
2256
+ function pipeLines(stream, onLine) {
2257
+ if (!stream || !onLine) return;
2258
+ const rl = createInterface2({ input: stream });
2259
+ rl.on("line", onLine);
2489
2260
  }
2490
- function materialize2(mem) {
2491
- let output = mem.chunks.join("\n");
2492
- if (output.length > MAX_RUN_LOG_CHARS) {
2493
- output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
2494
- mem.chunks = [output];
2495
- 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);
2496
2267
  }
2497
- return {
2498
- output,
2499
- running: mem.running,
2500
- exitCode: mem.exitCode,
2501
- scriptName: mem.scriptName
2502
- };
2503
- }
2504
- function safeScriptToken(scriptName) {
2505
- const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
2506
- return token || "dev";
2268
+ return pids;
2507
2269
  }
2508
- function runLogPath(key) {
2509
- return join7(threadsDir(), `${key}.run.log.json`);
2270
+ function listUnixListenerPids(port, selfPid) {
2271
+ try {
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);
2278
+ } catch {
2279
+ return [];
2280
+ }
2510
2281
  }
2511
- function runLogKeyForWorktree(worktreePath, scriptName) {
2512
- const norm = normalizeWorktreePath(worktreePath);
2513
- const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
2514
- return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
2282
+ function listenerCwd(pid) {
2283
+ if (!Number.isFinite(pid) || pid <= 0) return null;
2284
+ try {
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
+ }
2293
+ } catch {
2294
+ }
2295
+ return null;
2515
2296
  }
2516
- function emptyRunLog() {
2517
- return { ...EMPTY2 };
2297
+ function realpathOrResolve(path) {
2298
+ try {
2299
+ return realpathSync(path);
2300
+ } catch {
2301
+ return resolve(path);
2302
+ }
2518
2303
  }
2519
- function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
2520
- const max = Math.max(1, tailChars);
2521
- const outputChars = output.length;
2522
- if (outputChars <= max) return { output, truncated: false, outputChars };
2523
- const cut = output.slice(outputChars - max);
2524
- const nl = cut.indexOf("\n");
2525
- const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
2526
- return { output: text, truncated: true, outputChars };
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;
2527
2314
  }
2528
- var PERSIST_DEBOUNCE_MS2 = 500;
2529
- function persistNow2(key) {
2530
- const timer = persistTimers2.get(key);
2531
- if (timer) {
2532
- clearTimeout(timer);
2533
- persistTimers2.delete(key);
2534
- }
2535
- const mem = memory2.get(key);
2536
- if (!mem) return;
2315
+ function signalPid(pid, signal) {
2537
2316
  try {
2538
- writeFileSync4(runLogPath(key), `${JSON.stringify(materialize2(mem))}
2539
- `, "utf8");
2317
+ process.kill(pid, signal);
2540
2318
  } catch {
2541
2319
  }
2542
2320
  }
2543
- function schedulePersist2(key) {
2544
- if (persistTimers2.has(key)) return;
2545
- persistTimers2.set(
2546
- key,
2547
- setTimeout(() => {
2548
- persistTimers2.delete(key);
2549
- persistNow2(key);
2550
- }, PERSIST_DEBOUNCE_MS2)
2551
- );
2321
+ function isThisWorktreeListenerCwd(cwd, worktreePath) {
2322
+ const wt = realpathOrResolve(worktreePath);
2323
+ const c = realpathOrResolve(cwd);
2324
+ return c === wt || c.startsWith(wt + sep);
2552
2325
  }
2553
- function loadFromDisk2(key) {
2554
- const path = runLogPath(key);
2555
- if (!existsSync5(path)) return null;
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
+ }
2556
2334
  try {
2557
- const raw = JSON.parse(readFileSync5(path, "utf8"));
2558
- return {
2559
- output: typeof raw.output === "string" ? raw.output : "",
2560
- running: Boolean(raw.running),
2561
- exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
2562
- scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
2563
- };
2335
+ process.kill(-pid, "SIGTERM");
2564
2336
  } catch {
2565
- return null;
2337
+ signalPid(pid, "SIGTERM");
2566
2338
  }
2567
2339
  }
2568
- function memFor2(key) {
2569
- const cached = memory2.get(key);
2570
- if (cached) return cached;
2571
- const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
2572
- memory2.set(key, mem);
2573
- return mem;
2574
- }
2575
- function readRunLog(key) {
2576
- const cached = memory2.get(key);
2577
- if (cached) return materialize2(cached);
2578
- const disk = loadFromDisk2(key);
2579
- if (disk) {
2580
- memory2.set(key, fromSnapshot2(disk));
2581
- return disk;
2340
+ function killListenersOnPorts(ports) {
2341
+ const selfPid = process.pid;
2342
+ for (const port of ports) {
2343
+ if (!Number.isFinite(port) || port <= 0) continue;
2344
+ try {
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");
2359
+ }
2360
+ } catch {
2361
+ }
2582
2362
  }
2583
- return emptyRunLog();
2584
2363
  }
2585
- function beginRunLog(key, scriptName) {
2586
- const mem = {
2587
- chunks: [],
2588
- length: 0,
2589
- running: true,
2590
- exitCode: null,
2591
- scriptName
2592
- };
2593
- memory2.set(key, mem);
2594
- persistNow2(key);
2595
- return materialize2(mem);
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);
2380
+ }
2381
+ }
2596
2382
  }
2597
- function appendRunLog(key, line) {
2598
- const mem = memFor2(key);
2599
- mem.chunks.push(line);
2600
- mem.length += line.length + 1;
2601
- mem.running = true;
2602
- if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
2603
- schedulePersist2(key);
2604
- }
2605
- function finishRunLog(key, exitCode) {
2606
- const mem = memFor2(key);
2607
- mem.running = false;
2608
- mem.exitCode = exitCode;
2609
- persistNow2(key);
2610
- return materialize2(mem);
2611
- }
2612
- function flushRunLog(key) {
2613
- persistNow2(key);
2614
- }
2615
-
2616
- // src/git/orphan-cleanup.ts
2617
- import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "fs";
2618
- import { join as join8 } from "path";
2619
- function isSideboardWorktreePath(path) {
2620
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
2621
- }
2622
- async function findOrphanWorktrees(repoPaths) {
2623
- const threads = listThreads({ includeArchived: true });
2624
- const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
2625
- const repos = new Set(
2626
- repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
2627
- );
2628
- const homeRoot = sideboardWorkspacesDir();
2629
- if (existsSync6(homeRoot)) {
2630
- try {
2631
- for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
2632
- if (!entry.isDirectory()) continue;
2633
- void entry;
2634
- }
2635
- } catch {
2636
- }
2637
- }
2638
- const orphans = [];
2639
- const seen = /* @__PURE__ */ new Set();
2640
- for (const repoPath of repos) {
2641
- if (!repoPath || !existsSync6(repoPath)) continue;
2642
- try {
2643
- const wts = await listWorktrees(repoPath);
2644
- for (const wt of wts) {
2645
- const path = wt.path.replace(/\/$/, "");
2646
- if (!isSideboardWorktreePath(path)) continue;
2647
- if (known.has(path)) continue;
2648
- if (seen.has(path)) continue;
2649
- seen.add(path);
2650
- let mtimeMs = 0;
2651
- try {
2652
- mtimeMs = statSync(path).mtimeMs;
2653
- } catch {
2654
- mtimeMs = 0;
2655
- }
2656
- orphans.push({ path, repoPath, mtimeMs });
2657
- }
2658
- } catch {
2659
- }
2383
+ function killScriptTree(child, ports = []) {
2384
+ const pid = child.pid;
2385
+ if (pid) {
2660
2386
  try {
2661
- const root = worktreesRoot(repoPath);
2662
- if (existsSync6(root)) {
2663
- for (const entry of readdirSync2(root, { withFileTypes: true })) {
2664
- if (!entry.isDirectory()) continue;
2665
- const path = join8(root, entry.name).replace(/\/$/, "");
2666
- if (known.has(path) || seen.has(path)) continue;
2667
- if (!existsSync6(join8(path, ".git"))) continue;
2668
- seen.add(path);
2669
- 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(() => {
2670
2392
  try {
2671
- mtimeMs = statSync(path).mtimeMs;
2393
+ process.kill(-pid, "SIGKILL");
2672
2394
  } catch {
2673
- mtimeMs = Date.now();
2674
2395
  }
2675
- orphans.push({ path, repoPath, mtimeMs });
2676
- }
2396
+ }, 2500).unref?.();
2677
2397
  }
2678
2398
  } catch {
2399
+ try {
2400
+ child.kill("SIGTERM");
2401
+ } catch {
2402
+ }
2679
2403
  }
2680
2404
  }
2681
- return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
2405
+ killListenersOnPorts(ports);
2682
2406
  }
2683
- async function cleanupOrphanWorktrees(opts) {
2684
- const settings = loadAppSettings();
2685
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
2686
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
2687
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
2688
- const budget = Math.max(0, maxCount - activeCount);
2689
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
2690
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
2691
- const removed = [];
2692
- const kept = [];
2693
- for (const orphan of orphans) {
2694
- if (keep.has(orphan.path)) {
2695
- kept.push(orphan.path);
2696
- continue;
2697
- }
2698
- if (opts?.dryRun) {
2699
- removed.push(orphan.path);
2700
- continue;
2701
- }
2702
- try {
2703
- await removeWorktree(orphan.repoPath, orphan.path);
2704
- removed.push(orphan.path);
2705
- } catch {
2706
- kept.push(orphan.path);
2707
- }
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 {
2708
2425
  }
2709
- if (!opts?.dryRun) {
2710
- updateAdvancedSettings({
2711
- worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
2712
- });
2426
+ if (opts.heldPorts?.length) {
2427
+ await Promise.all(opts.heldPorts.map((h) => h.release()));
2713
2428
  }
2714
- 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
+ };
2715
2447
  }
2716
- function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
2717
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
2718
- const last = settings.advanced.worktreeLastCleanupAt;
2719
- if (!last) return true;
2720
- const elapsed = Date.now() - Date.parse(last);
2721
- 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;
2722
2455
  }
2723
- function worktreeCleanupSettings() {
2724
- 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);
2725
2468
  return {
2726
- worktreeMaxCount: a.worktreeMaxCount,
2727
- worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
2728
- worktreeLastCleanupAt: a.worktreeLastCleanupAt,
2729
- autoCleanupOrphans: a.autoCleanupOrphans
2469
+ ran: true,
2470
+ exitCode,
2471
+ source,
2472
+ kill: handle.kill
2730
2473
  };
2731
2474
  }
2732
-
2733
- // src/store/history-cleanup.ts
2734
- function stripArchivedTranscript(thread) {
2735
- writeThread(
2736
- {
2737
- ...thread,
2738
- attachments: [],
2739
- pendingTurnAttachments: [],
2740
- queueAttachments: [],
2741
- messages: [historyRetentionStubMessage(thread)]
2742
- },
2743
- { touch: false }
2744
- );
2745
- }
2746
- function cleanupArchivedHistory(opts = {}) {
2747
- const settings = loadAppSettings();
2748
- if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
2749
- return { stripped: [], purged: [] };
2750
- }
2751
- const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
2752
- const nowMs = opts.nowMs ?? Date.now();
2753
- const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
2754
- maxCount: opts.maxCount ?? historyMaxCount(settings),
2755
- maxDays: opts.maxDays ?? historyMaxDays(settings),
2756
- 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
2757
2484
  });
2758
- const stripped = [];
2759
- const purged = [];
2760
- if (opts.dryRun) {
2761
- return { stripped: plan.stripIds, purged: plan.purgeIds };
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`);
2762
2498
  }
2763
- for (const id of plan.stripIds) {
2764
- const thread = readThread(id);
2765
- if (!thread || thread.status !== "archived") continue;
2766
- try {
2767
- stripArchivedTranscript(thread);
2768
- stripped.push(id);
2769
- } catch {
2770
- }
2499
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
2500
+ if (!setup.ran) {
2501
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
2771
2502
  }
2772
- for (const id of plan.purgeIds) {
2773
- const thread = readThread(id);
2774
- if (!thread || thread.status !== "archived") continue;
2775
- try {
2776
- deleteThreadRecord(id);
2777
- purged.push(id);
2778
- } catch {
2779
- }
2503
+ if (!setup.ran) {
2504
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
2780
2505
  }
2781
- if (stripped.length > 0 || purged.length > 0) {
2782
- updateAdvancedSettings({
2783
- historyLastCleanupAt: new Date(nowMs).toISOString()
2784
- });
2785
- }
2786
- return { stripped, purged };
2787
- }
2788
- function shouldRunHistoryCleanup(settings = loadAppSettings()) {
2789
- if (!autoCleanupHistoryEnabled(settings)) return false;
2790
- const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
2791
- const last = settings.advanced.historyLastCleanupAt;
2792
- if (!last) return true;
2793
- const elapsed = Date.now() - Date.parse(last);
2794
- return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
2506
+ return setup;
2795
2507
  }
2796
-
2797
- // src/git/apply-into-main.ts
2798
- import { execa as execa3 } from "execa";
2799
- async function applyThreadIntoMain(thread, opts) {
2800
- const method = opts?.method ?? "merge";
2801
- const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
2802
- const status = await execa3("git", ["status", "--porcelain"], {
2803
- cwd: thread.repoPath,
2804
- reject: false
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
2805
2515
  });
2806
- if (status.stdout.trim()) {
2807
- throw new Error(
2808
- `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
2809
- );
2810
- }
2811
- const checkout = await execa3("git", ["checkout", target], {
2812
- cwd: thread.repoPath,
2813
- reject: false
2516
+ const exitCode = await handle.done;
2517
+ return { ran: true, exitCode };
2518
+ }
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");
2814
2525
  });
2815
- if (checkout.exitCode !== 0) {
2816
- throw new Error(
2817
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
2818
- );
2819
- }
2820
- if (method === "cherry-pick") {
2821
- const log = await execa3(
2822
- "git",
2823
- ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
2824
- { cwd: thread.repoPath, reject: false }
2825
- );
2826
- const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
2827
- if (!shas.length) {
2828
- return {
2829
- applied: false,
2830
- method,
2831
- targetBranch: target,
2832
- message: `No commits on ${thread.branchName} not already in ${target}`
2833
- };
2834
- }
2835
- for (const sha of shas) {
2836
- const cp = await execa3("git", ["cherry-pick", sha], {
2837
- cwd: thread.repoPath,
2838
- reject: false
2839
- });
2840
- if (cp.exitCode !== 0) {
2841
- await execa3("git", ["cherry-pick", "--abort"], {
2842
- cwd: thread.repoPath,
2843
- reject: false
2844
- });
2845
- throw new Error(
2846
- `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
2847
- );
2848
- }
2849
- }
2850
- return {
2851
- applied: true,
2852
- method,
2853
- targetBranch: target,
2854
- message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
2855
- };
2856
- }
2857
- const merge = await execa3(
2858
- "git",
2859
- ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
2860
- { cwd: thread.repoPath, reject: false }
2861
- );
2862
- if (merge.exitCode !== 0) {
2863
- await execa3("git", ["merge", "--abort"], {
2864
- cwd: thread.repoPath,
2865
- reject: false
2866
- });
2867
- throw new Error(
2868
- `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
2869
- );
2870
- }
2871
- return {
2872
- applied: true,
2873
- method: "merge",
2874
- targetBranch: target,
2875
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
2876
- };
2877
2526
  }
2878
-
2879
- // src/git/clone-repo.ts
2880
- import { existsSync as existsSync7 } from "fs";
2881
- import { basename as basename2, join as join9 } from "path";
2882
- import { execa as execa4 } from "execa";
2883
- async function cloneRepoIntoSideboard(opts) {
2884
- const url = opts.url.trim();
2885
- if (!url) throw new Error("Clone URL is required");
2886
- let name = opts.name?.trim();
2887
- if (!name) {
2888
- const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
2889
- name = leaf || "repo";
2890
- }
2891
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
2892
- const dest = join9(sideboardReposDir(), name);
2893
- if (existsSync7(dest)) {
2894
- const repoPath2 = await resolveRepoRoot(dest);
2895
- const workspace2 = await ensureWorkspace(repoPath2);
2896
- return { repoPath: repoPath2, workspace: workspace2 };
2897
- }
2898
- const result = await execa4("git", ["clone", url, dest], { reject: false });
2899
- if (result.exitCode !== 0) {
2900
- throw new Error(
2901
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
2902
- );
2903
- }
2904
- const repoPath = await resolveRepoRoot(dest);
2905
- const workspace = await ensureWorkspace(repoPath);
2906
- return { repoPath, workspace };
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;
2907
2531
  }
2908
-
2909
- // src/types/thread.ts
2910
- function isUnclaimedRunScriptRequest(req) {
2911
- return Boolean(req && !req.claimedAt && !req.fulfilledAt && !req.error);
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;
2537
+ }
2538
+ return getDefaultRunScript(worktreePath, repoPath);
2912
2539
  }
2913
- function isAdoptableRunScriptRequest(req) {
2914
- return Boolean(req && !req.fulfilledAt && !req.error);
2540
+ function getRunMode(worktreePath, repoPath) {
2541
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
2915
2542
  }
2916
-
2917
- // src/store/desktop-host.ts
2918
- import { readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
2919
- import { join as join10 } from "path";
2920
- function desktopHostPidPath() {
2921
- return join10(appDataDir(), "desktop-host.pid");
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
+ });
2922
2565
  }
2923
- function claimDesktopHost(pid = process.pid) {
2924
- writeFileSync5(desktopHostPidPath(), `${pid}
2925
- `, "utf8");
2566
+ async function allocatePort() {
2567
+ const held = await reservePort();
2568
+ await held.release();
2569
+ return held.port;
2926
2570
  }
2927
- function releaseDesktopHost(pid = process.pid) {
2928
- if (readDesktopHostPid() !== pid) return;
2929
- try {
2930
- unlinkSync(desktopHostPidPath());
2931
- } catch {
2932
- }
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
+ });
2933
2595
  }
2934
- function readDesktopHostPid() {
2935
- try {
2936
- const pid = Number.parseInt(readFileSync6(desktopHostPidPath(), "utf8").trim(), 10);
2937
- if (!Number.isFinite(pid) || pid <= 0) return null;
2938
- return pid;
2939
- } catch {
2940
- return null;
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);
2941
2605
  }
2606
+ return held;
2942
2607
  }
2943
- function pidAlive(pid) {
2944
- try {
2945
- process.kill(pid, 0);
2946
- return true;
2947
- } catch {
2948
- return false;
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;
2949
2617
  }
2618
+ return out;
2950
2619
  }
2951
- function isDesktopHostAlive() {
2952
- const pid = readDesktopHostPid();
2953
- return pid != null && pidAlive(pid);
2954
- }
2955
- function isThisProcessDesktopHost() {
2956
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
2957
- }
2958
- function thisProcessShouldDrainAgentQueues() {
2959
- if (isThisProcessDesktopHost()) return true;
2960
- return !isDesktopHostAlive();
2620
+ function delay(ms) {
2621
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2961
2622
  }
2962
-
2963
- // src/orchestrator/child-halt.ts
2964
- var HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
2965
- function isIncompleteChildStatus(status) {
2966
- return HALT_STATUSES.has(status);
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));
2627
+ try {
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;
2647
+ }
2967
2648
  }
2968
- function childHaltNotice(child, status) {
2969
- const title = child.title?.trim() || "Untitled";
2970
- const link = `[${title}](sideboard://thread/${child.id})`;
2971
- const why = child.lastError?.trim();
2972
- const extra = why ? ` lastError: ${why}` : "";
2973
- return [
2974
- `Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
2975
- "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."
2976
- ].join("\n");
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;
2977
2656
  }
2978
- function shouldNotifyParentAfterTurnError(opts) {
2979
- 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;
2980
2682
  }
2981
- function shouldNotifyParentOfChildHalt(opts) {
2982
- if (!isIncompleteChildStatus(opts.status)) return false;
2983
- if (!opts.child.parentThreadId) return false;
2984
- if (!opts.parent || opts.parent.status === "archived") return false;
2985
- if (opts.parent.id === opts.child.id) return false;
2986
- 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);
2987
2691
  }
2988
- var notified = /* @__PURE__ */ new Set();
2989
- function noticeKey(childId, status) {
2990
- 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;
2991
2697
  }
2992
- function notifyParentOfChildHalt(child, status, send) {
2993
- const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
2994
- if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
2995
- const key = noticeKey(child.id, status);
2996
- if (notified.has(key)) return false;
2997
- notified.add(key);
2998
- const parentId = parent.id;
2999
- void send(parentId, childHaltNotice(child, status)).catch(() => {
3000
- 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
3001
2705
  });
3002
- return true;
3003
- }
3004
-
3005
- // src/mcp/wait-for-job.ts
3006
- import {
3007
- appendFileSync as appendFileSync2,
3008
- existsSync as existsSync8,
3009
- mkdirSync as mkdirSync5,
3010
- readdirSync as readdirSync3,
3011
- readFileSync as readFileSync7,
3012
- writeFileSync as writeFileSync6
3013
- } from "fs";
3014
- import { dirname as dirname4, join as join11 } from "path";
3015
-
3016
- // src/mcp/wait-for-turn.ts
3017
- var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
3018
- function mcpWaitForTurnTimeoutMs(requested) {
3019
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
3020
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
3021
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
3022
- }
3023
- 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.";
3024
- 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.";
3025
- 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.";
3026
- var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
3027
- 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.";
3028
- 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.";
3029
- function mcpWaitTaskHint(taskState, status) {
3030
- switch (taskState) {
3031
- case "submitted":
3032
- return MCP_WAIT_QUEUED_HINT;
3033
- case "working":
3034
- return MCP_WAIT_STILL_RUNNING_HINT;
3035
- case "input-required":
3036
- return MCP_WAIT_INPUT_REQUIRED_HINT;
3037
- case "canceled":
3038
- return MCP_WAIT_STOPPED_HINT;
3039
- case "failed":
3040
- return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
3041
- default:
3042
- 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;
3043
2728
  }
3044
2729
  }
3045
2730
 
3046
- // src/mcp/wait-for-job.ts
3047
- var MAX_JOB_CONTINUES = 8;
3048
- var MAX_JOB_DELTA_LINES = 80;
3049
- var MAX_JOB_DELTA_CHARS = 4e3;
3050
- var DEFERRED_DONE_MAX_CHARS = 480;
3051
- var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
3052
- 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.";
3053
- 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.";
3054
- var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
3055
- function mcpWaitForJobTimeoutMs(requested) {
3056
- return mcpWaitForTurnTimeoutMs(requested);
3057
- }
3058
- function sanitizeDetachedJobId(id) {
3059
- const s = id.trim();
3060
- if (!JOB_ID_RE.test(s)) {
3061
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
3062
- }
3063
- 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;
3064
2749
  }
3065
- function jobAlive(pid) {
3066
- if (!Number.isInteger(pid) || pid <= 0) return false;
3067
- try {
3068
- process.kill(pid, 0);
3069
- return true;
3070
- } catch {
3071
- return false;
3072
- }
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
+ };
3073
2760
  }
3074
- function processGroupAlive(pgid) {
3075
- if (process.platform === "win32") return false;
3076
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
3077
- try {
3078
- process.kill(-pgid, 0);
3079
- return true;
3080
- } catch (err) {
3081
- 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;
3082
2767
  }
2768
+ return {
2769
+ output,
2770
+ running: mem.running,
2771
+ exitCode: mem.exitCode,
2772
+ source: mem.source
2773
+ };
3083
2774
  }
3084
- function jobTreeAlive(pid) {
3085
- if (pid == null) return false;
3086
- return jobAlive(pid) || processGroupAlive(pid);
2775
+ function setupLogPath(threadId) {
2776
+ return join7(threadsDir(), `${threadId}.setup.log.json`);
3087
2777
  }
3088
- function readIntFile(file) {
3089
- if (!existsSync8(file)) return null;
3090
- const n = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
3091
- return Number.isInteger(n) ? n : null;
2778
+ function setupLogKeyForWorktree(worktreePath) {
2779
+ const norm = normalizeWorktreePath(worktreePath);
2780
+ return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
3092
2781
  }
3093
- function jobDir(root, id, legacy = false) {
3094
- return join11(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
2782
+ function emptySetupLog() {
2783
+ return { ...EMPTY };
3095
2784
  }
3096
- function resolveJobDir(root, id) {
3097
- const modern = jobDir(root, id, false);
3098
- if (existsSync8(modern)) return modern;
3099
- const legacy = jobDir(root, id, true);
3100
- if (existsSync8(legacy)) return legacy;
3101
- return modern;
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;
3102
2792
  }
3103
- function listJobIdsIn(root, rel) {
3104
- const dir = join11(root, rel);
3105
- if (!existsSync8(dir)) return [];
3106
- return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
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 {
2810
+ }
3107
2811
  }
3108
- function listRunningDetachedJobs(worktreePath) {
3109
- const root = worktreePath.trim();
3110
- if (!root) return [];
3111
- const ids = /* @__PURE__ */ new Set([
3112
- ...listJobIdsIn(root, DETACHED_JOBS_DIR),
3113
- ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
3114
- ]);
3115
- const running = [];
3116
- for (const id of ids) {
3117
- const dir = resolveJobDir(root, id);
3118
- const pid = readIntFile(join11(dir, "pid"));
3119
- if (jobTreeAlive(pid)) running.push(id);
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
+ );
2821
+ }
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;
3120
2835
  }
3121
- return running.sort();
3122
2836
  }
3123
- function looksLikeDeferredDonePromise(text) {
3124
- const t = (text ?? "").trim();
3125
- if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
3126
- const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
3127
- if (!unquoted) return false;
3128
- 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);
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;
3129
2843
  }
3130
- function isJobContinuePrompt(text) {
3131
- if (typeof text !== "string") return false;
3132
- const t = text.trim();
3133
- return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
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;
2851
+ }
2852
+ return emptySetupLog();
3134
2853
  }
3135
- function collapseGhRunWatchLog(text) {
3136
- if (!text.includes(GH_WATCH_REFRESH)) return text;
3137
- const frames = text.split(/(?=Refreshing run status every 3 seconds\.)/);
3138
- const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
3139
- if (watchFrames.length <= 1) return text;
3140
- const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
3141
- const last = watchFrames[watchFrames.length - 1] ?? "";
3142
- const omitted = watchFrames.length - 1;
3143
- const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
3144
- `;
3145
- return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
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);
3146
2865
  }
3147
- function capJobLogDelta(text) {
3148
- const collapsed = collapseGhRunWatchLog(text);
3149
- const lines = collapsed.split("\n");
3150
- let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
3151
- ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
3152
- if (out.length > MAX_JOB_DELTA_CHARS) {
3153
- out = `\u2026(truncated ${out.length} chars)
3154
- ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
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);
2873
+ }
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);
2881
+ }
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;
3155
2912
  }
3156
- return out;
2913
+ return {
2914
+ output,
2915
+ running: mem.running,
2916
+ exitCode: mem.exitCode,
2917
+ scriptName: mem.scriptName
2918
+ };
3157
2919
  }
3158
- function formatJobStillRunningContinuePrompt(jobIds) {
3159
- const ids = jobIds.join(", ");
3160
- return [
3161
- `Detached job still running: ${ids}.`,
3162
- "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
3163
- "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
3164
- "Then report the result. Do not tell the user you will let them know later."
3165
- ].join(" ");
2920
+ function safeScriptToken(scriptName) {
2921
+ const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
2922
+ return token || "dev";
3166
2923
  }
3167
- function formatDeferredDoneContinuePrompt() {
3168
- return [
3169
- "You ended the turn after promising to report later, but no detached job is running.",
3170
- "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).",
3171
- "Do not say you will let the user know later."
3172
- ].join(" ");
2924
+ function runLogPath(key) {
2925
+ return join8(threadsDir(), `${key}.run.log.json`);
3173
2926
  }
3174
- function turnWatchedDetachedJob(parts) {
3175
- return parts.some((p) => {
3176
- if (p.type !== "tool") return false;
3177
- if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
3178
- const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
3179
- return /detached-job\.(?:js|cjs)\b/i.test(blob);
3180
- });
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)}`;
3181
2931
  }
3182
- function planJobContinue(opts) {
3183
- if (opts.isOrchestrator) return { action: "none" };
3184
- if (opts.agent === "brightsy") return { action: "none" };
3185
- if (opts.queueLength > 0) return { action: "none" };
3186
- if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
3187
- const farewell = looksLikeDeferredDonePromise(opts.chatText);
3188
- if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
2932
+ function emptyRunLog() {
2933
+ return { ...EMPTY2 };
2934
+ }
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 };
2943
+ }
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;
2953
+ try {
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"));
3189
2974
  return {
3190
- action: "wait",
3191
- jobIds: opts.runningJobIds,
3192
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
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
3193
2979
  };
2980
+ } catch {
2981
+ return null;
3194
2982
  }
3195
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
3196
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
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
+ );
3287
+ }
3288
+ return {
3289
+ applied: true,
3290
+ method: "merge",
3291
+ targetBranch: target,
3292
+ message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
3293
+ };
3294
+ }
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";
3307
+ }
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 };
3314
+ }
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
+ );
3197
3320
  }
3198
- return { action: "none" };
3199
- }
3200
- function tailProgress(logFile, maxLines = 12) {
3201
- if (!existsSync8(logFile)) return "(no log yet)";
3202
- const lines = readFileSync7(logFile, "utf8").split("\n");
3203
- return lines.slice(-maxLines).join("\n");
3321
+ const repoPath = await resolveRepoRoot(dest);
3322
+ const workspace = await ensureWorkspace(repoPath);
3323
+ return { repoPath, workspace };
3204
3324
  }
3205
- function lastProgressLine(progress) {
3206
- const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
3207
- return last ? last.slice(0, 72) : void 0;
3325
+
3326
+ // src/types/thread.ts
3327
+ function isUnclaimedRunScriptRequest(req) {
3328
+ return Boolean(req && !req.claimedAt && !req.fulfilledAt && !req.error);
3208
3329
  }
3209
- function readLogLines(file) {
3210
- if (!existsSync8(file)) return [];
3211
- const lines = readFileSync7(file, "utf8").split("\n");
3212
- if (lines.length && lines[lines.length - 1] === "") lines.pop();
3213
- return lines;
3330
+ function isAdoptableRunScriptRequest(req) {
3331
+ return Boolean(req && !req.fulfilledAt && !req.error);
3214
3332
  }
3215
- function takeDelta(logFile, cursorFile) {
3216
- const lines = readLogLines(logFile);
3217
- const cursor = readIntFile(cursorFile) ?? 0;
3218
- const start = Math.min(Math.max(0, cursor), lines.length);
3219
- return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
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");
3220
3339
  }
3221
- function snapshotJob(dir) {
3222
- const pid = readIntFile(join11(dir, "pid"));
3223
- const running = jobTreeAlive(pid);
3224
- return {
3225
- pid,
3226
- running,
3227
- exitCode: readIntFile(join11(dir, "exit")),
3228
- log: join11(dir, "log"),
3229
- cursor: join11(dir, "present.cursor"),
3230
- progress: tailProgress(join11(dir, "log"))
3231
- };
3340
+ function claimDesktopHost(pid = process.pid) {
3341
+ writeFileSync6(desktopHostPidPath(), `${pid}
3342
+ `, "utf8");
3232
3343
  }
3233
- function toResult(id, snap, extra) {
3234
- const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
3235
- const ok = !snap.running && snap.exitCode === 0;
3236
- const stillRunning = snap.running && !ok;
3237
- const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
3344
+ function releaseDesktopHost(pid = process.pid) {
3345
+ if (readDesktopHostPid() !== pid) return;
3238
3346
  try {
3239
- mkdirSync5(dirname4(snap.cursor), { recursive: true });
3240
- writeFileSync6(snap.cursor, `${nextCursor}
3241
- `);
3347
+ unlinkSync(desktopHostPidPath());
3242
3348
  } catch {
3243
3349
  }
3244
- const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
3245
- const progress = extra?.progress ?? snap.progress;
3246
- return {
3247
- stillRunning,
3248
- ok,
3249
- failed: Boolean(failed && !stillRunning && !ok),
3250
- status,
3251
- id,
3252
- pid: snap.pid,
3253
- exitCode: snap.exitCode ?? void 0,
3254
- phase: lastProgressLine(progress),
3255
- delta,
3256
- progress,
3257
- hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
3258
- };
3259
- }
3260
- async function sleepMs(ms) {
3261
- await new Promise((resolve2) => setTimeout(resolve2, ms));
3262
- }
3263
- async function waitForDetachedJob(cwd, id, opts) {
3264
- const jobId = sanitizeDetachedJobId(id);
3265
- const root = cwd.trim() || process.cwd();
3266
- const dir = resolveJobDir(root, jobId);
3267
- const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
3268
- const sleep = opts?.sleep ?? sleepMs;
3269
- if (!existsSync8(dir)) {
3270
- return {
3271
- stillRunning: false,
3272
- ok: false,
3273
- failed: true,
3274
- status: "failed",
3275
- id: jobId,
3276
- delta: "",
3277
- progress: "No detached job. Start one first.",
3278
- hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
3279
- };
3280
- }
3281
- const deadline = Date.now() + timeoutMs;
3282
- let snap = snapshotJob(dir);
3283
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
3284
- if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
3285
- return toResult(jobId, snap, {
3286
- failed: true,
3287
- progress: "No detached job. Start one first."
3288
- });
3289
- }
3290
- while (Date.now() < deadline) {
3291
- snap = snapshotJob(dir);
3292
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
3293
- if (!snap.running) return toResult(jobId, snap, { failed: true });
3294
- await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
3295
- }
3296
- snap = snapshotJob(dir);
3297
- return toResult(jobId, snap);
3298
3350
  }
3299
- function killProcessTree(pid, signal) {
3300
- if (!Number.isInteger(pid) || pid <= 0) return;
3301
- if (process.platform !== "win32") {
3302
- try {
3303
- process.kill(-pid, signal);
3304
- return;
3305
- } catch {
3306
- }
3307
- }
3351
+ function readDesktopHostPid() {
3308
3352
  try {
3309
- process.kill(pid, signal);
3353
+ const pid = Number.parseInt(readFileSync7(desktopHostPidPath(), "utf8").trim(), 10);
3354
+ if (!Number.isFinite(pid) || pid <= 0) return null;
3355
+ return pid;
3310
3356
  } catch {
3357
+ return null;
3311
3358
  }
3312
3359
  }
3313
- function appendJobLog(logFile, line) {
3360
+ function pidAlive(pid) {
3314
3361
  try {
3315
- mkdirSync5(dirname4(logFile), { recursive: true });
3316
- const body = line.endsWith("\n") ? line : `${line}
3317
- `;
3318
- appendFileSync2(logFile, body);
3362
+ process.kill(pid, 0);
3363
+ return true;
3319
3364
  } catch {
3365
+ return false;
3320
3366
  }
3321
3367
  }
3322
- async function stopDetachedJob(cwd, id, opts) {
3323
- const jobId = sanitizeDetachedJobId(id);
3324
- const root = cwd.trim() || process.cwd();
3325
- const dir = resolveJobDir(root, jobId);
3326
- const sleep = opts?.sleep ?? sleepMs;
3327
- const why = opts?.reason?.trim() ?? "";
3328
- if (!existsSync8(dir)) {
3329
- return {
3330
- stillRunning: false,
3331
- ok: false,
3332
- failed: true,
3333
- status: "failed",
3334
- id: jobId,
3335
- delta: "",
3336
- progress: "No detached job. Start one first.",
3337
- hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
3338
- stopped: false,
3339
- reason: "not-found"
3340
- };
3341
- }
3342
- let snap = snapshotJob(dir);
3343
- if (!snap.running) {
3344
- const result2 = toResult(jobId, snap);
3345
- return {
3346
- ...result2,
3347
- stopped: false,
3348
- reason: "not-running",
3349
- hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
3350
- };
3351
- }
3352
- appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
3353
- const targetPid = snap.pid;
3354
- if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
3355
- const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
3356
- const deadline = Date.now() + graceMs;
3357
- while (Date.now() < deadline && jobTreeAlive(targetPid)) {
3358
- await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
3359
- }
3360
- if (targetPid != null) {
3361
- killProcessTree(targetPid, "SIGKILL");
3362
- await sleep(100);
3363
- }
3364
- const exitFile = join11(dir, "exit");
3365
- if (!existsSync8(exitFile)) {
3366
- try {
3367
- writeFileSync6(exitFile, "1\n");
3368
- } catch {
3369
- }
3370
- }
3371
- snap = snapshotJob(dir);
3372
- const stillRunning = jobTreeAlive(targetPid) || snap.running;
3373
- const result = toResult(jobId, snap, { failed: true });
3374
- const stopped = !stillRunning;
3375
- return {
3376
- ...result,
3377
- stillRunning,
3378
- ok: false,
3379
- failed: true,
3380
- status: stillRunning ? "running" : "failed",
3381
- stopped,
3382
- reason: stopped ? "stopped" : "still-running",
3383
- stopReason: why || void 0,
3384
- hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
3385
- };
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;
3386
3420
  }
3387
3421
 
3388
3422
  // src/threads/create.ts
@@ -4027,7 +4061,7 @@ async function createThread(input, _onSetupLine) {
4027
4061
  });
4028
4062
  await requireAgent(resolved.agent);
4029
4063
  if (input.cowboy) {
4030
- const { cowboyModeEnabled } = await import("./app-settings-I27UBVI4.js");
4064
+ const { cowboyModeEnabled } = await import("./app-settings-VOPXJMGV.js");
4031
4065
  if (!cowboyModeEnabled()) {
4032
4066
  throw new Error(
4033
4067
  "Cowboy mode is off. Enable it in Settings \u2192 Advanced."
@@ -4085,7 +4119,7 @@ async function createThread(input, _onSetupLine) {
4085
4119
  let sourceIsFork = false;
4086
4120
  if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
4087
4121
  if (isAbleTimeConnected() && getIssueSource() === "abletime") {
4088
- const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-VSV6AUBV.js");
4122
+ const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-CWIJ7INC.js");
4089
4123
  const task = await ensureAbleTimeTask({
4090
4124
  title: title?.trim() || "Untitled work",
4091
4125
  description: input.prompt?.trim() || void 0
@@ -4187,7 +4221,7 @@ async function createThread(input, _onSetupLine) {
4187
4221
  return readThread(thread.id) ?? thread;
4188
4222
  }
4189
4223
  async function listLinearIssues(agent, repoPath) {
4190
- const { getAdapter: getAdapter2 } = await import("./agents-CDQCQVWU.js");
4224
+ const { getAdapter: getAdapter2 } = await import("./agents-Q23R4WOR.js");
4191
4225
  await requireAgent(agent, { requireLinear: true });
4192
4226
  const adapter = getAdapter2(agent);
4193
4227
  if (!adapter.listLinearIssues) {
@@ -4660,7 +4694,7 @@ async function adoptThread(input) {
4660
4694
  messages: input.messages ?? []
4661
4695
  });
4662
4696
  writeThread(thread);
4663
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-JX5R4V24.js");
4697
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-CFI7XVAE.js");
4664
4698
  await ensureWorkspace2(repoPath);
4665
4699
  const { ensureWorktreeSideboardIgnored: ensureWorktreeSideboardIgnored2 } = await import("./worktree-exclude-RXZPTCNH.js");
4666
4700
  await ensureWorktreeSideboardIgnored2(input.worktreePath);
@@ -6650,7 +6684,7 @@ function formatWorktreeDirective(thread, opts) {
6650
6684
  const slug = opts?.githubSlug ?? "<origin-owner/name>";
6651
6685
  const resolve2 = opts?.githubSlug ? "" : " Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree.";
6652
6686
  lines.push(
6653
- `- Prefer a draft PR first: \`gh pr create --draft -R ${slug}\` (update via \`gh pr edit -R ${slug}\`). Always pass \`-R\` \u2014 bare \`gh pr create\` may target upstream on dual-remote checkouts.${resolve2} Mark ready for review only when asked. If GitHub rejects the body as too long, retry with a short \`--body-file\` \u2014 do not paste a changelog or diff into \`--body\`.`
6687
+ `- Prefer a draft PR first: \`gh pr create --draft --assignee @me -R ${slug}\` (update via \`gh pr edit -R ${slug}\`). Always pass \`-R\` \u2014 bare \`gh pr create\` may target upstream on dual-remote checkouts.${resolve2} Assign to the GitHub-connected user (\`--assignee @me\`) unless the user named someone else. Mark ready for review only when asked. If GitHub rejects the body as too long, retry with a short \`--body-file\` \u2014 do not paste a changelog or diff into \`--body\`.`
6654
6688
  );
6655
6689
  }
6656
6690
  lines.push(
@@ -7372,7 +7406,7 @@ function formatScheduledPrompt(name, prompt) {
7372
7406
  ${prompt}`;
7373
7407
  }
7374
7408
  async function defaultDeps() {
7375
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-VYNPNHY4.js");
7409
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-ABF6QFE6.js");
7376
7410
  const orch = getOrchestrator2();
7377
7411
  return {
7378
7412
  findThread: (id) => findThreadByRef(id),
@@ -7697,7 +7731,7 @@ var Orchestrator = class {
7697
7731
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
7698
7732
  });
7699
7733
  }
7700
- const { autoCleanupOrphansEnabled } = await import("./app-settings-I27UBVI4.js");
7734
+ const { autoCleanupOrphansEnabled } = await import("./app-settings-VOPXJMGV.js");
7701
7735
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
7702
7736
  await cleanupOrphanWorktrees({ repoPaths });
7703
7737
  }
@@ -8044,7 +8078,7 @@ var Orchestrator = class {
8044
8078
  }
8045
8079
  await setup;
8046
8080
  if (skipSetup) return;
8047
- const { autoRunAfterSetupEnabled } = await import("./app-settings-I27UBVI4.js");
8081
+ const { autoRunAfterSetupEnabled } = await import("./app-settings-VOPXJMGV.js");
8048
8082
  if (autoRunAfterSetupEnabled()) {
8049
8083
  try {
8050
8084
  await this.startDev(threadId);
@@ -8295,7 +8329,8 @@ var Orchestrator = class {
8295
8329
  if (compact.didCompact) {
8296
8330
  thread = updateThread(threadId, {
8297
8331
  messages: compact.thread.messages,
8298
- sessionId: compact.thread.sessionId
8332
+ sessionId: compact.thread.sessionId,
8333
+ compactedMessages: compact.thread.compactedMessages
8299
8334
  });
8300
8335
  this.emit({
8301
8336
  type: "context_compacted",
@@ -8309,9 +8344,6 @@ var Orchestrator = class {
8309
8344
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
8310
8345
  thread = this.requireThread(threadId);
8311
8346
  }
8312
- if (this.stoppedTurns.has(threadId)) {
8313
- return;
8314
- }
8315
8347
  const promptText = typeof prompt === "string" ? prompt : "";
8316
8348
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
8317
8349
  const parked = takePendingTurnAttachments(thread);
@@ -8328,6 +8360,14 @@ var Orchestrator = class {
8328
8360
  agent: thread.agent,
8329
8361
  orch: isOrchestratorThread(thread)
8330
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
+ }
8331
8371
  thread = this.requireThread(threadId);
8332
8372
  const gitPrompt = expandCanonicalGitRequest(promptText);
8333
8373
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -8386,7 +8426,7 @@ var Orchestrator = class {
8386
8426
  ].filter(Boolean).join("\n\n") || void 0;
8387
8427
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
8388
8428
  const agentPrompt = [
8389
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
8429
+ planModeTurnInstruction(thread.planMode),
8390
8430
  orchestrationReminder,
8391
8431
  prGateDirective,
8392
8432
  injectedNoticeContext,
@@ -8408,7 +8448,7 @@ var Orchestrator = class {
8408
8448
  }
8409
8449
  const isBrightsy = fresh.agent === "brightsy";
8410
8450
  const isOrchestration = isOrchestratorThread(fresh);
8411
- const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-I27UBVI4.js");
8451
+ const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-VOPXJMGV.js");
8412
8452
  const gitAuthMode = getGithubGitAuthMode();
8413
8453
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
8414
8454
  githubSlug: await withTimeout(
@@ -9699,7 +9739,7 @@ var Orchestrator = class {
9699
9739
  if (!titled.userSetTitle && meta.title && titled.title !== meta.title) {
9700
9740
  updateThread(thread.id, { title: meta.title });
9701
9741
  }
9702
- const { autoArchiveOnMergeEnabled } = await import("./app-settings-I27UBVI4.js");
9742
+ const { autoArchiveOnMergeEnabled } = await import("./app-settings-VOPXJMGV.js");
9703
9743
  const latest = this.requireThread(thread.id);
9704
9744
  if (!shouldAutoArchiveOnPrMerge({
9705
9745
  previousPrState: prevState || null,
@@ -9981,7 +10021,7 @@ var Orchestrator = class {
9981
10021
  this.enforceHistoryRetention();
9982
10022
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
9983
10023
  try {
9984
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-JX5R4V24.js");
10024
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-CFI7XVAE.js");
9985
10025
  await ensureWorkspace2(thread.repoPath);
9986
10026
  } catch {
9987
10027
  }
@@ -10008,7 +10048,7 @@ var Orchestrator = class {
10008
10048
  await runArchiveScript(thread.repoPath, thread.worktreePath);
10009
10049
  } catch {
10010
10050
  }
10011
- const { deleteBranchOnPurgeEnabled } = await import("./app-settings-I27UBVI4.js");
10051
+ const { deleteBranchOnPurgeEnabled } = await import("./app-settings-VOPXJMGV.js");
10012
10052
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
10013
10053
  await removeWorktree(thread.repoPath, thread.worktreePath, {
10014
10054
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -10035,7 +10075,7 @@ var Orchestrator = class {
10035
10075
  `Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
10036
10076
  );
10037
10077
  }
10038
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-AGVPKDBH.js");
10078
+ const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-4UAOZUJ4.js");
10039
10079
  const slug = thread.worktreePath.split("/").pop();
10040
10080
  const dest = thread.worktreePath;
10041
10081
  await withRepoGitLock(thread.repoPath, async () => {
@@ -10145,7 +10185,7 @@ async function startOrchestration(opts) {
10145
10185
  sourceRef: "default",
10146
10186
  ...createOpts
10147
10187
  }).catch(async () => {
10148
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-AGVPKDBH.js");
10188
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-4UAOZUJ4.js");
10149
10189
  const repo = await resolveRepoRoot2(repoPath);
10150
10190
  const def = await resolveDefaultBranch2(repo);
10151
10191
  return orch.createThread({
@@ -10155,7 +10195,7 @@ async function startOrchestration(opts) {
10155
10195
  repoPath: repo
10156
10196
  });
10157
10197
  });
10158
- const { updateThread: upd } = await import("./thread-store-AYPBEZT5.js");
10198
+ const { updateThread: upd } = await import("./thread-store-SCV2IZYS.js");
10159
10199
  const updated = upd(thread.id, {
10160
10200
  sourceType: "orchestration",
10161
10201
  sourceRef: goal
@@ -10260,6 +10300,9 @@ export {
10260
10300
  findLastBrightsyContextSummary,
10261
10301
  messagesSinceLastBrightsyContextSummary,
10262
10302
  buildBrightsySessionSeed,
10303
+ compactArchiveProse,
10304
+ messagesForCompactArchive,
10305
+ appendCompactedMessages,
10263
10306
  applyCompaction,
10264
10307
  lastRequestOccupancy,
10265
10308
  shouldResetSessionForOccupancy,
@@ -10295,6 +10338,11 @@ export {
10295
10338
  worktreeRunPortsPath,
10296
10339
  loadWorktreeRunPorts,
10297
10340
  saveWorktreeRunPorts,
10341
+ mcpWaitForTurnTimeoutMs,
10342
+ mcpWaitTaskHint,
10343
+ mcpWaitForJobTimeoutMs,
10344
+ waitForDetachedJob,
10345
+ stopDetachedJob,
10298
10346
  PORT_RANGE_SIZE,
10299
10347
  readWorktreeInclude,
10300
10348
  resolveFilesToCopy,
@@ -10331,11 +10379,6 @@ export {
10331
10379
  shouldRunHistoryCleanup,
10332
10380
  applyThreadIntoMain,
10333
10381
  cloneRepoIntoSideboard,
10334
- mcpWaitForTurnTimeoutMs,
10335
- mcpWaitTaskHint,
10336
- mcpWaitForJobTimeoutMs,
10337
- waitForDetachedJob,
10338
- stopDetachedJob,
10339
10382
  detectAgents,
10340
10383
  requireAgent,
10341
10384
  createThread,