@sideboard-ai/core 0.1.100 → 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 (50) hide show
  1. package/dist/agents/cursor-runner.js +1 -1
  2. package/dist/{agents-CLP5BG4O.js → agents-UT5GCJH3.js} +8 -8
  3. package/dist/{agents-OXZDMPSE.js → agents-XZBAKBO2.js} +8 -8
  4. package/dist/{app-settings-BRF7VKTQ.js → app-settings-EKJM2344.js} +2 -2
  5. package/dist/{app-settings-ZXPAB3QN.js → app-settings-VDJK2BZ2.js} +2 -2
  6. package/dist/{caffeinate-hold-U3QJBV67.js → caffeinate-hold-NBRUWYXO.js} +2 -2
  7. package/dist/{caffeinate-hold-I7SG6UQR.js → caffeinate-hold-X3JNPR42.js} +2 -2
  8. package/dist/{chunk-MHJV4WS3.js → chunk-27MCESES.js} +3 -3
  9. package/dist/{chunk-QSLE4VEM.js → chunk-2GO3YKBA.js} +4 -4
  10. package/dist/{chunk-DKHGWYWR.js → chunk-7YKXHBIW.js} +1 -1
  11. package/dist/{chunk-JL4SJ6V7.js → chunk-BD2ICC4D.js} +4 -0
  12. package/dist/{chunk-NZPGMNFC.js → chunk-DCOZABNT.js} +1 -1
  13. package/dist/{chunk-CIRXAYWS.js → chunk-DHAGMJIK.js} +55 -28
  14. package/dist/{chunk-HYKZEP5A.js → chunk-ETMRBJKA.js} +11 -11
  15. package/dist/{chunk-OXC3MEFI.js → chunk-H6LP57EF.js} +6 -6
  16. package/dist/{chunk-GR4JKWR4.js → chunk-HHLZWO7U.js} +5 -5
  17. package/dist/{chunk-3WA37BRI.js → chunk-KGSVLZV6.js} +11 -4
  18. package/dist/{chunk-7KDBDMNC.js → chunk-KLBOWJ74.js} +1 -1
  19. package/dist/{chunk-EQPQTLW6.js → chunk-LANDJIAI.js} +6 -6
  20. package/dist/{chunk-MRBKGFTL.js → chunk-ND6AO7C5.js} +3 -3
  21. package/dist/{chunk-WQTTUC4N.js → chunk-OW34MVDN.js} +5 -5
  22. package/dist/{chunk-JOF3XIEM.js → chunk-P3BQ5DOL.js} +11 -4
  23. package/dist/{chunk-B4R2VB2A.js → chunk-QZQEXME2.js} +4 -0
  24. package/dist/{chunk-R7BQBSDT.js → chunk-WA3DVVWG.js} +55 -28
  25. package/dist/{chunk-6LFV4VFI.js → chunk-WSYCND2U.js} +1 -1
  26. package/dist/{chunk-FT2SQOL4.js → chunk-XHZJJWLI.js} +1 -1
  27. package/dist/{chunk-IFHKPZHA.js → chunk-YJADJH5U.js} +11 -11
  28. package/dist/{connected-teams-SWHJWK2A.js → connected-teams-J4I5AKMR.js} +2 -2
  29. package/dist/{connected-teams-TLVKZPJP.js → connected-teams-M5XNXRI5.js} +2 -2
  30. package/dist/{coordinator-prompt-OOBSQCWQ.js → coordinator-prompt-3DFZRMXE.js} +5 -5
  31. package/dist/{coordinator-prompt-HHRKQWCX.js → coordinator-prompt-UWHSD65V.js} +5 -5
  32. package/dist/{cursor-recover-S5ORPW34.js → cursor-recover-JBTIDARH.js} +1 -1
  33. package/dist/{cursor-recover-G34BYKL2.js → cursor-recover-ZUJG6XRQ.js} +1 -1
  34. package/dist/{global-workspace-YZWYHLQY.js → global-workspace-KJQS5S2C.js} +6 -6
  35. package/dist/{global-workspace-KYEBFWKB.js → global-workspace-UR4E3CCG.js} +6 -6
  36. package/dist/index.cjs +626 -153
  37. package/dist/index.d.cts +37 -5
  38. package/dist/index.d.ts +37 -5
  39. package/dist/index.js +545 -121
  40. package/dist/mcp/run-stdio.cjs +426 -177
  41. package/dist/mcp/run-stdio.js +330 -127
  42. package/dist/{paths-TZX4L5X7.js → paths-QCI3MTO5.js} +3 -1
  43. package/dist/{paths-JQM2XRAD.js → paths-UMEDHOS5.js} +3 -1
  44. package/dist/{thread-store-XAKWCV2E.js → thread-store-4OUEQ2DB.js} +2 -2
  45. package/dist/{thread-store-V7NOOXKE.js → thread-store-DQJGDKIJ.js} +2 -2
  46. package/dist/{workspaces-ENVUDK6C.js → workspaces-NTIVI6DI.js} +7 -7
  47. package/dist/{workspaces-NNPD7QVV.js → workspaces-TKJXPNV4.js} +7 -7
  48. package/dist/{worktree-EO5QAGJU.js → worktree-NKWN5ZTS.js} +4 -4
  49. package/dist/{worktree-DA4BOV7G.js → worktree-PEUPFAKL.js} +4 -4
  50. package/package.json +1 -1
@@ -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({
@@ -1798,7 +1809,7 @@ var init_teams = __esm({
1798
1809
 
1799
1810
  // src/git/worktree-labels.ts
1800
1811
  function normalizeWorktreePath(worktreePath) {
1801
- const trimmed = worktreePath.replace(/\/+$/, "") || "/";
1812
+ const trimmed = (worktreePath ?? "").replace(/\/+$/, "") || "/";
1802
1813
  const absolute = trimmed.startsWith("/");
1803
1814
  const parts = trimmed.split("/").filter((p) => p && p !== ".");
1804
1815
  const out = [];
@@ -3389,6 +3400,42 @@ var init_git_auth_mode = __esm({
3389
3400
  }
3390
3401
  });
3391
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
+
3392
3439
  // src/git/pr-gates.ts
3393
3440
  function prIsInMergeQueue(gate) {
3394
3441
  if (gate.isInMergeQueue) return true;
@@ -4542,12 +4589,13 @@ async function createThreadWorktree(opts) {
4542
4589
  `Invalid git reference: ${opts.sourceRef} (and fetch did not resolve it)`
4543
4590
  );
4544
4591
  }
4545
- const add = await git(
4546
- ["worktree", "add", "-b", branchName, worktreePath, startPoint],
4547
- opts.repoPath,
4548
- { reject: false, timeoutMs: 6e4 }
4549
- );
4550
- 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;
4551
4599
  const err = `${add.stderr}
4552
4600
  ${add.stdout}`;
4553
4601
  if (/already used by worktree|already exists|checked out/i.test(err)) {
@@ -4558,17 +4606,14 @@ ${add.stdout}`;
4558
4606
  opts.repoPath,
4559
4607
  { reject: false, timeoutMs: 6e4 }
4560
4608
  );
4561
- if (retry.exitCode === 0) {
4562
- branchName = alt;
4563
- await ensureGhPreferOrigin(worktreePath);
4564
- return { branchName, worktreePath };
4565
- }
4609
+ if (retry.exitCode === 0) return alt;
4566
4610
  }
4567
4611
  }
4568
4612
  throw new Error(
4569
4613
  `Failed to create worktree: ${add.stderr.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
4570
4614
  );
4571
- }
4615
+ });
4616
+ branchName = added;
4572
4617
  await ensureGhPreferOrigin(worktreePath);
4573
4618
  return { branchName, worktreePath };
4574
4619
  }
@@ -4592,12 +4637,13 @@ async function createExistingBranchWorktree(opts) {
4592
4637
  );
4593
4638
  }
