@sideboard-ai/core 0.1.99 → 0.1.103

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/agents/cursor-runner.cjs +22 -22
  2. package/dist/agents/cursor-runner.js +2 -2
  3. package/dist/{agents-ESJKIQQA.js → agents-UT5GCJH3.js} +8 -8
  4. package/dist/{agents-3MWWWSMF.js → agents-XZBAKBO2.js} +9 -9
  5. package/dist/{app-settings-BRF7VKTQ.js → app-settings-EKJM2344.js} +2 -2
  6. package/dist/{app-settings-ZXPAB3QN.js → app-settings-VDJK2BZ2.js} +2 -2
  7. package/dist/{caffeinate-hold-U3QJBV67.js → caffeinate-hold-NBRUWYXO.js} +2 -2
  8. package/dist/{caffeinate-hold-I7SG6UQR.js → caffeinate-hold-X3JNPR42.js} +2 -2
  9. package/dist/{chunk-UYQYK2RY.js → chunk-27MCESES.js} +3 -3
  10. package/dist/{chunk-QSLE4VEM.js → chunk-2GO3YKBA.js} +4 -4
  11. package/dist/{chunk-DKHGWYWR.js → chunk-7YKXHBIW.js} +1 -1
  12. package/dist/{chunk-JL4SJ6V7.js → chunk-BD2ICC4D.js} +4 -0
  13. package/dist/{chunk-NZPGMNFC.js → chunk-DCOZABNT.js} +1 -1
  14. package/dist/{chunk-CIRXAYWS.js → chunk-DHAGMJIK.js} +55 -28
  15. package/dist/{chunk-XUWDLRAE.js → chunk-ETMRBJKA.js} +11 -11
  16. package/dist/{chunk-MBP3XG57.js → chunk-H6LP57EF.js} +7 -7
  17. package/dist/{chunk-FO67IJTY.js → chunk-HHLZWO7U.js} +5 -5
  18. package/dist/{chunk-GXSYI7FH.js → chunk-HUKCGRAT.js} +209 -59
  19. package/dist/{chunk-3WA37BRI.js → chunk-KGSVLZV6.js} +11 -4
  20. package/dist/{chunk-7KDBDMNC.js → chunk-KLBOWJ74.js} +1 -1
  21. package/dist/{chunk-FYS2BULQ.js → chunk-LANDJIAI.js} +213 -63
  22. package/dist/{chunk-OANJQTVG.js → chunk-ND6AO7C5.js} +3 -3
  23. package/dist/{chunk-HI2OTFFR.js → chunk-OW34MVDN.js} +5 -5
  24. package/dist/{chunk-JOF3XIEM.js → chunk-P3BQ5DOL.js} +11 -4
  25. package/dist/{chunk-B4R2VB2A.js → chunk-QZQEXME2.js} +4 -0
  26. package/dist/{chunk-R7BQBSDT.js → chunk-WA3DVVWG.js} +55 -28
  27. package/dist/{chunk-6LFV4VFI.js → chunk-WSYCND2U.js} +1 -1
  28. package/dist/{chunk-FT2SQOL4.js → chunk-XHZJJWLI.js} +1 -1
  29. package/dist/{chunk-XH2GS2LO.js → chunk-YJADJH5U.js} +11 -11
  30. package/dist/{connected-teams-SWHJWK2A.js → connected-teams-J4I5AKMR.js} +2 -2
  31. package/dist/{connected-teams-TLVKZPJP.js → connected-teams-M5XNXRI5.js} +2 -2
  32. package/dist/{coordinator-prompt-OQOOD5ET.js → coordinator-prompt-3DFZRMXE.js} +5 -5
  33. package/dist/{coordinator-prompt-AKEY4WSO.js → coordinator-prompt-UWHSD65V.js} +5 -5
  34. package/dist/{cursor-recover-S5ORPW34.js → cursor-recover-JBTIDARH.js} +1 -1
  35. package/dist/{cursor-recover-G34BYKL2.js → cursor-recover-ZUJG6XRQ.js} +1 -1
  36. package/dist/{global-workspace-WMF3BJP5.js → global-workspace-KJQS5S2C.js} +6 -6
  37. package/dist/{global-workspace-RSQXRLT7.js → global-workspace-UR4E3CCG.js} +6 -6
  38. package/dist/index.cjs +851 -232
  39. package/dist/index.d.cts +39 -6
  40. package/dist/index.d.ts +39 -6
  41. package/dist/index.js +561 -138
  42. package/dist/mcp/run-stdio.cjs +651 -256
  43. package/dist/mcp/run-stdio.js +345 -143
  44. package/dist/{paths-TZX4L5X7.js → paths-QCI3MTO5.js} +3 -1
  45. package/dist/{paths-JQM2XRAD.js → paths-UMEDHOS5.js} +3 -1
  46. package/dist/{thread-store-XAKWCV2E.js → thread-store-4OUEQ2DB.js} +2 -2
  47. package/dist/{thread-store-V7NOOXKE.js → thread-store-DQJGDKIJ.js} +2 -2
  48. package/dist/{workspaces-MZVQHRSJ.js → workspaces-NTIVI6DI.js} +7 -7
  49. package/dist/{workspaces-4ZY4QPWQ.js → workspaces-TKJXPNV4.js} +7 -7
  50. package/dist/{worktree-EO5QAGJU.js → worktree-NKWN5ZTS.js} +4 -4
  51. package/dist/{worktree-DA4BOV7G.js → worktree-PEUPFAKL.js} +4 -4
  52. package/package.json +2 -2