4594
4639
  const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
4595
- const add = await git(
4596
- ["worktree", "add", worktreePath, startPoint],
4597
- opts.repoPath,
4598
- { reject: false }
4599
- );
4600
- 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;
4601
4647
  const retry = await git(
4602
4648
  ["worktree", "add", worktreePath, branchName],
4603
4649
  opts.repoPath,
@@ -4608,7 +4654,7 @@ async function createExistingBranchWorktree(opts) {
4608
4654
  `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
4609
4655
  );
4610
4656
  }
4611
- }
4657
+ });
4612
4658
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
4613
4659
  reject: false
4614
4660
  });
@@ -4619,12 +4665,14 @@ async function createExistingBranchWorktree(opts) {
4619
4665
  return { branchName, worktreePath };
4620
4666
  }
4621
4667
  async function removeWorktree(repoPath, worktreePath, opts) {
4622
- await git(["worktree", "remove", "--force", worktreePath], repoPath, {
4623
- 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
+ }
4624
4675
  });
4625
- if (opts?.deleteBranch) {
4626
- await git(["branch", "-D", opts.deleteBranch], repoPath, { reject: false });
4627
- }
4628
4676
  }
4629
4677
  async function listWorktrees(repoPath) {
4630
4678
  const { stdout } = await git(
@@ -4916,6 +4964,7 @@ var init_worktree = __esm({
4916
4964
  init_gh_errors();
4917
4965
  init_git_auth_mode();
4918
4966
  init_run();
4967
+ init_repo_git_lock();
4919
4968
  init_app_settings();
4920
4969
  init_pr_gates();
4921
4970
  init_stack();
@@ -5018,7 +5067,7 @@ function coordinatorGreenfieldPlaybook(reposDir) {
5018
5067
  "- Examples:",
5019
5068
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
5020
5069
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
5021
- "- 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>`).",
5022
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`.",
5023
5072
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
5024
5073
  ].join("\n");
@@ -5041,8 +5090,8 @@ function coordinatorTurnReminder(opts) {
5041
5090
  goal ? `- Goal / title: ${goal}` : null,
5042
5091
  accountDefaultsPlaybookLine(),
5043
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.`,
5044
- "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. 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.",
5045
- "- 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.",
5046
5095
  "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
5047
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."
5048
5097
  ].filter(Boolean).join("\n");
@@ -5095,8 +5144,8 @@ function ensureGlobalCoordinatorCwd(opts) {
5095
5144
  orchId ? `Pass parentThreadId="${orchId}" (or omit it). Never invent another parentThreadId.` : "Pass `parentThreadId` for children (this chat's id from the turn reminder).",
5096
5145
  "Omit `agent` / `model` on `create_thread` unless you have a reason to override Account defaults.",
5097
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.",
5098
- "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).",
5099
- "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.",
5100
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."
5101
5150
  ].join("\n");