@@ -416,6 +416,7 @@ __export(paths_exports, {
416
416
  sideboardReposDir: () => sideboardReposDir,
417
417
  sideboardWorkspacesDir: () => sideboardWorkspacesDir,
418
418
  threadFilePath: () => threadFilePath,
419
+ threadLivePath: () => threadLivePath,
419
420
  threadLockPath: () => threadLockPath,
420
421
  threadsDir: () => threadsDir,
421
422
  worktreesRoot: () => worktreesRoot
@@ -467,6 +468,9 @@ function worktreesRoot(repoPath) {
467
468
  function threadFilePath(id) {
468
469
  return (0, import_node_path5.join)(threadsDir(), `${id}.json`);
469
470
  }
471
+ function threadLivePath(id) {
472
+ return (0, import_node_path5.join)(threadsDir(), `${id}.live.json`);
473
+ }
470
474
  function threadLockPath(id) {
471
475
  return (0, import_node_path5.join)(locksDir(), `${id}.lock`);
472
476
  }
@@ -759,8 +763,11 @@ function writeThread(thread) {
759
763
  function idPath(id) {
760
764
  return id;
761
765
  }
766
+ function isThreadRecordFile(name) {
767
+ return name.endsWith(".json") && !name.endsWith(".live.json");
768
+ }
762
769
  function listThreads(opts) {
763
- const files = (0, import_node_fs7.readdirSync)(threadsDir()).filter((f) => f.endsWith(".json"));
770
+ const files = (0, import_node_fs7.readdirSync)(threadsDir()).filter(isThreadRecordFile);
764
771
  const threads = files.map((f) => {
765
772
  try {
766
773
  return normalizeThread(
@@ -769,7 +776,9 @@ function listThreads(opts) {
769
776
  } catch {
770
777
  return null;
771
778
  }
772
- }).filter((t) => t !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
779
+ }).filter(
780
+ (t) => t !== null && typeof t.id === "string" && t.id.length > 0
781
+ ).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
773
782
  if (opts?.includeArchived) return threads;
774
783
  return threads.filter((t) => t.status !== "archived");
775
784
  }
@@ -801,7 +810,9 @@ function setStatus(id, status, lastError) {
801
810
  }
802
811
  function findThreadByRef(ref) {
803
812
  const all = listThreads({ includeArchived: true });
804
- return all.find((t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref) ?? null;
813
+ return all.find(
814
+ (t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref
815
+ ) ?? null;
805
816
  }
806
817
  var import_node_crypto2, import_node_fs7, import_proper_lockfile;
807
818
  var init_thread_store = __esm({
@@ -883,6 +894,13 @@ function looksLikeMinifiedJsDump(line) {
883
894
  function looksLikeNestedElectronCrash(line) {
884
895
  return /HasCustomHostObject|ElectronInitializeICUandStartNode/i.test(line);
885
896
  }
897
+ function looksLikeHomebrewLibuvCrash(line) {
898
+ const uvRun = /uv_run/i.test(line);
899
+ const spin = /SpinEventLoopInternal/i.test(line);
900
+ const homebrewUv = /Cellar\/libuv|libuv\.\d\.dylib/i.test(line);
901
+ const homebrewNode = /Cellar\/node\//i.test(line);
902
+ return uvRun && (homebrewUv || spin || homebrewNode) || spin && (homebrewUv || homebrewNode);
903
+ }
886
904
  function clipStderr(text3, maxChars) {
887
905
  const trimmed = text3.trim();
888
906
  if (trimmed.length <= maxChars) return trimmed;
@@ -893,6 +911,7 @@ function summarizeTurnStderr(tail, maxChars = 500) {
893
911
  const cursorStartup = [...tail].reverse().find((line) => /cursor startup failed:/i.test(line));
894
912
  if (cursorStartup) return clipStderr(cursorStartup, maxChars);
895
913
  if (tail.some(looksLikeNestedElectronCrash)) return NESTED_ELECTRON_SUMMARY;
914
+ if (tail.some(looksLikeHomebrewLibuvCrash)) return HOMEBREW_LIBUV_SUMMARY;
896
915
  if (tail.some(
897
916
  (line) => /\[resource_exhausted\]|resource_exhausted/i.test(line) || /findFilesWithRipgrep/.test(line)
898
917
  )) {
@@ -915,6 +934,20 @@ function looksLikeInvalidAgentSession(text3) {
915
934
  if (!lower) return false;
916
935
  return /session not found/.test(lower) || /no conversation found/.test(lower) || /conversation .+ not found/.test(lower) || /thread .+ not found/.test(lower) || /unknown session/.test(lower) || /invalid session/.test(lower) || /session .+ (missing|expired|deleted|gone)/.test(lower) || /cannot resume/.test(lower) || /failed to (load|resume|open) session/.test(lower) || /corrupt local agent checkpoint/.test(lower) || /missing root blob/.test(lower) || /\bagent\b.{0,120}\bnot found\b/.test(lower);
917
936
  }
937
+ function looksLikeRetryableRunnerCrash(text3) {
938
+ if (looksLikeAgentFailureMessage(text3)) return false;
939
+ if (looksLikeInvalidAgentSession(text3)) return false;
940
+ const lower = text3.trim().toLowerCase();
941
+ if (/cannot find (?:package|module)|err_module_not_found/.test(lower)) return false;
942
+ if (!lower) return true;
943
+ return /uv_run|spineventloopinternal|libuv|homebrew node \+ shared libuv|hascustomhostobject|electroninitializeicuandstartnode|nested chromium|truncated crash dump|sig(?:segv|abrt|ill)|segmentation fault|illegal instruction|fatal error/.test(
944
+ lower
945
+ );
946
+ }
947
+ function shouldRetryFailedAgentTurn(detail, opts) {
948
+ if (looksLikeInvalidAgentSession(detail) && opts.hasSession) return true;
949
+ return looksLikeRetryableRunnerCrash(detail);
950
+ }
918
951
  function looksLikeAgentFailureMessage(text3) {
919
952
  const lower = text3.trim().toLowerCase();
920
953
  if (!lower) return false;
@@ -959,11 +992,22 @@ function humanizeAgentFailDetail(detail) {
959
992
  if (/hascustomhostobject|electroninitializeicuandstartnode|nested chromium/i.test(lower)) {
960
993
  return `${raw} \u2014 retry the turn; if it keeps failing, pick another agent.`;
961
994
  }
995
+ if (/homebrew node \+ shared libuv|uv_run|spineventloopinternal/i.test(lower)) {
996
+ return /brew install node@22/i.test(raw) ? raw : `${raw} \u2014 install Node 22 LTS (\`brew install node@22\`) and retry.`;
997
+ }
962
998
  if (/corrupt local agent checkpoint|missing root blob|truncated crash dump/.test(lower)) {
963
999
  return `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
964
1000
  }
965
1001
  return raw;
966
1002
  }
1003
+ function turnFailChatText(opts) {
1004
+ const chat = opts.assistantText.trim();
1005
+ if (chat) return chat;
1006
+ if (opts.exitCode === 0) return "";
1007
+ const detail = opts.detail.trim();
1008
+ if (detail) return humanizeAgentFailDetail(detail);
1009
+ return formatTurnExitError(opts.exitCode ?? 1, "");
1010
+ }
967
1011
  function formatTurnExitError(exitCode, stderrSummary) {
968
1012
  const code = exitCode ?? 1;
969
1013
  const raw = stderrSummary.trim();
@@ -977,12 +1021,13 @@ function formatTurnExitError(exitCode, stderrSummary) {
977
1021
  if (looksLikeAgentFailureMessage(raw)) return detail;
978
1022
  return `exit ${code}: ${detail}`;
979
1023
  }
980
- var NODE_VERSION_FOOTER, NESTED_ELECTRON_SUMMARY, MINIFIED_DUMP_SUMMARY;
1024
+ var NODE_VERSION_FOOTER, NESTED_ELECTRON_SUMMARY, HOMEBREW_LIBUV_SUMMARY, MINIFIED_DUMP_SUMMARY;
981
1025
  var init_error_detail = __esm({
982
1026
  "src/agents/error-detail.ts"() {
983
1027
  "use strict";
984
1028
  NODE_VERSION_FOOTER = /^Node\.js v\d+/i;
985
1029
  NESTED_ELECTRON_SUMMARY = "Cursor local agent crashed at Electron startup (nested Chromium / HasCustomHostObject)";
1030
+ HOMEBREW_LIBUV_SUMMARY = "Cursor runner crashed in Node (Homebrew Node + shared libuv). Install Node 22 LTS (`brew install node@22`) and retry.";
986
1031
  MINIFIED_DUMP_SUMMARY = "Cursor local agent crashed during startup (truncated crash dump)";
987
1032
  }
988
1033
  });
@@ -1764,7 +1809,7 @@ var init_teams = __esm({
1764
1809
 
1765
1810
  // src/git/worktree-labels.ts
1766
1811
  function normalizeWorktreePath(worktreePath) {
1767
- const trimmed = worktreePath.replace(/\/+$/, "") || "/";
1812
+ const trimmed = (worktreePath ?? "").replace(/\/+$/, "") || "/";
1768
1813
  const absolute = trimmed.startsWith("/");
1769
1814
  const parts = trimmed.split("/").filter((p) => p && p !== ".");
1770
1815
  const out = [];
@@ -3355,6 +3400,42 @@ var init_git_auth_mode = __esm({
3355
3400
  }
3356
3401
  });
3357
3402
 
3403
+ // src/util/enqueue-by-key.ts
3404
+ function enqueueByKey(key, fn) {
3405
+ const prev = tails.get(key) ?? Promise.resolve();
3406
+ const next = prev.then(fn, fn);
3407
+ tails.set(
3408
+ key,
3409
+ next.then(
3410
+ () => void 0,
3411
+ () => void 0
3412
+ )
3413
+ );
3414
+ return next;
3415
+ }
3416
+ var tails;
3417
+ var init_enqueue_by_key = __esm({
3418
+ "src/util/enqueue-by-key.ts"() {
3419
+ "use strict";
3420
+ tails = /* @__PURE__ */ new Map();
3421
+ }
3422
+ });
3423
+
3424
+ // src/git/repo-git-lock.ts
3425
+ function repoLockKey(repoPath) {
3426
+ const trimmed = repoPath.replace(/\/+$/, "");
3427
+ return `git-worktree:${trimmed || "/"}`;
3428
+ }
3429
+ function withRepoGitLock(repoPath, fn) {
3430
+ return enqueueByKey(repoLockKey(repoPath), fn);
3431
+ }
3432
+ var init_repo_git_lock = __esm({
3433
+ "src/git/repo-git-lock.ts"() {
3434
+ "use strict";
3435
+ init_enqueue_by_key();
3436
+ }
3437
+ });
3438
+
3358
3439
  // src/git/pr-gates.ts
3359
3440
  function prIsInMergeQueue(gate) {
3360
3441
  if (gate.isInMergeQueue) return true;
@@ -4508,12 +4589,13 @@ async function createThreadWorktree(opts) {
4508
4589
  `Invalid git reference: ${opts.sourceRef} (and fetch did not resolve it)`
4509
4590
  );
4510
4591
  }
4511
- const add = await git(
4512
- ["worktree", "add", "-b", branchName, worktreePath, startPoint],
4513
- opts.repoPath,
4514
- { reject: false, timeoutMs: 6e4 }
4515
- );
4516
- if (add.exitCode !== 0) {
4592
+ const added = await withRepoGitLock(opts.repoPath, async () => {
4593
+ const add = await git(
4594
+ ["worktree", "add", "-b", branchName, worktreePath, startPoint],
4595
+ opts.repoPath,
4596
+ { reject: false, timeoutMs: 6e4 }
4597
+ );
4598
+ if (add.exitCode === 0) return branchName;
4517
4599
  const err = `${add.stderr}
4518
4600
  ${add.stdout}`;
4519
4601
  if (/already used by worktree|already exists|checked out/i.test(err)) {
@@ -4524,17 +4606,14 @@ ${add.stdout}`;
4524
4606
  opts.repoPath,
4525
4607
  { reject: false, timeoutMs: 6e4 }
4526
4608
  );
4527
- if (retry.exitCode === 0) {
4528
- branchName = alt;
4529
- await ensureGhPreferOrigin(worktreePath);
4530
- return { branchName, worktreePath };
4531
- }
4609
+ if (retry.exitCode === 0) return alt;
4532
4610
  }
4533
4611
  }
4534
4612
  throw new Error(
4535
4613
  `Failed to create worktree: ${add.stderr.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
4536
4614
  );
4537
- }
4615
+ });
4616
+ branchName = added;
4538
4617
  await ensureGhPreferOrigin(worktreePath);
4539
4618
  return { branchName, worktreePath };
4540
4619
  }
@@ -4558,12 +4637,13 @@ async function createExistingBranchWorktree(opts) {
4558
4637
  );
4559
4638
  }
4560
4639
  const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
4561
- const add = await git(
4562
- ["worktree", "add", worktreePath, startPoint],
4563
- opts.repoPath,
4564
- { reject: false }
4565
- );
4566
- if (add.exitCode !== 0) {
4640
+ await withRepoGitLock(opts.repoPath, async () => {
4641
+ const add = await git(
4642
+ ["worktree", "add", worktreePath, startPoint],
4643
+ opts.repoPath,
4644
+ { reject: false }
4645
+ );
4646
+ if (add.exitCode === 0) return;
4567
4647
  const retry = await git(
4568
4648
  ["worktree", "add", worktreePath, branchName],
4569
4649
  opts.repoPath,
@@ -4574,7 +4654,7 @@ async function createExistingBranchWorktree(opts) {
4574
4654
  `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
4575
4655
  );
4576
4656
  }
4577
- }
4657
+ });
4578
4658
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
4579
4659
  reject: false
4580
4660
  });
@@ -4585,12 +4665,14 @@ async function createExistingBranchWorktree(opts) {
4585
4665
  return { branchName, worktreePath };
4586
4666
  }
4587
4667
  async function removeWorktree(repoPath, worktreePath, opts) {
4588
- await git(["worktree", "remove", "--force", worktreePath], repoPath, {
4589
- reject: false
4668
+ await withRepoGitLock(repoPath, async () => {
4669
+ await git(["worktree", "remove", "--force", worktreePath], repoPath, {
4670
+ reject: false
4671
+ });
4672
+ if (opts?.deleteBranch) {
4673
+ await git(["branch", "-D", opts.deleteBranch], repoPath, { reject: false });
4674
+ }
4590
4675
  });
4591
- if (opts?.deleteBranch) {
4592
- await git(["branch", "-D", opts.deleteBranch], repoPath, { reject: false });
4593
- }
4594
4676
  }
4595
4677
  async function listWorktrees(repoPath) {
4596
4678
  const { stdout } = await git(
@@ -4882,6 +4964,7 @@ var init_worktree = __esm({
4882
4964
  init_gh_errors();
4883
4965
  init_git_auth_mode();
4884
4966
  init_run();
4967
+ init_repo_git_lock();
4885
4968
  init_app_settings();
4886
4969
  init_pr_gates();
4887
4970
  init_stack();
@@ -4984,7 +5067,7 @@ function coordinatorGreenfieldPlaybook(reposDir) {
4984
5067
  "- Examples:",
4985
5068
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
4986
5069
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
4987
- "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
5070
+ "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
4988
5071
  "- Always target the child worktree's **origin** (`github:` slug from list_workspaces / `git remote get-url origin` in that worktree). Never open PRs against `upstream`.",
4989
5072
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
4990
5073
  ].join("\n");
@@ -5007,8 +5090,8 @@ function coordinatorTurnReminder(opts) {
5007
5090
  goal ? `- Goal / title: ${goal}` : null,
5008
5091
  accountDefaultsPlaybookLine(),
5009
5092
  `- For "what's going on": call list_threads (and list_workspaces if needed). Summarize fleet status \u2014 do not ls/git-status this synthetic home.`,
5010
- "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
5011
- "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft.",
5093
+ "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If stillRunning, the child is working (progress is tools/thinking) \u2014 wait_for_turn again; do not ping it. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
5094
+ "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
5012
5095
  "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
5013
5096
  "- If they will wait on Slack or leave the Mac, call set_caffeinate enabled=true. When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
5014
5097
  ].filter(Boolean).join("\n");
@@ -5061,8 +5144,8 @@ function ensureGlobalCoordinatorCwd(opts) {
5061
5144
  orchId ? `Pass parentThreadId="${orchId}" (or omit it). Never invent another parentThreadId.` : "Pass `parentThreadId` for children (this chat's id from the turn reminder).",
5062
5145
  "Omit `agent` / `model` on `create_thread` unless you have a reason to override Account defaults.",
5063
5146
  "Never pass `agent=codex` when you yourself are Codex \u2014 nested Codex deadlocks on shared ~/.codex locks. Omit agent (Account default) or use cursor/claude.",
5064
- "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
5065
- "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn \u2192 ask_git create-draft.",
5147
+ "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
5148
+ "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
5066
5149
  "Always ask worktree agents to commit, push, and open draft PRs (`ask_git` / `send_to_thread`). Tell them to merge only when the user explicitly asked. The worktree agent runs git/gh; never merge from this orchestration cwd."
5067
5150
  ].join("\n");
5068
5151
  try {
@@ -5130,7 +5213,7 @@ var init_coordinator_prompt = __esm({
5130
5213
  "- fork_worktree \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
5131
5214
  "- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
5132
5215
  "- send_to_thread \u2014 queue a prompt (start/continue a chat turn); pass force_stop: true to interrupt mid-turn / clear stale queued prompts before replacing with a new request",
5133
- "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
5216
+ "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply. wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). If stillRunning is true, progress is a live snapshot of tools/thinking \u2014 call wait_for_turn again. Do not send \u201Care you stuck?\u201D or assume a hang while lastActivityAt is recent. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
5134
5217
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
5135
5218
  "- archive_thread / restore_thread \u2014 archive (tears down worktree when last tab) or restore",
5136
5219
  "Setup / run:",
@@ -5138,8 +5221,8 @@ var init_coordinator_prompt = __esm({
5138
5221
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
5139
5222
  "Inspect / review / PRs:",
5140
5223
  "- get_diff \u2014 compact diff summary",
5141
- '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn / get_turn_result on the returned id',
5142
- "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn. Prefer this over paraphrasing.",
5224
+ '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id',
5225
+ "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing.",
5143
5226
  '- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
5144
5227
  '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
5145
5228
  "Process guides:",
@@ -6046,6 +6129,55 @@ var init_profile = __esm({
6046
6129
  }
6047
6130
  });
6048
6131
 
6132
+ // src/agents/packaged-runtime.ts
6133
+ function electronResourcesPath() {
6134
+ const resources = process.resourcesPath;
6135
+ if (typeof resources !== "string" || !resources) return null;
6136
+ return resources;
6137
+ }
6138
+ function packagedCursorRuntimeDir() {
6139
+ const resources = electronResourcesPath();
6140
+ if (!resources) return null;
6141
+ const dir = (0, import_node_path19.join)(resources, "cursor-runtime");
6142
+ if (!(0, import_node_fs18.existsSync)((0, import_node_path19.join)(dir, "core-dist", "agents", "cursor-runner.js"))) return null;
6143
+ return dir;
6144
+ }
6145
+ function packagedCursorRunnerPath() {
6146
+ const dir = packagedCursorRuntimeDir();
6147
+ return dir ? (0, import_node_path19.join)(dir, "core-dist", "agents", "cursor-runner.js") : null;
6148
+ }
6149
+ function packagedMcpDir() {
6150
+ const resources = electronResourcesPath();
6151
+ if (!resources) return null;
6152
+ const dir = (0, import_node_path19.join)(resources, "sideboard-mcp");
6153
+ if (!(0, import_node_fs18.existsSync)((0, import_node_path19.join)(dir, "core-dist", "mcp", "run-stdio.js"))) return null;
6154
+ return dir;
6155
+ }
6156
+ function packagedMcpStdioPath() {
6157
+ const dir = packagedMcpDir();
6158
+ return dir ? (0, import_node_path19.join)(dir, "core-dist", "mcp", "run-stdio.js") : null;
6159
+ }
6160
+ function packagedBundledNodePath() {
6161
+ const resources = electronResourcesPath();
6162
+ if (!resources) return null;
6163
+ const bin = (0, import_node_path19.join)(resources, "node", "bin", "node");
6164
+ if (!(0, import_node_fs18.existsSync)(bin)) return null;
6165
+ return bin;
6166
+ }
6167
+ function packagedCursorRipgrepCandidate(platformPkg, binName) {
6168
+ const dir = packagedCursorRuntimeDir();
6169
+ if (!dir) return null;
6170
+ return (0, import_node_path19.join)(dir, "node_modules", platformPkg, "bin", binName);
6171
+ }
6172
+ var import_node_fs18, import_node_path19;
6173
+ var init_packaged_runtime = __esm({
6174
+ "src/agents/packaged-runtime.ts"() {
6175
+ "use strict";
6176
+ import_node_fs18 = require("fs");
6177
+ import_node_path19 = require("path");
6178
+ }
6179
+ });
6180
+
6049
6181
  // src/agents/node-launch.ts
6050
6182
  function isAsarPath(filePath) {
6051
6183
  if (/\.asar\.unpacked([/\\]|$)/.test(filePath)) return false;
@@ -6055,24 +6187,129 @@ function unpackedAsarPath(filePath) {
6055
6187
  if (!isAsarPath(filePath)) return null;
6056
6188
  const unpacked = filePath.replace(/\.asar(?=[/\\])/, ".asar.unpacked");
6057
6189
  if (unpacked === filePath) return null;
6058
- return (0, import_node_fs18.existsSync)(unpacked) ? unpacked : null;
6190
+ return (0, import_node_fs19.existsSync)(unpacked) ? unpacked : null;
6059
6191
  }
6060
6192
  function nodeReadableScriptPath(scriptPath) {
6061
6193
  return unpackedAsarPath(scriptPath) ?? scriptPath;
6062
6194
  }
6063
- async function findSystemNode() {
6064
- const whichNode = await run("which", ["node"], { reject: false });
6065
- const fromWhich = whichNode.exitCode === 0 && whichNode.stdout.trim() ? whichNode.stdout.trim() : "";
6066
- if (fromWhich && !isElectronLikeCommand(fromWhich)) return fromWhich;
6067
- const fallbacks = [
6068
- ...WELL_KNOWN_NODE_BINS,
6069
- (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".local/share/fnm/aliases/default/bin/node"),
6070
- (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".nvm/current/bin/node")
6195
+ function parseNodeMajor(version) {
6196
+ const match = /^v?(\d+)/.exec(version.trim());
6197
+ if (!match) return null;
6198
+ const major = Number(match[1]);
6199
+ return Number.isInteger(major) ? major : null;
6200
+ }
6201
+ function scoreNodeForAgentRuntime(candidate) {
6202
+ const major = parseNodeMajor(candidate.version);
6203
+ if (major == null) return Number.NEGATIVE_INFINITY;
6204
+ if (major < 20) return major - 100;
6205
+ const posix = candidate.path.replace(/\\/g, "/");
6206
+ let score = 0;
6207
+ if (major % 2 === 0) {
6208
+ score += 1e3 + major * 10;
6209
+ } else {
6210
+ score += major;
6211
+ }
6212
+ if (/\/Cellar\/node\/\d/.test(posix) && !/\/Cellar\/node@\d+/.test(posix)) {
6213
+ score -= 50;
6214
+ }
6215
+ return score;
6216
+ }
6217
+ function pickPreferredNode(candidates) {
6218
+ let best = null;
6219
+ let bestScore = Number.NEGATIVE_INFINITY;
6220
+ for (const candidate of candidates) {
6221
+ const score = scoreNodeForAgentRuntime(candidate);
6222
+ if (score > bestScore) {
6223
+ bestScore = score;
6224
+ best = candidate;
6225
+ }
6226
+ }
6227
+ return best;
6228
+ }
6229
+ function versionDirNodeBins(root, toBin) {
6230
+ if (!(0, import_node_fs19.existsSync)(root)) return [];
6231
+ try {
6232
+ return (0, import_node_fs19.readdirSync)(root).map(toBin);
6233
+ } catch {
6234
+ return [];
6235
+ }
6236
+ }
6237
+ function defaultNodeBinCandidates(home = (0, import_node_os7.homedir)()) {
6238
+ const kegs = ["/opt/homebrew", "/usr/local"].flatMap(
6239
+ (prefix) => PREFERRED_LTS_MAJORS.map((major) => (0, import_node_path20.join)(prefix, "opt", `node@${major}`, "bin", "node"))
6240
+ );
6241
+ return [
6242
+ ...kegs,
6243
+ "/opt/homebrew/bin/node",
6244
+ "/usr/local/bin/node",
6245
+ (0, import_node_path20.join)(home, ".local/share/fnm/aliases/default/bin/node"),
6246
+ (0, import_node_path20.join)(home, ".nvm/current/bin/node"),
6247
+ (0, import_node_path20.join)(home, ".volta/bin/node"),
6248
+ (0, import_node_path20.join)(home, ".asdf/shims/node"),
6249
+ (0, import_node_path20.join)(home, ".local/share/mise/shims/node"),
6250
+ ...versionDirNodeBins(
6251
+ (0, import_node_path20.join)(home, ".nvm", "versions", "node"),
6252
+ (name) => (0, import_node_path20.join)(home, ".nvm", "versions", "node", name, "bin", "node")
6253
+ ),
6254
+ ...versionDirNodeBins(
6255
+ (0, import_node_path20.join)(home, ".local/share/fnm", "node-versions"),
6256
+ (name) => (0, import_node_path20.join)(home, ".local/share/fnm", "node-versions", name, "installation", "bin", "node")
6257
+ ),
6258
+ ...versionDirNodeBins(
6259
+ (0, import_node_path20.join)(home, ".volta", "tools", "image", "node"),
6260
+ (name) => (0, import_node_path20.join)(home, ".volta", "tools", "image", "node", name, "bin", "node")
6261
+ )
6071
6262
  ];
6072
- for (const bin of fallbacks) {
6073
- if ((0, import_node_fs18.existsSync)(bin) && !isElectronLikeCommand(bin)) return bin;
6263
+ }
6264
+ function uniqueExistingNodeBins(paths) {
6265
+ const seen = /* @__PURE__ */ new Set();
6266
+ const out = [];
6267
+ for (const raw of paths) {
6268
+ const p = raw.trim();
6269
+ if (!p || !(0, import_node_fs19.existsSync)(p) || isElectronLikeCommand(p)) continue;
6270
+ let key = p;
6271
+ try {
6272
+ key = (0, import_node_fs19.realpathSync)(p);
6273
+ } catch {
6274
+ continue;
6275
+ }
6276
+ if (seen.has(key)) continue;
6277
+ seen.add(key);
6278
+ out.push(p);
6074
6279
  }
6075
- return null;
6280
+ return out;
6281
+ }
6282
+ async function whichAllNode() {
6283
+ if (process.platform === "win32") {
6284
+ const located = await run("where", ["node"], { reject: false, timeoutMs: 5e3 });
6285
+ if (located.exitCode !== 0) return [];
6286
+ return located.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
6287
+ }
6288
+ const all = await run("which", ["-a", "node"], { reject: false, timeoutMs: 5e3 });
6289
+ if (all.exitCode === 0 && all.stdout.trim()) {
6290
+ return all.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
6291
+ }
6292
+ const single = await run("which", ["node"], { reject: false, timeoutMs: 5e3 });
6293
+ if (single.exitCode !== 0 || !single.stdout.trim()) return [];
6294
+ return [single.stdout.trim()];
6295
+ }
6296
+ async function probeNodeVersion(bin) {
6297
+ const probed = await run(bin, ["-v"], { reject: false, timeoutMs: 4e3 });
6298
+ if (probed.exitCode !== 0) return null;
6299
+ const version = probed.stdout.trim().split(/\r?\n/).find(Boolean);
6300
+ return version || null;
6301
+ }
6302
+ async function findSystemNode() {
6303
+ const fromPath = await whichAllNode();
6304
+ const bins = uniqueExistingNodeBins([...fromPath, ...defaultNodeBinCandidates()]);
6305
+ if (bins.length === 0) return null;
6306
+ const probed = await Promise.all(
6307
+ bins.map(async (path) => {
6308
+ const version = await probeNodeVersion(path);
6309
+ return version ? { path, version } : null;
6310
+ })
6311
+ );
6312
+ return pickPreferredNode(probed.filter((c) => c != null));
6076
6313
  }
6077
6314
  function applyNodeLaunch(launch, args) {
6078
6315
  const readableArgs = args.map(nodeReadableScriptPath);
@@ -6090,9 +6327,13 @@ function applyNodeLaunch(launch, args) {
6090
6327
  async function resolveNodeLaunch(scriptPath) {
6091
6328
  const script = nodeReadableScriptPath(scriptPath);
6092
6329
  if (!isAsarPath(script)) {
6093
- const nodeBin = await findSystemNode();
6094
- if (nodeBin) {
6095
- return { file: nodeBin, env: {} };
6330
+ const bundled = packagedBundledNodePath();
6331
+ if (bundled && !isElectronLikeCommand(bundled)) {
6332
+ return { file: bundled, env: {} };
6333
+ }
6334
+ const node = await findSystemNode();
6335
+ if (node) {
6336
+ return { file: node.path, env: {}, nodeVersion: node.version };
6096
6337
  }
6097
6338
  }
6098
6339
  return {
@@ -6100,61 +6341,17 @@ async function resolveNodeLaunch(scriptPath) {
6100
6341
  env: { ELECTRON_RUN_AS_NODE: "1" }
6101
6342
  };
6102
6343
  }
6103
- var import_node_fs18, import_node_os7, import_node_path19, WELL_KNOWN_NODE_BINS;
6344
+ var import_node_fs19, import_node_os7, import_node_path20, PREFERRED_LTS_MAJORS;
6104
6345
  var init_node_launch = __esm({
6105
6346
  "src/agents/node-launch.ts"() {
6106
6347
  "use strict";
6107
- import_node_fs18 = require("fs");
6348
+ import_node_fs19 = require("fs");
6108
6349
  import_node_os7 = require("os");
6109
- import_node_path19 = require("path");
6350
+ import_node_path20 = require("path");
6110
6351
  init_nested_electron_env();
6111
6352
  init_run();
6112
- WELL_KNOWN_NODE_BINS = [
6113
- "/opt/homebrew/bin/node",
6114
- "/usr/local/bin/node"
6115
- ];
6116
- }
6117
- });
6118
-
6119
- // src/agents/packaged-runtime.ts
6120
- function electronResourcesPath() {
6121
- const resources = process.resourcesPath;
6122
- if (typeof resources !== "string" || !resources) return null;
6123
- return resources;
6124
- }
6125
- function packagedCursorRuntimeDir() {
6126
- const resources = electronResourcesPath();
6127
- if (!resources) return null;
6128
- const dir = (0, import_node_path20.join)(resources, "cursor-runtime");
6129
- if (!(0, import_node_fs19.existsSync)((0, import_node_path20.join)(dir, "core-dist", "agents", "cursor-runner.js"))) return null;
6130
- return dir;
6131
- }
6132
- function packagedCursorRunnerPath() {
6133
- const dir = packagedCursorRuntimeDir();
6134
- return dir ? (0, import_node_path20.join)(dir, "core-dist", "agents", "cursor-runner.js") : null;
6135
- }
6136
- function packagedMcpDir() {
6137
- const resources = electronResourcesPath();
6138
- if (!resources) return null;
6139
- const dir = (0, import_node_path20.join)(resources, "sideboard-mcp");
6140
- if (!(0, import_node_fs19.existsSync)((0, import_node_path20.join)(dir, "core-dist", "mcp", "run-stdio.js"))) return null;
6141
- return dir;
6142
- }
6143
- function packagedMcpStdioPath() {
6144
- const dir = packagedMcpDir();
6145
- return dir ? (0, import_node_path20.join)(dir, "core-dist", "mcp", "run-stdio.js") : null;
6146
- }
6147
- function packagedCursorRipgrepCandidate(platformPkg, binName) {
6148
- const dir = packagedCursorRuntimeDir();
6149
- if (!dir) return null;
6150
- return (0, import_node_path20.join)(dir, "node_modules", platformPkg, "bin", binName);
6151
- }
6152
- var import_node_fs19, import_node_path20;
6153
- var init_packaged_runtime = __esm({
6154
- "src/agents/packaged-runtime.ts"() {
6155
- "use strict";
6156
- import_node_fs19 = require("fs");
6157
- import_node_path20 = require("path");
6353
+ init_packaged_runtime();
6354
+ PREFERRED_LTS_MAJORS = [24, 22, 20];
6158
6355
  }
6159
6356
  });
6160
6357
 
@@ -8645,17 +8842,17 @@ __export(plan_file_exports, {
8645
8842
  });
8646
8843
  function ensureAttachmentsGitignore2(worktreePath) {
8647
8844
  const gitignoreAbs = (0, import_node_path36.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
8648
- if ((0, import_node_fs39.existsSync)(gitignoreAbs)) return;
8649
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(gitignoreAbs), { recursive: true });
8650
- (0, import_node_fs39.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
8845
+ if ((0, import_node_fs40.existsSync)(gitignoreAbs)) return;
8846
+ (0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(gitignoreAbs), { recursive: true });
8847
+ (0, import_node_fs40.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
8651
8848
  }
8652
8849
  function planFileAbs(worktreePath) {
8653
8850
  return (0, import_node_path36.join)(worktreePath, PLAN_FILE_REL);
8654
8851
  }
8655
8852
  function readTextIfPresent2(abs) {
8656
- if (!(0, import_node_fs39.existsSync)(abs)) return null;
8853
+ if (!(0, import_node_fs40.existsSync)(abs)) return null;
8657
8854
  try {
8658
- const content = (0, import_node_fs39.readFileSync)(abs, "utf8");
8855
+ const content = (0, import_node_fs40.readFileSync)(abs, "utf8");
8659
8856
  return content.trim() ? content : null;
8660
8857
  } catch {
8661
8858
  return null;
@@ -8667,16 +8864,16 @@ function readPlanFile(worktreePath) {
8667
8864
  function writePlanFile(worktreePath, content) {
8668
8865
  ensureAttachmentsGitignore2(worktreePath);
8669
8866
  const abs = planFileAbs(worktreePath);
8670
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(abs), { recursive: true });
8867
+ (0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(abs), { recursive: true });
8671
8868
  const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
8672
- (0, import_node_fs39.writeFileSync)(abs, body, "utf8");
8869
+ (0, import_node_fs40.writeFileSync)(abs, body, "utf8");
8673
8870
  return PLAN_FILE_REL;
8674
8871
  }
8675
- var import_node_fs39, import_node_path36;
8872
+ var import_node_fs40, import_node_path36;
8676
8873
  var init_plan_file = __esm({
8677
8874
  "src/plan/plan-file.ts"() {
8678
8875
  "use strict";
8679
- import_node_fs39 = require("fs");
8876
+ import_node_fs40 = require("fs");
8680
8877
  import_node_path36 = require("path");
8681
8878
  init_workspace_scratch();
8682
8879
  init_plan_present();
@@ -8732,9 +8929,9 @@ function uniqueIds(ids) {
8732
8929
  }
8733
8930
  function readHold() {
8734
8931
  const path = caffeinateHoldPath();
8735
- if (!(0, import_node_fs40.existsSync)(path)) return null;
8932
+ if (!(0, import_node_fs41.existsSync)(path)) return null;
8736
8933
  try {
8737
- const parsed = JSON.parse((0, import_node_fs40.readFileSync)(path, "utf8"));
8934
+ const parsed = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
8738
8935
  if (typeof parsed?.pid === "number" && parsed.pid > 0) {
8739
8936
  return {
8740
8937
  pid: parsed.pid,
@@ -8756,7 +8953,7 @@ function writeHold(pid, threadIds) {
8756
8953
  }
8757
8954
  function clearHold() {
8758
8955
  try {
8759
- (0, import_node_fs40.unlinkSync)(caffeinateHoldPath());
8956
+ (0, import_node_fs41.unlinkSync)(caffeinateHoldPath());
8760
8957
  } catch {
8761
8958
  }
8762
8959
  }
@@ -8841,12 +9038,12 @@ function releaseCaffeinateHoldForThread(threadId) {
8841
9038
  }
8842
9039
  return setCaffeinateHold(false, { threadId: id });
8843
9040
  }
8844
- var import_node_child_process4, import_node_fs40, import_node_path37, hooks;
9041
+ var import_node_child_process4, import_node_fs41, import_node_path37, hooks;
8845
9042
  var init_caffeinate_hold = __esm({
8846
9043
  "src/store/caffeinate-hold.ts"() {
8847
9044
  "use strict";
8848
9045
  import_node_child_process4 = require("child_process");
8849
- import_node_fs40 = require("fs");
9046
+ import_node_fs41 = require("fs");
8850
9047
  import_node_path37 = require("path");
8851
9048
  init_paths();
8852
9049
  init_private_file();
@@ -8863,9 +9060,9 @@ function recoverFinishedCursorRun(opts) {
8863
9060
  const agentId = opts.agentId.trim();
8864
9061
  if (!agentId) return null;
8865
9062
  const runsPath = (0, import_node_path38.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
8866
- if (!(0, import_node_fs41.existsSync)(runsPath)) return null;
9063
+ if (!(0, import_node_fs42.existsSync)(runsPath)) return null;
8867
9064
  try {
8868
- const lines = (0, import_node_fs41.readFileSync)(runsPath, "utf8").split("\n");
9065
+ const lines = (0, import_node_fs42.readFileSync)(runsPath, "utf8").split("\n");
8869
9066
  let best = null;
8870
9067
  for (const line of lines) {
8871
9068
  const trimmed = line.trim();
@@ -8891,11 +9088,11 @@ function recoverFinishedCursorRun(opts) {
8891
9088
  return null;
8892
9089
  }
8893
9090
  }
8894
- var import_node_fs41, import_node_path38;
9091
+ var import_node_fs42, import_node_path38;
8895
9092
  var init_cursor_recover = __esm({
8896
9093
  "src/agents/cursor-recover.ts"() {
8897
9094
  "use strict";
8898
- import_node_fs41 = require("fs");
9095
+ import_node_fs42 = require("fs");
8899
9096
  import_node_path38 = require("path");
8900
9097
  init_paths();
8901
9098
  }
@@ -9419,7 +9616,7 @@ async function refreshSlackReplyBadges(opts) {
9419
9616
  }
9420
9617
 
9421
9618
  // src/orchestrator/orchestrator.ts
9422
- var import_node_fs42 = require("fs");
9619
+ var import_node_fs43 = require("fs");
9423
9620
  init_error_detail();
9424
9621
 
9425
9622
  // src/agents/spawn.ts
@@ -9673,7 +9870,7 @@ function createAgentStreamCoalescer(emit, opts) {
9673
9870
  const intervalMs = opts?.intervalMs ?? 32;
9674
9871
  let pending = null;
9675
9872
  let timer = null;
9676
- const flush = () => {
9873
+ const flush2 = () => {
9677
9874
  if (timer) {
9678
9875
  clearTimeout(timer);
9679
9876
  timer = null;
@@ -9685,13 +9882,13 @@ function createAgentStreamCoalescer(emit, opts) {
9685
9882
  };
9686
9883
  const schedule = () => {
9687
9884
  if (timer) return;
9688
- timer = setTimeout(flush, intervalMs);
9885
+ timer = setTimeout(flush2, intervalMs);
9689
9886
  timer.unref?.();
9690
9887
  };
9691
9888
  return {
9692
9889
  push(event) {
9693
9890
  if (!isTextEvent(event)) {
9694
- flush();
9891
+ flush2();
9695
9892
  emit(event);
9696
9893
  return;
9697
9894
  }
@@ -9699,12 +9896,12 @@ function createAgentStreamCoalescer(emit, opts) {
9699
9896
  if (pending && pending.type === event.type) {
9700
9897
  pending.data += event.data;
9701
9898
  } else {
9702
- flush();
9899
+ flush2();
9703
9900
  pending = { type: event.type, data: event.data };
9704
9901
  }
9705
9902
  schedule();
9706
9903
  },
9707
- flush
9904
+ flush: flush2
9708
9905
  };
9709
9906
  }
9710
9907
 
@@ -11020,8 +11217,11 @@ function worktreeBindingFrom(from) {
11020
11217
  };
11021
11218
  }
11022
11219
  function threadsSharingWorktree(worktreePath) {
11220
+ if (!worktreePath) return [];
11023
11221
  const key = normalizeWorktreePath(worktreePath);
11024
- return listThreads({ includeArchived: true }).filter((t) => sameWorktreePath(t.worktreePath, key) && t.status !== "archived").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
11222
+ return listThreads({ includeArchived: true }).filter(
11223
+ (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
11224
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
11025
11225
  }
11026
11226
  function formatTranscriptMarkdown(title, messages) {
11027
11227
  const body = formatMessagesAsTranscript(messages, { tools: "full" });
@@ -11092,9 +11292,132 @@ function forkChatTab(input) {
11092
11292
  return tab;
11093
11293
  }
11094
11294
 
11295
+ // src/orchestrator/orchestrator.ts
11296
+ init_enqueue_by_key();
11297
+ init_run();
11298
+ init_repo_git_lock();
11299
+
11300
+ // src/store/turn-live.ts
11301
+ var import_node_fs32 = require("fs");
11302
+ init_paths();
11303
+ var buffers = /* @__PURE__ */ new Map();
11304
+ var FLUSH_MS = 800;
11305
+ var MAX_PARTS = 80;
11306
+ function trimParts(parts) {
11307
+ if (parts.length <= MAX_PARTS) return parts;
11308
+ return parts.slice(-Math.floor(MAX_PARTS / 2));
11309
+ }
11310
+ function lastText(parts, type) {
11311
+ for (let i = parts.length - 1; i >= 0; i--) {
11312
+ const p = parts[i];
11313
+ if (p.type === type && p.text.trim()) return p.text.trim();
11314
+ }
11315
+ return "";
11316
+ }
11317
+ function summarizeTurnLive(parts) {
11318
+ const tools = parts.filter(
11319
+ (p) => p.type === "tool"
11320
+ );
11321
+ const lastTool = tools[tools.length - 1];
11322
+ const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
11323
+ const running = lastTool?.status === "running";
11324
+ const thinking = lastText(parts, "thinking");
11325
+ const text3 = lastText(parts, "text");
11326
+ const excerptRaw = thinking || text3;
11327
+ const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
11328
+ let summary = "Working\u2026";
11329
+ if (lastToolLabel) {
11330
+ const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
11331
+ summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
11332
+ } else if (thinking) {
11333
+ summary = "Thinking\u2026";
11334
+ } else if (text3) {
11335
+ summary = "Writing reply\u2026";
11336
+ }
11337
+ return {
11338
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11339
+ summary,
11340
+ lastTool: lastToolLabel,
11341
+ toolCount: tools.length,
11342
+ excerpt
11343
+ };
11344
+ }
11345
+ function flush(threadId) {
11346
+ const buf = buffers.get(threadId);
11347
+ if (!buf) return;
11348
+ if (buf.timer) {
11349
+ clearTimeout(buf.timer);
11350
+ buf.timer = null;
11351
+ }
11352
+ writeTurnLive(threadId, summarizeTurnLive(buf.parts));
11353
+ }
11354
+ function scheduleFlush(threadId) {
11355
+ const buf = buffers.get(threadId);
11356
+ if (!buf) return;
11357
+ if (buf.timer) return;
11358
+ buf.timer = setTimeout(() => {
11359
+ const live = buffers.get(threadId);
11360
+ if (live) live.timer = null;
11361
+ flush(threadId);
11362
+ }, FLUSH_MS);
11363
+ }
11364
+ function noteTurnLiveEvent(threadId, event) {
11365
+ if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
11366
+ return;
11367
+ }
11368
+ const prev = buffers.get(threadId) ?? { parts: [], timer: null };
11369
+ prev.parts = trimParts(applyAgentEvent(prev.parts, event));
11370
+ buffers.set(threadId, prev);
11371
+ if (event.type === "exit") {
11372
+ flush(threadId);
11373
+ const cur = buffers.get(threadId);
11374
+ if (cur?.timer) clearTimeout(cur.timer);
11375
+ buffers.delete(threadId);
11376
+ return;
11377
+ }
11378
+ const immediate = event.type === "tool_use" || event.type === "tool_result";
11379
+ if (immediate) flush(threadId);
11380
+ else scheduleFlush(threadId);
11381
+ }
11382
+ function writeTurnLive(threadId, progress) {
11383
+ const path = threadLivePath(threadId);
11384
+ const tmp = `${path}.${process.pid}.tmp`;
11385
+ try {
11386
+ (0, import_node_fs32.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
11387
+ (0, import_node_fs32.renameSync)(tmp, path);
11388
+ } catch {
11389
+ try {
11390
+ (0, import_node_fs32.unlinkSync)(tmp);
11391
+ } catch {
11392
+ }
11393
+ }
11394
+ }
11395
+ function readTurnLive(threadId) {
11396
+ const path = threadLivePath(threadId);
11397
+ if (!(0, import_node_fs32.existsSync)(path)) return null;
11398
+ try {
11399
+ const raw = JSON.parse((0, import_node_fs32.readFileSync)(path, "utf8"));
11400
+ if (!raw || typeof raw.summary !== "string") return null;
11401
+ return raw;
11402
+ } catch {
11403
+ return null;
11404
+ }
11405
+ }
11406
+ function clearTurnLive(threadId) {
11407
+ const buf = buffers.get(threadId);
11408
+ if (buf?.timer) clearTimeout(buf.timer);
11409
+ buffers.delete(threadId);
11410
+ const path = threadLivePath(threadId);
11411
+ if (!(0, import_node_fs32.existsSync)(path)) return;
11412
+ try {
11413
+ (0, import_node_fs32.unlinkSync)(path);
11414
+ } catch {
11415
+ }
11416
+ }
11417
+
11095
11418
  // src/review/request-review.ts
11096
11419
  var import_node_crypto5 = require("crypto");
11097
- var import_node_fs32 = require("fs");
11420
+ var import_node_fs33 = require("fs");
11098
11421
  var import_node_path30 = require("path");
11099
11422
  init_global_workspace();
11100
11423
  init_thread_store();
@@ -11243,9 +11566,9 @@ function shouldRefreshReviewRequestTemplate(content) {
11243
11566
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
11244
11567
  }
11245
11568
  function readTextIfPresent(abs) {
11246
- if (!(0, import_node_fs32.existsSync)(abs)) return null;
11569
+ if (!(0, import_node_fs33.existsSync)(abs)) return null;
11247
11570
  try {
11248
- const content = (0, import_node_fs32.readFileSync)(abs, "utf8");
11571
+ const content = (0, import_node_fs33.readFileSync)(abs, "utf8");
11249
11572
  return content.trim() ? content : null;
11250
11573
  } catch {
11251
11574
  return null;
@@ -11253,9 +11576,9 @@ function readTextIfPresent(abs) {
11253
11576
  }
11254
11577
  function ensureAttachmentsGitignore(worktreePath) {
11255
11578
  const gitignoreAbs = (0, import_node_path30.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
11256
- if ((0, import_node_fs32.existsSync)(gitignoreAbs)) return;
11257
- (0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(gitignoreAbs), { recursive: true });
11258
- (0, import_node_fs32.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
11579
+ if ((0, import_node_fs33.existsSync)(gitignoreAbs)) return;
11580
+ (0, import_node_fs33.mkdirSync)((0, import_node_path30.dirname)(gitignoreAbs), { recursive: true });
11581
+ (0, import_node_fs33.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
11259
11582
  }
11260
11583
  function resolveReviewGuidelines(worktreePath) {
11261
11584
  const repoAbs = (0, import_node_path30.join)(worktreePath, REPO_REVIEW_PATH);
@@ -11289,8 +11612,8 @@ function resolveReviewGuidelines(worktreePath) {
11289
11612
  };
11290
11613
  }
11291
11614
  ensureAttachmentsGitignore(worktreePath);
11292
- (0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(localAbs), { recursive: true });
11293
- (0, import_node_fs32.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
11615
+ (0, import_node_fs33.mkdirSync)((0, import_node_path30.dirname)(localAbs), { recursive: true });
11616
+ (0, import_node_fs33.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
11294
11617
  return {
11295
11618
  path: REVIEW_REQUEST_PATH,
11296
11619
  name: REVIEW_REQUEST_NAME,
@@ -11490,7 +11813,7 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
11490
11813
 
11491
11814
  // src/threads/adopt.ts
11492
11815
  var import_node_child_process3 = require("child_process");
11493
- var import_node_fs33 = require("fs");
11816
+ var import_node_fs34 = require("fs");
11494
11817
  var import_node_os10 = require("os");
11495
11818
  var import_node_path31 = require("path");
11496
11819
  var import_node_module4 = require("module");
@@ -11521,21 +11844,21 @@ function mapAgentType(raw) {
11521
11844
  return null;
11522
11845
  }
11523
11846
  function resolveConductorCursorAgentId(workspacePath) {
11524
- if (!workspacePath || !(0, import_node_fs33.existsSync)(CURSOR_SDK_STORE)) return null;
11847
+ if (!workspacePath || !(0, import_node_fs34.existsSync)(CURSOR_SDK_STORE)) return null;
11525
11848
  const normalized = workspacePath.replace(/\/$/, "");
11526
11849
  let best = null;
11527
11850
  let hashes;
11528
11851
  try {
11529
- hashes = (0, import_node_fs33.readdirSync)(CURSOR_SDK_STORE);
11852
+ hashes = (0, import_node_fs34.readdirSync)(CURSOR_SDK_STORE);
11530
11853
  } catch {
11531
11854
  return null;
11532
11855
  }
11533
11856
  for (const hash of hashes) {
11534
11857
  const agentsFile = (0, import_node_path31.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
11535
- if (!(0, import_node_fs33.existsSync)(agentsFile)) continue;
11858
+ if (!(0, import_node_fs34.existsSync)(agentsFile)) continue;
11536
11859
  let text3;
11537
11860
  try {
11538
- text3 = (0, import_node_fs33.readFileSync)(agentsFile, "utf8");
11861
+ text3 = (0, import_node_fs34.readFileSync)(agentsFile, "utf8");
11539
11862
  } catch {
11540
11863
  continue;
11541
11864
  }
@@ -11559,7 +11882,7 @@ function resolveConductorCursorAgentId(workspacePath) {
11559
11882
  return best?.agentId ?? null;
11560
11883
  }
11561
11884
  async function adoptThread(input) {
11562
- if (!(0, import_node_fs33.existsSync)(input.worktreePath)) {
11885
+ if (!(0, import_node_fs34.existsSync)(input.worktreePath)) {
11563
11886
  throw new Error(`Worktree not found: ${input.worktreePath}`);
11564
11887
  }
11565
11888
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -11583,18 +11906,18 @@ async function adoptThread(input) {
11583
11906
  return thread;
11584
11907
  }
11585
11908
  function listConductorWorkspaces() {
11586
- if (!(0, import_node_fs33.existsSync)(CONDUCTOR_DB)) {
11909
+ if (!(0, import_node_fs34.existsSync)(CONDUCTOR_DB)) {
11587
11910
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
11588
11911
  }
11589
- const tmp = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0, import_node_os10.tmpdir)(), "sideboard-conductor-"));
11912
+ const tmp = (0, import_node_fs34.mkdtempSync)((0, import_node_path31.join)((0, import_node_os10.tmpdir)(), "sideboard-conductor-"));
11590
11913
  const snapshot = (0, import_node_path31.join)(tmp, "conductor.db");
11591
11914
  try {
11592
- (0, import_node_fs33.copyFileSync)(CONDUCTOR_DB, snapshot);
11915
+ (0, import_node_fs34.copyFileSync)(CONDUCTOR_DB, snapshot);
11593
11916
  for (const suffix of ["-wal", "-shm"]) {
11594
11917
  const src = `${CONDUCTOR_DB}${suffix}`;
11595
- if ((0, import_node_fs33.existsSync)(src)) {
11918
+ if ((0, import_node_fs34.existsSync)(src)) {
11596
11919
  try {
11597
- (0, import_node_fs33.copyFileSync)(src, `${snapshot}${suffix}`);
11920
+ (0, import_node_fs34.copyFileSync)(src, `${snapshot}${suffix}`);
11598
11921
  } catch {
11599
11922
  }
11600
11923
  }
@@ -11670,22 +11993,22 @@ function listConductorWorkspaces() {
11670
11993
  db.close();
11671
11994
  }
11672
11995
  } finally {
11673
- (0, import_node_fs33.rmSync)(tmp, { recursive: true, force: true });
11996
+ (0, import_node_fs34.rmSync)(tmp, { recursive: true, force: true });
11674
11997
  }
11675
11998
  }
11676
11999
  function importConductorWorkspace(workspaceId) {
11677
- if (!(0, import_node_fs33.existsSync)(CONDUCTOR_DB)) {
12000
+ if (!(0, import_node_fs34.existsSync)(CONDUCTOR_DB)) {
11678
12001
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
11679
12002
  }
11680
- const tmp = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0, import_node_os10.tmpdir)(), "sideboard-conductor-"));
12003
+ const tmp = (0, import_node_fs34.mkdtempSync)((0, import_node_path31.join)((0, import_node_os10.tmpdir)(), "sideboard-conductor-"));
11681
12004
  const snapshot = (0, import_node_path31.join)(tmp, "conductor.db");
11682
12005
  try {
11683
- (0, import_node_fs33.copyFileSync)(CONDUCTOR_DB, snapshot);
12006
+ (0, import_node_fs34.copyFileSync)(CONDUCTOR_DB, snapshot);
11684
12007
  for (const suffix of ["-wal", "-shm"]) {
11685
12008
  const src = `${CONDUCTOR_DB}${suffix}`;
11686
- if ((0, import_node_fs33.existsSync)(src)) {
12009
+ if ((0, import_node_fs34.existsSync)(src)) {
11687
12010
  try {
11688
- (0, import_node_fs33.copyFileSync)(src, `${snapshot}${suffix}`);
12011
+ (0, import_node_fs34.copyFileSync)(src, `${snapshot}${suffix}`);
11689
12012
  } catch {
11690
12013
  }
11691
12014
  }
@@ -11703,7 +12026,7 @@ function importConductorWorkspace(workspaceId) {
11703
12026
  ).get(workspaceId);
11704
12027
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
11705
12028
  const worktreePath = String(row.workspacePath);
11706
- if (!(0, import_node_fs33.existsSync)(worktreePath)) {
12029
+ if (!(0, import_node_fs34.existsSync)(worktreePath)) {
11707
12030
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
11708
12031
  }
11709
12032
  let sessionId = null;
@@ -11766,7 +12089,7 @@ function importConductorWorkspace(workspaceId) {
11766
12089
  db.close();
11767
12090
  }
11768
12091
  } finally {
11769
- (0, import_node_fs33.rmSync)(tmp, { recursive: true, force: true });
12092
+ (0, import_node_fs34.rmSync)(tmp, { recursive: true, force: true });
11770
12093
  }
11771
12094
  }
11772
12095
  async function importConductorWorkspaceAsync(workspaceId) {
@@ -11774,7 +12097,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
11774
12097
  }
11775
12098
 
11776
12099
  // src/threads/stack-layers.ts
11777
- var import_node_fs34 = require("fs");
12100
+ var import_node_fs35 = require("fs");
11778
12101
  init_run();
11779
12102
  init_stack();
11780
12103
  init_worktree();
@@ -11842,7 +12165,7 @@ async function openStackLayer(input, _onSetupLine) {
11842
12165
  let createdWorktree = false;
11843
12166
  const trees = await listWorktrees(repoPath);
11844
12167
  const checkedOut = trees.find((w) => w.branch === branchName);
11845
- if (checkedOut?.path && (0, import_node_fs34.existsSync)(checkedOut.path)) {
12168
+ if (checkedOut?.path && (0, import_node_fs35.existsSync)(checkedOut.path)) {
11846
12169
  if (input.reuseExistingWorktree !== false) {
11847
12170
  worktreePath = checkedOut.path;
11848
12171
  } else {
@@ -11984,7 +12307,7 @@ async function initStackFromThread(input, onSetupLine) {
11984
12307
  async function createPrStack(input, onSetupLine) {
11985
12308
  await requireAgent(input.agent);
11986
12309
  const repoPath = await resolveRepoRoot(input.repoPath);
11987
- if (!(0, import_node_fs34.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
12310
+ if (!(0, import_node_fs35.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
11988
12311
  if (!input.branches.length) throw new Error("At least one branch name required");
11989
12312
  const status = await detectGhStack(repoPath);
11990
12313
  if (!status.available) throw new Error(status.reason);
@@ -12051,7 +12374,7 @@ async function createPrStack(input, onSetupLine) {
12051
12374
  }
12052
12375
  }
12053
12376
  const claimed = new Set(threads.map((t) => t.worktreePath));
12054
- if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs34.existsSync)(bootstrap.worktreePath)) {
12377
+ if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs35.existsSync)(bootstrap.worktreePath)) {
12055
12378
  try {
12056
12379
  await removeWorktree(repoPath, bootstrap.worktreePath, {
12057
12380
  deleteBranch: bootstrap.branchName
@@ -12066,12 +12389,12 @@ async function createPrStack(input, onSetupLine) {
12066
12389
  init_worktree();
12067
12390
 
12068
12391
  // src/diff/diff.ts
12069
- var import_node_fs35 = require("fs");
12392
+ var import_node_fs36 = require("fs");
12070
12393
  var import_node_path32 = require("path");
12071
12394
  init_run();
12072
12395
  init_worktree();
12073
12396
  async function inspectGitWorktree(worktreePath) {
12074
- if (!worktreePath || !(0, import_node_fs35.existsSync)(worktreePath)) return "missing_worktree";
12397
+ if (!worktreePath || !(0, import_node_fs36.existsSync)(worktreePath)) return "missing_worktree";
12075
12398
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
12076
12399
  reject: false
12077
12400
  });
@@ -12079,7 +12402,7 @@ async function inspectGitWorktree(worktreePath) {
12079
12402
  return "ok";
12080
12403
  }
12081
12404
  async function initializeGitRepository(worktreePath) {
12082
- if (!worktreePath || !(0, import_node_fs35.existsSync)(worktreePath)) {
12405
+ if (!worktreePath || !(0, import_node_fs36.existsSync)(worktreePath)) {
12083
12406
  throw new Error("Worktree not found");
12084
12407
  }
12085
12408
  const status = await inspectGitWorktree(worktreePath);
@@ -12217,9 +12540,9 @@ new file mode 100644
12217
12540
  async function untrackedPatch(worktreePath, path, maxHunk) {
12218
12541
  const abs = (0, import_node_path32.join)(worktreePath, path);
12219
12542
  try {
12220
- const st = (0, import_node_fs35.statSync)(abs);
12543
+ const st = (0, import_node_fs36.statSync)(abs);
12221
12544
  if (st.isFile() && st.size > maxHunk) {
12222
- const buf = (0, import_node_fs35.readFileSync)(abs).subarray(0, maxHunk);
12545
+ const buf = (0, import_node_fs36.readFileSync)(abs).subarray(0, maxHunk);
12223
12546
  return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
12224
12547
  }
12225
12548
  } catch {
@@ -12721,7 +13044,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
12721
13044
  assertSafeRelativePath(relativePath);
12722
13045
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
12723
13046
  const abs = (0, import_node_path32.join)(worktreePath, relativePath);
12724
- const st = (0, import_node_fs35.statSync)(abs);
13047
+ const st = (0, import_node_fs36.statSync)(abs);
12725
13048
  if (!st.isFile()) {
12726
13049
  throw new Error(`Not a file: ${relativePath}`);
12727
13050
  }
@@ -12730,7 +13053,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
12730
13053
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
12731
13054
  );
12732
13055
  }
12733
- const buf = (0, import_node_fs35.readFileSync)(abs);
13056
+ const buf = (0, import_node_fs36.readFileSync)(abs);
12734
13057
  return {
12735
13058
  path: relativePath,
12736
13059
  contentBase64: buf.toString("base64"),
@@ -12741,11 +13064,11 @@ function readWorktreeFile(worktreePath, relativePath, opts) {
12741
13064
  assertSafeRelativePath(relativePath);
12742
13065
  const maxBytes = opts?.maxBytes ?? 2e5;
12743
13066
  const abs = (0, import_node_path32.join)(worktreePath, relativePath);
12744
- const st = (0, import_node_fs35.statSync)(abs);
13067
+ const st = (0, import_node_fs36.statSync)(abs);
12745
13068
  if (!st.isFile()) {
12746
13069
  throw new Error(`Not a file: ${relativePath}`);
12747
13070
  }
12748
- const buf = (0, import_node_fs35.readFileSync)(abs);
13071
+ const buf = (0, import_node_fs36.readFileSync)(abs);
12749
13072
  if (isImageRelativePath(relativePath)) {
12750
13073
  const maxImageBytes = Math.max(maxBytes, 15e6);
12751
13074
  const truncated2 = buf.length > maxImageBytes;
@@ -12789,8 +13112,8 @@ function assertSafeRelativePath(relativePath) {
12789
13112
  function writeWorktreeFile(worktreePath, relativePath, content) {
12790
13113
  assertSafeRelativePath(relativePath);
12791
13114
  const abs = (0, import_node_path32.join)(worktreePath, relativePath);
12792
- (0, import_node_fs35.mkdirSync)((0, import_node_path32.dirname)(abs), { recursive: true });
12793
- (0, import_node_fs35.writeFileSync)(abs, content, "utf8");
13115
+ (0, import_node_fs36.mkdirSync)((0, import_node_path32.dirname)(abs), { recursive: true });
13116
+ (0, import_node_fs36.writeFileSync)(abs, content, "utf8");
12794
13117
  return { path: relativePath };
12795
13118
  }
12796
13119
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -12894,7 +13217,7 @@ async function confirmLand(thread, opts) {
12894
13217
  }
12895
13218
 
12896
13219
  // src/skills/discover.ts
12897
- var import_node_fs36 = require("fs");
13220
+ var import_node_fs37 = require("fs");
12898
13221
  var import_node_os11 = require("os");
12899
13222
  var import_node_path33 = require("path");
12900
13223
  function toCommand(name) {
@@ -12928,7 +13251,7 @@ function parseFrontmatter(content) {
12928
13251
  }
12929
13252
  function readSkill(skillMd, source) {
12930
13253
  try {
12931
- const content = (0, import_node_fs36.readFileSync)(skillMd, "utf8");
13254
+ const content = (0, import_node_fs37.readFileSync)(skillMd, "utf8");
12932
13255
  const { name: fmName, description } = parseFrontmatter(content);
12933
13256
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
12934
13257
  const name = fmName || dirName;
@@ -12947,19 +13270,19 @@ function readSkill(skillMd, source) {
12947
13270
  }
12948
13271
  }
12949
13272
  function scanSkillsDir(dir, source, out) {
12950
- if (!(0, import_node_fs36.existsSync)(dir)) return;
13273
+ if (!(0, import_node_fs37.existsSync)(dir)) return;
12951
13274
  let entries;
12952
13275
  try {
12953
- entries = (0, import_node_fs36.readdirSync)(dir);
13276
+ entries = (0, import_node_fs37.readdirSync)(dir);
12954
13277
  } catch {
12955
13278
  return;
12956
13279
  }
12957
13280
  for (const entry of entries) {
12958
13281
  if (entry.startsWith(".")) continue;
12959
13282
  const skillMd = (0, import_node_path33.join)(dir, entry, "SKILL.md");
12960
- if (!(0, import_node_fs36.existsSync)(skillMd)) continue;
13283
+ if (!(0, import_node_fs37.existsSync)(skillMd)) continue;
12961
13284
  try {
12962
- if (!(0, import_node_fs36.statSync)(skillMd).isFile()) continue;
13285
+ if (!(0, import_node_fs37.statSync)(skillMd).isFile()) continue;
12963
13286
  } catch {
12964
13287
  continue;
12965
13288
  }
@@ -12968,12 +13291,12 @@ function scanSkillsDir(dir, source, out) {
12968
13291
  }
12969
13292
  }
12970
13293
  function scanClaudePluginSkills(pluginsRoot, out) {
12971
- if (!(0, import_node_fs36.existsSync)(pluginsRoot)) return;
13294
+ if (!(0, import_node_fs37.existsSync)(pluginsRoot)) return;
12972
13295
  const walk = (dir, depth, lookingForSkillsDir) => {
12973
13296
  if (depth > 7) return;
12974
13297
  let entries;
12975
13298
  try {
12976
- entries = (0, import_node_fs36.readdirSync)(dir);
13299
+ entries = (0, import_node_fs37.readdirSync)(dir);
12977
13300
  } catch {
12978
13301
  return;
12979
13302
  }
@@ -12985,7 +13308,7 @@ function scanClaudePluginSkills(pluginsRoot, out) {
12985
13308
  if (entry === "node_modules" || entry === ".git") continue;
12986
13309
  const full = (0, import_node_path33.join)(dir, entry);
12987
13310
  try {
12988
- if (!(0, import_node_fs36.statSync)(full).isDirectory()) continue;
13311
+ if (!(0, import_node_fs37.statSync)(full).isDirectory()) continue;
12989
13312
  } catch {
12990
13313
  continue;
12991
13314
  }
@@ -13025,7 +13348,7 @@ function discoverSkills(worktreePath) {
13025
13348
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
13026
13349
  }
13027
13350
  function readSkillBody(skillPath, maxChars = 12e3) {
13028
- const raw = (0, import_node_fs36.readFileSync)(skillPath, "utf8");
13351
+ const raw = (0, import_node_fs37.readFileSync)(skillPath, "utf8");
13029
13352
  if (raw.startsWith("---")) {
13030
13353
  const end = raw.indexOf("\n---", 3);
13031
13354
  if (end >= 0) {
@@ -13118,7 +13441,7 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
13118
13441
  }
13119
13442
 
13120
13443
  // src/composer/stage-files.ts
13121
- var import_node_fs37 = require("fs");
13444
+ var import_node_fs38 = require("fs");
13122
13445
  var import_node_path34 = require("path");
13123
13446
  var import_node_crypto7 = require("crypto");
13124
13447
  init_workspace_scratch();
@@ -13156,21 +13479,21 @@ function imageMimeType(filePath) {
13156
13479
  }
13157
13480
  function ensureAttachmentsDir(worktreePath) {
13158
13481
  const dir = (0, import_node_path34.join)(worktreePath, ATTACHMENTS_DIR);
13159
- (0, import_node_fs37.mkdirSync)(dir, { recursive: true });
13482
+ (0, import_node_fs38.mkdirSync)(dir, { recursive: true });
13160
13483
  const gi = (0, import_node_path34.join)(dir, ".gitignore");
13161
- if (!(0, import_node_fs37.existsSync)(gi)) {
13162
- (0, import_node_fs37.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
13484
+ if (!(0, import_node_fs38.existsSync)(gi)) {
13485
+ (0, import_node_fs38.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
13163
13486
  }
13164
13487
  return dir;
13165
13488
  }
13166
13489
  function uniqueAttachmentName(dir, originalName) {
13167
13490
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
13168
- if (!(0, import_node_fs37.existsSync)((0, import_node_path34.join)(dir, safe))) return safe;
13491
+ if (!(0, import_node_fs38.existsSync)((0, import_node_path34.join)(dir, safe))) return safe;
13169
13492
  const ext = (0, import_node_path34.extname)(safe);
13170
13493
  const stem = ext ? safe.slice(0, -ext.length) : safe;
13171
13494
  for (let i = 1; i < 1e4; i++) {
13172
13495
  const candidate = `${stem}-${i}${ext}`;
13173
- if (!(0, import_node_fs37.existsSync)((0, import_node_path34.join)(dir, candidate))) return candidate;
13496
+ if (!(0, import_node_fs38.existsSync)((0, import_node_path34.join)(dir, candidate))) return candidate;
13174
13497
  }
13175
13498
  return `${stem}-${(0, import_node_crypto7.randomUUID)()}${ext}`;
13176
13499
  }
@@ -13228,13 +13551,13 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
13228
13551
  for (const abs of absolutePaths) {
13229
13552
  const originalName = (0, import_node_path34.basename)(abs);
13230
13553
  try {
13231
- const st = (0, import_node_fs37.statSync)(abs);
13554
+ const st = (0, import_node_fs38.statSync)(abs);
13232
13555
  if (!st.isFile()) continue;
13233
13556
  const name = uniqueAttachmentName(dir, originalName);
13234
13557
  const destAbs = (0, import_node_path34.join)(dir, name);
13235
- (0, import_node_fs37.copyFileSync)(abs, destAbs);
13558
+ (0, import_node_fs38.copyFileSync)(abs, destAbs);
13236
13559
  const rel = `${ATTACHMENTS_DIR}/${name}`;
13237
- const buf = (0, import_node_fs37.readFileSync)(destAbs);
13560
+ const buf = (0, import_node_fs38.readFileSync)(destAbs);
13238
13561
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
13239
13562
  } catch (err) {
13240
13563
  out.push({
@@ -13247,17 +13570,17 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
13247
13570
  }
13248
13571
  return out;
13249
13572
  }
13250
- function stageBuffersAsAttachments(worktreePath, buffers) {
13251
- if (buffers.length === 0) return [];
13573
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
13574
+ if (buffers2.length === 0) return [];
13252
13575
  const dir = ensureAttachmentsDir(worktreePath);
13253
13576
  const out = [];
13254
- for (const item of buffers) {
13577
+ for (const item of buffers2) {
13255
13578
  const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
13256
13579
  try {
13257
13580
  const buf = Buffer.from(item.dataBase64, "base64");
13258
13581
  const name = uniqueAttachmentName(dir, originalName);
13259
13582
  const destAbs = (0, import_node_path34.join)(dir, name);
13260
- (0, import_node_fs37.writeFileSync)(destAbs, buf);
13583
+ (0, import_node_fs38.writeFileSync)(destAbs, buf);
13261
13584
  const rel = `${ATTACHMENTS_DIR}/${name}`;
13262
13585
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
13263
13586
  } catch (err) {
@@ -13286,9 +13609,9 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
13286
13609
  const name = (0, import_node_path34.basename)(rel);
13287
13610
  try {
13288
13611
  const abs = (0, import_node_path34.join)(worktreePath, rel);
13289
- const st = (0, import_node_fs37.statSync)(abs);
13612
+ const st = (0, import_node_fs38.statSync)(abs);
13290
13613
  if (!st.isFile()) continue;
13291
- const buf = (0, import_node_fs37.readFileSync)(abs);
13614
+ const buf = (0, import_node_fs38.readFileSync)(abs);
13292
13615
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
13293
13616
  } catch (err) {
13294
13617
  out.push({
@@ -13303,7 +13626,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
13303
13626
  }
13304
13627
 
13305
13628
  // src/agents/instructions.ts
13306
- var import_node_fs38 = require("fs");
13629
+ var import_node_fs39 = require("fs");
13307
13630
  var import_node_path35 = require("path");
13308
13631
  init_git_auth_mode();
13309
13632
  init_worktree_labels();
@@ -13507,6 +13830,15 @@ function isPidAlive(pid) {
13507
13830
  return false;
13508
13831
  }
13509
13832
  }
13833
+ async function waitForPidExit(pid, timeoutMs) {
13834
+ const start = Date.now();
13835
+ while (isPidAlive(pid)) {
13836
+ if (Date.now() - start >= timeoutMs) return false;
13837
+ await new Promise((r) => setTimeout(r, 80));
13838
+ }
13839
+ return true;
13840
+ }
13841
+ var STALE_AGENT_PID_WAIT_MS = 2500;
13510
13842
  function writeLiveStatus(threadId, status, lastError) {
13511
13843
  const latest = readThread(threadId);
13512
13844
  if (!latest || latest.status === "archived") return latest;
@@ -13584,7 +13916,7 @@ var Orchestrator = class {
13584
13916
  }
13585
13917
  continue;
13586
13918
  }
13587
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
13919
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
13588
13920
  setStatus(thread.id, "broken", "Worktree missing on disk");
13589
13921
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
13590
13922
  continue;
@@ -13905,14 +14237,10 @@ var Orchestrator = class {
13905
14237
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
13906
14238
  const livePid = current.agentPid;
13907
14239
  const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
13908
- if (inFlight) {
14240
+ if (inFlight || foreignLive) {
13909
14241
  this.stop(thread.id, { clearQueue: false, continueQueue: true });
13910
- } else {
13911
- if (foreignLive) {
13912
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
13913
- }
13914
- void this.drainQueue(thread.id);
13915
14242
  }
14243
+ void this.drainQueue(thread.id);
13916
14244
  return this.requireThread(thread.id);
13917
14245
  }
13918
14246
  async drainQueue(threadId) {
@@ -13941,7 +14269,18 @@ var Orchestrator = class {
13941
14269
  }
13942
14270
  const livePid = thread.agentPid;
13943
14271
  if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
13944
- await new Promise((r) => setTimeout(r, 200));
14272
+ const exited = await waitForPidExit(livePid, STALE_AGENT_PID_WAIT_MS);
14273
+ if (!exited && isPidAlive(livePid)) {
14274
+ try {
14275
+ process.kill(livePid, "SIGKILL");
14276
+ } catch {
14277
+ }
14278
+ await waitForPidExit(livePid, 400);
14279
+ try {
14280
+ updateThread(threadId, { agentPid: null });
14281
+ } catch {
14282
+ }
14283
+ }
13945
14284
  continue;
13946
14285
  }
13947
14286
  const prompt = thread.queue[0];
@@ -14098,6 +14437,7 @@ var Orchestrator = class {
14098
14437
  { cachedPrefix, prompt: agentPrompt },
14099
14438
  (event) => {
14100
14439
  this.emit({ type: "turn_output", threadId, event });
14440
+ noteTurnLiveEvent(threadId, event);
14101
14441
  if (event.type === "session_id") {
14102
14442
  updateThread(threadId, { sessionId: event.data });
14103
14443
  }
@@ -14161,19 +14501,16 @@ var Orchestrator = class {
14161
14501
  }
14162
14502
  let lastStderr = summarizeTurnStderr(stderrTail);
14163
14503
  let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
14164
- if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && looksLikeInvalidAgentSession(detail) && this.requireThread(threadId).sessionId && this.requireThread(threadId).agent !== "brightsy") {
14504
+ if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && this.requireThread(threadId).agent !== "brightsy" && shouldRetryFailedAgentTurn(detail, {
14505
+ hasSession: Boolean(this.requireThread(threadId).sessionId)
14506
+ })) {
14165
14507
  updateThread(threadId, { sessionId: null });
14166
- pushTurnStderr(
14167
- stderrTail,
14168
- "Agent session missing \u2014 starting a fresh session"
14169
- );
14508
+ const retryNote = looksLikeInvalidAgentSession(detail) ? "Agent session missing \u2014 starting a fresh session" : "Agent runner crashed \u2014 restarting Node once";
14509
+ pushTurnStderr(stderrTail, retryNote);
14170
14510
  this.emit({
14171
14511
  type: "turn_output",
14172
14512
  threadId,
14173
- event: {
14174
- type: "stderr",
14175
- data: "Agent session missing \u2014 starting a fresh session"
14176
- }
14513
+ event: { type: "stderr", data: retryNote }
14177
14514
  });
14178
14515
  const retryThread = this.requireThread(threadId);
14179
14516
  const prior = retryThread.messages.slice(0, -1);
@@ -14222,10 +14559,7 @@ var Orchestrator = class {
14222
14559
  lastStderr = summarizeTurnStderr(stderrTail);
14223
14560
  detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
14224
14561
  }
14225
- let chatText = assistantText;
14226
- if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
14227
- chatText = humanizeAgentFailDetail(detail);
14228
- }
14562
+ let chatText = this.stoppedTurns.has(threadId) ? assistantText : turnFailChatText({ exitCode, assistantText, detail });
14229
14563
  if (chatText || parts.length > 0) {
14230
14564
  appendMessage(threadId, {
14231
14565
  role: "agent",
@@ -14308,6 +14642,7 @@ var Orchestrator = class {
14308
14642
  this.processes.delete(`${threadId}:agent`);
14309
14643
  this.stoppedTurns.delete(threadId);
14310
14644
  this.runningCount = Math.max(0, this.runningCount - 1);
14645
+ clearTurnLive(threadId);
14311
14646
  try {
14312
14647
  updateThread(threadId, { agentPid: null });
14313
14648
  } catch {
@@ -14581,51 +14916,56 @@ var Orchestrator = class {
14581
14916
  }
14582
14917
  return created;
14583
14918
  }
14584
- async waitForTurn(threadRef, timeoutMs = 6e5) {
14919
+ async waitForTurn(threadRef, timeoutMs = 6e5, opts) {
14585
14920
  const thread = this.requireThread(threadRef);
14921
+ if (!["running", "queued"].includes(thread.status)) {
14922
+ return thread;
14923
+ }
14586
14924
  const start = Date.now();
14587
14925
  return new Promise((resolve, reject) => {
14588
- if (!["running", "queued"].includes(thread.status)) {
14589
- resolve(thread);
14590
- return;
14591
- }
14926
+ let settled = false;
14927
+ let timer;
14928
+ const finish = (fn) => {
14929
+ if (settled) return;
14930
+ settled = true;
14931
+ if (timer) clearInterval(timer);
14932
+ off();
14933
+ fn();
14934
+ };
14592
14935
  const off = this.on((event) => {
14593
14936
  if (event.type === "turn_finished" && event.threadId === thread.id) {
14594
- off();
14595
14937
  const latest = readThread(thread.id);
14596
14938
  if (!latest) {
14597
- reject(new Error(`Thread not found: ${thread.id}`));
14939
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
14598
14940
  return;
14599
14941
  }
14600
- resolve(latest);
14942
+ finish(() => resolve(latest));
14601
14943
  }
14602
14944
  if (event.type === "error" && event.threadId === thread.id) {
14603
- off();
14604
14945
  const latest = readThread(thread.id);
14605
14946
  if (!latest) {
14606
- reject(new Error(`Thread not found: ${thread.id}`));
14947
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
14607
14948
  return;
14608
14949
  }
14609
- resolve(latest);
14950
+ finish(() => resolve(latest));
14610
14951
  }
14611
14952
  });
14612
- const timer = setInterval(() => {
14613
- if (Date.now() - start > timeoutMs) {
14614
- clearInterval(timer);
14615
- off();
14616
- reject(new Error("wait_for_turn timed out"));
14617
- }
14953
+ timer = setInterval(() => {
14618
14954
  const current = readThread(thread.id);
14619
14955
  if (!current) {
14620
- clearInterval(timer);
14621
- off();
14622
- reject(new Error(`Thread not found: ${thread.id}`));
14956
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
14623
14957
  return;
14624
14958
  }
14625
14959
  if (!["running", "queued"].includes(current.status)) {
14626
- clearInterval(timer);
14627
- off();
14628
- resolve(current);
14960
+ finish(() => resolve(current));
14961
+ return;
14962
+ }
14963
+ if (Date.now() - start > timeoutMs) {
14964
+ if (opts?.resolveIfStillRunning) {
14965
+ finish(() => resolve(current));
14966
+ } else {
14967
+ finish(() => reject(new Error("wait_for_turn timed out")));
14968
+ }
14629
14969
  }
14630
14970
  }, 200);
14631
14971
  });
@@ -14633,10 +14973,18 @@ var Orchestrator = class {
14633
14973
  getTurnResult(threadRef) {
14634
14974
  const thread = this.requireThread(threadRef);
14635
14975
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
14976
+ const lastError = thread.lastError ?? null;
14977
+ const text3 = (lastAgent?.text ?? "").trim() || (thread.status === "error" ? lastError ?? "" : "");
14978
+ const stillRunning = thread.status === "running" || thread.status === "queued";
14979
+ const live = stillRunning ? readTurnLive(thread.id) : null;
14636
14980
  return {
14637
- text: lastAgent?.text ?? "",
14981
+ text: text3,
14638
14982
  status: thread.status,
14639
- sessionId: thread.sessionId
14983
+ sessionId: thread.sessionId,
14984
+ lastError,
14985
+ stillRunning,
14986
+ progress: live?.summary ?? null,
14987
+ lastActivityAt: live?.updatedAt ?? null
14640
14988
  };
14641
14989
  }
14642
14990
  assertNotGlobal(thread, action) {
@@ -15062,7 +15410,19 @@ var Orchestrator = class {
15062
15410
  const thread = this.requireThread(threadRef);
15063
15411
  return threadsSharingWorktree(thread.worktreePath);
15064
15412
  }
15413
+ /**
15414
+ * Last-tab teardown must see siblings already archived. Serialize per
15415
+ * worktree so parallel archive/purge of sibling tabs still removes once.
15416
+ */
15417
+ enqueueWorktreeTeardown(thread, fn) {
15418
+ const key = isGlobalThread(thread) ? `archive:global:${thread.id}` : `archive:wt:${normalizeWorktreePath(thread.worktreePath)}`;
15419
+ return enqueueByKey(key, fn);
15420
+ }
15065
15421
  async archive(threadRef) {
15422
+ const thread = this.requireThread(threadRef);
15423
+ return this.enqueueWorktreeTeardown(thread, () => this.archiveUnlocked(threadRef));
15424
+ }
15425
+ async archiveUnlocked(threadRef) {
15066
15426
  const thread = this.requireThread(threadRef);
15067
15427
  this.stop(thread.id);
15068
15428
  this.releaseOrchestratorCaffeinate(thread);
@@ -15098,6 +15458,10 @@ var Orchestrator = class {
15098
15458
  return archived;
15099
15459
  }
15100
15460
  async purge(threadRef, opts) {
15461
+ const thread = this.requireThread(threadRef);
15462
+ await this.enqueueWorktreeTeardown(thread, () => this.purgeUnlocked(threadRef, opts));
15463
+ }
15464
+ async purgeUnlocked(threadRef, opts) {
15101
15465
  const thread = this.requireThread(threadRef);
15102
15466
  this.stop(thread.id);
15103
15467
  this.releaseOrchestratorCaffeinate(thread);
@@ -15132,13 +15496,12 @@ var Orchestrator = class {
15132
15496
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
15133
15497
  return restored2;
15134
15498
  }
15135
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
15499
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
15136
15500
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
15137
- const { execa: execa7 } = await import("execa");
15138
15501
  const slug = thread.worktreePath.split("/").pop();
15139
15502
  const dest = thread.worktreePath;
15140
- await execa7("git", ["worktree", "add", dest, thread.branchName], {
15141
- cwd: thread.repoPath
15503
+ await withRepoGitLock(thread.repoPath, async () => {
15504
+ await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
15142
15505
  });
15143
15506
  void createThreadWorktree2;
15144
15507
  void slug;
@@ -15702,6 +16065,15 @@ function mcpArchiveBlockedReason(thread) {
15702
16065
  // src/mcp/server.ts
15703
16066
  init_profile();
15704
16067
 
16068
+ // src/mcp/wait-for-turn.ts
16069
+ var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
16070
+ function mcpWaitForTurnTimeoutMs(requested) {
16071
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
16072
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
16073
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
16074
+ }
16075
+ var MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send a check-in prompt or assume a hang while progress is updating.";
16076
+
15705
16077
  // src/mcp/slack-tools.ts
15706
16078
  var import_zod = require("zod");
15707
16079
 
@@ -16373,7 +16745,9 @@ async function startMcpServer() {
16373
16745
  const threads = orch.getThreads(true);
16374
16746
  const lines = threads.map((t) => {
16375
16747
  const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path39.basename)(t.repoPath) || t.repoPath;
16376
- return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
16748
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
16749
+ const progress = live?.summary ? ` ${live.summary}` : "";
16750
+ return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
16377
16751
  });
16378
16752
  return {
16379
16753
  content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
@@ -16382,13 +16756,14 @@ async function startMcpServer() {
16382
16756
  );
16383
16757
  server.tool(
16384
16758
  "get_thread",
16385
- "Get a compact thread summary by id/ref",
16759
+ "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
16386
16760
  { ref: import_zod3.z.string() },
16387
16761
  async ({ ref }) => {
16388
16762
  const t = orch.getThread(ref);
16389
16763
  if (!t) {
16390
16764
  return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
16391
16765
  }
16766
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
16392
16767
  const summary = {
16393
16768
  id: t.id,
16394
16769
  title: t.title,
@@ -16403,7 +16778,10 @@ async function startMcpServer() {
16403
16778
  messageCount: t.messages.length,
16404
16779
  devPort: t.devPort,
16405
16780
  prUrl: t.prUrl,
16406
- lastError: t.lastError ?? null
16781
+ lastError: t.lastError ?? null,
16782
+ stillRunning: t.status === "running" || t.status === "queued",
16783
+ progress: live?.summary ?? null,
16784
+ lastActivityAt: live?.updatedAt ?? null
16407
16785
  };
16408
16786
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
16409
16787
  }
@@ -16729,13 +17107,15 @@ async function startMcpServer() {
16729
17107
  );
16730
17108
  server.tool(
16731
17109
  "wait_for_turn",
16732
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
17110
+ "Wait until the thread finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. If stillRunning is true, progress is tools/thinking \u2014 call wait_for_turn again. Do not send a check-in prompt or assume a hang. On status error, lastError/text is the failure.",
16733
17111
  {
16734
17112
  ref: import_zod3.z.string(),
16735
17113
  timeoutMs: import_zod3.z.number().optional()
16736
17114
  },
16737
17115
  async ({ ref, timeoutMs }) => {
16738
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
17116
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
17117
+ resolveIfStillRunning: true
17118
+ });
16739
17119
  const result = orch.getTurnResult(thread.id);
16740
17120
  return {
16741
17121
  content: [
@@ -16744,7 +17124,12 @@ async function startMcpServer() {
16744
17124
  text: JSON.stringify({
16745
17125
  id: thread.id,
16746
17126
  status: result.status,
16747
- text: result.text
17127
+ text: result.text,
17128
+ lastError: result.lastError,
17129
+ stillRunning: result.stillRunning,
17130
+ progress: result.progress,
17131
+ lastActivityAt: result.lastActivityAt,
17132
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
16748
17133
  })
16749
17134
  }
16750
17135
  ]
@@ -16753,11 +17138,21 @@ async function startMcpServer() {
16753
17138
  );
16754
17139
  server.tool(
16755
17140
  "get_turn_result",
16756
- "Final assistant message only (not full transcript)",
17141
+ "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript.",
16757
17142
  { ref: import_zod3.z.string() },
16758
17143
  async ({ ref }) => {
16759
17144
  const result = orch.getTurnResult(ref);
16760
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
17145
+ return {
17146
+ content: [
17147
+ {
17148
+ type: "text",
17149
+ text: JSON.stringify({
17150
+ ...result,
17151
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
17152
+ })
17153
+ }
17154
+ ]
17155
+ };
16761
17156
  }
16762
17157
  );
16763
17158
  server.tool(
@@ -16874,7 +17269,7 @@ async function startMcpServer() {
16874
17269
  );
16875
17270
  server.tool(
16876
17271
  "request_review",
16877
- 'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn / get_turn_result on the returned review tab id.',
17272
+ 'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
16878
17273
  { ref: import_zod3.z.string().describe("Worktree thread id/ref to review") },
16879
17274
  async ({ ref }) => {
16880
17275
  try {
@@ -16902,7 +17297,7 @@ async function startMcpServer() {
16902
17297
  );
16903
17298
  server.tool(
16904
17299
  "ask_git",
16905
- "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` if SSH is missing). When dirty, queues the worktree agent to commit; then wait_for_turn. Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd.",
17300
+ "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` if SSH is missing). When dirty, queues the worktree agent to commit; then wait_for_turn (loop while stillRunning). Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd.",
16906
17301
  {
16907
17302
  ref: import_zod3.z.string().describe("Worktree thread id/ref"),
16908
17303
  action: import_zod3.z.enum(AGENT_GIT_ACTIONS).describe(
@@ -16953,7 +17348,7 @@ async function startMcpServer() {
16953
17348
  );
16954
17349
  server.tool(
16955
17350
  "fork_worktree",
16956
- "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn on the returned id.",
17351
+ "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id.",
16957
17352
  {
16958
17353
  ref: import_zod3.z.string().describe("Worktree thread id/ref to fork"),
16959
17354
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -16998,7 +17393,7 @@ async function startMcpServer() {
16998
17393
  );
16999
17394
  server.tool(
17000
17395
  "fork_chat",
17001
- "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
17396
+ "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
17002
17397
  {
17003
17398
  ref: import_zod3.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
17004
17399
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),