5102
5151
  try {
@@ -5164,7 +5213,7 @@ var init_coordinator_prompt = __esm({
5164
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.",
5165
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.",
5166
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",
5167
- "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
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.",
5168
5217
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
5169
5218
  "- archive_thread / restore_thread \u2014 archive (tears down worktree when last tab) or restore",
5170
5219
  "Setup / run:",
@@ -5172,8 +5221,8 @@ var init_coordinator_prompt = __esm({
5172
5221
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
5173
5222
  "Inspect / review / PRs:",
5174
5223
  "- get_diff \u2014 compact diff summary",
5175
- '- 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',
5176
- "- 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.",
5177
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.',
5178
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.',
5179
5228
  "Process guides:",
@@ -8793,17 +8842,17 @@ __export(plan_file_exports, {
8793
8842
  });
8794
8843
  function ensureAttachmentsGitignore2(worktreePath) {
8795
8844
  const gitignoreAbs = (0, import_node_path36.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
8796
- if ((0, import_node_fs39.existsSync)(gitignoreAbs)) return;
8797
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(gitignoreAbs), { recursive: true });
8798
- (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");
8799
8848
  }
8800
8849
  function planFileAbs(worktreePath) {
8801
8850
  return (0, import_node_path36.join)(worktreePath, PLAN_FILE_REL);
8802
8851
  }
8803
8852
  function readTextIfPresent2(abs) {
8804
- if (!(0, import_node_fs39.existsSync)(abs)) return null;
8853
+ if (!(0, import_node_fs40.existsSync)(abs)) return null;
8805
8854
  try {
8806
- const content = (0, import_node_fs39.readFileSync)(abs, "utf8");
8855
+ const content = (0, import_node_fs40.readFileSync)(abs, "utf8");
8807
8856
  return content.trim() ? content : null;
8808
8857
  } catch {
8809
8858
  return null;
@@ -8815,16 +8864,16 @@ function readPlanFile(worktreePath) {
8815
8864
  function writePlanFile(worktreePath, content) {
8816
8865
  ensureAttachmentsGitignore2(worktreePath);
8817
8866
  const abs = planFileAbs(worktreePath);
8818
- (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 });
8819
8868
  const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
8820
- (0, import_node_fs39.writeFileSync)(abs, body, "utf8");
8869
+ (0, import_node_fs40.writeFileSync)(abs, body, "utf8");
8821
8870
  return PLAN_FILE_REL;
8822
8871
  }
8823
- var import_node_fs39, import_node_path36;
8872
+ var import_node_fs40, import_node_path36;
8824
8873
  var init_plan_file = __esm({
8825
8874
  "src/plan/plan-file.ts"() {
8826
8875
  "use strict";
8827
- import_node_fs39 = require("fs");
8876
+ import_node_fs40 = require("fs");
8828
8877
  import_node_path36 = require("path");
8829
8878
  init_workspace_scratch();
8830
8879
  init_plan_present();
@@ -8880,9 +8929,9 @@ function uniqueIds(ids) {
8880
8929
  }
8881
8930
  function readHold() {
8882
8931
  const path = caffeinateHoldPath();
8883
- if (!(0, import_node_fs40.existsSync)(path)) return null;
8932
+ if (!(0, import_node_fs41.existsSync)(path)) return null;
8884
8933
  try {
8885
- const parsed = JSON.parse((0, import_node_fs40.readFileSync)(path, "utf8"));
8934
+ const parsed = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
8886
8935
  if (typeof parsed?.pid === "number" && parsed.pid > 0) {
8887
8936
  return {
8888
8937
  pid: parsed.pid,
@@ -8904,7 +8953,7 @@ function writeHold(pid, threadIds) {
8904
8953
  }
8905
8954
  function clearHold() {
8906
8955
  try {
8907
- (0, import_node_fs40.unlinkSync)(caffeinateHoldPath());
8956
+ (0, import_node_fs41.unlinkSync)(caffeinateHoldPath());
8908
8957
  } catch {
8909
8958
  }
8910
8959
  }
@@ -8989,12 +9038,12 @@ function releaseCaffeinateHoldForThread(threadId) {
8989
9038
  }
8990
9039
  return setCaffeinateHold(false, { threadId: id });
8991
9040
  }
8992
- 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;
8993
9042
  var init_caffeinate_hold = __esm({
8994
9043
  "src/store/caffeinate-hold.ts"() {
8995
9044
  "use strict";
8996
9045
  import_node_child_process4 = require("child_process");
8997
- import_node_fs40 = require("fs");
9046
+ import_node_fs41 = require("fs");
8998
9047
  import_node_path37 = require("path");
8999
9048
  init_paths();
9000
9049
  init_private_file();
@@ -9011,9 +9060,9 @@ function recoverFinishedCursorRun(opts) {
9011
9060
  const agentId = opts.agentId.trim();
9012
9061
  if (!agentId) return null;
9013
9062
  const runsPath = (0, import_node_path38.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
9014
- if (!(0, import_node_fs41.existsSync)(runsPath)) return null;
9063
+ if (!(0, import_node_fs42.existsSync)(runsPath)) return null;
9015
9064
  try {
9016
- const lines = (0, import_node_fs41.readFileSync)(runsPath, "utf8").split("\n");
9065
+ const lines = (0, import_node_fs42.readFileSync)(runsPath, "utf8").split("\n");
9017
9066
  let best = null;
9018
9067
  for (const line of lines) {
9019
9068
  const trimmed = line.trim();
@@ -9039,11 +9088,11 @@ function recoverFinishedCursorRun(opts) {
9039
9088
  return null;
9040
9089
  }
9041
9090
  }
9042
- var import_node_fs41, import_node_path38;
9091
+ var import_node_fs42, import_node_path38;
9043
9092
  var init_cursor_recover = __esm({
9044
9093
  "src/agents/cursor-recover.ts"() {
9045
9094
  "use strict";
9046
- import_node_fs41 = require("fs");
9095
+ import_node_fs42 = require("fs");
9047
9096
  import_node_path38 = require("path");
9048
9097
  init_paths();
9049
9098
  }
@@ -9567,7 +9616,7 @@ async function refreshSlackReplyBadges(opts) {
9567
9616
  }
9568
9617
 
9569
9618
  // src/orchestrator/orchestrator.ts
9570
- var import_node_fs42 = require("fs");
9619
+ var import_node_fs43 = require("fs");
9571
9620
  init_error_detail();
9572
9621
 
9573
9622
  // src/agents/spawn.ts
@@ -9821,7 +9870,7 @@ function createAgentStreamCoalescer(emit, opts) {
9821
9870
  const intervalMs = opts?.intervalMs ?? 32;
9822
9871
  let pending = null;
9823
9872
  let timer = null;
9824
- const flush = () => {
9873
+ const flush2 = () => {
9825
9874
  if (timer) {
9826
9875
  clearTimeout(timer);
9827
9876
  timer = null;
@@ -9833,13 +9882,13 @@ function createAgentStreamCoalescer(emit, opts) {
9833
9882
  };
9834
9883
  const schedule = () => {
9835
9884
  if (timer) return;
9836
- timer = setTimeout(flush, intervalMs);
9885
+ timer = setTimeout(flush2, intervalMs);
9837
9886
  timer.unref?.();
9838
9887
  };
9839
9888
  return {
9840
9889
  push(event) {
9841
9890
  if (!isTextEvent(event)) {
9842
- flush();
9891
+ flush2();
9843
9892
  emit(event);
9844
9893
  return;
9845
9894
  }
@@ -9847,12 +9896,12 @@ function createAgentStreamCoalescer(emit, opts) {
9847
9896
  if (pending && pending.type === event.type) {
9848
9897
  pending.data += event.data;
9849
9898
  } else {
9850
- flush();
9899
+ flush2();
9851
9900
  pending = { type: event.type, data: event.data };
9852
9901
  }
9853
9902
  schedule();
9854
9903
  },
9855
- flush
9904
+ flush: flush2
9856
9905
  };
9857
9906
  }
9858
9907
 
@@ -11168,8 +11217,11 @@ function worktreeBindingFrom(from) {
11168
11217
  };
11169
11218
  }
11170
11219
  function threadsSharingWorktree(worktreePath) {
11220
+ if (!worktreePath) return [];
11171
11221
  const key = normalizeWorktreePath(worktreePath);
11172
- 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));
11173
11225
  }
11174
11226
  function formatTranscriptMarkdown(title, messages) {
11175
11227
  const body = formatMessagesAsTranscript(messages, { tools: "full" });
@@ -11240,9 +11292,132 @@ function forkChatTab(input) {
11240
11292
  return tab;
11241
11293
  }
11242
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
+
11243
11418
  // src/review/request-review.ts
11244
11419
  var import_node_crypto5 = require("crypto");
11245
- var import_node_fs32 = require("fs");
11420
+ var import_node_fs33 = require("fs");
11246
11421
  var import_node_path30 = require("path");
11247
11422
  init_global_workspace();
11248
11423
  init_thread_store();
@@ -11391,9 +11566,9 @@ function shouldRefreshReviewRequestTemplate(content) {
11391
11566
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
11392
11567
  }
11393
11568
  function readTextIfPresent(abs) {
11394
- if (!(0, import_node_fs32.existsSync)(abs)) return null;
11569
+ if (!(0, import_node_fs33.existsSync)(abs)) return null;
11395
11570
  try {
11396
- const content = (0, import_node_fs32.readFileSync)(abs, "utf8");
11571
+ const content = (0, import_node_fs33.readFileSync)(abs, "utf8");
11397
11572
  return content.trim() ? content : null;
11398
11573
  } catch {
11399
11574
  return null;
@@ -11401,9 +11576,9 @@ function readTextIfPresent(abs) {
11401
11576
  }
11402
11577
  function ensureAttachmentsGitignore(worktreePath) {
11403
11578
  const gitignoreAbs = (0, import_node_path30.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
11404
- if ((0, import_node_fs32.existsSync)(gitignoreAbs)) return;
11405
- (0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(gitignoreAbs), { recursive: true });
11406
- (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");
11407
11582
  }
11408
11583
  function resolveReviewGuidelines(worktreePath) {
11409
11584
  const repoAbs = (0, import_node_path30.join)(worktreePath, REPO_REVIEW_PATH);
@@ -11437,8 +11612,8 @@ function resolveReviewGuidelines(worktreePath) {
11437
11612
  };
11438
11613
  }
11439
11614
  ensureAttachmentsGitignore(worktreePath);
11440
- (0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(localAbs), { recursive: true });
11441
- (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");
11442
11617
  return {
11443
11618
  path: REVIEW_REQUEST_PATH,
11444
11619
  name: REVIEW_REQUEST_NAME,
@@ -11638,7 +11813,7 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
11638
11813
 
11639
11814
  // src/threads/adopt.ts
11640
11815
  var import_node_child_process3 = require("child_process");
11641
- var import_node_fs33 = require("fs");
11816
+ var import_node_fs34 = require("fs");
11642
11817
  var import_node_os10 = require("os");
11643
11818
  var import_node_path31 = require("path");
11644
11819
  var import_node_module4 = require("module");
@@ -11669,21 +11844,21 @@ function mapAgentType(raw) {
11669
11844
  return null;
11670
11845
  }
11671
11846
  function resolveConductorCursorAgentId(workspacePath) {
11672
- 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;
11673
11848
  const normalized = workspacePath.replace(/\/$/, "");
11674
11849
  let best = null;
11675
11850
  let hashes;
11676
11851
  try {
11677
- hashes = (0, import_node_fs33.readdirSync)(CURSOR_SDK_STORE);
11852
+ hashes = (0, import_node_fs34.readdirSync)(CURSOR_SDK_STORE);
11678
11853
  } catch {
11679
11854
  return null;
11680
11855
  }
11681
11856
  for (const hash of hashes) {
11682
11857
  const agentsFile = (0, import_node_path31.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
11683
- if (!(0, import_node_fs33.existsSync)(agentsFile)) continue;
11858
+ if (!(0, import_node_fs34.existsSync)(agentsFile)) continue;
11684
11859
  let text3;
11685
11860
  try {
11686
- text3 = (0, import_node_fs33.readFileSync)(agentsFile, "utf8");
11861
+ text3 = (0, import_node_fs34.readFileSync)(agentsFile, "utf8");
11687
11862
  } catch {
11688
11863
  continue;
11689
11864
  }
@@ -11707,7 +11882,7 @@ function resolveConductorCursorAgentId(workspacePath) {
11707
11882
  return best?.agentId ?? null;
11708
11883
  }
11709
11884
  async function adoptThread(input) {
11710
- if (!(0, import_node_fs33.existsSync)(input.worktreePath)) {
11885
+ if (!(0, import_node_fs34.existsSync)(input.worktreePath)) {
11711
11886
  throw new Error(`Worktree not found: ${input.worktreePath}`);
11712
11887
  }
11713
11888
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -11731,18 +11906,18 @@ async function adoptThread(input) {
11731
11906
  return thread;
11732
11907
  }
11733
11908
  function listConductorWorkspaces() {
11734
- if (!(0, import_node_fs33.existsSync)(CONDUCTOR_DB)) {
11909
+ if (!(0, import_node_fs34.existsSync)(CONDUCTOR_DB)) {
11735
11910
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
11736
11911
  }
11737
- 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-"));
11738
11913
  const snapshot = (0, import_node_path31.join)(tmp, "conductor.db");
11739
11914
  try {
11740
- (0, import_node_fs33.copyFileSync)(CONDUCTOR_DB, snapshot);
11915
+ (0, import_node_fs34.copyFileSync)(CONDUCTOR_DB, snapshot);
11741
11916
  for (const suffix of ["-wal", "-shm"]) {
11742
11917
  const src = `${CONDUCTOR_DB}${suffix}`;
11743
- if ((0, import_node_fs33.existsSync)(src)) {
11918
+ if ((0, import_node_fs34.existsSync)(src)) {
11744
11919
  try {
11745
- (0, import_node_fs33.copyFileSync)(src, `${snapshot}${suffix}`);
11920
+ (0, import_node_fs34.copyFileSync)(src, `${snapshot}${suffix}`);
11746
11921
  } catch {
11747
11922
  }
11748
11923
  }
@@ -11818,22 +11993,22 @@ function listConductorWorkspaces() {
11818
11993
  db.close();
11819
11994
  }
11820
11995
  } finally {
11821
- (0, import_node_fs33.rmSync)(tmp, { recursive: true, force: true });
11996
+ (0, import_node_fs34.rmSync)(tmp, { recursive: true, force: true });
11822
11997
  }
11823
11998
  }
11824
11999
  function importConductorWorkspace(workspaceId) {
11825
- if (!(0, import_node_fs33.existsSync)(CONDUCTOR_DB)) {
12000
+ if (!(0, import_node_fs34.existsSync)(CONDUCTOR_DB)) {
11826
12001
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
11827
12002
  }
11828
- 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-"));
11829
12004
  const snapshot = (0, import_node_path31.join)(tmp, "conductor.db");
11830
12005
  try {
11831
- (0, import_node_fs33.copyFileSync)(CONDUCTOR_DB, snapshot);
12006
+ (0, import_node_fs34.copyFileSync)(CONDUCTOR_DB, snapshot);
11832
12007
  for (const suffix of ["-wal", "-shm"]) {
11833
12008
  const src = `${CONDUCTOR_DB}${suffix}`;
11834
- if ((0, import_node_fs33.existsSync)(src)) {
12009
+ if ((0, import_node_fs34.existsSync)(src)) {
11835
12010
  try {
11836
- (0, import_node_fs33.copyFileSync)(src, `${snapshot}${suffix}`);
12011
+ (0, import_node_fs34.copyFileSync)(src, `${snapshot}${suffix}`);
11837
12012
  } catch {
11838
12013
  }
11839
12014
  }
@@ -11851,7 +12026,7 @@ function importConductorWorkspace(workspaceId) {
11851
12026
  ).get(workspaceId);
11852
12027
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
11853
12028
  const worktreePath = String(row.workspacePath);
11854
- if (!(0, import_node_fs33.existsSync)(worktreePath)) {
12029
+ if (!(0, import_node_fs34.existsSync)(worktreePath)) {
11855
12030
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
11856
12031
  }
11857
12032
  let sessionId = null;
@@ -11914,7 +12089,7 @@ function importConductorWorkspace(workspaceId) {
11914
12089
  db.close();
11915
12090
  }
11916
12091
  } finally {
11917
- (0, import_node_fs33.rmSync)(tmp, { recursive: true, force: true });
12092
+ (0, import_node_fs34.rmSync)(tmp, { recursive: true, force: true });
11918
12093
  }
11919
12094
  }
11920
12095
  async function importConductorWorkspaceAsync(workspaceId) {
@@ -11922,7 +12097,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
11922
12097
  }
11923
12098
 
11924
12099
  // src/threads/stack-layers.ts
11925
- var import_node_fs34 = require("fs");
12100
+ var import_node_fs35 = require("fs");
11926
12101
  init_run();
11927
12102
  init_stack();
11928
12103
  init_worktree();
@@ -11990,7 +12165,7 @@ async function openStackLayer(input, _onSetupLine) {
11990
12165
  let createdWorktree = false;
11991
12166
  const trees = await listWorktrees(repoPath);
11992
12167
  const checkedOut = trees.find((w) => w.branch === branchName);
11993
- if (checkedOut?.path && (0, import_node_fs34.existsSync)(checkedOut.path)) {
12168
+ if (checkedOut?.path && (0, import_node_fs35.existsSync)(checkedOut.path)) {
11994
12169
  if (input.reuseExistingWorktree !== false) {
11995
12170
  worktreePath = checkedOut.path;
11996
12171
  } else {
@@ -12132,7 +12307,7 @@ async function initStackFromThread(input, onSetupLine) {
12132
12307
  async function createPrStack(input, onSetupLine) {
12133
12308
  await requireAgent(input.agent);
12134
12309
  const repoPath = await resolveRepoRoot(input.repoPath);
12135
- 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}`);
12136
12311
  if (!input.branches.length) throw new Error("At least one branch name required");
12137
12312
  const status = await detectGhStack(repoPath);
12138
12313
  if (!status.available) throw new Error(status.reason);
@@ -12199,7 +12374,7 @@ async function createPrStack(input, onSetupLine) {
12199
12374
  }
12200
12375
  }
12201
12376
  const claimed = new Set(threads.map((t) => t.worktreePath));
12202
- 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)) {
12203
12378
  try {
12204
12379
  await removeWorktree(repoPath, bootstrap.worktreePath, {
12205
12380
  deleteBranch: bootstrap.branchName
@@ -12214,12 +12389,12 @@ async function createPrStack(input, onSetupLine) {
12214
12389
  init_worktree();
12215
12390
 
12216
12391
  // src/diff/diff.ts
12217
- var import_node_fs35 = require("fs");
12392
+ var import_node_fs36 = require("fs");
12218
12393
  var import_node_path32 = require("path");
12219
12394
  init_run();
12220
12395
  init_worktree();
12221
12396
  async function inspectGitWorktree(worktreePath) {
12222
- if (!worktreePath || !(0, import_node_fs35.existsSync)(worktreePath)) return "missing_worktree";
12397
+ if (!worktreePath || !(0, import_node_fs36.existsSync)(worktreePath)) return "missing_worktree";
12223
12398
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
12224
12399
  reject: false
12225
12400
  });
@@ -12227,7 +12402,7 @@ async function inspectGitWorktree(worktreePath) {
12227
12402
  return "ok";
12228
12403
  }
12229
12404
  async function initializeGitRepository(worktreePath) {
12230
- if (!worktreePath || !(0, import_node_fs35.existsSync)(worktreePath)) {
12405
+ if (!worktreePath || !(0, import_node_fs36.existsSync)(worktreePath)) {
12231
12406
  throw new Error("Worktree not found");
12232
12407
  }
12233
12408
  const status = await inspectGitWorktree(worktreePath);
@@ -12365,9 +12540,9 @@ new file mode 100644
12365
12540
  async function untrackedPatch(worktreePath, path, maxHunk) {
12366
12541
  const abs = (0, import_node_path32.join)(worktreePath, path);
12367
12542
  try {
12368
- const st = (0, import_node_fs35.statSync)(abs);
12543
+ const st = (0, import_node_fs36.statSync)(abs);
12369
12544
  if (st.isFile() && st.size > maxHunk) {
12370
- const buf = (0, import_node_fs35.readFileSync)(abs).subarray(0, maxHunk);
12545
+ const buf = (0, import_node_fs36.readFileSync)(abs).subarray(0, maxHunk);
12371
12546
  return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
12372
12547
  }
12373
12548
  } catch {
@@ -12869,7 +13044,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
12869
13044
  assertSafeRelativePath(relativePath);
12870
13045
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
12871
13046
  const abs = (0, import_node_path32.join)(worktreePath, relativePath);
12872
- const st = (0, import_node_fs35.statSync)(abs);
13047
+ const st = (0, import_node_fs36.statSync)(abs);
12873
13048
  if (!st.isFile()) {
12874
13049
  throw new Error(`Not a file: ${relativePath}`);
12875
13050
  }
@@ -12878,7 +13053,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
12878
13053
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
12879
13054
  );
12880
13055
  }
12881
- const buf = (0, import_node_fs35.readFileSync)(abs);
13056
+ const buf = (0, import_node_fs36.readFileSync)(abs);
12882
13057
  return {
12883
13058
  path: relativePath,
12884
13059
  contentBase64: buf.toString("base64"),
@@ -12889,11 +13064,11 @@ function readWorktreeFile(worktreePath, relativePath, opts) {
12889
13064
  assertSafeRelativePath(relativePath);
12890
13065
  const maxBytes = opts?.maxBytes ?? 2e5;
12891
13066
  const abs = (0, import_node_path32.join)(worktreePath, relativePath);
12892
- const st = (0, import_node_fs35.statSync)(abs);
13067
+ const st = (0, import_node_fs36.statSync)(abs);
12893
13068
  if (!st.isFile()) {
12894
13069
  throw new Error(`Not a file: ${relativePath}`);
12895
13070
  }
12896
- const buf = (0, import_node_fs35.readFileSync)(abs);
13071
+ const buf = (0, import_node_fs36.readFileSync)(abs);
12897
13072
  if (isImageRelativePath(relativePath)) {
12898
13073
  const maxImageBytes = Math.max(maxBytes, 15e6);
12899
13074
  const truncated2 = buf.length > maxImageBytes;
@@ -12937,8 +13112,8 @@ function assertSafeRelativePath(relativePath) {
12937
13112
  function writeWorktreeFile(worktreePath, relativePath, content) {
12938
13113
  assertSafeRelativePath(relativePath);
12939
13114
  const abs = (0, import_node_path32.join)(worktreePath, relativePath);
12940
- (0, import_node_fs35.mkdirSync)((0, import_node_path32.dirname)(abs), { recursive: true });
12941
- (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");
12942
13117
  return { path: relativePath };
12943
13118
  }
12944
13119
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -13042,7 +13217,7 @@ async function confirmLand(thread, opts) {
13042
13217
  }
13043
13218
 
13044
13219
  // src/skills/discover.ts
13045
- var import_node_fs36 = require("fs");
13220
+ var import_node_fs37 = require("fs");
13046
13221
  var import_node_os11 = require("os");
13047
13222
  var import_node_path33 = require("path");
13048
13223
  function toCommand(name) {
@@ -13076,7 +13251,7 @@ function parseFrontmatter(content) {
13076
13251
  }
13077
13252
  function readSkill(skillMd, source) {
13078
13253
  try {
13079
- const content = (0, import_node_fs36.readFileSync)(skillMd, "utf8");
13254
+ const content = (0, import_node_fs37.readFileSync)(skillMd, "utf8");
13080
13255
  const { name: fmName, description } = parseFrontmatter(content);
13081
13256
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
13082
13257
  const name = fmName || dirName;
@@ -13095,19 +13270,19 @@ function readSkill(skillMd, source) {
13095
13270
  }
13096
13271
  }
13097
13272
  function scanSkillsDir(dir, source, out) {
13098
- if (!(0, import_node_fs36.existsSync)(dir)) return;
13273
+ if (!(0, import_node_fs37.existsSync)(dir)) return;
13099
13274
  let entries;
13100
13275
  try {
13101
- entries = (0, import_node_fs36.readdirSync)(dir);
13276
+ entries = (0, import_node_fs37.readdirSync)(dir);
13102
13277
  } catch {
13103
13278
  return;
13104
13279
  }
13105
13280
  for (const entry of entries) {
13106
13281
  if (entry.startsWith(".")) continue;
13107
13282
  const skillMd = (0, import_node_path33.join)(dir, entry, "SKILL.md");
13108
- if (!(0, import_node_fs36.existsSync)(skillMd)) continue;
13283
+ if (!(0, import_node_fs37.existsSync)(skillMd)) continue;
13109
13284
  try {
13110
- if (!(0, import_node_fs36.statSync)(skillMd).isFile()) continue;
13285
+ if (!(0, import_node_fs37.statSync)(skillMd).isFile()) continue;
13111
13286
  } catch {
13112
13287
  continue;
13113
13288
  }
@@ -13116,12 +13291,12 @@ function scanSkillsDir(dir, source, out) {
13116
13291
  }
13117
13292
  }
13118
13293
  function scanClaudePluginSkills(pluginsRoot, out) {
13119
- if (!(0, import_node_fs36.existsSync)(pluginsRoot)) return;
13294
+ if (!(0, import_node_fs37.existsSync)(pluginsRoot)) return;
13120
13295
  const walk = (dir, depth, lookingForSkillsDir) => {
13121
13296
  if (depth > 7) return;
13122
13297
  let entries;
13123
13298
  try {
13124
- entries = (0, import_node_fs36.readdirSync)(dir);
13299
+ entries = (0, import_node_fs37.readdirSync)(dir);
13125
13300
  } catch {
13126
13301
  return;
13127
13302
  }
@@ -13133,7 +13308,7 @@ function scanClaudePluginSkills(pluginsRoot, out) {
13133
13308
  if (entry === "node_modules" || entry === ".git") continue;
13134
13309
  const full = (0, import_node_path33.join)(dir, entry);
13135
13310
  try {
13136
- if (!(0, import_node_fs36.statSync)(full).isDirectory()) continue;
13311
+ if (!(0, import_node_fs37.statSync)(full).isDirectory()) continue;
13137
13312
  } catch {
13138
13313
  continue;
13139
13314
  }
@@ -13173,7 +13348,7 @@ function discoverSkills(worktreePath) {
13173
13348
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
13174
13349
  }
13175
13350
  function readSkillBody(skillPath, maxChars = 12e3) {
13176
- const raw = (0, import_node_fs36.readFileSync)(skillPath, "utf8");
13351
+ const raw = (0, import_node_fs37.readFileSync)(skillPath, "utf8");
13177
13352
  if (raw.startsWith("---")) {
13178
13353
  const end = raw.indexOf("\n---", 3);
13179
13354
  if (end >= 0) {
@@ -13266,7 +13441,7 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
13266
13441
  }
13267
13442
 
13268
13443
  // src/composer/stage-files.ts
13269
- var import_node_fs37 = require("fs");
13444
+ var import_node_fs38 = require("fs");
13270
13445
  var import_node_path34 = require("path");
13271
13446
  var import_node_crypto7 = require("crypto");
13272
13447
  init_workspace_scratch();
@@ -13304,21 +13479,21 @@ function imageMimeType(filePath) {
13304
13479
  }
13305
13480
  function ensureAttachmentsDir(worktreePath) {
13306
13481
  const dir = (0, import_node_path34.join)(worktreePath, ATTACHMENTS_DIR);
13307
- (0, import_node_fs37.mkdirSync)(dir, { recursive: true });
13482
+ (0, import_node_fs38.mkdirSync)(dir, { recursive: true });
13308
13483
  const gi = (0, import_node_path34.join)(dir, ".gitignore");
13309
- if (!(0, import_node_fs37.existsSync)(gi)) {
13310
- (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");
13311
13486
  }
13312
13487
  return dir;
13313
13488
  }
13314
13489
  function uniqueAttachmentName(dir, originalName) {
13315
13490
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
13316
- 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;
13317
13492
  const ext = (0, import_node_path34.extname)(safe);
13318
13493
  const stem = ext ? safe.slice(0, -ext.length) : safe;
13319
13494
  for (let i = 1; i < 1e4; i++) {
13320
13495
  const candidate = `${stem}-${i}${ext}`;
13321
- 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;
13322
13497
  }
13323
13498
  return `${stem}-${(0, import_node_crypto7.randomUUID)()}${ext}`;
13324
13499
  }
@@ -13376,13 +13551,13 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
13376
13551
  for (const abs of absolutePaths) {
13377
13552
  const originalName = (0, import_node_path34.basename)(abs);
13378
13553
  try {
13379
- const st = (0, import_node_fs37.statSync)(abs);
13554
+ const st = (0, import_node_fs38.statSync)(abs);
13380
13555
  if (!st.isFile()) continue;
13381
13556
  const name = uniqueAttachmentName(dir, originalName);
13382
13557
  const destAbs = (0, import_node_path34.join)(dir, name);
13383
- (0, import_node_fs37.copyFileSync)(abs, destAbs);
13558
+ (0, import_node_fs38.copyFileSync)(abs, destAbs);
13384
13559
  const rel = `${ATTACHMENTS_DIR}/${name}`;
13385
- const buf = (0, import_node_fs37.readFileSync)(destAbs);
13560
+ const buf = (0, import_node_fs38.readFileSync)(destAbs);
13386
13561
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
13387
13562
  } catch (err) {
13388
13563
  out.push({
@@ -13395,17 +13570,17 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
13395
13570
  }
13396
13571
  return out;
13397
13572
  }
13398
- function stageBuffersAsAttachments(worktreePath, buffers) {
13399
- if (buffers.length === 0) return [];
13573
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
13574
+ if (buffers2.length === 0) return [];
13400
13575
  const dir = ensureAttachmentsDir(worktreePath);
13401
13576
  const out = [];
13402
- for (const item of buffers) {
13577
+ for (const item of buffers2) {
13403
13578
  const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
13404
13579
  try {
13405
13580
  const buf = Buffer.from(item.dataBase64, "base64");
13406
13581
  const name = uniqueAttachmentName(dir, originalName);
13407
13582
  const destAbs = (0, import_node_path34.join)(dir, name);
13408
- (0, import_node_fs37.writeFileSync)(destAbs, buf);
13583
+ (0, import_node_fs38.writeFileSync)(destAbs, buf);
13409
13584
  const rel = `${ATTACHMENTS_DIR}/${name}`;
13410
13585
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
13411
13586
  } catch (err) {
@@ -13434,9 +13609,9 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
13434
13609
  const name = (0, import_node_path34.basename)(rel);
13435
13610
  try {
13436
13611
  const abs = (0, import_node_path34.join)(worktreePath, rel);
13437
- const st = (0, import_node_fs37.statSync)(abs);
13612
+ const st = (0, import_node_fs38.statSync)(abs);
13438
13613
  if (!st.isFile()) continue;
13439
- const buf = (0, import_node_fs37.readFileSync)(abs);
13614
+ const buf = (0, import_node_fs38.readFileSync)(abs);
13440
13615
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
13441
13616
  } catch (err) {
13442
13617
  out.push({
@@ -13451,7 +13626,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
13451
13626
  }
13452
13627
 
13453
13628
  // src/agents/instructions.ts
13454
- var import_node_fs38 = require("fs");
13629
+ var import_node_fs39 = require("fs");
13455
13630
  var import_node_path35 = require("path");
13456
13631
  init_git_auth_mode();
13457
13632
  init_worktree_labels();
@@ -13655,6 +13830,15 @@ function isPidAlive(pid) {
13655
13830
  return false;
13656
13831
  }
13657
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;
13658
13842
  function writeLiveStatus(threadId, status, lastError) {
13659
13843
  const latest = readThread(threadId);
13660
13844
  if (!latest || latest.status === "archived") return latest;
@@ -13732,7 +13916,7 @@ var Orchestrator = class {
13732
13916
  }
13733
13917
  continue;
13734
13918
  }
13735
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
13919
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
13736
13920
  setStatus(thread.id, "broken", "Worktree missing on disk");
13737
13921
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
13738
13922
  continue;
@@ -14053,14 +14237,10 @@ var Orchestrator = class {
14053
14237
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
14054
14238
  const livePid = current.agentPid;
14055
14239
  const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
14056
- if (inFlight) {
14240
+ if (inFlight || foreignLive) {
14057
14241
  this.stop(thread.id, { clearQueue: false, continueQueue: true });
14058
- } else {
14059
- if (foreignLive) {
14060
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
14061
- }
14062
- void this.drainQueue(thread.id);
14063
14242
  }
14243
+ void this.drainQueue(thread.id);
14064
14244
  return this.requireThread(thread.id);
14065
14245
  }
14066
14246
  async drainQueue(threadId) {
@@ -14089,7 +14269,18 @@ var Orchestrator = class {
14089
14269
  }
14090
14270
  const livePid = thread.agentPid;
14091
14271
  if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
14092
- 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
+ }
14093
14284
  continue;
14094
14285
  }
14095
14286
  const prompt = thread.queue[0];
@@ -14246,6 +14437,7 @@ var Orchestrator = class {
14246
14437
  { cachedPrefix, prompt: agentPrompt },
14247
14438
  (event) => {
14248
14439
  this.emit({ type: "turn_output", threadId, event });
14440
+ noteTurnLiveEvent(threadId, event);
14249
14441
  if (event.type === "session_id") {
14250
14442
  updateThread(threadId, { sessionId: event.data });
14251
14443
  }
@@ -14450,6 +14642,7 @@ var Orchestrator = class {
14450
14642
  this.processes.delete(`${threadId}:agent`);
14451
14643
  this.stoppedTurns.delete(threadId);
14452
14644
  this.runningCount = Math.max(0, this.runningCount - 1);
14645
+ clearTurnLive(threadId);
14453
14646
  try {
14454
14647
  updateThread(threadId, { agentPid: null });
14455
14648
  } catch {
@@ -14723,51 +14916,56 @@ var Orchestrator = class {
14723
14916
  }
14724
14917
  return created;
14725
14918
  }
14726
- async waitForTurn(threadRef, timeoutMs = 6e5) {
14919
+ async waitForTurn(threadRef, timeoutMs = 6e5, opts) {
14727
14920
  const thread = this.requireThread(threadRef);
14921
+ if (!["running", "queued"].includes(thread.status)) {
14922
+ return thread;
14923
+ }
14728
14924
  const start = Date.now();
14729
14925
  return new Promise((resolve, reject) => {
14730
- if (!["running", "queued"].includes(thread.status)) {
14731
- resolve(thread);
14732
- return;
14733
- }
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
+ };
14734
14935
  const off = this.on((event) => {
14735
14936
  if (event.type === "turn_finished" && event.threadId === thread.id) {
14736
- off();
14737
14937
  const latest = readThread(thread.id);
14738
14938
  if (!latest) {
14739
- reject(new Error(`Thread not found: ${thread.id}`));
14939
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
14740
14940
  return;
14741
14941
  }
14742
- resolve(latest);
14942
+ finish(() => resolve(latest));
14743
14943
  }
14744
14944
  if (event.type === "error" && event.threadId === thread.id) {
14745
- off();
14746
14945
  const latest = readThread(thread.id);
14747
14946
  if (!latest) {
14748
- reject(new Error(`Thread not found: ${thread.id}`));
14947
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
14749
14948
  return;
14750
14949
  }
14751
- resolve(latest);
14950
+ finish(() => resolve(latest));
14752
14951
  }
14753
14952
  });
14754
- const timer = setInterval(() => {
14755
- if (Date.now() - start > timeoutMs) {
14756
- clearInterval(timer);
14757
- off();
14758
- reject(new Error("wait_for_turn timed out"));
14759
- }
14953
+ timer = setInterval(() => {
14760
14954
  const current = readThread(thread.id);
14761
14955
  if (!current) {
14762
- clearInterval(timer);
14763
- off();
14764
- reject(new Error(`Thread not found: ${thread.id}`));
14956
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
14765
14957
  return;
14766
14958
  }
14767
14959
  if (!["running", "queued"].includes(current.status)) {
14768
- clearInterval(timer);
14769
- off();
14770
- 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
+ }
14771
14969
  }
14772
14970
  }, 200);
14773
14971
  });
@@ -14777,11 +14975,16 @@ var Orchestrator = class {
14777
14975
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
14778
14976
  const lastError = thread.lastError ?? null;
14779
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;
14780
14980
  return {
14781
14981
  text: text3,
14782
14982
  status: thread.status,
14783
14983
  sessionId: thread.sessionId,
14784
- lastError
14984
+ lastError,
14985
+ stillRunning,
14986
+ progress: live?.summary ?? null,
14987
+ lastActivityAt: live?.updatedAt ?? null
14785
14988
  };
14786
14989
  }
14787
14990
  assertNotGlobal(thread, action) {
@@ -15207,7 +15410,19 @@ var Orchestrator = class {
15207
15410
  const thread = this.requireThread(threadRef);
15208
15411
  return threadsSharingWorktree(thread.worktreePath);
15209
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
+ }
15210
15421
  async archive(threadRef) {
15422
+ const thread = this.requireThread(threadRef);
15423
+ return this.enqueueWorktreeTeardown(thread, () => this.archiveUnlocked(threadRef));
15424
+ }
15425
+ async archiveUnlocked(threadRef) {
15211
15426
  const thread = this.requireThread(threadRef);
15212
15427
  this.stop(thread.id);
15213
15428
  this.releaseOrchestratorCaffeinate(thread);
@@ -15243,6 +15458,10 @@ var Orchestrator = class {
15243
15458
  return archived;
15244
15459
  }
15245
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) {
15246
15465
  const thread = this.requireThread(threadRef);
15247
15466
  this.stop(thread.id);
15248
15467
  this.releaseOrchestratorCaffeinate(thread);
@@ -15277,13 +15496,12 @@ var Orchestrator = class {
15277
15496
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
15278
15497
  return restored2;
15279
15498
  }
15280
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
15499
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
15281
15500
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
15282
- const { execa: execa7 } = await import("execa");
15283
15501
  const slug = thread.worktreePath.split("/").pop();
15284
15502
  const dest = thread.worktreePath;
15285
- await execa7("git", ["worktree", "add", dest, thread.branchName], {
15286
- cwd: thread.repoPath
15503
+ await withRepoGitLock(thread.repoPath, async () => {
15504
+ await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
15287
15505
  });
15288
15506
  void createThreadWorktree2;
15289
15507
  void slug;
@@ -15847,6 +16065,15 @@ function mcpArchiveBlockedReason(thread) {
15847
16065
  // src/mcp/server.ts
15848
16066
  init_profile();
15849
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
+
15850
16077
  // src/mcp/slack-tools.ts
15851
16078
  var import_zod = require("zod");
15852
16079
 
@@ -16518,7 +16745,9 @@ async function startMcpServer() {
16518
16745
  const threads = orch.getThreads(true);
16519
16746
  const lines = threads.map((t) => {
16520
16747
  const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path39.basename)(t.repoPath) || t.repoPath;
16521
- 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}`;
16522
16751
  });
16523
16752
  return {
16524
16753
  content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
@@ -16527,13 +16756,14 @@ async function startMcpServer() {
16527
16756
  );
16528
16757
  server.tool(
16529
16758
  "get_thread",
16530
- "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.",
16531
16760
  { ref: import_zod3.z.string() },
16532
16761
  async ({ ref }) => {
16533
16762
  const t = orch.getThread(ref);
16534
16763
  if (!t) {
16535
16764
  return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
16536
16765
  }
16766
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
16537
16767
  const summary = {
16538
16768
  id: t.id,
16539
16769
  title: t.title,
@@ -16548,7 +16778,10 @@ async function startMcpServer() {
16548
16778
  messageCount: t.messages.length,
16549
16779
  devPort: t.devPort,
16550
16780
  prUrl: t.prUrl,
16551
- 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
16552
16785
  };
16553
16786
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
16554
16787
  }
@@ -16874,13 +17107,15 @@ async function startMcpServer() {
16874
17107
  );
16875
17108
  server.tool(
16876
17109
  "wait_for_turn",
16877
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git. Returns status, text, and lastError. If status is error, lastError/text is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
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.",
16878
17111
  {
16879
17112
  ref: import_zod3.z.string(),
16880
17113
  timeoutMs: import_zod3.z.number().optional()
16881
17114
  },
16882
17115
  async ({ ref, timeoutMs }) => {
16883
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
17116
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
17117
+ resolveIfStillRunning: true
17118
+ });
16884
17119
  const result = orch.getTurnResult(thread.id);
16885
17120
  return {
16886
17121
  content: [
@@ -16890,7 +17125,11 @@ async function startMcpServer() {
16890
17125
  id: thread.id,
16891
17126
  status: result.status,
16892
17127
  text: result.text,
16893
- lastError: result.lastError
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
16894
17133
  })
16895
17134
  }
16896
17135
  ]
@@ -16899,11 +17138,21 @@ async function startMcpServer() {
16899
17138
  );
16900
17139
  server.tool(
16901
17140
  "get_turn_result",
16902
- "Final assistant message (and lastError when the turn failed). Not the full transcript.",
17141
+ "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript.",
16903
17142
  { ref: import_zod3.z.string() },
16904
17143
  async ({ ref }) => {
16905
17144
  const result = orch.getTurnResult(ref);
16906
- 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
+ };
16907
17156
  }
16908
17157
  );
16909
17158
  server.tool(
@@ -17020,7 +17269,7 @@ async function startMcpServer() {
17020
17269
  );
17021
17270
  server.tool(
17022
17271
  "request_review",
17023
- '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.',
17024
17273
  { ref: import_zod3.z.string().describe("Worktree thread id/ref to review") },
17025
17274
  async ({ ref }) => {
17026
17275
  try {
@@ -17048,7 +17297,7 @@ async function startMcpServer() {
17048
17297
  );
17049
17298
  server.tool(
17050
17299
  "ask_git",
17051
- "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.",
17052
17301
  {
17053
17302
  ref: import_zod3.z.string().describe("Worktree thread id/ref"),
17054
17303
  action: import_zod3.z.enum(AGENT_GIT_ACTIONS).describe(
@@ -17099,7 +17348,7 @@ async function startMcpServer() {
17099
17348
  );
17100
17349
  server.tool(
17101
17350
  "fork_worktree",
17102
- "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.",
17103
17352
  {
17104
17353
  ref: import_zod3.z.string().describe("Worktree thread id/ref to fork"),
17105
17354
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -17144,7 +17393,7 @@ async function startMcpServer() {
17144
17393
  );
17145
17394
  server.tool(
17146
17395
  "fork_chat",
17147
- "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.",
17148
17397
  {
17149
17398
  ref: import_zod3.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
17150
17399
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),